Objective-C中的block是一种特别的结构,block与普通的instance不同的地方,不止更在于它的语法,更在于它的不同表现以及内存分配。
虽然block对于Objective-C来说已经早不新鲜了,但现如今很多博文讲述的block行为是基于MRC的,这与ARC下的表现是不同的。现代Objective-C也应该渐渐淘汰MRC和GC(其实GC已经淘汰了,在macOS Sierra已经无法使用,iOS从来不支持
)本文所提及情况均限于ARC
ARC下不同类型的block表现
很多博文都提到过,block通过llvm编译后,会生成对应的三种Class的实例变量,分别是:NSStackBlock
、NSGlobalBlock
、NSMallocBlock
,分配区域分别位于进程的栈,TEXT段,堆。ARC下为了简化block的内存管理,以及性能优化,llvm会对不同情形下的block进行不同的类型变化,
1 | int a = 1; |
猜猜输出是什么?
1 | __NSGlobalBlock__ |
从这里也可以总结出规律:
- 如果block不捕获任何外部变量(包括了
Primitives
(基本类型)),既没有对外部任何对象retain,也没有copy基本类型,那么这个block不存在任何内存泄漏的风险,也不需要引用计数,所以类型为__NSGlobalBlock__
- 如果block捕获了外部变量(包括基本类型),但并没有被任何对象所引用(retian),而是直接被用于直接执行或者发送消息,那么它不会有任何引用计数问题,类型为
__NSStackBlock__
。由于位于栈区,这个block在函数返回后将被销毁,不过请放心,在ARC下,因为没有被任何对象引用,所以它始终是安全的(一旦之后被引用,立即会由Runtime负责通过Block_copy()
转换为__NSMallocBlock__
) - 通常情况下,如果block捕获了外部变量,且只要有对象持有(注意,无论引用是
__strong
还是__weak
还是__copy
,参考llvm-blocks),都会通过Runtime的Block_copy()
和Block_release()
,由编译器自动地将原本在栈的block拷贝到堆上,因此会像普通对象一样,交由ARC自动管理引用计数
__block的影响
__block
的关键字的作用大家都知道,默认情况下block是无法修改外部实例变量的(能读,也就是捕获),而经过__block修饰的实例变量可以通过block外修改。
但是的表现是否单纯可以概括为”捕获了一份实例变量到堆上,并修改了原来的引用”呢?
看看这个:
1 | __block NSMutableArray *array1 = [[NSMutableArray alloc] initWithCapacity:10]; |
输出结果:
1 | object addr: 0x7ffe8a60d800, pointer addr: 0x7fff5548c988 |
从中可以看出,由于Objective-C所有的实例变量都分配在堆上,而对于ARC下的block,如果不加__block
关键字,那么在捕获后,外部的引用(Objective-C的指针,其实就是一个对象的引用,类似于Java)不会受到任何影响(只是对引用进行了拷贝)。而如果使用__block
的话,那么会将原来的引用修改(注意到地址值的变化)。
当然,实际上的__block
捕获的实例变量,会额外追加一些字段,用于Runtime进行内存管理和处理引用(参考block- marked-variables)
1 | struct _block_byref_foo { |
因此可以知道,__block
是好,但每个捕获变量都会多出至少20字节……虽然llvm的优化能力很好,盲目的标记__block
也并不是一件好事(还会增加Runtime的开销和少量内存开销)
其他
顺便一说,最近在补iOS开发基础知识,发现这个《招聘一个靠谱的iOS》答案-38题的说法是有问题的,不存在什么”block的变量copy到堆区”,只要你的block被引用,那么这个block一定在堆区,而且并不是所谓的”加入__block后才copy”,,真正变化的,只是那个引用的地址变了罢了。大家希望看到后不要被误导……
PS:
- 如果想了解更多Runtime实现block的方式和具体block的内存分布,可以参考llvm-block
- 如果你真的需要MRC,可以参考这篇文章,附带一个小题目测试一下你的掌握情况MRC-block-quiz