本质
Block 的本质是一个 Objective-C 对象, 它内部也拥有一个 isa 指针.
Block 是封装了函数及其调用环境的 Objective-C 对象
底层数据结构
一个简单示例:
- int main(int argc, const char * argv[]) {
- void (^block)(void) = ^{
- NSLog(@"hey");
- };
- block();
- return 0;
- }
将以上 Objective-C 源码转换成 c++ 相关源码, 使用命令行 : xcrun -sdk iphoneos xclang -arch arm64 -rewrite-objc 文件名
c++ 的结构体与一般的类相似.
- int main(int argc, const char * argv[]) {
- void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
- ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
- return 0;
- }
其中 Block 的数据结构为:
- struct __main_block_impl_0 {
- struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- };
impl 变量数据结构:
- struct __block_impl {
- void *isa;
- int Flags;
- int Reserved;
- void *FuncPtr;
- };
FuncPtr: 函数实际调用的地址, 因为 Block 可看作是捕获自动变量的匿名函数.
Desc 变量数据结构:
- static struct __main_block_desc_0 {
- size_t reserved;
- size_t Block_size;
- }
Block 的类型
Objective-C 中 Block 有三种类型, 其最终类型都是 NSBlock .
- NSGlobalBlock (_NSConcreteGlobalBlock)
- NSStackBlock (_NSConcreteStackBlock)
- NSMallocBlock (_NSConcreteMallocBlock)
Block 类型的不同, 主要根据捕获变量的不同行为产生:
Block 类型 | 行为 |
---|---|
NSGlobalBlock | 没有访问 auto 变量 |
NSStackBlock | 访问 auto 变量 |
NSMallocBlock | NSStackBlock 调用 copy |
在内存中的存储位置
内存五大区: 栈, 堆, 静态区(BSS 段), 常量区(数据段), 代码段
copy 行为
不同类型的 Block 调用 copy 操作, 也会产生不同的复制效果:
Block 类型 | 副本源的配置存储域 | 复制效果 |
---|---|---|
__NSConcreteStackBlock | 栈 | 从栈复制到堆 |
__NSConcreteGlobalBlock | 数据段(常量区) | 什么也不做 |
__NSConcreteMallocBlock | 堆 | 引用计数增加 |
在 ARC 环境下, 编译器会在以下情况自动将栈上的 Block 复制到堆上:
Block 作为函数返回值
将 Block 赋值给 __strong 指针
苹果 Cocoa,GCD 等 API 中方法参数是 block 类型
在 ARC 环境下, 声明的 block 属性用 copy 或 strong 修饰的效果是一样的, 但在 MRC 环境下, 则用 copy 修饰.
捕获变量
为了保证在 Block 内部能够正常访问外部变量, Block 有一套变量捕获机制:
变量类型 | 是否捕获到 Block 内部 | 访问方式 |
---|---|---|
局部 auto 变量 | 是 | 值传递 |
局部 static 变量 | 是 | 指针传递 |
全局变量 | 否 | 直接访问 |
若局部 static 变量是基础类型 int val , 则访问方式为 int *val 若局部 static 变量是对象类型 JAObject *obj , 则访问方式为 JAObject **obj
基础类型变量
一个简单示例:
- int age = 10;
- // static int age = 10;
- void (^block)(void) = ^{
- NSLog(@"age is %d", age);
- };
- block();
捕获局部 auto 基础类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
- struct __main_block_impl_0 {
- ...
- int age; // 传递值
- }
捕获局部 static 基础类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
- struct __main_block_impl_0 {
- ...
- int *age; // 传递指针
- }
捕获全局基础类型变量生成的结构体 struct __main_block_impl_0 没有包含 age , 因为作用域为全局, 可直接访问.
对象类型变量
一个简单示例:
- JAPerson *person = [[JAPerson alloc] init];
- person.age = 10;
- void (^block)(void) = ^{
- NSLog(@"age is %d", person.age);
- };
- block();
捕获局部 auto 对象类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
- struct __main_block_impl_0 {
- ...
- JAPerson *person;
- }
捕获局部 static 对象类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
- struct __main_block_impl_0 {
- ...
- JAPerson **person;
- }
捕获全局对象类型变量生成的结构体 struct __main_block_impl_0 没有包含 person , 因为作用域为全局, 可直接访问.
copy 和 dispose 函数
当捕获的变量是对象类型或者使用 __Block 将变量包装成一个 __Block_byref_变量名_0 类型的 Objective-C 对象时, 会产生 copy 和 dispose 函数.
一个简单示例:
- JAPerson *person = [[JAPerson alloc] init];
- person.age = 10;
- void (^block)(void) = ^{
- NSLog(@"age is %d", person.age);
- };
- block();
其中生成的 Block 的数据结构中多了 JAPerson 类型指针变量 person :
- struct __main_block_impl_0 {
- ...
- JAPerson *person;
- }
Desc 变量数据结构多了内存管理相关的函数:
- static struct __main_block_desc_0 {
- ...
- void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
- void (*dispose)(struct __main_block_impl_0*);
- }
这两个函数的调用时机:
函数 | 调用时机 |
---|---|
copy | 栈上的 Block 复制到堆时 |
dispose | 堆上的 Block 被废弃时 |
copy 和 dispose 底层相关源码
- // Create a heap based copy of a Block or simply add a reference to an existing one.
- // This must be paired with Block_release to recover memory, even when running
- // under Objective-C Garbage Collection.
- BLOCK_EXPORT void *_Block_copy(const void *aBlock)
- __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
- // Lose the reference, and if heap based and last reference, recover the memory
- BLOCK_EXPORT void _Block_release(const void *aBlock)
- __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
- // Used by the compiler. Do not call this function yourself.
- BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
- __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
- // Used by the compiler. Do not call this function yourself.
- BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
- __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
当 Block 内部访问了对象类型的 auto 变量时:
如果 Block 是在栈上, 将不会对 auto 变量产生强引用.
如果 Block 被拷贝到堆上, 会调用 Block 内部的 copy 函数, copy 函数内部会调用
_Block_object_assign
函数,
_Block_object_assign
函数会根据 auto 变量的修饰符 (__strong,__weak,__unsafe_unretain) 作出相应的内存管理操作.
注意: 若此时变量类型为对象类型, 这里仅限于 ARC 时会 retain ,MRC 时不会 retain .
如果 Block 从堆上移除, 会调用 Block 内部的 dispose 函数, dispose 函数内部会调用
_Block_object_dispose
函数,
_Block_object_dispose
函数会自动 release 引用的 auto 变量.
使用 __weak 修饰的 OC 代码转换对应的 c++ 代码会报错: error: cannot create __weak reference because the current deployment target does not support weak references 此时终端命令需支持 ARC 并指定 Runtime 版本: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=iOS-8.0.0 main.m
内存管理
修改局部 auto 变量
局部 static 变量 (指针访问), 全局变量(直接访问) 都可以在 Block 内部直接修改捕获的变量, 而局部 auto 变量则主要通过使用 __block 存储域修饰符来修改捕获的变量.
__block 修饰符可以用于解决 Block 内部无法修改局部 auto 变量值的问题
__block 修饰符不能用于修饰全局变量, 静态变量(static)
编译器会将 __block 修饰的变量包装成一个 Objective-C 对象.
一个简单示例:
- __block int age = 10;
- void (^block)(void) = ^{
- NSLog(@"age is %d", age);
- };
- block();
其中 Block 的数据结构多了一个 __Block_byref_age_0 类型的指针:
- struct __main_block_impl_0 {
- ...
- __Block_byref_age_0 *age; // by ref
- }
__Block_byref_age_0 结构体:
- struct __Block_byref_age_0 {
- void *__isa;
- __Block_byref_age_0 *__forwarding;
- int __flags;
- int __size;
- int age; // age 真正存储的地方
- };
两个注意点:
此处指针 val 是指向 age 的指针, 而第二个 val 指的是 age 的值.
源码里面通过
age->__forwarding->age
的方式去取值, 是因为这两个 age 都可能仍在栈上, 此时直接 age->age 访问会有问题, 而 copy 操作时 __forwarding 会指向堆上的 __Block_byref_age_0 , 此时就算第一个 age 仍在栈上, 通过 age->__forwarding 会重新指向堆上的 __Block_byref_age_0 , 此时再访问 age 便不会有问题
- age->__forwarding->age
- .
__block 的内存管理
使用 __block 修饰符时的内存管理情况:
当 Block 存储在栈上时, 并不会对 __block 变量强引用.
当 Block 被 copy 到堆上时, 会调用 Block 内部的 copy 函数, copy 函数会调用
__main_block_copy_0
函数对 __block 变量产生一个强引用. 如下图
当 Block 从堆上被移除时, 会调用 Block 内部的 dispose 函数, dispose 函数会调用
_Block_object_dispose
函数自动 release __block 变量. 如下图
__weak 和 __block 修饰时的引用情况
仅用 __weak 修饰
一个简单的示例:
- JAPerson *person = [[JAPerson alloc] init];
- person.age = 10;
- __weak typeof(person) weakPerson = person;
- void (^block)(void) = ^{
- NSLog(@"person's age is %d", weakPerson.age);
- };
使用 __block __weak 修饰
一个简单的示例:
- JAPerson *person = [[JAPerson alloc] init];
- person.age = 10;
- __block __weak typeof(person) weakPerson = person;
- void (^block)(void) = ^{
- NSLog(@"person's age is %d", weakPerson.age);
- };
- block();
- return 0;
循环引用
常见的循环引用问题:
ARC 环境下解决循环引用
弱引用持有: 使用 __weak 或 __unsafe__unretain 解决
手动将一方置为 nil : 使用 __block 解决, 在 block 内部将一方置为 nil , 因此必须执行该 block
MRC 环境下解决循环引用
弱引用持有: 使用 __unsafe__unretain 解决
直接使用 __block 解决, 无需手动将一方置为 nil , 因为底层
_Block_object_assign
函数在 MRC 环境下对 block 内部的对象不会进行 retain 操作.
来源: https://juejin.im/post/5bb09160f265da0adb30e30d