iOS BAT 关于Block的面试题

iOS BAT 关于Block的面试题

我先附上一篇我觉得关于 block 源码分析的还不错的博文

关于 block 的面试问题无外乎就是下面五点,牢牢掌握面试就再也不怕了,block 之所以在面试中难,就是因为细节太多了

  1. 什么是 block
  2. block 的截获变量特性?
  3. __block 的理解
  4. 栈上 block 和堆上 block 的区别?在 MRC和 ARC下有什么需要注意的?
  5. 常见循环引用

什么是 block?

block 就是将函数执行上下文封装起来的对象

block的本质就是对象,其内部数据结构如下

block 的截获变量特性?

  • 基本数据类型的局部变量 Block 可以截获其值
  • 对于对象类型的局部变量连同所有权修饰符一起截获
  • 局部静态变量以指针的形式进行截获
  • 全局变量和静态全局变量,block 是不截获的

为什么要理解 block 截获变量特性呢? 下面我举个小例子,你就清楚了

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        int m = 6;
        int(^Block)(int) = ^int(int num) {
            return num * m;
        };
        NSLog(@"%d",Block(2));
    }
    return 0;
}


上述代码输出结果想必大家都知道了,12 嘛。如果我们将代码改进一下,如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        int m = 6;
        int(^Block)(int) = ^int(int num) {
            return num * m;
        };
        m = 8;
        NSLog(@"%d",Block(2));
    }
    return 0;
}


这个时候你再看,结果是啥呢?我们在 xcode 中运行一下,结果还是 12,为什么呢? 我们看上面截获变量特性就能理解了,局部基本数据类型,截获其值,什么是截获,就是在 block 执行体内部,编译器会对 m 的值进行 copy 操作,所以无论你下面 m 如何修改,其结果是不变的。

如果是静态局部变量 ’static int m = 6‘, 截获指针,那么上述代码的结果就是 16,因为指针指向的是该数据的内存地址,我们通过内部地址访问该值,所以下面修改,访问到的值也是最终修改的值。

全局变量是不截获的,所以你外部如何修改,在 block 最终调用的时候都是你修改后的值。

__block 的理解

通过__block 修饰的变量最终会经系统编译成为一个结构体,结构体中有一个 isa 指针,和一个值 m 以及 __forwarding 指针,经过修饰以后,我们调用赋值操作其实就会转变为 m._forwarding->m = x,而在 block 内部也会对该__block 对象进行拷贝,故通过__block可以修改被截获变量的值


全局 Block

当我们声明一个block时,如果这个block没有捕获外部的变量,那么这个block就位于全局区,此时对NSGlobalBlock的retain、copy、release操作都无效。ARC和MRC环境下都是如此。

栈 Block
这里可能有人会问,平时编程的时候很少遇到位于栈区的block,为什么呢?因为在ARC环境下,当我们声明并且定义了一个block,并且没有为Block添加额外的修饰符(默认是__strong修饰符),如果该Block捕获了外部的变量,实质上是有一个从__NSStackBlock__转变到__NSMallocBlock__的过程,只不过是系统帮我们完成了copy操作,将栈区的block迁移到堆区,延长了Block的生命周期。对于栈区block而言,栈block在当函数退出的时候,该空间就会被回收。
那什么时候在ARC的环境下出现__NSStackBlock__呢?如果我们在声明一个block的时候,使用了__weak或者__unsafe__unretained的修饰符,那么系统就不会为我们做copy的操作,不会将其迁移到堆区。

堆 Block
在MRC环境下,我们需要手动调用copy方法才可以将block迁移到堆区,而在ARC环境下,__strong修饰的(默认)block只要捕获了外部变量就会位于堆区,NSMallocBlock支持retain、release,会对其引用计数+1或 -1。声明以及定义位于堆区的block如上图所示。


常见循环引用

block 自循坏引用问题

代码如下

        _array = [NSMutableArray arrayWithObject:@"testBlock"];
        _strBlk = ^NSString*(NSString *num) {
            return [NSString stringWithFormat:@"hello %@", _array.firstObject];
        };
        _strBlk(@"hello");


分析如下图

解决方案,使用__weak 对 array 进行修饰,代码如下:

        _array = [NSMutableArray arrayWithObject:@"testBlock"];
        __weak NSMutableArray *weakArray = _array;
        _strBlk = ^NSString*(NSString *num) {
            return [NSString stringWithFormat:@"hello %@", weakArray.firstObject];
        };
        _strBlk(@"hello");

block 大循坏引用问题

        __block BlockObject *objc = self;
        _blk = ^int(int age) {
            int res = age * objc.num;
            return res;
        };
        _blk(3);

分析如下:

_block 变量强持有对象,对象持有block,block 又持有了__block 的变量,故产生了大环循环引用

解决方案,咱们使用断环的方式去解决,如图右

代码如下

        __block BlockObject *objc = self;
        _blk = ^int(int age) {
            int res = age * objc.num;
            objc = nil;
            return res;
        };
        _blk(3);

这样解决有一个小弊端,如果我们这个 blk block 永远没有不被调用,那么循环引用就一直存在。

暂时就这么多吧,水文一篇

发布于 2020-06-18 08:41