美文网首页
12 iOS底层原理 - Block外部变量捕获

12 iOS底层原理 - Block外部变量捕获

作者: 程序小胖 | 来源:发表于2020-02-06 16:18 被阅读0次

大家在面试的时候是不是经常遇到这样的面试题:
运行下面的代码,打印结果是是什么?为什么?

// 全局变量
NSString *name_ = @"张三";

- (void)testBlock {
    int age = 18;
    static int height = 180;
    void (^myBlock)(void) = ^{
         NSLog(@"myBlock:age=%d,height=%d,name=%@", age, height, name_);
    };
    age = 28;
    height = 160;
    name_ = @"李四";
    myBlock();
}

这个面试题呢,也就是今天要主要说的内容:Block的值捕获。
那么,下面就针对,局部变量、静态变量、全局变量这三种变量,研究一下,block在底层到底是怎么捕获外部的局部变量的,还有全局变量到底有没有捕获呢?(其实,还有一个对象类型的变量,后面的章节会说到)

一,Block捕获外部局部变量

1. 查看打印结果

运行代码:

- (void)testBlock {
    int age = 18;
    static int height = 180;
    void (^myBlock)(void) = ^{
         NSLog(@"myBlock:age=%d,height=%d", age, height);
    };
    age = 28;
    height = 160;
    myBlock();
}

// myBlock:age=18,height=160

通过打印结果,发现,只有静态变量height的值变了,变量age的值没变。
这是为啥呢???
下面咋们看看clang编译后的c++代码,他俩到底有啥区别?

注意两个关键字 auto和static

  • auto:自动变量,默认就是auto的,离开作用域就销毁
  • static:静态变量,不销毁
int age;
// 等价于
auto int age;
2. clang编译

在终端通过编译ViewController.m文件:

& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m

生成ViewController.cpp文件:

3. Block捕获的变量最终去了哪?

分析c++代码(c++代码就不粘贴了),我汇总了一个示意图,可以清晰的看出,变量值的传递过程,以及存储地方,如下图所示:

image.png

根据上图简单说明几点:

  • oc中声明 int age; 是省略了auto变量的,c++代码就可以看出
  • auto变量是一个值传递
  • static变量是指针传递,因为对height做了取地址符&操作了,相当于将这个值对应的地址取出来,然后传到方法里面去;
  • 可以看出,这个block就是一个指向结构体的指针;
  • 通过一个返回结构体的函数,将这两个变量存储到了block指针指向的结构体内存中。

说明一下:

  • 值传递,就好比,将一个房间里面的人,给传出去,你这个房间以后在不在我就不关心;
  • 指针传递,就好比,将这个房间传出去,也就是说,以后我就可以通过这个房间来找最新的人。
4. 捕获的变量是如何使用的?

现在通过block已经将外部的局部变量,捕获到了block的内存里了。
那么,具体用到这个值的时候,是怎么取出来的呢?

还是看图,我已经将相关c++代码片段做了流程说明,如图所示:

image.png

简单说明下这个示意图:

  • 执行block时,就会通过block指针找到所指向的结构体,在结构体里面找到FuncPtr这个函数地址;
  • 通过FuncPtr这个函数地址,找到这个函数的实现;
  • 将block自己作为参数,传入这个函数;
  • 可以看出,block本质上就是一个指向结构体的指针,所以,就可以通过该指针找到结构体,然后从结构体里面取出变量。

二,Block捕获全局变量

1. 添加全局变量,查看打印结果
int age_ = 10;
static int height_ = 170;

- (void)testBlock {
    NSLog(@"myBlock前:age_=%d,height_=%d", age_, height_);
    void (^myBlock)(void) = ^{
         NSLog(@"myBlock后:age_=%d,height_=%d", age_, height_);
    };
    
    age_ = 30;
    height_ = 200;
    myBlock();
}

// myBlock前:age_=10,height_=170
// myBlock后:age_=30,height_=200

发现两个全局变量的值都改变了。
那么,接下来,我们看看age_和height_是不是也被block捕获了??

2. clang编译

在终端通过编译ViewController.m文件:

& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m

生成ViewController.cpp文件:

3. 分析全部变量到底去哪了?
1> 查看Block底层数据结构

