这一篇我们来研究一下objc的block并回答一下面试中的下列问题:
- 1.block的内部实现,结构体是什么样的
- 2.block是类吗,有哪些类型
- 3.一个int变量被
__block
修饰与否的区别?block的变量截获 - 4.block在修改NSMutableArray,需不需要添加
__block
- 5.怎么进行内存管理的
- 6.block可以用strong修饰吗
- 7.解决循环引用时为什么要用
__strong
、__weak
修饰 - 8.
block
发生copy
时机 - 9.
Block
访问对象类型的auto
变量时,在ARC
和MRC
下有什么区别
在回答所有问题之前我们需要了解一些block背景相关的知识. 如下:
- 如何查看Block的内部实现,也就是说转换成背后真正的c/c++代码的block是什么样的?以及转换格式或者原理等. -关于变量的作用域
Objective-C 转 C++的方法
下面我写了个示例TestClass.m
类其中block代码如下
OC代码:
1 | @interface TestClass () |
经过上述转换操作我们在TestClass.cpp中最下面发现如下代码
C++代码
1 | // @interface TestClass () |
上面的代码生成是通过如下操作:
打开终端,cd到TestClass.m所在文件夹,使用如下命令
1 | clang -rewrite-objc TestClass.m |
就会在当前文件夹内自动生成对应的TestClass.cpp文件
注意: 如果提示clang没有的话 需要安装, 输入如下
1 | brew install clang-format |
通过上述代码我们发现Block的其实是一个结构体类型
底层实现 会根据 __
类名__
方法名_
block_
impl_
下标 (0代表这个方法或者这个类中第0个block 下面如果还有将会 第1个block 第2个…)
1 | struct __类名__方法名_block_impl_下标 |
关于变量的作用域
c语言的函数中可能使用的参数变量种类
- 参数类型
- 自动变量(局部变量)
- 静态变量(静态局部变量)
- 静态全局变量
- 全局变量
由于存储区域特殊,这其中有三种变量是可以在任何时候以任何状态调用的.
- 静态变量
- 静态全局变量
- 全局变量
而其他两种,则是有各自相应的作用域,超过作用域后,会被销毁.
1.block的内部实现,结构体是什么样的
看了上面的背景知识我们来回到一下这个问题
block的内部实现如下:
1 | struct __TestClass__testMethods_block_impl_0 { |
可以看得出来__TestClass__testMethods_block_impl_0
有3个部分组成
- impl 函数指针指向
__TestClass__testMethods_block_impl_0
1 | struct __block_impl { |
- Desc 指向
__TestClass__testMethods_block_impl_0
的Desc指针,用于描述当前这个block的附加信息的,包括结构体的大小等等信息.
1 | static struct __TestClass__testMethods_block_desc_0 { |
__TestClass__testMethods_block_impl_0()
构造函数,也就是该block的具体实现
1 | __TestClass__testMethods_block_impl_0(void *fp, struct __TestClass__testMethods_block_desc_0 *desc, int flags=0) { |
此结构体中
- isa指针保持这所属类的结构体的实例的指针.
struct __TestClass__testMethods_block_impl_0
相当于Objective-C类对象的结构体_NSConcreteStackBlock
相当于Block的结构体实例,也就是说block其实就是Objective-C对于闭包的对象实现
讲到这里block的内部实现你看懂了吗?结构体是什么样的你记住了吗? 其实看着繁琐 细心观察代码会发现还是比较简单的.
2.block是类吗,有哪些类型?
block也算是个类,因为它有isa指针,block.isa的类型包括
- _NSConcreteGlobalBlock 跟全局变量一样,设置在程序的数据区域(.data)中
- _NSConcreteStackBlock栈上(前面讲的都是栈上的 block)
- _NSConcreteMallocBlock 堆上
这个isa可以按位运算
3.一个int变量被 __block
修饰与否的区别?block的变量截获
被__block
修饰与否的区别
用一段示例代码来解答这个问题吧:
1 | __block int a = 10; |
通过__block
修饰int
a
,block体中对这个变量的引用是指针拷贝,它会作为block结构体构造参数传入到结构体中且复制这个变量的指针引用,从而达到可以修改变量的作用.
int
b
没有被__block
修饰,block内部对b
是值copy.所以在block内部修改b
不影响外部b的变化.
block的变量截获
通过如下代码我们来观察要一下变量的捕获
1 | blk_t blk; |
输出打印
1 | block_demo[28963:1629127] array count = 1 |
我们把上面的代码翻译成C++看下
1 | struct __main_block_impl_0 { |
在Objc中,C结构体里不能含有被__strong
修饰的变量,因为编译器不知道应该何时初始化和废弃C结构体。但是Objc的运行时库能够准确把握Block
从栈复制到堆,以及堆上的block被废弃的时机,在实现上是通过__TestClass__testMethods_block_copy_0
函数和__TestClass__testMethods_block_dispose_0
函数进行的
1 | static void __TestClass__testMethods_block_copy_0(struct __TestClass__testMethods_block_impl_0*dst, struct __TestClass__testMethods_block_impl_0*src) { |
_Block_object_assign
相当于retain操作,将对象赋值在对象类型的结构体成员变量中._Block_object_dispose
相当于release操作.
这两个函数调用的时机是在什么时候呢?
函数 | 被调用时机 |
---|---|
__TestClass__testMethods_block_copy_0 |
从栈复制到堆时 |
__TestClass__testMethods_block_dispose_0 |
堆上的Block被废弃时 |
什么时候栈上的Block会被复制到堆呢?
调用block的copy函数时。
Block作为函数返回值返回时。
将Block赋值给附有
__strong
修饰符id类型的类或者Block类型成员变量时。方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时。
什么时候Block被废弃呢?
- 堆上的Block被释放后,谁都不再持有Block时调用dispose函数。
以上就是变量被block捕获的内容
4.block
在修改NSMutableArray
,需不需要添加__block
- 如修改
NSMutableArray
的存储内容的话,是不需要添加__block
修饰的。 - 如修改
NSMutableArray
对象的本身,那必须添加__block
修饰。
5.怎么进行内存管理的?
在上面Block的构造函数__TestClass__testMethods_block_impl_0
中的isa指针指向的是&_NSConcreteStackBlock,它表示当前的Block位于栈区中.
block内存操作 | 存储域/存储位置 | copy操作的影响 |
---|---|---|
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteStackBlock | 栈 | 从栈拷贝到堆 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
全局Block:
_NSConcreteGlobalBlock
的结构体实例设置在程序的数据存储区,所以可以在程序的任意位置通过指针来访问,它的产生条件:- 记述全局变量的地方有block语法时.
- block不截获的自动变量.
以上两个条件只要满足一个就可以产生全局Block. 参考
栈Block:
_NSConcreteStackBlock
在生成Block以后,如果这个Block不是全局Block,那它就是栈Block,生命周期在其所属的变量作用域内.(也就是说如果销毁取决于所属的变量作用域).如果Block变量和__block
变量复制到了堆上以后,则不再会受到变量作用域结束的影响了,因为它变成了堆Block.堆Block:
_NSConcreteMallocBlock
将栈block复制到堆以后,block结构体的isa成员变量变成了_NSConcreteMallocBlock
。
6.block可以用strong修饰吗?
在ARC中可以,因为在ARC环境中的block只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆中的操作.
在MRC中不行,因为要有拷贝过程.如果执行copy用strong的话会crash, strong
是ARC中引入的关键字.如果使用retain相当于忽视了block的copy过程.
7.解决循环引用时为什么要用__strong
、__weak
修饰?
首先因为block捕获变量的时候 结构体构造时传入了self,造成了默认的引用关系,所以一般在block外部对操作对象会加上__weak
,在Block内部使用__strong
修饰符的对象类型的自动变量,那么当Block从栈复制到堆的时候,该对象就会被Block所持有,但是持有的是我们上面加了__weak
所以行程了比消此长的链条,刚好能解决block延迟销毁的时候对外部对象生命周期造成的影响.如果不这样做很容易造成循环引用.
8.block发生copy时机?
在ARC中,编译器将创建在栈中的block会自动拷贝到堆内存中,而block作为方法或函数的参数传递时,编译器不会做copy操作.
调用block的copy函数时。
Block作为函数返回值返回时。
将Block赋值给附有
__strong
修饰符id类型的类或者Block类型成员变量时。方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时。
9.Block访问对象类型的auto变量时,在ARC和MRC下有什么区别?
ARC下会对这个对象强引用,MRC下不会