block系列2-block的转C++实现

2018-04-24  本文已影响0人  Emiya_zhang

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块具体流程如下

  1. 定义用以表示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;
      }
    };
    
  2. 传参构造用以表示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内部修改变量该怎么办?

  1. 全局变量和静态全局变量直接通过变量名使用
  2. 静态局部变量block捕获其地址,通过地址访问
  3. __block修饰符修饰的自动变量,将会构造一个结构体储存其地址与值。后面会讲到处理步骤。

(2).global的block对象

两种情况将会把block对象储存于程序的data区

  1. 配置在全局变量区域的block对象
  2. 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 类型,所以唐巧结论兴许是正确的,但是我没有在官方文档中找到说明,暂时做个标记。

上一篇下一篇

猜你喜欢

热点阅读