Block 是 C 语言的扩充功能,实质是带有局部变量的匿名函数。其定义如下
^ 返回值类型 参数列表 表达式
具体使用有很多省略方法,可以查看 How Do I Declare A Block in Objective-C?
Block 可以截获内部变量的值,但不能再进行改写,如果是 OC 对象类型的内部变量,可以调用变更其对象的方法,但不能重新赋值。要实现改写和赋值,必须用到 __block
说明符。
目前的 Block 没有实现对 C 语言数组的截获,因此要使用指针来实现。
const char text[] = "hello";
void (^blk)(void) = ^{
[printf("%c", text[2]);
};
这样会报错,要改成下面的
const char *text = "hello";
void (^blk)(void) = ^{
printf("%c", text[2]);
};
对于 Block 的代码可以用 clang 进行转换,成为可以阅读的 C 语言源码,具体命令是 "clang -rewrite-objc 源文件名",它会生成一个 cpp 文件。接下来分析各种情况下的 Block 源码实现。
不截获变量
首先是不带局部变量的情况,也就是不涉及对 Block 之外的局部变量或者其他变量进行截获的情况。
#import <Foundation/Foundation.h>
int main()
{
void (^blk)(void) = ^{printf("Hello world\n");};
blk();
return 0;
}
它转换后变为以下代码
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) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello world\n");}
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()
{
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
来分析下它的实现。
首先看 main 函数中,对于源码中的第一句对 block 的定义,main 函数是这样转换的
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
其中包含以下几个元素,首先是一个函数指针,它将会负责执行我们定义的 Block,而它被赋值为一个 __main_block_impl_0
结构体实例,这个实例定义如下
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) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
它定义了一些内部变量
-
__block_impl
类型的 impl -
__main_block_desc_0
类型的指针 Desc - 还有一个初始化方法,这个方法接受一个函数指针,一个
__main_block_desc_0
指针,一个 flag 标志位。
逐一来看,首先是 __block_impl,它的定义如下
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
这里 isa 指针变量是个很重要的变量。首先要明确在 OC 中一切类的定义都是对象,同时 OC 包含对象实例、类、元类(metaClass)三种元素,isa 就是一个 Class 类型的指针,在实例中 isa 指向它的类对象,而类对象的 isa 指针指向它的元类,元类保存了类方法的列表,而类则保存了成员变量和成员方法的列表。元类的 isa 指针会指向元类的父类,最终指向一个根元类,根元类的 isa 指针则指向它本身形成一个闭环。所以 这里 isa 其实是指向 Block 的类对象。Flag 是个标志位,Reserced 是保留空间,而 FuncPtr 指针则会保存一个方法的指针。
实例、类、元类的继承关系如下图所示
然后是 __main_block_desc_0
类型的结构体,它的定义如下
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)};
可以看到这里直接将结构体实例化并赋值给一个 __main_block_desc_0_DATA
变量。其中 reserved 也是保留位,Block_size
被赋值为结构体 __main_block_impl_0
的大小,也就是我们的 Block 的实际大小。
最后是一个初始化方法,这个方法具体内容很容易看明白,就是对一些内部变量的赋值,唯独 impl.isa 被赋值为 _NSConcreteStackBlock
的地址,这里相当于指明 impl 对象的类对象是 _NSConcreteStackBlock
,因此 Block 实际上就是 OC 对象的一个对象而已,它的类可以是以下几种
_NSConcreteStackBlock
_NSConcreteGlobalBlock
_NSConcreteMallocBlock
之后涉及到局部变量截获并超出作用域可访问时就会区分它们的区别。
那么在 main 函数中如何使用到这个初始化函数呢
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))
它进行了初始化后转换为函数指针类型赋值给了 blk 变量,而初始化传入的参数第一个是 __main_block_func_0
,它的参数含义就是一个方法的指针,而它的定义如下
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello world\n");}
看到了我们定义的打印方法,所以最终 Block 内部的方法都会转换为一个静态 C 语言函数,命名方式为 Block 语法所属函数名和 Block 语法在函数中出现的顺序,这里的参数 __cself
变量是一个 __main_block_impl_0
指针,后面执行时会看到它。
因此在 main 函数中用 (void *)__main_block_func_0
方法指针和 __main_block_desc_0_DATA
结构体指针初始化了这个函数指针变量。
接下来就是调用部分
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
简化后可以看作
(blk->FunPtr)(blk);
前面提过这个 FunPtr 在初始化时其实被赋值为了 (void *)__main_block_func_0
方法指针,这里就是对它的调用,而传入的 self 参数则是 blk 自身,符合这个方法的参数定义。
截获基本类型局部变量
首先看一段使用了非 __blocking
的局部变量的 Block 代码
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
int value1 = 256;
int value2 = 128;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, value2);};
value2 = 64;
fmt = "new val = %d\n";
blk();
return 0;
}
由于没有使用 __blocking
修饰符所以这里修改局部变量对于 Block 运行是不会有影响的,打印结果是
val = 128
那么它的实现代码如下
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int value2;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _value2, int flags=0) : fmt(_fmt), value2(_value2) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int value2 = __cself->value2; // bound by copy
printf(fmt, value2);}
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[])
{
int value1 = 256;
int value2 = 128;
const char *fmt = "val = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, value2));
value2 = 64;
fmt = "new val = %d\n";
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
这里基本过程和前面类似,主要关注对于局部变量的截获操作。可以看到在 __main_block_impl_0
方法的参数列表中多了两个参数,对应地在结构体 __main_block_impl_0
中也有两个成员变量 fmt 和 value2,而根据 OC 代码生成的方法函数 __main_block_func_0
只需要获取到 self 变量后就可以进行相关的打印操作了,因此截获自动变量就是在执行 Block 时,将 Block 所使用的自动变量值保存到 Block 结构体实例中。
由于截获后的数据并不能监听到原局部变量的值的变化,所以对原局部变量进行赋值或修改操作不能影响到 Block 结构体的成员变量,因此也不能改变 Block 内的值。
__Block 变量的截获操作
如果要实现在 Block 内部保存值或者响应外部值的变化,可以通过静态变量、静态全局变量、全局变量来实现,也可以使用 __block
修饰符。
首先看静态变量和全局变量在 Block 的实现中是如何实现的。
#import <Foundation/Foundation.h>
int global_val = 1;
static int static_global_val = 2;
int main(int argc, const char * argv[])
{
static int static_val = 3;
void (^blk)(void) = ^{
global_val += 1;
static_global_val += 1;
static_val += 1;
};
blk();
return 0;
}
它的实现预测如下
int global_val = 1;
static int static_global_val = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
global_val += 1;
static_global_val += 1;
(*static_val) += 1;
}
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[])
{
static int static_val = 3;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
对于全局变量,无论是静态还是非静态,都可以被作用域内所有函数访问,包括 Block 函数,所以可以看到没有对它们进行额外的操作。而对于局部静态变量,由于 Block 方法域内要实现对这个静态变量的修改赋值操作,因此要进行地址传递,将静态局部变量的地址作为参数传给 Block 结构体,在 Block 方法内通过地址修改静态局部变量的值。
接下来是 __block
修饰符的实现过程
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
__block int val = 10;
void (^blk)(void) = ^{
val = 20;
};
blk();
return 0;
}
它的实现预测如下
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 20;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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[])
{
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
可以看到,实现中把 __block
修饰的变量用一个 __Block_byref_val_0
结构体存储,这个结构体的定义如下
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
其中最重要的成员变量是 __Block_byref_val_0
指针,它的作用主要体现在可以使 Block 截获的变量超出其作用域,但是目前还没用到这一特性。
最终 __block
修饰的局部变量被初始化为一个结构体
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
然后对于具体的赋值操作,实现如下
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 20;
}
与静态局部变量的实现不同,这里的赋值操作是通过对 val 结构体实例的 __forwarding
指针的 val 成员变量进行赋值操作,当然在这里实际上就是对 val 结构体实例的 val 成员变量进行赋值,因此是可以改变原局部变量值的。
由于 block 变量的结构体 __Block_byref_val_0
被定义在全局范围,所以可以被多个 Block 复用。例如下面的代码
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
__block int val = 10;
void (^blk0)(void) = ^{
val = 20;
};
void (^blk1)(void) = ^{
val = 20;
};
return 0;
}
其实现预测如下
int main(int argc, const char * argv[])
{
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk0)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
void (*blk1)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_val_0 *)&val, 570425344));
return 0;
}
这样将 Block 与 block 变量分离,可以便捷实现一个 Block 使用多个 block 变量或者一个 block 变量被多个 Block 使用。
Block 存储域
根据上面的解读,Block 实际上是栈上的结构体实例,block 变量也是栈上的结构体实例,而前面提过 isa 指针指向的是 Block 的类对象,它一般有三个值
_NSConcreteStackBlock
_NSConcreteGlobalBlock
_NSConcreteMallocBlock
这三个值实际上表达了 Block 的存储域,_NSConcreteStackBlock
将 Block 存储在栈上,_NSConcreteGlobalBlock
将 Block 存储在数据区域,_NSConcreteMallocBlock
将 Block 存储在堆上。
要注意一点是,使用 clang 命令转换代码时可能会与实际实现有所不同。
简单来说,在 Linux 下一个应用程序的内存分布如下所示
区域 | 说明 |
---|---|
程序区域 .text | 编译后程序的主体,也就是程序的机器指令 |
数据区域 .data | 保存已初始化的全局变量 |
.bss | 保存只声明没有初始化的全局变量 |
堆 heap | 保存动态分配的内存,向高地址方向增长 |
栈 stack | 用于函数调用,保存函数参数、临时变量、返回地址等,向低地址方向增长 |
具体到 Block 中,定义在全局变量的 Block 以及在函数中但是没有截获局部变量的 Block 的 isa 都会被赋值为 _NSConcreteGlobalBlock
,也就是会被存储在数据区域,而截获了局部变量或是 block 变量的 Block 则有些复杂。前面提到 block 变量是分配在栈上的,因此如果其所属变量作用域结束了,这个 block 变量也就被废弃了,但是事实上 Block 依然可以提供对 block 变量的跨作用域访问,这种机制的实现是将 Block 和 block 变量从栈上复制到堆上,由于堆的生命周期是由应用程序控制的,因而可以实现跨作用域的访问效果。而这时 Block 的 isa 指针就会指向 _NSConcreteMallocBlock
了。
一般来说当我们向方法或函数的参数中传递 Block 时,需要对 Block 进行手动复制操作,也就是执行 copy 方法,这一方法是没有副作用的,因此可以多次对一个 Block 调用。
Block 类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock |
栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock |
程序的数据区域 | 什么也不做 |
_NSConcreteMallocBlock |
堆 | 引用计数增加 |
当我们返回一个 Block 类型的返回值时,如果 Block 捕获了一个局部变量,这种我们也需要进行手动 copy 操作。
blk_int func(id rate){
return [^(int count) {
return (int)rate * count;
} copy];
}
当然如果方法和函数已经复制了 Block 参数,就不需要手动复制了,包括以下情况
- Cocoa 框架的方法且方法名中含有 usingBlock 等
- GCD 的 API
block 变量存储域
上节提到 Block 会从栈复制到堆,同时也会把 block 变量从栈复制到堆上,并且持有这一 block 变量,增加引用计数,最终 block 变量会按照引用计数的方式被管理。最关键的一点是,当 block 变量被复制到堆上以后,栈上还存在的 block 变量的 __forwarding
指针会指向堆上的 block 变量,而堆上的 __forwarding
指针则依然指向堆上的 block 变量自身。
看一段代码
#import <Foundation/Foundation.h>
typedef int (^blk_t)(int);
int main(int argc, const char * argv[])
{
__block int val = 0;
void (^blk)(void) = [^{++val;} copy];
++val;
blk();
return 0;
}
这里定义了一个 block 变量,并且分别在 Block 内外对它进行自增一操作,它的实现预测如下
int main(int argc, const char * argv[])
{
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};
void (*blk)(void) = (void (*)())((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344)), sel_registerName("copy"));
++(val.__forwarding->val);
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
可以看到,在 Block 内部访问的是堆上的 block 变量,在 Block 外部访问的是栈上的 block 变量,但是由于都是通过 __forwarding
指针来访问的,所以实际效果都是访问到了堆上的 block 变量。
截获 OC 对象
对于 oc 对象也可以在 Block 中使用,并且会自动被 Block 截获。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
typedef void (^blk_t)(id obj);
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj){
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
}
这里我们手动 copy 了 Block,但是在 ARC 下会自动帮我们 copy 所不用担心程序会强制结束的问题。下面是预测的实现源码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id array;
__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;
}
};
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_qj_zz_x2p3n7nv4t1r7gfbpd1q80000gn_T_SimpleBlock_267687_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
}
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*/);}
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[])
{
typedef void (*blk_t)(id obj);
blk_t blk;
{
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"));
blk = (blk_t)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));
}
((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new")));
((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new")));
((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new")));
}
可以看到大致过程与前面几种情况类似,但在 main 函数中对 Block 变量调用了 copy 方法,这个也是符合逻辑的。
当 Block 被从栈复制到堆的时候,会调用 __main_block_copy_0
方法,这个方法实际上调用的是 _Block_object_assign
方法,它会对 Block 的引用计数增加一。而当 Block 没有引用的时候,会被废弃,此时会调用 __main_block_dispose_0
方法,它实际上调用的是 _Block_object_dispose
方法,类似于 release 方法,释放 Block 对象。
调用 _Block_object_assign
方法的时机,也就是将 Block 从栈复制到堆的时机有
- 调用 Block 的 copy 方法
- Block 作为函数返回值
- Block 赋值给 strong 变量或 Block 变量
- 方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API
总结
这里主要是对 《OC 高级编程》 第二章的总结,而第二章的论述很多是基于 MRC 的,实际上在 ARC 环境下,Block 一般都会直接分配在 _NSConcreteMallocBlock
上,因而很多 copy 操作不需要手动去做,一些会引发 crash 的操作也被做了保护,比如在作用域外,通过 block 对于一个数组进行操作,由于 block 被自动复制了,所以不会引发异常,对于 weak 变量也是同理。