详解 iOS 中的闭包(block)
block 的概念
这篇文章我打算来深究一下 OC 中的 block 到底是何方神圣。后面会介绍用可爱的 clang
指令来看看 block 底层的实现。
块对象 (block Object)是在 mac OS 10.6 及 iOS 4.0 平台下可以使用的功能,他不是 OC 而是 C 语言的功能实现。苹果公司的文档中将其称为块对象或 Block,在其他编程语言中,他与闭包(closure)的功能基本相同。
从 C 语言 block 说起
先从一个 C 函数说起
#include <stdio.h>
void myfunc(int m, void (^b)(void)) {
printf("%d: ", m);
b();
}
int global = 1000; // 外部变量(全局静态变量)
int main(int argc, const char * argv[]) {
void (^block)(void);
static int s = 20; // 局部静态变量
int a = 20; // 自动变量(局部变量)
block = ^{ // ============ 1
printf("%d, %d, %d\n", global, s, a);
};
myfunc(1, block);
s = 0;
a = 0;
global = 5000;
myfunc(2, block);
block = ^{ // ============ 2
printf("%d, %d, %d\n", global, s, a);
};
myfunc(3, block);
return 0;
}
仔细读代码,想想输出结果是什么
输出结果是
1: 1000, 20, 20
2: 5000, 0, 20
3: 5000, 0, 0
上面结果中,第一行没有问题,第二行是为什么呢?可以发现,变量 global 和 s 的值都改变了,但是局部变量 a 的值没有改变。第三行显示的是在代码 2 处代入块对象后的变量值,此处的变量 a 的值已经改变了。
综上,块对象貌似只在块句法中保存自动变量的值。(我们所说的自动变量其实就是函数内的局部变量,通常不用 static 关键字修饰)
块对象就是把可以执行的代码和代码中可访问的变量封装起来,使得之后可以进一步处理的包。
综上,总结一下
-
block 内部可以直接访问全局变量(外部变量)和静态变量,也可以直接改变其值
-
但是对于局部变量,块句法会将其从 栈区 copy 一份到 堆区,所以即使最初的变量发生了变化,块内部在使用的时候也不知道。而且变量的值只可以被读取不能被改变。自动变量在运行时就相当于 const 修饰的变量。
image
可以通过 __block
来完成在 block 内部对局部变量的修改。
注意:
__block 变量不是静态变量,它在块句法每次执行块句法时获取变量的内存区域。也就是说,__block 变量在同一个变量作用域中被多个 块对象 访问的时候,其实访问的是同一块内存区域。
OC 中 block 的注意点解析
块句法中使用其他任意实例对象
前面已经讲了块句法中有外部变量或自动变量时这些变量的行为,现在我们来介绍一下块句法内使用对象时的行为,特别是引用计数器的处理。
void (^cp)(void); // 可以保存块的静态变量
- (void)someMethod {
id obj = ...; // 引用任意实例对象
int n = 10;
void (^block)(void) = ^{
[obj calc: n];
};
// ...
cp = [block copy];
}
如上代码,块对象在栈上生成,变量 obj 引用任何实例变量时,块对象内使用的变量 obj 也会访问同一个对象,这时实例变量的引用计数不会发生改变。接着块对象复制到堆区,实例对象的引用计数加 1,由于方法执行结束后自动变量 obj 也会消失,因此这时块对象就成为了所有者。注意实例对象是被共享的,不是复制的。所以不只是从块对象,从哪里都可以发送消息。
image块句法中使用同一类的实例变量
先上代码
void (^cp)(void); // 可以保存块的静态变量
- (void)someMethod {
int n = 10;
void (^block)(void) = ^{
[ivar calc: n]; // 注:ivar 为该类实例变量
};
// ...
cp = [block copy];
}
这种情况下,当对象呗复制时,self 的引用计数会加 1,而非 ivar。注意,块句法中的实例变量为整数或实数时也是一样的(这点容易搞错)。
image综上总结
- 方法定义内的块句法中存在实例变量时,可以直接访问实例变量,也可以修改其值。(因为是指向同一块内存区域)
- 方法定义内的块句法中存在实例变量时,如果被 copy 到堆区,self 引用计数会加 1。实例变量不一定是对象。
- 块句法中存在非实例变量的实例对象时,被 copy 后,这个对象的引用计数会加 1。
- 已经复制后,堆区中某个块对象即使再次收到 copy 方法,结果也只是块对象自身的引用计数 1。包含的对象的引用计数不变。
- 复制的块对象在被释放时,也会向包含的对象发送 release。
OC 中的 block 到底是什么呢?
本着刨根问底的精神,就来一探究竟,block 到底是何方神圣。
我们创建一个纯净的 Command Line Tool
项目,在 main.m
中书写一下简单的代码:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block)() = ^{
NSLog(@"======>%d", age);
};
age = 20;
block();
}
return 0;
}
然后打开终端,cd 该目录下,键入
ZK$ clang -rewrite-objc main.m
然后在该路径下生成 main.cpp 文件,打开后惊奇发现短短几句 OC 代码,竟然生成了 九万多行 C++ 代码,别怕,我们写的核心 block 代码其实也没多少行。拉到最下面,就是我们重写出来的 block C++ 代码,为了阅读方便,我对这些代码进行了稍微处理,比如去掉类型强转等干扰性代码,就得到了下面这一片精美的 C++ 代码,我还贴心地加了一些注释。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 下面这些代码值这个结构体的构造函数
// `int flags=0` 是默认值
// `: age(_age)` C++ 语法,将 _age 传给 age 属性,可知在没有 __block 情况下,从外部传进来的 age 直接就赋值给这个结构体的 age。所以相当于写死了,不能修改。外部改变了也无法获知。
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; // block 生成的函数被保存在这个属性中
Desc = desc;
}
};
// 下面这个函数就是 block 最终生成的一个函数体
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d0_jq4b136d16j6t2ktg954pmcw0000gn_T_main_0f9b1b_mi_0, age);
}
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)}; // 创建结构体并赋值
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
// 从下面这句代码得知,block 就是指向一个结构体的指针。
// 参1:block 生成的函数
// 参2:`__main_block_desc_0_DATA` 结构体的指针
// 参3:将上面的自动变量直接传递进去
void (*block)() = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age)); // 在这里是直接将 10 传递进去
age = 20; // 该处的 age 赋值在 block 里面根本无法感知
// 调用 block,`(block)->FuncPtr` 就是 `__main_block_impl_0` 函数
((block)->FuncPtr)(block);
}
return 0;
}
必要的说明已经在上面代码的注释中说的很明白,我来总结一下,定义 block 的时候,首先会生成一个结构体 __main_block_impl_0
,他有三个参数,参1是 block 生成的函数__main_block_func_0
,参2是结构体 __main_block_desc_0_DATA
的地址。参3 就是我们直接传递进去的自动变量。三个参数传递进去 __main_block_impl_0
后会直接出发其构造函数,上面注释说明很明确。
那么,目光转回 __main_block_func_0
函数,int age = __cself->age;
这句代码是将 age 属性直接取出来,而这个 age 就是我们刚一开始上面提到的参3传递进去的自动变量的值 10,固然打印出来的是 10,不是 20。
还不过瘾?那么我们 __block
修饰一下自动变量,看看有什么神奇的地方
注意啦,OC 代码改成如下
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
void (^block)() = ^{
NSLog(@"======>%d", age);
};
age = 20;
block();
}
return 0;
}
运行 clang
指令,让我们看看有哪些变化。
// 这个结构体用来修饰 __block 的自动变量,竟然发现了我们熟悉的老面孔 `isa`!说明他也是一个对象。
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_d0_jq4b136d16j6t2ktg954pmcw0000gn_T_main_cff06d_mi_0, (age->__forwarding->age));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 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};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 变化1:age 不是用 int 修饰了,而是增加一个名为 `__Block_byref_age_0` 的结构体,详见上面这个结构体的定义有注释。
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {0, &age, 0, sizeof(__Block_byref_age_0), 10};
// 变化2:注意,下面的参3的 age 多了个 `&` 符号取地址,说明 `__main_block_impl_0` 引用的是结构体 `__Block_byref_age_0`的指针,不向之前直接将自动变量的值传递进去了,这也就是为什么 定义 block 后外部自动变量修改了,block 内部依然可以读到最新值。同时,这样我们也可以在 block 内部修改外部自动变量的值。
void (*block)() = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &age, 570425344));
(age.__forwarding->age) = 20;
((block)->FuncPtr)(block);
}
return 0;
}
上面的主要变化已经在注释说明了,我再总结一下重要的变化:
- 变化1:age 不是用 int 修饰了,而是增加一个名为
__Block_byref_age_0
的结构体,这个结构体用来修饰 __block 的自动变量,竟然发现了我们熟悉的老面孔isa
!说明他也是一个对象。 - 变化2:
__main_block_impl_0
的参3的 age 多了个&
符号取地址,说明__main_block_impl_0
引用的是结构体__Block_byref_age_0
的指针,不向之前直接将自动变量的值传递进去了,这也就是为什么 定义 block 后外部自动变量修改了,block 内部依然可以读到最新值。同时,这样我们也可以在 block 内部修改外部自动变量的值。 - 变化3:还有像添加了
__main_block_copy_0
,__main_block_dispose_0
结构体等变化