在入行做 iOS 开发没多久的时候,感觉 block 挺神秘的,当时在跟同事交流的时候一直称呼它为 代码块
,想当然的把它理解成为是一个匿名函数。但是随着技术能力的增长,以及大神们在博客中无私的分享,对 block 渐渐有了更加深入的了解,也算是走出了误区。
什么是 Block,Block 的本质是什么?
block 是封装了函数调用以及函数调用环境的 OC 对象,派生自 NSBlock,它内部也有 isa 指针。
Block捕获变量
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制。
为什么局部变量需要捕获?
考虑作用域的问题,需要跨函数访问,就需要捕获。
block里访问self是否会捕获?
会,self是当调用block函数的参数,参数是局部变量,self指向调用者。
block里访问成员变量是否会捕获?
会,成员变量的访问其实是self->xx
,先捕获self,再通过self访问里面的成员变量。
block对auto和static变量捕获有什么差异?
auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可。【auto变量block访问方式是值传递(如果是普通类型,则进行值拷贝,如果是对象类型,会使其引用计数加1),static变量block访问方式是指针传递】
block对全局变量的捕获方式是?
block不需要对全局变量捕获,都是直接采用取值的。
Block的类型
block有哪几种类型?
block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。
- NSGlobalBlock ( _NSConcreteGlobalBlock ) 在数据区
- NSStackBlock ( _NSConcreteStackBlock ) 在栈区
- NSMallocBlock ( _NSConcreteMallocBlock ) 在堆区
iOS 的内存布局 由地地址向高地址 依次为:
代码区(.text):存放代码二进制文件。
数据区(.data):存放变量数据。
堆区:由程序员申请并释放(直接或间接调用 alloc 函数开辟内存)。
栈区:普通局部变量,由系统管理释放(编译时已确定),一般是出了作用域自动释放。
如何判断 block 是哪种类型?
- 没有访问 auto 变量的 block 是
__NSGlobalBlock__
,放在数据段(全局变量)。 - 访问了 auto 变量的 block 是
__NSStackBlock__
。 [__NSStackBlock__ copy]
操作就变成了__NSMallocBlock__
。
这里有一个示例:
1 | int age = 1; |
通过上面的示例,也验证了前面的说法。
但是有一个问题,示例代码中的 block2 和 block3 应该是一样的才对啊,为什么 block2 在堆区,而 block3 在栈区呢?
原来,在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上。
在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上的几种情况?
block 作为函数返回值时
将 block 赋值给
__strong
指针时block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
block 作为 GCD API 的方法参数时
为了验证第二点,我们特地写个 Demo 来看一下:
1 | int age = 1; |
从 Demo 中可以看出,block2 和 block3 的区别就在于 block2 有一个 __strong
的指针引用,正因为此,block2 的内存地址与 block3 完全不在同一个区间。
博客原文作者也注明了使用 block 作为属性时,应该使用什么关键词进行描述。
MRC 下 block 属性的建议写法@property (copy, nonatomic) void (^block)(void);
ARC 下 block 属性的建议写法@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
这里应该才参考第二点,将 block 赋值给 __strong 指针时,其实已经将 block 拷贝到堆区了,再用 copy 或者是 strong 都是对其引用计数 +1,所以 ARC 下并没有严格的限制。如果大家在使用过程中遇到了什么问题,欢迎一起讨论一下。
对每种类型 block 调用 copy 操作后是什么结果?
__NSGlobalBlock__
调用copy操作后,什么也不做。__NSStackBlock__
调用copy操作后:从栈复制到堆;副本存储位置是堆。__NSMallocBlock__
调用copy操作后:引用计数增加;副本存储位置是堆。
对象类型的 auto 变量
无论 MRC 还是 ARC,栈空间上的 block,不会持有对象;堆空间的 block,会持有对象。
当 block 内部访问了对象类型的 auto 变量时,是否会强引用?
答案:分情况讨论,分为栈 block 和堆 block
栈 block
a) 如果 block 是在栈上,将不会对 auto 变量产生强引用
b) 栈上的 block 随时会被销毁,也没必要去强引用其他对象
堆 block
如果 block 被拷贝到堆上:
a) 会调用 block 内部的 copy 函数
b) copy 函数内部会调用 _Block_object_assign 函数
c) _Block_object_assign 函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained
)做出相应的操作,形成强引用(retain)或者弱引用如果 block 从堆上移除
a) 会调用 block 内部的 dispose 函数
b) dispose 函数内部会调用 _Block_object_dispose 函数
c) _Block_object_dispose 函数会自动释放引用的 auto 变量(release)
正确答案:
- 如果 block 在
栈
空间,不管外部变量是强引用还是弱引用,block 都会弱引用访问对象。 - 如果 block 在
堆
空间,如果外部强引用,block 内部也是强引用;如果外部弱引用,block 内部也是弱引用。
这里有一个疑问:
上面的结论是根据别人博客内容以及理论推导的,但是我自己进行 Demo 演练的时候,却发现栈区的 block 似乎也会对 auto 类型的对象进行强引用。
1 | Person *p = [Person new]; |
这里 block 引用了 auto 变量,同时没有触发拷贝到堆空间的条件,实际打印出来的内存地址也确实在栈区。但是 auto 类型的对象 p,打印结果显示,block 引用后,p 的引用计数确实增加了1,不知道是我的理解有误,还是这个结论是不正确的,希望以后能够解开这个谜团,也希望大神看到后能给一些指导。
参考文章
本文只是从本人的一些疑惑点来讨论 block 的一些知识点。参考原文中对 Block 的剖析更加详细,大家可以认真研读一下。
传送门:iOS-Block本质