一直使用Block,只是把它当做一个类似于匿名代码段的东西来使用,最近根据网上的高人的脚步深入的学习了下,做一下学习的笔记~
首先记录一个Block的语法的网站(域名屌屌的) http://fuckingblocksyntax.com
根据高人们的脚步得知,要想了解Block的实现机制,可以通过高大上的clang来进行,将objc的代码重写为C++的格式,然后去学习Block的原理,重写的方法是
clang -rewrite-objc xxx.m
首先从最简单的入手,写一个最简单的Block
1 |
|
然后用clang进行rewrite,在生成的cpp中可以看到如下的结构
1 |
|
来详细的分析下cpp中被重写的Block结构:
Block会被重写为struct,Block的真正实现会被放到一个函数中,Block的struct中保存着该函数的指针
blockWithPrintf_block_impl_0是一个结构体,从重写的blockWithPrintf()中可以看出,这也是Block被重写后的真正面目;包含两个成员变量和一个构造函数,第一个成员变量是block_impl结构体,第二个成员变量是__blockWithPrintf_block_desc_0结构体;
block_impl是一个结构体,第一个成员是一个void *isa,这和objc_class有几分相似,可以说Block也是一个object,可以向Block发送copy或retain/release等消息;objc_class中的isa被指向实例的classObject,而在blockWithPrintf_block_impl_0的构造函数中,isa被指向了_NSConcreteStackBlock,这里稍后总结;成员变量FuncPtr则指向真正的Block实现
__blockWithPrintf_block_desc_0中有用的是Block_size,用来保存struct的大小
blockWithPrintf_block_func_0中是Block中的代码被重写成的函数,该函数的第一个参数为blockWithLocal_block_impl_0结构体指针
然后写一个访问局部变量的Block
1 | void blockWithLocal() |
然后用clang进行rewrite后
1 | struct __blockWithLocal_block_impl_0 { |
来详细的分析下cpp中被重写的Block结构:
__blockWithLocal_block_impl_0中增加了一个变量localVar(和引用的局部变量同名),对引用的局部变量进行了一次只读的浅拷贝,所以在Block中是不可以修改其值的
因为blockWithLocal_block_impl_0的大小会发生改变,所以每次都需要通过blockWithLocal_block_desc_0去重新计算
static变量和全局变量在__blockWithLocal_block_impl_0中是其内存地址的只读浅拷贝,因而可以直接在Block内进行修改(类似于int const a,a不可修改,但可以修改a)
为了方便对比,在上一个的基础上,给localVar加上__block前缀,同时在Block内对localVar进行赋值操作,然后rewrite
1 | struct __Block_byref_localVar_0 { |
来详细的分析下cpp中被重写的Block结构:
被block修饰的变量会被重写成一个Block_byref_localVar_0结构体,__Block_byref_localVar_0的第一个成员也是一个void *isa,不过在初始化的时候被置空了;最后一个成员保存着实际的值
blockWithLocal_block_impl_0中增加的是Block_byref_localVar_0指针,这样就可以达到修改局部变量的目的,相对应的就是构造函数的变化,但isa依旧是指向的_NSConcreteStackBlock
blockWithLocal_block_desc_0中增加了copy和dispose指针,通过构造函数,可以看出他们分别对应blockWithLocal_block_copy_0和blockWithLocal_block_dispose_0方法,通过这两个方法,可以去管理引用的Block_byref_localVar_0结构体的内存
Block与内存相关的问题
关于NSStackBlock和NSMallocBlock以及NSGlobalBlock,之前说到Block也是一个Object,可以在objc中通过class去访问isa的值
首先定义一个Block
1 | typedef void (^SimpleBlock)(); |
接下来在MRC环境下去试验,声明一个不引用局部变量的Block
1 | SimpleBlock aBlock = ^{}; |
通过上面的Log输出可以看出:
不引用局部变量的Block的类型为NSGlobalBlock
对NSGlobalBlock类型的Block发送retain、release消息不会改变Block的retainCount(Log_1处执行release后依旧可以打印出Block信息)
对NSGlobalBlock类型的Block发送copy消息不会产生新的Block,也不会改变Block类型
声明一个引用局部变量的Block
1 | NSMutableString *aObject = [[NSMutableString alloc] initWithString:@"OriginString"]; |
引用局部变量的Block类型为NSStackBlock,不会增加局部变量的retainCount
对NSStackBlock类型的Block发送retain、release消息不会改变retainCount(Log_1处执行release后依旧可以打印出Block信息)
对NSStackBlock类型的Block发送copy消息,会把Block复制到堆中产生新的Block,堆中的Block类型为NSMallocBlock(Log_2处),同时也会将引用的局部变量copy到堆中
3.1.如之前说到的,如果是int、struct等基本类型,会在堆的Block中产生一个只读拷贝,在Block外对引用的局部变量进行修改,不会影响Block内的制度拷贝的值
3.2.如果是一个Objcet类型,堆的Block中保存的只是该实例对象的指针的只读拷贝(相当于NSObject const a,a不可变,但可以修改a),如果该实例对象不在堆中,则会将该实例对象copy到堆上,如果该实例对象本身就在堆中,则增加retainCount(Log_4处)
对NSMallocBlock类型的Block发送retain、release消息不会改变retainCount,但通过下面的BAD_ACCESS可以看出(Log_3处),虽然retainCount不会改变,但NSMallocBlock内部还是有一个值保存着引用计数,起到了类似retainCount的作用(http://www.cocoawithlove.com/2009/10/how-blocks-are-implemented-and.html 这篇文章说是通过reserved值保存的,没验证过)
对NSMallocBlock类型的Block发送copy消息,因为Block已经在堆中,所以不会再次进行copy,也就不会改变retainCount,通过下面的BAD_ACCESS可以看出(Log_6处),也会改变NSMallocBlock内部的引用计数的值;同时,堆Blcok不会再次进行copy,并且堆Block引用的实例对象肯定是在堆中的,这也就不会增加实例对象的retainCount、不会对基本类型进行只读拷贝(Log_5、Log_6处)
另外,如果引用被block修饰的变量,如前面说到的,block变量会被保存在Block_byref_localVar_0中,则NSStackBlock__执行copy生成NSMallocBlock时,进行只读拷贝的将是Block_byref_localVar_0指针,因此并不会retain一次block变量
在ARC环境下,由于ARC在编译时对Block做了很多的处理与优化,所以不能完全遵循MRC下的规律,需要注意一些其他问题
ARC环境下,所有的NSStackBlock类型Block都会被转移到堆上,成为NSMallocBlock类型
之前说到MRC下Block进行copy不会增加block变量的引用计数,但是在ARC下,Block进行copy后会retain一次block变量,需要使用weak或者unsafe_unretaed避免循环引用
Block在作为函数返回值时,会被copy;作为参数传递时不会被copy
Block中引用self时,需要用weak修改self避免retain cycle;当引用self中成员变量的时候,self会被retain,需要将self修改为weak,避免retain cycle
1 | - (SimpleBlock)weakSelfBlock |
- 第四条的情况下,如果在多线程条件下,可以用weak–strong dance避免retain cycle
1 | - (SimpleBlock)weakStrongDance |