一、block的类型
block既然有isa指针,那就说明block是一个OC对象,而isa也会指向block所属的对象类型。
block有3种类型,可以通过调用class
方法或者isa
指针查看具体类型,最终都是继承自NSBlock
类型,同时NSBlock
又继承自NSObject
(block中的isa本质上就是继承自NSObject的isa)。
__NSGlobalBlock__
(_NSConcreteGlobalBlock
)__NSStackBlock__
(_NSConcreteStackBlock
)__NSMallocBlock__
(_NSConcreteMallocBlock
)
不同类型的block存储的内存区域也不一样。_NSConcreteGlobalBlock
存储在数据段(也叫全局区),_NSConcreteMallocBlock
存储在堆段中,_NSConcreteStackBlock
存放在栈段中。
为了方便研究block的真实类型,我们把ARC关掉。因为ARC会帮助我们处理很多逻辑,导致无法看到block的真实类型。
环境不同会造成block类型有所差异。
3.1. NSGlobalBlock
示例代码:
1 | int b = 20; |
上面示例代码中的block有3种情况:没有访问任何外部变量、访问的是静态局部变量、访问的是全局变量。最终显示block的类型是__NSGlobalBlock__
。
总结:没有访问auto变量的block类型是__NSGlobalBlock__
。
3.2. NSStackBlock
示例代码:
1 | int main(int argc, const char * argv[]) { |
注意:上面的代码在MRC环境下显示block的类型是
__NSStackBlock__
,ARC环境下的block类型是__NSMallocBlock__
,因为访问auto变量的block被一个强指针所引用,会自动进行copy操作。
上面示例代码中的block是__NSStackBlock__
类型,也就是放在栈段中的。
疑问:栈中的block不会被释放么?看下面的示例代码。
1 | void (^block)(void); |
栈中的数据会随着函数调用的结束而释放内存,当执行test函数时,外部的block变量指向test函数中定义的block,而block的执行体本质上是一个被封装的函数地址,而block又被定义在栈中,所以test函数执行结束后,定义的block是会被释放的,外部的block变量指向的就是一块被释放的内存地址。
为了避免栈中的block被释放导致程序错误,我们可以尝试把栈中的block放到堆中,让block成为__NSMallocBlock__
类型。
3.3. NSMallocBlock
只要把__NSStackBlock__
类型的block进行copy
操作,block就会变成__NSMallocBlock__
类型(放到堆段)。
修改test函数中的block:
1 | void test() { |
上面函数中的block进行copy操作后变成了__NSMallocBlock__
类型。
注意:如果是MRC情况下进行的copy,需要手动进行释放操作。ARC环境下会自动释放。
二、block的copy
只有__NSStackBlock__
类型的block进行copy后会变成__NSMallocBlock__
类型,其他类型的block进行copy后基本保持原样不动。
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
- block作为函数返回值时
- 将block赋值给
__strong
指针时 - block作为Cocoa API中方法名含有usingBlock的方法参数时(例如,数组排序的方法)
- block作为GCD API的方法参数时
MRC下block属性的建议写法(只有使用copy修饰符才能让栈上的block被copy到堆上):
1 | @property (copy, nonatomic) void (^block)(void); |
ARC下block属性的建议写法:
1 | @property (strong, nonatomic) void (^block)(void); |
虽然在ARC环境下block使用strong
和copy
进行修饰基本没有区别,但为了统一写法,建议使用copy
修饰符。
2.1. 对象类型的auto变量
当block内部访问了对象类型的auto变量时:
- 如果block是在栈上,将不会对auto变量产生强引用。
- 如果block被拷贝到堆上,会自动调用block内部的copy函数,copy函数内部会调用
_Block_object_assign
函数,_Block_object_assign
函数会根据auto变量的修饰符(__strong
、__weak
、__unsafe_unretained
)做出相应的操作,形成强引用(retain)或者弱引用。 - 如果block从堆上移除,会自动调用block内部的
dispose
函数,dispose
函数内部会调用_Block_object_dispose
函数,_Block_object_dispose
函数会自动释放引用的auto变量(release)。
默认引用:
1 | typedef void (^DBBlock)(void); |
默认情况下或使用__strong
时,block会对person进行强引用,直到block的出了作用域,person才会被释放。主要是因为block被外面的block指针强引用了,导致block暂时不会被释放,直到block所在作用域结束后才会释放block,紧接着person会被释放。
使用__weak
:
1 | typedef void (^DBBlock)(void); |
转C++:
1 | struct __main_block_impl_0 { |
新创建的weakPerson
指针使用__weak
修饰,在block内部引用的person
也是使用weak
修饰的(弱引用),所以出了person
所在作用域后person
就会被释放,因为没有任何指针强引用它,此时执行block就会找不到person
。
注意:如果OC代码含有__weak
,在使用clang转换为C++代码时,可能会遇到以下问题:
1 | cannot create __weak reference in file using manual reference |
解决方案:支持ARC、指定运行时系统版本,比如:
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m |
三、__block修饰符
示例代码:
1 | int age = 10; |
疑问:可以在block执行体内修改外面变量age的值么?
解答:不可以。首先编译器会报错。其次,在block内部是把age传入当做值传入的,和外面的age不是同一个变量(不是同一个内存地址)。
如果要想在block内部修改age的值,可以把变量修改为静态变量,如static int age = 10
,或者把age变为全局变量。因为这样的做法会让block内部的变量和外部变量是同一个内存地址。
但是有时候使用上面的做法会让代码变得难以维护,有其他更好的办法么?有,在变量前面加上修饰符__block
。
1 | __block int age = 10; |
__block
可以用于解决block内部无法修改auto变量值的问题。__block
不能修饰全局变量、静态变量(static)。编译器会将__block
变量包装成一个对象。
1 | // age被包装成对象 |
转成C++后,发现int age
被包装成了__Block_byref_age_0 age
。__Block_byref_age_0
是一个对象,内部有isa和age的真正类型。而且结构体__main_block_impl_0
内部捕获的变量age是包装后的类型__Block_byref_age_0
。
结构体__Block_byref_age_0
中的__forwarding
变量指向的是自己,int age
传入的是原来定义的age的值(即代码中的10)。
疑问:使用__block
修饰后,后面访问的age是结构体__Block_byref_age_0
中的age,还是__main_block_impl_0
中的age?
示例代码:
1 | __block int age = 10; |
通过对比内存地址可以发现,__block
修饰后的age地址和__Block_byref_age_0
结构体中的age地址是同一个。
一个__Block_byref_age_0
占用28个字节,通过每一个变量的偏移地址也可以计算出age的地址(0x7ffeefbff460 + 24 = 0x7ffeefbff460 + 16 + 8 = 0x7ffeefbff478)。
1 | struct __Block_byref_age_0 { |
请注意下方的代码:
1 | NSMutableArray *array = [NSMutableArray array]; |
array需要加上__block
么?不需要,因为这里只是使用了array指针,并没有修改array(就像只访问age变量,但没有修改age的值)。虽然加上__block
也没有问题,但是通过上面的讲解可以看到,能不加__block
就不加,因为block内部会重新创建一个对象,并做一些内存管理的操作。