上篇讲到了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
就是为右边的block
copy后的处于堆
上了,这是变量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
对对象的内存影响还是蛮大的。
(完)