美文网首页
block系列2-block的转C++实现

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

作者: Emiya_zhang | 来源:发表于2018-04-24 15:42 被阅读0次

    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 类型,所以唐巧结论兴许是正确的,但是我没有在官方文档中找到说明,暂时做个标记。

    相关文章

      网友评论

          本文标题:block系列2-block的转C++实现

          本文链接:https://www.haomeiwen.com/subject/dwejlftx.html