Open ShannonChenCHN opened 7 years ago
示例代码:
一、Block 简介
__block
)二、Block 的实现
__block
的实现__block
变量是怎么存储的__block
时发生了什么三、Block 循环引用
四、非 ARC 下的 block 与 copy
/release
五、Q&A
block 是可以在函数内定义的匿名函数。
block 的语法跟函数相比,多了 ^
,少了函数名:
BOOL isOdd(int num) {
return num % 2 != 0;
}
// block 的语法跟函数相比,多了 ^,少了函数名
BOOL (^isOdd)(int) = ^BOOL (int num) {
return num % 2 != 0;
};
返回值类型可以省略:
BOOL (^isOdd)(int) = ^(int num) {
return (BOOL)(num % 2 != 0);
};
如果不需要传参数,那么参数列表也可以省略:
void (^foo)(void) = ^void (void) {
printf("This block has no arguments.");
};
// 如果不需要传参数,那么参数列表也可以省略
void (^foo)(void) = ^void {
printf("This block has no arguments.");
};
// 进一步简化
void (^foo)(void) = ^{
printf("This block has no arguments.");
};
函数指针:
BOOL isOdd(int num) {
return num % 2 != 0;
}
BOOL (*funcPtr)(int) = &isOdd;
block 类型的声明跟函数指针相比,只是把 *
换成了 ^
:
BOOL (^isOdd)(int) = ^BOOL (int num) {
return num % 2 != 0;
};
block 作为方法和函数的参数和返回值:
// 当 block 作为方法的参数时,参数类型括号里只写类型 BOOL(^)(int),参数名放到了括号后面
- (void)doSomething:(BOOL(^)(int))isOdd;
// 当 block 作为方法的返回值,不写参数名,只有类型 BOOL(^)(int)
- (BOOL(^)(int))doSomething;
// 当 block 作为函数的返回值,跟 block 变量类型名定义的类似,将函数名加 `()`放到中间
BOOL(^doSomething())(int) {
return ^BOOL(int num) {
return num % 2 != 0;
};
}
使用 typedef 可以让 block 类型名的可读性更好:
// 写法跟定义一个 block 类型的自动变量很相似,变量名变成了新的类型名
typedef BOOL(^NumberValidator)(void);
- (void)doSomething:(NumberValidator)isOdd;
- (NumberValidator)doSomething;
int count = 20;
void (^foo)(void) = ^void() {
printf("%d\n", count); // 输出:20
};
foo();
知识点:什么是自动变量?
C 语言中主要有以下 5 种变量:
- 自动变量(非静态的局部变量)
- 函数的参数
- 静态变量(静态局部变量)
- 静态全局变量
- 全局变量
(1)在计算机编程领域,自动变量(Automatic Variable)指的是局部作用域变量,具体来说即是在控制流进入变量作用域时系统自动为其分配存储空间,并在离开作用域时释放空间的一类变量。在许多程序语言中,自动变量与术语“局部变量”(Local Variable)所指的变量实际上是同一种变量,所以通常情况下“自动变量”与“局部变量”是同义的。
(2)默认情况下,在代码块内声明的变量都是自动变量,函数中的局部变量,如果不用关键字static加以声明,编译系统会对他们自动分配存储空间的,函数的形参和在函数中定义的变量,在调用函数时,系统给形参和定义的变量分配存储空间,数据存储在动态区中。在函数调用结束时就自动释放这些空间。如果是在复合语句(比如 for 循环)中定义的变量,则在变量定义时分配存储空间,在复合语句结束时自动释放空间。
(3)而静态存储类型的局部变量是在静态存储区内分配内存单元,在程序的整个运行期间内都不释放空间。
(4)静态局部变量是在编译时赋初始值,并且只赋一次初值,在以后每次调用函数时,只是使用上一次函数被调用结束时变量的值。而自动局部变量的初值不是在编译时赋予的,而是在函数调用时赋予的,每调用一次函数都会对变量重新赋一次初值。
__block
)block 中可以修改捕获到的非全局的静态变量、静态全局变量和全局变量的值。但是不能修改捕获到自动变量值。
如果想要在 block 中修改捕获到自动变量值,需要在变量前声明 __block
:
__block int count = 20; // 需要声明 __block,才能在 block 中改变值
void (^foo)(void) = ^void() {
count = 25;
};
printf("%d\n", count); // 输出:20
foo();
printf("%d\n", count); // 输出:25
block 可以捕获 Objective-C 对象类型的变量,我们可以在 block 内修改可变对象的内容:
NSMutableArray *array = [NSMutableArray array];
void (^blockCaptureObject)(void) = ^void(void) {
[array addObject:@"shannon"];
[array addObject:@"chen"];
};
blockCaptureObject();
NSLog(@"%@", array); // 输出:( shannon, chen )
但是如果要在 block 内修改变量的值,还是需要加 __block
:
__block NSMutableArray *array = [NSMutableArray array];
void (^blockCaptureObject)(void) = ^void(void) {
array = @[@"Tony"].mutableCopy;
};
blockCaptureObject();
NSLog(@"%@", array); // 输出:( Tony )
首先,关于 block 的数据结构和 runtime 是开源的,可以在LLVM 开源项目中的 Blocks Runtime 中看到,或者下载苹果的 libclosure 库的源码来看(也可以在线查看),其中包含了很多示例和文档说明。
另外,在公开的 usr/include/Block.h
头文件中,和 objc4 runtime 源码的 Block_private.h 中也可以看到 block 相关的结构定义和 api。
除此之外,我们还可以使用 clang -rewrite-objc main.m
命令,将含有 block 定义的代码转成 C++ 代码,也可以看到 block 的真实面貌。
main.m
中的代码如下:
int main() {
void (^blk)(void) = ^void(void) {
printf("Block\n");
};
blk();
return 0;
}
使用 clang -rewrite-objc main.m
命令将上面的转成 C++ 代码:
/// block 的执行内容转成的静态函数
/// 参数 __cself 为指向 __main_block_impl_0 结构实例本身的变量
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
int main() {
// 这里实际上是初始化一个 __main_block_impl_0 结构体实例,然后再将这个实例的指针赋值给变量 blk
// 第一个参数是一个函数指针,指向由 block 语法转换的 C 函数 __main_block_func_0
// 第二个参数是一个结构体指针,指向这个 block 结构存储相关的结构体 __main_block_desc_0_DATA
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// 执行 blk 时,是从 __main_block_impl_0 实例中取出函数指针,调用该函数
// 函数参数是这个 __main_block_impl_0 实例本身
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
下面是 __main_block_impl_0
,__block_impl
和 __main_block_desc_0_DATA
的实现:
/// 存储 block 关键信息的结构体
struct __block_impl {
void *isa; // 像 Objective-C 对象一样,识别身份的 isa 指针
int Flags; // 记录某些标记,比如引用计数
int Reserved; // 今后版本升级所需的区域
void *FuncPtr; // 存储 block 执行函数的指针
};
/// 存储 block 描述信息的结构体
/// 这里直接创建了一个静态全局变量
static struct __main_block_desc_0 {
size_t reserved; // 今后版本升级所需的区域
size_t Block_size; // block 的大小
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
/// block 转成的结构体,这个就是 block 的真实面貌
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
/// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
// 该 block 的类型为 _NSConcreteStackBlock
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
结论:
objc_object
和 objc_class
一样,都有一个 isa
成员变量。__main_block_impl_0
结构体实例的指针赋值给变量 blk
。__main_block_impl_0
结构体实例中了。main.m
中的代码如下:
int main () {
int val1 = 256;
int val2 = 10;
const char *fmt = "val2 = %d\n";
void (^myBlock)(void) = ^void(void) {
printf(fmt, val2);
};
val2 = 2;
fmt = "These values were changed. val2 = %d\n";
myBlock();
return 0;
}
使用 clang -rewrite-objc main.m
命令将上面的转成 C++ 代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy,获取捕获到的自动变量(保存在成员变量中)
int val2 = __cself->val2; // bound by copy,获取捕获到的自动变量(保存在成员变量中)
printf(fmt, val2);
}
int main () {
int val1 = 256; // block 中没用到,也就不会截获
int val2 = 10;
const char *fmt = "val2 = %d\n";
// 构造函数多了两个参数,用于保存捕获到的自动变量
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val2));
val2 = 2;
fmt = "These values were changed. val2 = %d\n";
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
下面是 __main_block_impl_0
,__block_impl
和 __main_block_desc_0_DATA
的实现:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
// block 对应的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// block 中捕获的自动变量被声明成了成员变量
const char *fmt;
int val2;
// 构造函数中也多了两个参数,用来传入捕获的自动变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val2, int flags=0) : fmt(_fmt), val2(_val2) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
结论:
block 中不能修改捕获到自动变量值。但是可以修改捕获到的非全局的静态变量、静态全局变量和全局变量的值。这是怎么做到的呢?
main.m
中的代码如下:
int global_val = 60;
static int static_global_val = 10;
int main() {
int val = 10;
static int static_val = 4;
void (^myBlock)(void) = ^void(void) {
static_val = 30;
global_val = 6;
static_global_val = 50;
printf("myBlock:val(10) = %d,\n static_val(4) = %d,\n global_val(60) = %d,\n static_global_val(10) = %d\n", val, static_val, global_val, static_global_val);
};
myBlock();
}
使用 clang -rewrite-objc main.m
命令将上面的转成 C++ 代码:
int global_val = 60;
static int static_global_val = 10;
// block 的实现对应的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy,
int val = __cself->val; // bound by copy
// 通过访问静态局部变量的指针来改变变量值
(*static_val) = 30;
// 全局变量和静态变量可以直接访问
global_val = 6;
static_global_val = 50;
// 自动变量无法访问,只能读取变量值
printf("myBlock:val(10) = %d,\n static_val(4) = %d,\n global_val(60) = %d,\n static_global_val(10) = %d\n", val, (*static_val), global_val, static_global_val);
}
int main() {
int val = 10;
static int static_val = 4;
// 这里把静态变量的指针传入了 __main_block_impl_0 结构体的构造函数并保存
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0,
&__main_block_desc_0_DATA,
&static_val,
val));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}
下面是__main_block_impl_0
,__block_impl
和 __main_block_desc_0_DATA
的实现:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
// block 的数据结构
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val; // 捕获进来的 static 变量的地址
int val; // 捕获进来的自动变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int _val, int flags=0) : static_val(_static_val), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
结论:
__block
的实现__block
说明符类似于 static
、auto
和 register
说明符用于指定将变量值存储到哪个存储域中。
知识点:C 语言中的存储域类说明符 C 语言中的存储域类说明符主要有以下几个:
typedef
extern
static
:存储在静态存储区auto
:局部变量,存储在栈区register
:register
变量存储在寄存器中,可以实现快速访问参考:
int main() {
__block int block_val = 8;
void (^myBlock)(void) = ^void(void) {
block_val = 9;
printf("myBlock: block_val(8) = %d\n", block_val);
};
myBlock();
printf("block_val = %d\n", block_val);
}
使用 clang -rewrite-objc main.m
命令将上面的转成 C++ 代码:
/// 捕获的 __block 变量转为了结构体
struct __Block_byref_block_val_0 {
void *__isa; // 结构体中带有 isa,说明它也是一个对象
__Block_byref_block_val_0 *__forwarding; // __forwarding ,存储 __block 结构体的地址
int __flags;
int __size;
int block_val; // 捕获的参数值
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_block_val_0 *block_val = __cself->block_val; // bound by ref
(block_val->__forwarding->block_val) = 9; // 通过 __forwarding 成员变量来获取捕获的 __block 变量
printf("myBlock: block_val(8) = %d\n", (block_val->__forwarding->block_val));
}
int main() {
// __block 变量被转成了 __Block_byref_block_val_0 结构体类型
// 其中第二个参数是把 block_val 的地址传给了成员变量 __forwarding
// 第三个参数是把__block 变量的值传给了保存数值的成员变量
__attribute__((__blocks__(byref))) __Block_byref_block_val_0 block_val = {(void*)0,
(__Block_byref_block_val_0 *)&block_val,
0,
sizeof(__Block_byref_block_val_0),
8};
// __block 的结构体变量的地址被传进了 block 结构体构造函数,这样就可以达到修改外部变量的作用。
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_block_val_0 *)&block_val, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
// 后面继续使用 __block 变量时,是用 block_val 来访问的
printf("block_val = %d\n", (block_val.__forwarding->block_val));
}
下面是__main_block_impl_0
,__block_impl
和 __main_block_desc_0_DATA
的实现:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->block_val, (void*)src->block_val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->block_val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
// 我们需要负责 __Block_byref_block_val_0 结构体相关的内存管理,
// 所以 main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改相应变量的引用计数。
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_block_val_0 *block_val; // by ref,存储 __block 结构体变量的成员变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_block_val_0 *_block_val, int flags=0) : block_val(_block_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
结论:
__block
变量被转成了一个 __Block_byref_block_val_0
结构体,其中一个成员变量 block_val
用来保存捕获的值,还有一个成员变量 __forwarding
用来存储__Block_byref_block_val_0
结构体变量的指针。(关于 __forwarding
存在的意义,更多细节在后面两节讨论。)__Block_byref_block_val_0
结构体中也有一个 isa
指针,所以我们也可以把__block
变量看做是一个对象。__block
变量的值,是因为它通过成员变量保存了 __Block_byref_block_val_0
结构体变量的指针,通过这个指针来间接访问其中的 block_val
。__Block_byref_block_val_0
结构中的字段没有直接定义在 __main_block_impl_0
中,是因为一个 __block
变量可能会在多个 block 中使用。__Block_byref_block_val_0
结构体相关的内存管理,所以 main_block_desc_0
中增加了 copy 和 dispose 函数指针,用于在调用前后修改相应变量的引用计数。(更多细节见第5,6, 7节)__block
变量时,也是通过访问 __Block_byref_block_val_0
结构体变量中的 block_val
来访问的。__block
变量直接保存成指针呢?因为 block 的生命周期是可以超过被截获的自动变量的,当被截获的自动变量因作用域结束被销毁后,如果再继续通过指针访问这个自动变量的话,就会出现异常。(至于为什么 block 超出变量作用域仍然存活,更多细节在后面两节讨论。)block 的存储域主要有三种:
_NSConcreteStackBlock
:存储在栈上_NSConcreteGlobalBlock
::存储在数据区域,也就是.data
区域_NSConcreteMallocBlock
::存储在堆上_NSConcreteStackBlock
和 _NSConcreteGlobalBlock
我们先来看看_NSConcreteStackBlock
和 _NSConcreteGlobalBlock
这两种类型。
ARC 下执行下面的代码:
void (^globalBlock)(void) = ^ { };
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%@", ^{}); // 输出:<__NSGlobalBlock__: 0x100004188>
NSLog(@"%@", globalBlock); // 输出:<__NSGlobalBlock__: 0x1000040f8>
int num = 1;
int (^stackBlock)(int) = ^int(int a) {
return a + num;
};
stackBlock(1);
NSLog(@"%@", stackBlock); // 输出:<__NSMallocBlock__: 0x10058da30>
}
return 0;
}
非 ARC 下执行下面的代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%@", ^{}); //输出:<__NSGlobalBlock__: 0x100004188>
NSLog(@"%@", globalBlock); // 输出:<__NSGlobalBlock__: 0x1000040f8>
int num = 1;
int (^stackBlock)(int) = ^int(int a) {
return a + num;
};
stackBlock(1);
NSLog(@"%@", stackBlock); // 输出:<__NSStackBlock__: 0x7ffeeee4d5b8>
}
return 0;
}
_NSConcreteMallocBlock
那么什么时候 block 会出现在堆上呢?
ARC 下,编译器会自动将 block 从栈上复制到堆上。其实在上面的示例代码中已经看到了一些表现了。
(1)block 作为函数返回值
// block 作为函数返回值
typedef int (^BlockType)(int);
BlockType foo() {
return ^int(int a) {
return a + 1;
};
}
转成汇编代码后:
_foo: ## @foo
.cfi_startproc
## %bb.0:
...
leaq ___block_literal_global(%rip), %rdi
callq _objc_retainBlock
...
jmp _objc_autoreleaseReturnValue ## TAILCALL
.cfi_endproc
(2)block 作为函数参数
// block 作为函数参数
void foo() {
int val = 10;
id a = [[NSArray alloc] initWithObjects:^{ printf("block1: val = %d", val); }, nil];
printf("%p", a);
}
转成汇编代码后:
_foo: ## @foo
.cfi_startproc
## %bb.0:
...
callq *_objc_msgSend@GOTPCREL(%rip)
leaq "___block_descriptor_36_e5_v8?0l"(%rip), %rcx
leaq ___foo_block_invoke(%rip), %rdx
movq __NSConcreteStackBlock@GOTPCREL(%rip), %rsi
...
callq _objc_retainBlock
...
callq *_objc_msgSend@GOTPCREL(%rip)
...
callq *_objc_release@GOTPCREL(%rip)
...
callq _printf
...
callq _objc_storeStrong
...
.cfi_endproc
(3)将 block 赋值给一个 __strong
变量
typedef int (^BlockType)(void);
int foo(int base) {
BlockType blk = ^int() {
return base + 5;
};
return blk();
}
转成汇编代码后:
_foo: ## @foo
.cfi_startproc
## %bb.0:
...
leaq "___block_descriptor_36_e5_i8?0l"(%rip), %rax
leaq ___foo_block_invoke(%rip), %rcx
movq __NSConcreteStackBlock@GOTPCREL(%rip), %rdx
...
callq _objc_retainBlock
...
callq _objc_storeStrong
...
.cfi_endproc
(4)将 block 赋值给一个 __weak
变量,并使用了这个变量
typedef int (^BlockType)(void);
int foo(int base) {
__weak BlockType blk = ^int() {
return base + 5;
};
return blk();
}
转成汇编代码后:
_foo: ## @foo
Lfunc_begin0:
.cfi_startproc
...
movq __NSConcreteStackBlock@GOTPCREL(%rip), %rax
...
leaq ___foo_block_invoke(%rip), %rax
...
leaq "___block_descriptor_36_e5_i8?0l"(%rip), %rax
...
callq _objc_initWeak
...
callq _objc_loadWeakRetained
...
callq *_objc_release@GOTPCREL(%rip)
...
callq _objc_destroyWeak
(5)将 block 赋值给一个 __unsafe_unretained
变量
typedef int (^BlockType)(void);
int foo(int base) {
__unsafe_unretained BlockType blk = ^int() {
return base + 5;
};
return blk();
}
转成汇编代码后:
_foo: ## @foo
.cfi_startproc
## %bb.0:
...
leaq "___block_descriptor_36_e5_i8?0l"(%rip), %rax
leaq ___foo_block_invoke(%rip), %rcx
movq __NSConcreteStackBlock@GOTPCREL(%rip), %rdx
...
.cfi_endproc
在 objc4 runtime 源码中可以看到 objc_retainBlock
函数的实现:
//
// The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
//
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
// 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);
在苹果开源的 libclosure 中可以看到 _Block_copy
的实现。
_Block_copy
的调用逻辑如下:
_Block_copy
_Block_copy_internal
// if it‘s a stack block. Make a copy.
malloc
memmove
_Block_call_copy_helper
_Block_call_copy_helper
的实现:
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;
(*desc->copy)(result, aBlock); // do fixup
}
block 类型的属性一般声明为 copy
。
@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end
苹果官方文档中的建议:
Note: You should specify
copy
as the property attribute, because a block needs to be copied to keep track of its captured state outside of the original scope. This isn’t something you need to worry about when using Automatic Reference Counting, as it will happen automatically, but it’s best practice for the property attribute to show the resultant behavior. For more information, see Blocks Programming Topics.
下面是 Clang 文档中的描述:
The compiler will treat
Blocks
as objects when synthesizing property setters and getters, will characterize them as objects when generating garbage collection strong and weak layout information in the same manner as objects, and will issue strong and weak write-barrier assignments in the same manner as objects.
_NSConcreteStackBlock
:存储在栈上_NSConcreteGlobalBlock
::存储在数据区域,也就是.data
区域_NSConcreteMallocBlock
::存储在堆上_NSConcreteGlobalBlock
类型主要在以下两种情况出现
_NSConcreteStackBlock
类型__strong
变量或者当做返回值,编译器就会自动插入 objc_retainBlock
函数将 block 从栈上复制到堆上。ARC 下,编译器对 block 的处理基本跟对普通 Objective-C 的处理一致。objc_retainBlock
函数,所以现在基本上不存在需要手动将 block 从栈上复制到堆上的情况了_NSConcreteStackBlock
:从栈中复制到了堆上_NSConcreteGlobalBlock
:什么也不做_NSConcreteMallocBlock
:引用计数增加__block
变量是怎么存储的__block
变量的存储域与 Block 的关系Block 从栈复制到堆时,对 __block
变量产生的影响:
初始时 __block 变量的存储域 |
Block 从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆上,并被 Block 持有 |
堆 | 被 Block 持有(引用计数加1?) |
__block
变量如果在一个 block 中使用了 __block
变量,那么当这个 block 被复制到堆上时,它使用的所有的 __block
变量也都会被复制到堆上,并且被这个 block 所持有(如下图所示)。
如果 block 中使用的这个__block
变量已经在堆上了,当这个 block 被复制到堆上时,那么这个 __block
变量就会被持有,引用计数增加。
如果对一个已经被复制到堆上的 block 进行再次 copy,该操作对 block 中使用的__block
变量没有影响。
__block
变量一开始所有的 block 和 __block
变量都存储在栈上,当其中一个 block 被复制到堆上时,__block
变量也会一并从栈中复制到堆上,并被这个 block 所持有。当后面其他的 block 被复制到堆上时,会持有这个 __block
变量,并增加它的引用计数(如下图所示)。
如果堆上的 block 被销毁,那么它所持有的 __block
变量也会被释放。
__block
变量转成的结构体__Block_byref_block_val_0
中的成员变量 __forwarding
的目的是什么?在第 4 节中的 C++ 代码中,我们可以看到被捕获的 __block
变量最终被转成了 __Block_byref_block_val_0
结构体类型的变量,不论是在 block 中,还是在 block 后面的代码中,所有的 int
类型都被替换成了这个 __Block_byref_block_val_0
类型。
int main() {
// int 类型被转成了 __Block_byref_block_val_0 结构体类型
// 第二个参数是把 block_val 的地址传给了成员变量 __forwarding
__attribute__((__blocks__(byref))) __Block_byref_block_val_0 block_val = {(void*)0,
(__Block_byref_block_val_0 *)&block_val,
0,
sizeof(__Block_byref_block_val_0),
8};
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_block_val_0 *)&block_val, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
// 后面继续使用 __block 变量时,是用 block_val 来访问的
printf("block_val = %d\n", (block_val.__forwarding->block_val));
}
当 __block
变量被复制到堆上之后,block 内使用的 block_val
是复制到堆上的 __Block_byref_block_val_0
实例,而 block 后面使用的 block_val
仍然是栈上的 __Block_byref_block_val_0
实例。
只是栈上的 __Block_byref_block_val_0
实例在 __block
变量被复制到堆上时,会将 __forwarding
指针修改成被复制到堆上的 __Block_byref_block_val_0
实例的地址(如下图所示)。
在上一节中我们通过苹果开源的 libclosure 源码,看到了_Block_copy
最终会调用 _Block_call_copy_helper
函数,而这个函数做的就是取出 block 结构体中的 descriptor
,调用它的 copy
函数:
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;
(*desc->copy)(result, aBlock); // do fixup
}
在第4节 block 捕获 __block
变量的代码转成 C++ 代码后,我们可以看到这个 copy
函数调用的是 _Block_object_assign
。该函数在 libclosure 中的实现是判断它的第三个参数 flag,比如,如果捕获的是栈上的 __block
变量,那就是 BLOCK_FIELD_IS_BYREF
,这种 case 下调用的就是 _Block_byref_assign_copy
函数:
// Runtime entry points for maintaining the sharing knowledge of byref data blocks.
// A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
// Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
// We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment it.
// Otherwise we need to copy it and update the stack forwarding pointer
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
struct Block_byref **destp = (struct Block_byref **)dest;
struct Block_byref *src = (struct Block_byref *)arg;
...
// src points to stack
bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
// if its weak ask for an object (only matters under GC)
struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack
copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
...
_Block_memmove(copy+1, src+1,
src->size - sizeof(struct Block_byref));
...
// assign byref data block pointer into new Block
_Block_assign(src->forwarding, (void **)destp);
}
main.m
中的代码如下:
int main() {
typedef void(^blk_t) (id obj);
blk_t blk;
{
// 捕获 Objective-C 对象
id array = [[NSMutableArray alloc] init];
blk = ^void(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
}; // 在 ARC 中这里并不需要手动 copy,在 MRR 中就需要手动 copy(p125)
}
blk([[NSObject alloc] init]);
return 0;
}
使用 clang -rewrite-objc main.m
命令将上面的转成 C++ 代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id array = __cself->array; // bound by copy
((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_nt_bkkycgbs2hv63tthr5dbd2g40000gn_T_main8_c8bb70_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}
int main() {
typedef void(*blk_t) (id obj);
blk_t blk;
{
// Objective-C 对象没有变化
id array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));
// Objective-C 对象直接当成参数传了
blk = ((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
}
((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
return 0;
}
下面是 __main_block_impl_0
,__block_impl
和 __main_block_desc_0_DATA
的实现:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
// 其中有两个管理内存的函数指针 copy 和 dispose
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id array; // 捕获到的 Objective-C 对象
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
结论:
block 捕获 Objective-C 对象时,是直接将 Objective-C 对象保存到成员变量中了,同时 __main_block_desc_0
中有两个函数指针,分别指向__main_block_copy_0
和 __main_block_dispose_0
。
一般来讲,在 Objective-C 中,C 语言结构体不能含有 Objective-C 对象类型的成员变量,因为编译器不知道什么时候初始化和销毁结构体,不能很好地管理内存。但是 objc runtime 运行时库知道block 从栈复制到堆上以及从堆上被销毁的时机,也就是 __main_block_copy_0
和 __main_block_dispose_0
的回调,所以 block 的结构中可以直接包含对象,并进行内存管理。
第二点进一步证明了 “block 本质上是一个 Objective-C” 这一事实,既有 isa
指针,又有相应的内存管理方法
__main_block_desc_0
中保存的 copy
和 dispose
函数指针什么时候调用在前面的第5节中,我们知道 block 从栈上复制到堆上时,会调用_Block_copy
,通过苹果开源的 libclosure 源码,看到了 _Block_copy
的调用逻辑:
_Block_copy
_Block_copy_internal
// if it‘s a stack block. Make a copy.
malloc
memmove
_Block_call_copy_helper
而 _Block_call_copy_helper
函数会取出 block 结构体中的 descriptor
,调用它的 copy
函数指针指向的函数,也就是上面转换出来的 C++ 代码中的__main_block_copy_0
函数。
__main_block_copy_0
函数内部调用了 _Block_object_assign
,上一节中我们看到了当 block 捕获的是栈上的__block
变量时,那就走 BLOCK_FIELD_IS_BYREF
的 case。而这里捕获的是 Objective-C 对象,_Block_object_assign
的第三个参数就变成了 BLOCK_FIELD_IS_OBJECT
。
objc4 runtime 源码中的定义:
// Runtime entry point called by compiler when assigning objects inside copy helper routines
BLOCK_EXPORT void _Block_object_assign(void *destAddr, const void *object, const int flags);
// BLOCK_FIELD_IS_BYREF is only used from within block copy helpers
// runtime entry point called by the compiler when disposing of objects inside dispose helper routine
BLOCK_EXPORT void _Block_object_dispose(const void *object, const int flags);
libclosure 中的源码实现:
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
switch (osx_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
_Block_assign((void *)object, destAddr);
break;
...
...
}
objc4 runtime 源码中定义了一个枚举:
// Runtime support functions used by compiler when generating copy/dispose helpers enum { // see function implementation for a more complete description of these fields and combinations BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ... BLOCK_FIELD_IS_BLOCK = 7, // a block variable BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines. };
结论:
_Block_object_assign
被调用,_Block_object_assign
将捕获到的对象赋值给 block 的结构体成员变量 array
,并持有该对象,这个函数相当于 objc_retain
:_Block_object_dispose
被调用,_Block_object_dispose
会释放 block 的结构体成员变量 array
,也就是捕获到的对象,这个函数相当于 objc_release
。补充:
在示例代码中添加符号断点进行调试可以看到 _Block_object_assign
和 _Block_object_dispose
的调用堆栈(如下图所示)。
另外,在 iOS 中的 block 是如何持有对象的这篇文章中,也提到了在 block 被销毁时,dispose_helper
会负责向所有捕获的变量发送 release
消息:
其实最开始笔者对这个
dispose_helper
实现的机制并不是特别的肯定,只是有一个猜测,但是在询问了FBBlockStrongRelationDetector
的作者之后,才确定dispose_helper
确实会负责向所有捕获的变量发送release
消息,如果有兴趣可以看这个 issue。这部分的代码其实最开始源于 mikeash 大神的 Circle,不过对于他是如何发现这一点的,笔者并不清楚,如果各位有相关的资料或者合理的解释,可以随时联系我。
__block
时发生了什么int main() {
typedef void(^blk_t) (id obj);
blk_t blk;
{
// 带有 __block 的 Objective-C 对象
__block id array = [[NSMutableArray alloc] init];
blk = ^void(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
return 0;
}
转成 C++ 代码:
/// 捕获的 __block 对象类型变量被转成了结构体
struct __Block_byref_array_0 {
void *__isa;
__Block_byref_array_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*); // 比 __block C 语言变量多了两个管理内存的函数指针
void (*__Block_byref_id_object_dispose)(void*);
id array; // 保存原来的 array 对象的引用
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
__Block_byref_array_0 *array = __cself->array; // bound by ref
((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)(array->__forwarding->array), sel_registerName("addObject:"), (id)obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_nt_bkkycgbs2hv63tthr5dbd2g40000gn_T_main8_cb5b3d_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)(array->__forwarding->array), sel_registerName("count")));
}
int main() {
typedef void(*blk_t) (id obj);
blk_t blk;
{
__attribute__((__blocks__(byref))) __Block_byref_array_0 array = {(void*)0,(__Block_byref_array_0 *)&array, 33554432, sizeof(__Block_byref_array_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"))};
blk = ((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_array_0 *)&array, 570425344));
}
((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
return 0;
}
block 被转成了结构体 __main_block_impl_0
:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->array, (void*)src->array, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->array, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
// block 被转成结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_array_0 *array; // 保存了指向捕获对象 array 转换后的结构体的指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_array_0 *_array, int flags=0) : array(_array->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
结论:
__block
对象类型变量,跟 block 捕获 __block
C 语言变量的实现差不多,都是用一个新的”对象“去保存原来的变量值,不通点在于,block 捕获 __block
对象类型变量时,这个新的对象 __Block_byref_array_0
多了两个管理内存的函数指针__block
对象类型变量(就是__Block_byref_array_0
对象),__block
对象类型变量会持有被赋值的对象。(书 p128~p134)
self
持有 block,block 捕获并持有 self
self
持有 block,block 捕获 __block
临时变量,__block
临时变量持有 self
__weak
(非 ARC 下使用 __unsafe_unretained
),编译器会自动不持有捕获的变量__block
时,需要在 block 回调中将 __block
变量赋值为 nil
,手动打断循环引用环苹果的官方文档 "Transitioning to ARC Release Notes" 中的建议如下:
In manual reference counting mode,
__block id x;
has the effect of not retainingx
. In ARC mode,__block id x;
defaults to retaining x (just like all other values). To get the manual reference counting mode behavior under ARC, you could use__unsafe_unretained __block id x;
. As the name __unsafe_unretained implies, however, having a non-retained variable is dangerous (because it can dangle) and is therefore discouraged. Two better options are to either use__weak
(if you don’t need to support iOS 4 or OS X v10.6), or set the__block
value tonil
to break the retain cycle.
"Transitioning to ARC Release Notes" 中有一条这样的建议:
For non-trivial cycles, however, you should use:
MyViewController *myController = [[MyViewController alloc] init…]; // ... MyViewController * __weak weakMyController = myController; myController.completionHandler = ^(NSInteger result) { MyViewController *strongMyController = weakMyController; if (strongMyController) { // ... [strongMyController dismissViewControllerAnimated:YES > completion:nil]; // ... } else { // Probably nothing... } };
官方文档也没有讲清楚为什么要在 block 里面用 strongMyController
。
不过在 bs 的这篇文章中讲了一个场景:self
在主线程中被释放了,但是捕获了 self
的 block 是在子线程中调用的,这个时候就需要使用 strong
来“保活”了。
__weak MyViewController *wself = self;
self.completionHandler = ^(NSInteger result) {
__strong __typeof(wself) sself = wself; // 强引用一次
[sself.property removeObserver: sself forKeyPath:@"pathName"];
};
我们可不可以换种方式,不用 storngSelf
,而是在 block 内部通过判断 self 是不是 nil
来决定要不要继续往下走?答案是不行的,因为可能在子线程执行判断 if 语句时还没有被释放,真正去用的时候却释放了。
顺便讲一下另外一个 strongSelf
的问题,虽然跟 block 没有关系, sunnyxx 在 ARC对self的内存管理 这篇文章中,就 YTKNetwork 中使用 self
出现崩溃的真实案例进行了分析,原因是 ARC 下,为了优化性能,针对方法中的 self
参数做了内存管理简化:
ARC下,
self
既不是strong
也不是weak
,而是unsafe_unretained
的,也就是说,入参的self
被表示为:(init
系列方法的self
除外)- (void)start { const __unsafe_unretained YTKRequest *self; // ... }
所以,有可能方法执行到一般,当前的对象已经被释放了,但是传参进来的 self
没有被置为 nil
,所以就出现 BAD_ACCESS
(野指针?垂悬指针?)错误了。
@weakify
和 @strongify
这两个宏展开后实际上就是:
// DEBUG
autoreleasepool {} __weak __typeof__(self) self_weak_ = self;
autoreleasepool {} __strong __typeof__(self) self_strong_ = self;
// 非 DEBUG
try {} @catch (...) {} __weak __typeof__(self) self_weak_ = self;
try {} @catch (...) {} __strong __typeof__(self) self_strong_ = self;
参考:
copy
/release
(1)手动调用 copy
/release
非 ARC 下,可以通过调用 copy
方法或者 Block_copy
函数来将 block 从栈上复制到堆上,以及持有 block:
void (^blk_on_heap)(void) = [blk_on_stack copy]; // 从栈上复制到堆上,blk_on_heap 持有该 block
void (^blk_on_heap_1)(void) = [blk_on_heap copy]; // 引用计数增加
非 ARC 下,可以通过调用 release
方法或者 Block_release
函数来释放堆上的 block:
[blk_on_heap release];
(2)非 ARC 下,__block
对象类型的变量
不管是 ARC 和非 ARC,下面的代码都会导致循环引用,因为 self 持有 block,block 持有 self:
typedef void (^blk_t)(void);
@interface MyObject : NSObject {
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^{
NSLog(@"self = %@", self);
};
return self;
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
int main() {
id o = [[MyObject alloc] init];
NSLog(@"%@", o);
return 0;
}
不过非 ARC 下,可以用 __block
来避免循环引用。当 block 从栈复制到堆中时,block 会持有捕获的对象类型变量,但是如果对象类型变量带有 __block
, block 就不会持有该__block
变量,这样就打破循环引用了,不管__block
变量是否持有被赋值的对象。
- (id)init {
self = [super init];
__block id tmp = self;
blk_ = ^{
NSLog(@"self = %@", tmp);
};
return self;
}
苹果的官方文档 "Transitioning to ARC Release Notes" 中的建议如下:
In manual reference counting mode,
__block id x;
has the effect of not retainingx
. In ARC mode,__block id x;
defaults to retaining x (just like all other values). To get the manual reference counting mode behavior under ARC, you could use__unsafe_unretained __block id x;
. As the name __unsafe_unretained implies, however, having a non-retained variable is dangerous (because it can dangle) and is therefore discouraged. Two better options are to either use__weak
(if you don’t need to support iOS 4 or OS X v10.6), or set the__block
value tonil
to break the retain cycle.
参考:
Clang 文档中有如下描述:
Blocks
may containBlock
literal expressions. Any variables used within inner blocks are imported into all enclosingBlock
scopes even if the variables are not used. This includesconst
imports as well as__block
variables.
int (^completion)(int);
__block __weak int (^weakCompletion)(int);
weakCompletion = completion = ^int(int a) {
if (a == 0) {
return 1;
} else {
return a * weakCompletion(a-1);
}
};
参考:
目录