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

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

// 全局变量
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_的改变值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容