上篇讲到了Block其实就是oc对象
本文主要讲解Block值捕获以及如何修改block捕获的变量,说明下本文的目录结构(简书竟然没做目录功能,有点失望,给个简书生成目录的链接吧)
注意把示例中的这行代码改成下面的形式,否则无效
1 | // @match https://www.jianshu.com/p/* |
一、Block是如何捕获变量的
1.1 局部变量
1.1.1 auto类型变量
1.1.1.1 基本数据类型
修改下main.m的代码
1 |
|
发现打印的是
1 | 10 |
转成C++代码发现有些变化了
1 | struct __main_block_impl_0 { |
变化1:发现block多了个int类型的变量a;变化2:调用block的时候除了传递block本身外,把a的值也就是10传递给了里面的变量a变化3:最后方法执行的地方__main_block_func_0也是把block的变量a取出
1.1.1.2 对象数据类型
原来block的结构还不是定的,会随着拥有的变量改变内存结构
再举个例子来进一步理解下值捕获,改造下代码
1 |
|
输出
1 | 进入block前的地址----0x100001078 |
c++代码,这次简单写下就是多了个*name属性
1 | struct __main_block_impl_0 { |
解释下为什么是输出jack
在进入block前name地址为0x100001078,当到了__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, name))的时候相当于把name的值也就是0x100001078给了__main_block_impl_0里面的name变量,当再次name = @"rose";的时候之前的name的值已经不是0x100001078,而是0x1000010d8了,所以后来当调用的时候访问的是__main_block_impl_0的name变量的值0x100001078所存的值是jack
弄了一张图帮助理解下

