1.block
的内部实现,结构体是什么样的
block
本质上是一个OC对象,它内部也有isa指针,这个对象封装了函数调用地址以及函数调用环境(函数参数、返回值、捕获的外部变量等)。当我们定义一个block
,在编译后它的底层存储结构是怎样的呢?
下面我们来看一个例子,定义了一个block,并在block里面访问量block外面的变量age,它底层存储结构如下图所示,block底层就是一个结构体__main_block_impl_0
。
impl->isa
:就是isa指针,可见它就是一个OC对象。impl->FuncPtr
:是一个函数指针,也就是底层将block中要执行的代码封装成了一个函数,然后用这个指针指向那个函数。Desc->Block_size
:block占用的内存大小。age
:捕获的外部变量age,可见block会捕获外部变量并将其存储在block的底层结构体中。
当我们调用block时(block()
),实际上就是通过函数指针FuncPtr
找到封装的函数并将block的地址作为参数传给这个函数进行执行,把block传给函数是因为函数执行中需要用到的某些数据是存在block的结构体中的(比如捕获的外部变量)。如果定义的是带参数的block,调用block时是将block地址和block的参数一起传给封装好的函数。
2.block是类吗,有哪些类型
- block也算是个类,因为它有isa指针,block.isa的类型包括(这个isa可以按位运算)
- _NSConcreteGlobalBlock 跟全局变量一样,设置在程序的数据区域(.data)中
- _NSConcreteStackBlock栈上(前面讲的都是栈上的 block)
- _NSConcreteMallocBlock 堆上
3.一个int
变量被 __block
修饰与否的区别?block的变量截获
- 通过
__block
修饰int
a
,block体中对这个变量的引用是指针拷贝,它会作为block结构体构造参数传入到结构体中且复制这个变量的指针引用,从而达到可以修改变量的作用. int
b
没有被__block
修饰,block内部对b
是值copy.所以在block内部修改b
不- 小结:
- 全局变量--不会捕获,是直接访问。
- 静态局部变量--是捕获变量地址。
- 普通局部变量--是捕获变量的值
age
用__block
修饰后,在block的结构体中变成了__Block_byref_age_0 *age;
,而__Block_byref_age_0
是个结构体,里面有个成员int age;
,这个才是真正捕获到的外部变量age
,实际上外部的age
的地址也是指向这里的,所以不管是外面还是block里面修改age
时其实都是通过地址找到这里来修改的。- __block修饰变量的内存管理:
__block
不管是修饰基础数据类型还是修饰对象数据类型,底层都是将它包装成一个对象(我这里取个名字叫__blockObj
),然后block结构体中有个指针指向__blockObj
- 当block在栈上时,block内部并不会对
__blockObj
产生强引用。 - 当block调用
copy
函数从栈拷贝到堆中时,它同时会将__blockObj
也拷贝到堆上,并对__blockObj
产生强引用。 - 当block从堆中移除时,会调用block内部的
dispose
函数,dispose
函数内部又会调用_Block_object_dispose
函数来释放__blockObj
。
- 当block在栈上时,block内部并不会对
4.block
在修改NSMutableArray
,需不需要添加__block
- 如修改
NSMutableArray
的存储内容的话,是不需要添加__block
修饰的。 - 如修改
NSMutableArray
对象的本身,那必须添加__block
修饰。
5.怎么进行内存管理的
- 全局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
下有什么区别
- block对对象类型和对基本数据类型变量的捕获是不一样的,对象类型的变量涉及到强引用和弱引用的问题
- 如果block是在栈上,不管捕获的对象时强指针还是弱指针,block内部都不会对这个对象产生强引用
- 当block被拷贝到堆上时是调用的
copy
函数,copy
函数内部会调用_Block_object_assign
函数,_Block_object_assign
函数就会根据__strong
、__weak
、__unsafe_unretained
这3个关键字来进行操作。- 如果关键字是
__strong
,那block内部就会对这个对象进行一次retain
操作,引用计数+1,也就是block会强引用这个对象。也正是这个原因,导致在使用block时很容易造成循环引用 - 如果关键字是
__weak
或__unsafe_unretained
,那block对这个对象是弱引用,不会造成循环引用。所以我们通常在block外面定义一个__weak
或__unsafe_unretained
修饰的弱指针指向对象,然后在block内部使用这个弱指针来解决循环引用的问题。
- 如果关键字是
- block从堆上移除时,则会调用block内部的
dispose
函数,dispose
函数内部调用_Block_object_dispose
函数会自动释放强引用的变量。