block系列2-block的转C++实现
1.block的转C++实现
(1).栈区block示例
我们通过clang指令将我们的OCblock代码转为.cpp观察如何实现这一匿名函数。
clang -rewrite-objc test.m
#import <Foundation/Foundation.h>
int main(){
int a = 0;
int (^blk)(int) = ^(int b){return a+b;};
a++;
int c = blk(5);
return 0;
}
执行命令后得到test.cpp文件,拉到底部查看main
int main(){
int a = 0;
int (*blk)(int) = ((int (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a++;
int c = ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);
return 0;
}
我们看到blk被定义为一个函数实现指针,指向一个__main_block_impl_0结构体或者说对象地址,参数为(void *,&data,a)这里可以发现传入了a,即传入了一个复制的临时变量,但是这个临时变量是readonly的,不可赋值,尝试赋值编译器会报错。
查看__main_block_impl_0
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
显然这就是block块的实现对象了,我们知道C++中struct与class都可以定义对象,区别在于默认属性(public还是private的不同),
里面包含一个对象impl和一个静态对象指针Desc,一个int a变量。这里的int a与main中的无关,只是同名变量而已,在下面的__ main_block_impl_0构造函数块实现中,用初始化列表对block块中的a进行初始化,从main传参来看即main中的a初始化block块内的a变量。
构造函数还有一个fp指针和flags初始化 __ main_block_impl_0对象内impl对象的FuncPtr和Flags,对象指针desc初始化 __main_block_impl_0对象的Desc对象指针。
查看__ block_impl和__main_block_desc_0
struct __block_impl {
void *isa; //对象的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实现的块大小结构体
还差一个__main_block_func_0
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int b) {
int a = __cself->a; // bound by copy
return a+b;}
这下就很清晰了。
<1>定义block块时
int (*blk)(int) = ((int (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
定义block块具体流程如下
定义用以表示block块的 __ main_block_impl_0对象及构造函数
struct __main_block_impl_0 { struct __block_impl impl; //ISA指针、是否global、引用计数、函数实现地址 struct __main_block_desc_0* Desc; //指向记录块大小的静态结构体指针 int a; //copy自外部变量的a __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { //初始化impl结构体和Desc指针 impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
传参构造用以表示block块的 __ main_block_impl_0对象,并取得对象地址给blk
int (*blk)(int) = ((int (*)(int))&__main_block_impl_0((void *)main_block_func_0/*匿名函数地址*/, &main_block_desc_0_DATA/*定义的静态结构体地址*/, a/*传参机制实现所谓的捕获外部变量*/));
这里我们已经通过对象构造函数传参构造出一个有具体实现的block对象,其地址赋值给了blk指针,值得注意的是我们没有使用new/alloc,显然这个对象存活在栈区。
<2>调用block块
int c = ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);
流程非常清晰
通过blk->FuncPtr(blk,5)调用匿名函数,传入blk即block对象的self指针,5即匿名函数的参量
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int b) { int a = __cself->a; // bound by copy return a+b;}
我们可以看到,block匿名函数的整个构造和使用中,通过在block对象中定义一个同名变量,构造时传参赋值初始化实现所谓的截获自动变量,在实现函数中又定义了同名变量,同过传入的blk指针初始化使用。
<3>block内使用外部变量
如我们上边的代码所示,block对于自动变量的处理是通过传参复制了自动变量的值,并没有对自动变量进行操作。那我们想在block内部修改变量该怎么办?
- 全局变量和静态全局变量直接通过变量名使用
- 静态局部变量block捕获其地址,通过地址访问
- __block修饰符修饰的自动变量,将会构造一个结构体储存其地址与值。后面会讲到处理步骤。
(2).global的block对象
两种情况将会把block对象储存于程序的data区
- 配置在全局变量区域的block对象
- block表达式不使用截获的临时变量
(3).堆区的block对象
我们经常在函数内定义block对象,这种对象常常是存活于栈的block对象,作用域结束后block对象也消亡
那我们返回生成的block对象时,为了保持其生命周期,需要将其复制到堆上,ARC机制下将会调用_Block_copy(x);将其复制到堆区。
//
// 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);
ARC有效时,编译器在大多数情况下会判断进行栈区block对象到堆区block对象的复制,即调用_Block_copy(x);
唐巧在其博客谈Objective-C block的实现
在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
首先OC高阶编程中提到的栈区BLOCK导致的异常例子(P114)编译与运行并未出现异常,也的确显示为NSConcreteMallocBlock。我们在方法内创建的带有截获自动变量的Block对象也是NSConcreteMallocBlock 类型,所以唐巧结论兴许是正确的,但是我没有在官方文档中找到说明,暂时做个标记。