1.1.2 static类型变量
1.1.2.1 对象类型
接着我们再次修改下代码
1 | int main(int argc, const char * argv[]) { |
就是加了一个static
输出
1 | ----rose-- |
这次的结果完全不一样了,这又是为什么呢,继续看c++的实现
1 | struct __main_block_impl_0 { |
看到代码1处已经是二重指针,2处这个时候给的不是name指向的内存地址,而是name变量的地址,3处在调用的时候取出的是指向name变量地址的内存,而不是name所指向的内存,所以要想获取name所指向的内存,4处通过*name取值进行传参
也就是红色箭头的变化
1.2 全局变量
1.2.1 非static修饰的全局变量
1.2.1.1 对象类型
当变量是全局的变量时
1 |
|
发现也是输出
1 | ---rose-- |
额,这又是怎么回事呢,看源码吧
1 | NSString *name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_0; |
我们可以发现这个时候block内部并没有像之前那样生成一个同名的变量,也就是对于全局变量block是不会捕获的,当变量是static全局变量时也和全局变量一样,留给读者自行测试了
1.2.1.2 基本类型
略…
1.2.2 static修饰的全局变量
略… 读者自行写demo
说了这么多我们来梳理下,我们思考一个问题,block为什么要捕获变量,是因为里面有个方法,方法需要使用变量,
- 如果是局部变量的话,如果不持有他的话是不是过了作用域就释放了,那就不能完成方法的正常调用,所以对于局部变量,一定会捕获的;
- 对于全局变量,刚才说了
block捕获变量的原因要使用变量,既然是全局变量,那在哪都可以访问,所以不需要捕获; - 那为什么局部变量有的是传地址有的是传值呢,对于非
static修饰的局部变量其实是auto的,这种变量是放在栈区的,过了作用域就会被系统回收,如果block捕获变量的地址的话,那可能捕获的地址已经被系统回收,或者已经被其他的对象占用了,这个时候程序会出现无法预料的异常,但是如果是static修饰的,是放在数据区的,不会随着作用域的而销毁,从而放地址是安全的
总结就是下面这张图
二、 Block捕获对对象的引用计数的影响
2.1 __NSMallocBlock__对对象的引用计数的影响 ARC环境
我们知道基本数据类型是放在栈中的,回收是由系统自动回收的无需考虑,所以我们这里只考虑auto类型的对象类型的引用计数
需要新增一个MyPerson类
1 | //MyPerson.h |
main.m代码如下
1 | int main(int argc, const char * argv[]) { |

输出
1 | __NSMallocBlock__ |
这里p并没有释放掉,按道理过了120行应该就释放的,其实此时MyPerson *p = [[MyPerson alloc] init];和__strong MyPerson *p = [[MyPerson alloc] init];是等价的
说明__NSMallocBlock__类型的myBlock会对__strong修饰的p对象的引用计数产生影响
再修改main.m函数
此时输出
1 | __NSMallocBlock__ |
发现p正常释放了
说明__NSMallocBlock__类型的myBlock不会对__weak修饰的 p对象的引用计数产生影响
2.2 __NSStackBlock__对对象的引用计数的影响 MRC环境
把项目改成MRC

MyPerson
1 |
|
mian.m如图

此时打印
1 | block类型---__NSStackBlock__ |
发现p正常释放了,此时的myBlock的类型为__NSStackBlock__类型的,
说明__NSStackBlock__类型的myBlock不会对__strong修饰的 p对象的引用计数产生影响
再修改一下main.m的代码如图,新增的代码看红色处

会发现此时只会输出
1 | block类型---__NSMallocBlock__ |
但是p并没有释放
说明__NSMallocBlock__类型的myBlock会对__strong修饰的p对象的引用计数产生影响
2.3 结论
得出以下结论:
当block内部访问了对象类型的auto变量时
如果block是在栈上,将不会对auto变量产生强引用如果block被拷贝到堆上
会调用block内部的copy函数;
copy函数内部会调用_Block_object_assign函数_Block_object_assign函数会根据auto变量的修饰符(strong、weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用如果block从堆上移除
会调用block内部的dispose函数;
dispose函数内部会调用_Block_object_dispose函数_Block_object_dispose函数会自动释放引用的auto变量(release)
三、__block如何做到可以修改变量的
由前面的了解我们知道block要想可以修改变量,那么就不能值捕获,也就是不能放在栈内存中,因为栈内存是的释放无法控制,所以要买放在全局区,要么放在堆区,来看下苹果是放在哪里的。
3.1 __block变量的内存结构
1 | int main(int argc, const char * argv[]) { |
像这样在block的内部直接修改变量是会报错的, 要想修改需要借助__block修饰符
1 | int main(int argc, const char * argv[]) { |
我们看看转成的c++的代码

可以看到和之前没有__block修饰的不同的是这次的p变成了类型为__Block_byref_p_0 *p
再看初始化的地方

一行__block MyPerson *p = [[MyPerson alloc] init];就变成了初识化一个__Block_byref_p_0类型的结构体,然后把该结构体的指针给到myBlock,而我们初始化的那个p则给了__Block_byref_p_0内部的p对象
3.2 __block变量的内存管理
当__block变量在栈上时,不会对指向的对象产生强引用
当block变量被copy到堆时
会调用block变量内部的copy函数,copy函数内部会调用_Block_object_assign函数_Block_object_assign函数会根据所指向对象的修饰符(strong、weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(MRC除外,MRC时不会retain)如果block变量从堆上移除
会调用block变量内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放指向的对象(release)
和没有__block修饰的auto对象变量差不多,只是第二条中对MRC不起作用


这里的Block0相当于myBlock,__block变量就是p,15行的时候变量MyPerson类型的变量p,就变成了指向__Block_byref_p_0类型的指针了,且处于栈中,到了21行结束,由于是arc环境,myBlock就是为右边的blockcopy后的处于堆上了,这是变量p也会被拷贝到堆上,当23执行的时候调用的就是堆上的block,访问的也是堆上的内容,对于block内部的NSLog(@"---%@",p.name);则是结构体p内部的MyPerson类型的p对象
3.3 block的forwarding指针
可以看到__Block_byref_p_0结构如下

有一个__forwarding,在main.m中初始化的时候传的就是__Block_byref_p_0自身的地址,取值的时候也是通过p->__forwarding->p去取值岂不是多此一举?

其实这是不管当__block修饰的结构体变量,处于栈上海市被复制到堆上,都可以访问到同一个p变量
我们把代码改下

此时会输出rose
代码过了21行,myBlock就已经在堆上了,到了23行访问还是栈中的结构体变量,那为何还是打印rose呢,就是因为有了__forwarding指针的作用,保证了此时不管在栈中还是在堆中都可以访问到同一个MyPerson类型的变量p,当__block修饰的变量从栈中copy到堆中的时候发送的事情入下图

可以看出苹果的实现是通过把变量放在堆区的方式来实现修改__block捕获的变量的,也可以看出__block对对象的内存影响还是蛮大的。
(完)