Block 的本质
本质
- 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 操作。
- 直接使用 __block 解决,无需手动将一方置为 nil ,因为底层