如下图所示,全局变量并没有被存储在结构体里面,也就是说,block压根就没有去捕获全局变量。

image.png

那么,为啥全局变量没有被捕获,也会随着其改变而改变呢?

那是因为:

可以这么理解,全局变量的值都可以访问,内存也不会释放(存储在数据段),block实现里面使用的时候,就不用担心这个值随时会释放的问题,反正我随时都可以访问你的内存,为啥我block还要再存储呢,对不对。

然而,局部变量需要被捕获的原因就是:

在block底层,使用变量的时候,是跨函数调用的。如果是一个局部变量,就需要将变量值提前存到block的struct里面。局部变量中的静态变量static修饰的变量,在底层是以地址(指针)的方式存储的,所以在block实现时,会去变量地址里面找最新的值。

三,Block捕获对象类型的auto变量

1. 创建一个Person对象
// 声明
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

// 实现
@implementation Person
-(void)dealloc {
    NSLog(@"%s", __func__);
}
@end
2. 运行代码
- (void)viewDidLoad {
    [super viewDidLoad];
    {
        Person *person = [[Person alloc]init];
        person.age = 18;
    }
  NSLog(@"------");
}

打印结果是:

-[Person dealloc]
------

在分割符前就打印了dealloc,说明这个Person对象在超出{}作用域后,就会销毁释放。
那么,用block捕获Person对象的属性,Person对象还会释放吗??

2. 用block捕获Person对象的属性
- (void)viewDidLoad {
    [super viewDidLoad];
    
    void(^block)(void);
    {
        Person *person = [[Person alloc]init];
        person.age = 18;
        block = ^{
            NSLog(@"%d", person.age);
        };
    }
    
    block();
    NSLog(@"block类型 = %@", [block class]);
    NSLog(@"------");
}

打印结果

age = 18
block类型 = __NSMallocBlock__
------
-[Person dealloc]

通过打印结果发现,在分割符前并没有打印-[Person dealloc],说明这个Person对象在离开{}作用域后没有释放。这是为啥呢??

1> Block捕获了对象的属性

初始化的这个实例对象person,其实也是一个auto变量。当Block捕获了auto变量,Block会存储在堆区。此时,就算Person的作用域结束了,Person对象还是保存在Block底层结构体数据里面的。

说明堆空间的Block对Person是强引用的,只有Block销毁了,Person才会被销毁。

看看Person对象在Block内存中是以什么样的形式存储的。

clang编译ViewController.m文件

& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m

编译后的代码如下所示,这是Block内部的数据结构,其中存储着Block捕获的Person对象。

image.png
2> MRC环境下,会提前释放Person吗?

上面的测试都是在ARC下进行的,如果是在MRC环境下,此时的Block是在栈区,那么就会打印-[Person dealloc]这句话。

说明栈空间的Block不会持有外面的对象的,不会保住Person的命(MRC下没有强引用的说法)。

3> __weak修饰对象,会提前释放Person吗?
- (void)viewDidLoad {
    [super viewDidLoad];
    
    void(^block)(void);
    {
        Person *person = [[Person alloc]init];
        person.age = 18;
        __weak Person *weakPerson = person;
        block = ^{
            NSLog(@"%d", weakPerson age);
        };
    }
    
    block();
    NSLog(@"block类型 = %@", [block class]);
    NSLog(@"------");
}

打印结果是:

-[Person dealloc]
age = 0
block类型 = __NSMallocBlock__
------

通过打印结果就可知道,{}作用域一结束,Person就释放掉了。

说明,在对象被__weak修饰后,堆空间的Block对Person是弱引用的,Person会随着作用域结束而销毁。

用clang编译ViewController.m文件,

// 这个命令行支持ARC、指定运行时系统版本
& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 ViewController.m

下面就是编译后的代码片段截图,从中可以看出,Block内部结构体在存储Perosn对象的时候,也是用弱引用存储的。

image.png
3. block除了捕获对象类型的auto变量,还干了啥?

我们已经知道了,block不管是捕获基本数据类型的变量,还是捕获对象类型的auto变量,都会存储在block内存中的。
但是,在block捕获对象类型的auto变量时,还发生了什么事呢??

如下图所示:

image.png

一句话概括:

