1.block的类型
1.1 全局block
1 | @property (nonatomic , copy) TextBlock textBlock; |
全局block
在编译期就被置于macho
中的数据段。
在
block
内部没有使用外部变量,或者只使用的是静态变量或全局变量时,为__NSGlobalBlock__
。即使声明的block
属性使用了copy
修饰也是如此。
1.2 栈block
1 | int textA = 0; |
在MRC
下,捕获外界变量时,此block
为栈block
,但是ARC
下系统默认使用__strong
修饰且会自动进行copy
成堆block
。故需要__weak
声明.
内部使用了局部变量或者oc属性,但是没有赋值给强引用或者copy修饰的变量,为
__NSStackBlock__
1.3 堆block
1 | int textA = 0; |
在ARC
下,捕获了外界变量(不需要__Block
和手动调用copy
),就会被拷贝到堆区。
内部使用了局部变量或者oc属性,且赋值给强引用或者copy修饰的变量,为
__NSMallocBlock__
总的来说,
block
是什么类型,先看它是否使用了外部变量,没使用就是全局block
,不管是否强引用;使用了就是栈或者堆,这时候在判断block
是否使用了强引用,强引用就是堆,否则就是栈。
1.4 其他的block
这三种block
是系统提供程序员使用的,实际上还有另外三种block
的存在,只是系统自身使用。
libclosure源码中存在如下定义:
1 | /******************** |
总结:一共有6种block,它们都属于NSBlock,只不过只有其中3种是能使用到的
2.循环引用
block
使用不当容易循环引用,导致页面无法被释放。
2.1 循环引用产生
1 | self.block = ^{ |
以上两个代码很明显是第一个会产生循环引用。
因为self
持有了block
,在block
内部又持有了self
,导致了互相持有。第二段代码不存在互相持有的关系。
要想断开互相持有的关系,必定要有一边弱引用。如果是开头弱引用会造成对象提早被置空的尴尬,那只能是尾部的对象弱引用。
是否循环引用,主要就是看是否能画出互相持有的关系图。比如例子中的:self -> block -> self
2.2 循环引用的解决
2.2.1 第一种方式(强弱共舞)
1 | __weak typeof(self) weakSelf = self; |
把后面使用的self
指向weakSelf
,weakSelf
是被添加到全局的弱引用表中,不会对引用计数做+1
处理,当作用域结束时就会被释放。这样就达到目的了。
但只是这样处理是不保险的,如果在block
内部执行的是耗时的方法,那在页面销毁时self
就被释放,也就意味着弱应用表中的weakSelf
被释放,因为它们是指向同一片内存地址,后续依赖weakSelf
的操作自然也就毫无意义了。
正确的做法应该是在block
内部在局部强引用下weakSelf
,防止这种情况的发生。
因为这样操作后,在页面销毁时,self
相当于还是被强持有,不会调用dealloc
释放self
,只有当2秒延时执行后,block
内部的作用域结束,局部变量strongSelf
是在栈空间,被系统回收,强持有结束,才会调用dealloc
释放self
。
1 | __weak typeof(self) weakSelf = self; |
所以最终正确的做法,应该是外部__weak
修饰一下,内部在用__strong
修饰一下。
2.2.2 第二种方式(手动置空)
1 | __block ViewController *vc = self; |
既然系统在weakSelf
作用域结束时会自动置空,那也可以仿造此行为:
- 局部强应用
self
,在不需要时手动将其置空。
2.2.3 第三种方式(参数形式)
1 | self.block = ^(ViewController * vc) { |
前两种方法是持有已经形成,采用对尾部的对象弱引用的思想。而参数形式的方法是直接不让self
被block
持有,自然就没循环引用的可能了。
2.3 哪些页面有循环引用
实际开发中,页面没有及时释放,多半就是因为发生了循环引用。
这里提供一种检查页面是否释放的方式,在基类的dealloc
方法中打印,打印的内容可以夸张些(表情符号之类),易于识别。
1 | - (void)dealloc { |
当发现哪个页面pop
之后却没有打印信息,就找找这个控制器中哪里存在循环引用吧。
总结:推荐使用的处理方式
- 手动置空的方式依赖于程序员的调用,如果忘记调用就存在循环引用,明显这是不可取的
- 参数形式很优雅,但是可能会导致
block
过重,可以使用 - 推荐使用第一种强弱共舞的方式,把对象生命周期交给系统管理是最可靠的,如果觉得写那两句代码很烦,可以用宏简化下
3.Block的clang分析
block
的本质是什么,为什么能自动捕获变量,为什么需要调用等问题是我们关心的,这一切都需要看看block
的底层结构。
block
语法看起来好像很特别,其实它实际上是被做为普通的c
语言源代码处理的。通过clang
可以把block
语法的源代码转化为c++
源代码。虽说是c++
源代码,但底层也是struct
结构体,本质上还是c
的源代码。
转换如下:
1 | clang -rewrite-objc 源文件名称 -o 目标文件名称.cpp |
如果头文件有用到系统库,需要指定下路径:
1 | clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk 源文件名称 -o 目标文件名称.cpp |
编写一个极为简单的block.c
文件,
1 | int main(){ |
转换后得到block.cpp
,截取其中重要内容并简化后得到:
1.block的本质是什么
block
被转化为__main_block_impl_0
类型的结构体,内部还有__block_impl
类型的结构体和__main_block_desc_0
类型的结构体做为属性。
所以block
的本质就是个结构体,是个对象,这也是为什么它能被%@
打印的原因。
2.block为什么能捕获自动变量
看__main_block_impl_0
内部的结构和main
函数源代码。
block
在使用自动变量时,把自动变量textA
声明为自身__main_block_impl_0
结构体内的一个属性,并在__main_block_impl_0
构造函数传入textA
将其赋值,所以能捕获自动变量。
3.block为什么需要调用
看block
的实现函数,__main_block_impl_0
内部的构造函数。
block
的实现在底层被隐式的声明为__main_block_func_0
函数,入参是__main_block_impl_0
类型的__cself
,也就是自身。
1 | __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _textA, int flags=0) : textA(_textA) { |
__main_block_impl_0
构造函数中,把__main_block_func_0
当做入参,保存在__block_impl
结构体内的FuncPtr
属性。
所以需要外界调用block
,也就是调用被保存的在自身的FuncPtr
。
4.block为什么不能直接修改自动变量
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
在使用自动变量时,把结构体内部的textA
值以__cself->textA
形式赋值给新声明的textA
中,再对这个新声明的textA
做操作。
如果直接修改自动变量,相当于修改的是新声明的这个textA
,可外界希望修改的是原textA
,显然这毫无意义,且会产生代码歧义
,编译器不知道你要修改哪个textA
,产生报错。
5.__block为什么能修改自动变量
增加__block
修饰后,继续使用clang
转换成新的.cpp
文件。
转换后的代码增加了不少,和原先最主要的区别在于:
textA
被声明成__Block_byref_textA_0
类型的结构体,不再是单纯的int
- 新增了
__main_block_copy_0
和__main_block_dispose_0
函数,它们分别调用了_Block_object_assign
和_Block_object_dispose
(这两个函数后续分析)
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
在调用函数(图片写成结构体了)__main_block_func_0
时,textA
是使用__cself->textA
方式获取到的被保存在__main_block_impl_0
结构体内的textA
指针。也就是原先传入的textA
的,后续对textA
的操作,就是对外界textA
的操作。所以使用__block
修饰后的自动变量可以被修改。
1 | __Block_byref_textA_0 * __forwarding; |
使用
__block
后,会生产__Block_byref
类型的结构体,并且在结构体内部保存了指向自身的__forwarding
指针,外界修改时,就是通过指针获取原自动变量进行修改
6.栈block何时拷贝到堆区
可以看到,__main_block_impl_0
构造函数内的block
的isa
被定义为_NSConcreteStackBlock
,可是在第一节block的类型
中说明ARC
捕获自动变量后为_NSConcreteMallocBlock
。
那么可以肯定的是,一定是存在某种操作,使block
产生从栈到堆的改变。
在block
汇编源码中的第一次跳转objc_retainBlock
时,打个断点,然后读取下x0寄存器
的值
1 | register read x0 |
证实此时的确是个_NSConcreteStackBlock
,单步执行跟进去,
发现又跳转到_Block_copy
这个函数,这个关键函数记住,后续还要分析。
在libObjc
源码中搜索,也能确认objc_retainBlock
就是调用了_Block_copy
。
1 | id objc_retainBlock(id x) { |
进到_Block_copy
的汇编,在最后准备return
的地方打上断点,再次读取x0寄存器
的值
证实在调用_Block_copy
前,block
还是_NSConcreteStackBlock
,调用之后变成了_NSConcreteMallocBlock
。
block
在构造函数初始化阶段确实是_NSConcreteStackBlock
类型,只不过因为是ARC
(未使用__block
时),在合适的时机,系统会自动做拷贝到堆的操作,导致结果打印的类型却是_NSConcreteMallocBlock
。
这里两次都读取x0寄存器的原因是不同的。第一次是因为做为函数的第一个参数,block被存放在x0寄存器;第二次是因为block做为返回值被存放在x0寄存器。
有以下的情况栈上的block
都会被拷贝到堆上:
- 调用
block
的copy
实例方法时 block
做为函数返回值返回时- 将
block
赋值给带有__strong
修饰符id
类型的类或block
类型成员变量时 - 在方法名中带有
usingBlock
的系统方法时 - 使用
GCD
的API
传递的block
不过这些情况都是因为调用了_Block_copy
函数。
需要注意的是:并非所有的
block
执行了_Block_copy
都会变成堆block
,比如全局block
就不行。只有当是栈block
时,这些情况才能成立。
7.block的签名是什么
在block
的打印信息中可以看到:
1 | signature: "v8@?0" |
其中,v
代表返回值是void
,8
代表8
字节,而@?
就代表block
类型。
可以使用代码验证下:
1 | [NSMethodSignature signatureWithObjCTypes:"@?"] |
1 | flags {isObject,isBlock} |
打印信息证明@?
是isBlock
,也是isObject
。
总结:
block
本质是个结构体,是个对象block
内部使用自动变量时,会把自动变量声明为自身结构体的属性,产生捕获的功能block
的实现被保存在自身结构体中,需要外界调用- 不能直接修改自动变量是因为:
block
内部会生成新的变量,这个变量和捕获的自动变量不是同一个(指向的地址不同,只是值相同), - 而使用
__block
修饰后,会生成相应的结构体,保存原始变量的指针,修改的就是原始的变量 - 调用
_Block_copy
后,栈block
被拷贝到堆区,在堆区生成对应的block
(必须是栈)
有趣的冷门小知识:
我们常说,block
是带有自动变量的匿名函数。
但经过clang
的分析,发现在.cpp
底层中block
其实也是有函数名称的,匿名是指外界不需要声明函数名。
并且它的名称有一定的规律:
block在函数中时格式为:
__函数名_block_impl_此block是函数中的第几个block
比如:__main_block_impl_0
block在方法中时格式为:
__文件名_方法名_block_impl_此block是方法中的第几个block
比如:__ViewController__addblcok__block_impl_0
1 | 多层嵌套时可以通过最后的参数查看是第几个block,这个小知识是本人测试所得,不敢保证全部情况试用。 |
4.block的底层原理
经过clang
分析后,能大概明白block
在底层是以什么样的方式存在的,但是它还不能说明一些东西。
比如:_Block_copy
做了什么,__block
是怎么实现的,_Block_object_assign
和_Block_object_dispose
又是什么。
这一切只能打开block
的源码libclosure探索了。
这源码还是比较好理解的,内容也较少,推荐阅读
一共只有四个文件,且只需要关注Block_private.h
和runtime.cpp
这两个文件即可。
Block_private.h
:主要是block
相关的结构体声明和枚举值的定义
runtime.cpp
:具体的实现
试想:
block
的结构体在.cpp
中命名是动态的,可真实的底层结构体命名不可能是动态的,一定有个与之匹配固定的结构体名称。
来到Block_private.h
寻找,果然存在和.cpp
中相同的结构体,只是名称不同。
4.1 block的结构体Block_layout
block
真实的结构体如下:
1 | struct Block_layout { |
isa
:block
的类型指向,类似对象中的isa
flags
:标志位。且因为flags
影响block
的诸多操作,需要谨慎读取。所以使用volatile
关键字以确保本条指令不会因编译器的优化而省略,且要求每次直接读值reserved
:系统保留字段,暂不使用invoke
:保存block
的实现部分Block_descriptor_1
:block
的大小信息,必有的Block_descriptor_2
:block
是否有copy
和dispose
函数,可选的Block_descriptor_3
:block
的签名和拓展,可选的
4.2 _Block_copy分析
之前分析发现Block_copy
是个重要函数,现在来跟进分析下。
内部只是根据标志位flags
做对应的操作
② BLOCK_IS_GLOBAL
如果是GLOBALBLOCK
直接返回,这证明了调用Block_copy
时,并不是都会被拷贝到堆上
③ // Its a stack block. Make a copy.
系统给出的注释,说明处理栈block
。
调用malloc
在堆上开辟空间,大小就是传入block
的大小,把原block
内存上的数据全部复制到新开辟的block
上,然后设置新block
的一些属性,最后把isa
置为_NSConcreteMallocBlock
。其中了调用_Block_call_copy_helper
会做拷贝成员变量的工作,并且内部调用_Block_object_assign
① BLOCK_NEEDS_FREE
不确定此标志位的意思,但是反推下。已经有栈block
和全局block
的处理了,那它肯定包含堆block
的处理。
1 | static int32_t latching_incr_int(volatile int32_t *where) { |
只是对block
的引用计数做处理。
_Block_copy
所做的事,以下表格基本可以说明:
block的类型 | 原blcok存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock |
栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock |
数据区 | 什么也不做 |
_NSConcreteMallocBlock |
堆 | 引用计数增加 |
4.3 _Block_object_assign分析
用__block
修饰一个对象,然后重新生成.cpp
。
发现__block
会生成的对应结构体
而源码中也给出了Block_byref
的结构体
1 | struct Block_byref { |
isa
:可能是结构体的类型,但是源码中赋值总是NULL
forwarding
:自身指向的指针flags
:标志位size
:大小byref_keep
:__Block_byref_id_object_copy_131
函数,也就是_Block_object_assign
byref_destroy
:__Block_byref_id_object_dispose_131
函数,也就是_Block_object_dispose
__block
对应结构体的构造函数
搜索构造函数中的第五个参数,
1 | static void __Block_byref_id_object_copy_131(void *dst, void *src) { |
发现直接调用了_Block_object_assign
函数。
来到源码,
第一句是重点代码,但很容易被忽略:
1 | const void **dest = (const void **)destArg; |
block
捕获变量时,使用二级指针指向真正的目标指针,这是block
能使用__weak
解决循环引用的关键所在。
不同的flags
代表传入的是不同类型:
BLOCK_FIELD_IS_OBJECT
: 表示是一个对象BLOCK_FIELD_IS_BLOCK
:表示是一个block
BLOCK_FIELD_IS_BYREF
:表示是一个byref
,一个被__block
修饰的变量BLOCK_FIELD_IS_WEAK
:__block 变量还被 __weak 修饰时
① 如果是对象类型,调用_Block_retain_object
1 | static void _Block_retain_object_default(const void *ptr __unused) { } |
可是_Block_retain_object
是个空实现,其实就是直接以指针的形式赋值*dest = object
这是因为,对象的引用计数是由ARC
管理的,不需要block
插手,只要通过指针获取即可。
② 如果是block
类型,调用_Block_copy
(已分析)
③ 如果是被__block
修饰,调用_Block_byref_copy
(新函数,下面分析)
④和⑤ 如果是其他类型,也是直接指针赋值即可。
大概可以看得出来,_Block_object_assign
函数是根据捕获自动变量的类型做对应的内存管理。
至于为什么入参是硬编码+40
,
从__block
生成的结构体可以略知一二,
捕获的自动变量在结构体的偏移值为40
字节,+40
即可得到此自动变量。
4.3.1 捕获的变量类型
当用__weak
修饰变量时:
当用__strong
修饰变量时:
block捕获变量时还有一个特点:遇到强引用捕获的就是强引用,遇到弱引用捕获的就是弱引用。意味着在block结构体内声明的属性类型和修饰符与捕获的变量一致。而这会导致block捕获变量后引用计数产生区别。
4.4 _Block_byref_copy分析
当使用__block
修饰,会调用_Block_byref_copy
函数。
① 新生成一个Block_byref
类型的结构体,并赋初始值,其中这两句代码至关重要
1 | copy->forwarding = copy; |
堆上的结构体的forwarding
指向自身,栈上的结构体的forwarding
指向堆上
通过该功能,无论是在block
语法中,还是block
语法外使用__block
变量,还是__block
变量配置在栈上还是堆上,都可以顺利访问到同一个__block
变量。
这也就说明了,为什么使用__block
修饰的变量具有修改能力。
② 如果结构体的标志位为BLOCK_BYREF_HAS_COPY_DISPOSE
,则向Block_byref
结构体的byref_keep
和byref_destroy
赋值(也就是copy
和DISPOSE
函数),然后调用
1 | (*src2->byref_keep)(copy, src); |
其实也就是调用_Block_object_assign
,对捕获的变量的内存进行操作。
③ 如果__block
结构体本身已在堆上,直接对引用计数操作即可。
4.5 _Block_object_dispose分析
copy
函数和dispose
函数是堆block
生命周期的开始和结束,他们对应的调用时机:
函数 | 调用时机 |
---|---|
copy函数 | 栈上的block复制到堆时 |
dispose函数 | 堆上的block被废弃时 |
按释放顺序依次说明:
③ 调用_Block_release_object
释放捕获的自动变量,不过其也是个空实现,变量的释放也是ARC管理的
① 调用_Block_byref_release
减少引用计数
1 | static void _Block_byref_release(const void *arg) { |
通过结构体内部指向自身的forwarding
指针找到自身(如果byref
已经被拷贝,则取得是堆上的byref
,否则是栈上的,栈上的不需要 release,也没有引用计数),如果是堆则减少引用计数,如果引用计数减到了0调用free
释放(因为如果之前已经在堆上调用_Block_byref_copy
只是引用计数加1,调用_Block_byref_release
时也要一次一次减1)。
② 调用_Block_release
减少引用计数
- 2.1
block
在堆上,才需要release
,在全局区和栈区直接返回. - 2.2 引用计数减1,如果引用计数减到了0,调用
free
释放block
类似于拷贝时的一层层拷贝,释放也是一层层释放。
总结:
block
从栈复制到堆上时,先复制自身到堆,在堆上生成对应的__block
结构体(如果有使用__block
),在拷贝捕获的自动变量。
这个过程相当于三层拷贝对应三个函数,block
自身拷贝(_Block_copy
函数),__block
拷贝(_Block_byref_copy
函数),自动变量拷贝(_Block_object_assign
函数)
而释放过程亦是如此。_Block_release_object
处理捕获的自动变量,_Block_byref_release
处理__block
对应的结构体,__Block_release
处理block
自身。
写在后面
block
的面试题,首先需要判断block
的类型,因为作用域对它的影响是很大的,然后搞清楚不同作用域的block
生命周期和block
的函数处理,以及捕获变量时是啥捕啥的原理。