Blocks 是 C 语言的扩充功能,Apple 在 OS X Snow Leopard 和 iOS 4 中引入了这个新功能。
一句话来形容 Blocks:带有自动变量(局部变量)的匿名函数。
Block 相关源码:https://opensource.apple.com/source/clang/clang-800.0.42.1/src/projects/compiler-rt/lib/BlocksRuntime/
研究工具:clang
为了研究编译器的实现原理,我们需要使用 clang 命令。clang 命令可以将 Objetive-C 的源码改写成 C / C++ 语言的,借此可以研究 block 中各个特性的源码实现方式。
命令一:clang -rewrite-objc main.m -o main.c
命令二:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.c
Block 的实现
Block 在 OC 中的实现如下:
/* Revised new layout. */
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
从结构图中很容易看到 isa
,所以 OC 处理 Block 是按照对象来处理的。在 iOS 中,isa
常见的有 3 种:
-
_NSConcreteStackBlock
:只用到外部局部变量、成员属性变量,且没有强指针引用的 block 都是 StackBlock。StackBlock 的生命周期由系统控制的,一旦返回之后,就被系统销毁了。 -
_NSConcreteMallocBlock
:有强指针引用或 copy 修饰的成员属性引用的 block 会被复制一份到堆中成为 MallocBlock,没有强指针引用即销毁,生命周期由程序员控制。 -
_NSConcreteGlobalBlock
:没有用到外界变量或只用到全局变量、静态变量的 block 为 _NSConcreteGlobalBlock,生命周期从创建到应用程序结束。
另外在 GC 环境下还有 3 种:
_NSConcreteFinalizingBlock
_NSConcreteAutoBlock
_NSConcreteWeakBlockVariable
Block 捕获外部变量
说到外部变量,先了解一下 C 语言中的几种类型变量:自动变量、静态变量、静态全局变量、全局变量。
写出 Block 的测试代码:
#import
int global_i = 1;
static int static_global_j = 2;
int main(int argc, const char * argv[]) {
static int static_k = 3;
int val = 4;
void (^myBlock)(void) = ^{
global_i++;
static_global_j++;
static_k++;
val++;
NSLog(@"Block中 global_i = %d, static_global_j = %d, static_k = %d, val = %d", global_i, static_global_j, static_k, val);
};
global_i++;
static_global_j++;
static_k++;
val++;
NSLog(@"Block外 global_i = %d, static_global_j = %d, static_k = %d, val = %d", global_i, static_global_j, static_k, val);
myBlock();
return 0;
}
上面代码在编译器中会报一个错误,变量 val
在 Block 中是不可赋值的,需要指定 __block
。故先将 val++
注释起来,先测试其他类型的变量。
运行结果如下:
2019*** Block外 global_i = 2, static_global_j = 3, static_k = 4, val = 5
2019*** Block中 global_i = 3, static_global_j = 4, static_k = 5, val = 4
可以看到自动变量的值没有增加,而其他几个变量的值增加了。自动变量是什么时候被 Block 捕获进去的呢?
为了弄清楚,我们用 clang 先将代码转换为 C 代码再进行分析:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.c
转换后的代码如下:
#pragma clang assume_nonnull end
int global_i = 1;
static int static_global_j = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_k;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_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_k = __cself->static_k; // bound by copy
int val = __cself->val; // bound by copy
global_i++;
static_global_j++;
(*static_k)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kd_qrsf4xd10_qc1sx9sddh096w0000gn_T_main_538daf_mi_0, global_i, static_global_j, (*static_k), val);
}
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_k = 3;
int val = 4;
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
global_i++;
static_global_j++;
static_k++;
val++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kd_qrsf4xd10_qc1sx9sddh096w0000gn_T_main_538daf_mi_1, global_i, static_global_j, static_k, val);
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
首先全局变量 global_i
和静态全局变量 static_global_j
的值增加了,这一点很好理解,因为是全局的,作用域很广,所以可以在 Block 里面进行 ++
操作,Block 结束之后,它们的值依旧可以保存下来。
接下来看看自动变量和静态变量,在 __main_block_impl_0
中,可以看到静态变量 static_k
和自动变量 val
,被 Block 从外面捕获进来,成为 __main_block_impl_0
这个结构体的成员变量。并且在构造了函数中,自动变量和静态变量被捕获为成员变量并追加到了构造函数中。到此,__main_block_impl_0
结构体就是这样把自动变量捕获进来的。也就是说,在执行 Block 语法的时候,Block 语法表达式所使用的自动变量的值是被保存进了 Block 的结构体实例中,也就是 Block 自身中。
Block 捕获外部变量仅仅只捕获 Block 闭包里面会用到的值,其他用不到的值,它并不会去捕获。
再研究一下源码,注意到 __main_block_func_0
这个函数的实现。可以发现,系统自动给我们加上的注释 bound by copy
,自动变量 val
虽然被捕获进来了,但是是用 __cself->val
来访问的。Block 仅仅捕获了 val
的值,并没有捕获 val
的内存地址,所以在 __main_block_func_0
这个函数中即使我们重写这个自动变量 val 的值,依旧没法去改变 Block 外面自动变量 val
的值。
OC 可能是基于这一点,在编译层面就防止开发者可能犯的错误,因为自动变量没法在 Block 中改变外部变量的值,所以编译过程中就直接报编译错误。
小结一下:自动变量是以值传递方式传递到 Block 的构造函数里面去的,只捕获会用到的变量。由于只捕获自动变量的值,并非内存地址,所以 Block 内部不能改变自动变量的值。而局部的静态变量,Block 捕获的则是指针,因此可以改变静态变量的值。
根据官方文档我们可以了解到,苹果要求我们在自动变量前加入
__block
关键字(__block storage-class-specifier
存储域类说明符),就可以在 Block 里面改变外部自动变量的值了。
加上 __block 的改变
测试代码中取消 val++
的注释,并添加 __block
。
运行输出的结果变为:
2019*** Block外 global_i = 2, static_global_j = 3, static_k = 4, val = 5
2019*** Block中 global_i = 3, static_global_j = 4, static_k = 5, val = 6
看到结果是成功改变了自动变量的值了,用 clang 转换一下源码:
#pragma clang assume_nonnull end
int global_i = 1;
static int static_global_j = 2;
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;
int *static_k;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, __Block_byref_val_0 *_val, int flags=0) : static_k(_static_k), 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
int *static_k = __cself->static_k; // bound by copy
global_i++;
static_global_j++;
(*static_k)++;
(val->__forwarding->val)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kd_qrsf4xd10_qc1sx9sddh096w0000gn_T_main_72a7da_mi_0, global_i, static_global_j, (*static_k), (val->__forwarding->val));
}
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[]) {
static int static_k = 3;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 4};
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, (__Block_byref_val_0 *)&val, 570425344));
global_i++;
static_global_j++;
static_k++;
(val.__forwarding->val)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kd_qrsf4xd10_qc1sx9sddh096w0000gn_T_main_72a7da_mi_1, global_i, static_global_j, static_k, (val.__forwarding->val));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
在 __main_block_func_0
里面可以看到传递的是指针,并且在 __main_block_impl_0
中含有 val
的指针,最终是通过 (val->__forwarding->val)++;
来修改自动变量的值。
Block 的 copy
为什么 Block 用 copy 属性修饰?这个问题会关系到 Block 存储域的问题。
属性 Block 是 _NSConcreteMallocBlock
,内存分配在堆上,而赋值的 Block 通常可能是在栈上,因此需要使用 copy
拷贝到推上,来改变存储的位置。
附
Block的本质与使用
block详解
关于block(二)----为什么使用copy,为什么使用__block
Block原理,为什么block能捕获变量,为什么需要加__block