在block捕获对象类型的auto变量时,block内存中还会生成两个函数:copy函数 和 dipose函数

那么block的内存分布就可以是这样的,如图所示:

image.png

从上图可以看出,block捕获一个对象类型的auto变量的话,在block内存中会多出两个函数

  • copy函数
  • dispose函数

那么,这两个函数到底是干啥的呢???

4. block内存中的copy函数和dipose函数是干啥的?
1>copy函数

请看示意图:

image.png

简单说明下:

  1. 当block被拷贝到堆上时,block会自动调用内存中的copy函数,然后找到_Block_object_assign函数;
  2. _Block_object_assign函数,会根据block内存中的auto变量person是什么类型的,会对person产生强引用或者弱引用。也可以这么理解,_Block_object_assign内部会对person进行引用计数的操作。
    如果person是被__strong或没有修饰的,那么就是强引用,引用计数就会+1;
    如果person是被__weak修饰的,那么就是弱引用,引用计数就不会变。
2> dispose函数

请看示意图:

image.png

简单说明下:

  1. 当block从堆区移除时,block会自动调用内存中的dispose函数,然后调用__Block_object_dispose函数;
  2. __Block_object_dispose函数会自动释放引用的auto变量,相当于release操作,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

四,Block变量捕获总结

1. block捕获基本数据类型的变量

为了保证block内部能正常访问外部变量,block有个变量捕获机制:

image.png

简言之就是:

  1. 只有局部变量才能被block捕获,全局变量不会被捕获;
  2. 局部变量 auto类型属于值传递,不会因为该值的改变,使得block实现里面的值也改变;
  3. 局部变量static类型属于指针传递,该值改变,会导致block实现里面的值也跟着改变。
2. block捕获对象类型的auto变量
  1. 如果block(匿名block)是在栈上,将不会对对象类型auto变量产生强引用,对象随着作用域销毁而销毁.
  1. 如果block被拷贝到了堆上:
    a>. 当block被拷贝到堆上时,block会自动调用内存中的copy函数,然后找到_Block_object_assign函数;
    b>. _Block_object_assign函数,会根据block内存中的auto变量person是什么类型的,会对person产生强引用或者弱引用。
    也可以这么理解,_Block_object_assign内部会对person进行引用计数的操作。
    如果person是被__strong修饰或没有修饰的,那么就是强引用,引用计数就会+1;
    如果person是被__weak修饰的,那么就是弱引用,引用计数就不会变。
  1. 如果block从堆上移除
    a> 当block从堆区移除时,block会自动调用内存中的dispose函数,然后调用__Block_object_dispose函数;
    b> __Block_object_dispose函数会自动释放引用的auto变量,相当于release操作,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。
  1. 被捕获的对象什么时候销毁,取决于强引用什么时候销毁,强引用销毁了,对象也就销毁了(前提是自己的引用计数为0)。

五,回答文章开头的面试题

运行下面的代码,打印结果是是什么?为什么?

// 全局变量
NSString *name_ = @"张三";

- (void)testBlock {
    int age = 18;
    static int height = 180;
    void (^myBlock)(void) = ^{
         NSLog(@"myBlock:age=%d,height=%d,name=%@", age, height, name_);
    };
    age = 28;
    height = 160;
    name_ = @"李四";
    myBlock();
}

打印结果是:

myBlock:age=18,height=160,name=@"李四"

原因:

  • age和height属于局部变量,在block内部使用,会被block捕获,存储在block内存中。
    因为,在block底层,使用变量的时候,是通过跨函数调用的。如果是一个局部变量,就需要将变量值提前存储到block的struct里面。
    age默认使用auto变量类型修饰的,属于值传递,所以在block实现时,会直接从struct里面取出已经存储的值;
    height使用staitc静态变量修饰的,属于指针传递,在block内存中是以地址的方式存储的,所以在block实现时,会先在struct里面找到这个变量的地址值,然后去变量地址找最新的值。

  • name_属于全局变量,所以不会被block捕获。但是,正因为是一个全局变量,在哪都可以访问,所以,block外部改变后,内部也会访问到name_的改变值。

相关文章

网友评论

      本文标题:12 iOS底层原理 - Block外部变量捕获

      本文链接:https://www.haomeiwen.com/subject/hgeyxhtx.html