美文网首页block
iOS源码分析:Block的本质

iOS源码分析:Block的本质

作者: 康小曹 | 来源:发表于2019-12-26 08:28 被阅读0次

    新建一个命令行项目,代码如下:

    #import <Foundation/Foundation.h>
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
        }
        return 0;
    }
    
    void test(){
        int a = 2;
        int b = 3;
     
        long (^myBlock)(void) = ^long() {
            return a * b;
        };
     
        long r = myBlock();
    }
    

    使用编译指令编译成 cpp 文件查看源码:

    xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc main.m -o main.cpp
    

    得到的原始代码中 test 函数的源码为:

    void test(){
        int a = 2;
        int b = 3;
    
        long (*myBlock)(void) = ((long (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a, b));
    
        long r = ((long (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    }
    

    去掉强制类型转换,简化代码:

    void test(){
        int a = 2;
        int b = 3;
    
        long (*myBlock)(void) = &__test_block_impl_0(__test_block_func_0, &__test_block_desc_0_DATA, a, b));
    
        long r = (myBlock->FuncPtr)(myBlock);
    }
    

    其实还是有点懵逼的,再认真分析下第一行代码:

    long (*myBlock)(void) = &__test_block_impl_0(__test_block_func_0, &__test_block_desc_0_DATA, a, b));
    

    从代码中可以看到这行代码定义了一个函数类型的指针myBlock,并且将其指向了__test_block_impl_0返回值的地址。这里把内存地址赋值给指针是指针用法的常规操作,就不多说了。于是乎就会想看看__test_block_impl_0这是个什么东东,源码如下:

    struct __test_block_impl_0 {
      struct __block_impl impl;
      struct __test_block_desc_0* Desc;
      int a;
      int b;
    
      __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    这里需要解释的是:

    1. __test_block_impl_0()函数和结构体同名,这里是 C++ 语法,C语言的结构体不允许存在函数;
    2. : a(_a), b(_b)冒号这种语法是 C++ 中的构造函数中初始化成员列表,也就是将入参直接赋值给成员 a 和 b,简化了代码。类似的语法在很多其他语言中也存在,比如 Dart;

    所以就可以得出结果,第一行代码的作用就是:创建一个__test_block_impl_0变量,并且赋值给myBlock

    这里预留个疑问:
    这个long (*myBlock)(void)是声明一个指向函数的指针。可是明明是指向结构体变量的指针,可以使用struct __test_block_impl_0 *p来写,代码如下:

    struct __test_block_impl_0 block;
    struct __test_block_impl_0 *myBlock = &block;
    

    可是问什么要声明成一个函数指针呢?先留着吧~

    继续,第一行看完了,知道是在做啥了,那现在就应该看看__test_block_impl_0到底是个啥玩意了,还是先看源码:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __test_block_impl_0 {
      // 特别注意,这里impl不是指针哦,所以相当于直接将__block_impl中的四个成员复制过来
      struct __block_impl impl;
      struct __test_block_desc_0* Desc;
      int a;
      int b;
    
      __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b) {
        // 默认赋值为_NSConcreteStackBlock,但是还有其他的类对象类型
        impl.isa = &_NSConcreteStackBlock;
        // 默认是0,不用理他
        impl.Flags = flags;
        // 这里就是封装了block内部逻辑的函数
        impl.FuncPtr = fp;
        // 计算size用的,不用理
        Desc = desc;
      }
    };
    
    long (*myBlock)(void) = ((long (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a, b));
    

    其实第一行代码可以简化成:

    // 原来的代码
    long (*myBlock)(void) = &__test_block_impl_0(__test_block_func_0, &__test_block_desc_0_DATA, a, b));
    
    // 简化:
    struct __test_block_impl_0 block;
    struct __test_block_impl_0 *myBlock = &block;
    myBlock->impl.isa = &_NSConcreteStackBlock;
    myBlock->impl.Flags = 0;
    myBlock->impl.FuncPtr = __test_block_func_0;
    myBlock->Desc = &__test_block_desc_0_DATA;
    myBlock->a = a;
    myBlock->b = b;
    

    这样就很清晰了,继续看__test_block_func_0__test_block_desc_0_DATA,看源码:

    // __test_block_func_0源码:
    static long __test_block_func_0(struct __test_block_impl_0 *__cself) {
      int a = __cself->a;
      int b = __cself->b;
      return a * b;
    }
    
    // __test_block_desc_0_DATA的源码:
    static struct __test_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
    

    从源码可知:

    1. __test_block_func_0将 block 内部的逻辑封装成了一个函数;
    2. __test_block_desc_0生成了一个__test_block_desc_0_DATA的变量,reserved为占位保留字段,不用管,而__test_block_impl_0结构体(也就是 block 结构体的第一个成员),的内存大小被赋值给了__test_block_impl_0

    这样就可以看到本质了:

    • block 本质是一个 C++ 结构体
      block 是通过 C++ 的结构体而不是 C 的结构体实现的。被捕获的变量会生成为 block 的成员变量。内部的成员变量impl包含了面向对象的基础和 block 内部代码逻辑的实现。而构造函数则执行了赋值操作,完成了结构体变量的初始化。
    • block 也是一个对象
      block 的第一个成员是__block_impl结构体,而__block_impl第一个成员就是isa指针,所以,block 的本质就是对象,默认指向_NSConcreteStackBlock类,也就是 block 本质是一个_NSConcreteStackBlock类的对象(默认情况下,后面还会解释)

    继续 go on,再看看第二行代码:

    long r = ((long (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    

    去掉强制类型转换后:

    long r = myBlock->FuncPtr(myBlock);
    

    也就是直接调用FuncPtr这个函数,参数为myBlock

    这里有两个疑问:

    1. 函数指针类型为什么不是直接调用而需要通过 ->来访问内部成员变量调用呢?
      正常来讲,函数指针的使用如下:
    long func(int a) {
        return a;
    }
    
    long (*p)(int) = &func;
    printf("%li\n", p(10)); // 结果为10
    

    但是呢,通过上面的源码分析可以看到 myBlock这个变量并不是一个指向函数的指针,而是一个 struct __test_block_impl_0 类型的指针,所以如果不看他的类型,正常使用->访问成员变量是逻辑正确的,所以这个问题本质仍然是为什么要用long (*myBlock)(void)类型的指针来指向struct __test_block_impl_0类型的指针,同上,暂时先不管~~~

    1. FuncPtr是内部成员变量impl的成员变量,为什么可以直接访问?
      正常来讲应该这样访问:
    long r = myBlock->impl.FuncPtr(myBlock);
    

    因为impl是结构体的第一个成员变量,所以其内存地址就是结构体变量的地址,也就是说这个地址就是impl成员变量的地址,三者是等价的,也就是说:myBlock = &myBlock.impl = &block(参照上文的最终简化的代码逻辑),所以这样直接调用impl.FuncPtr就是正确的。

    这个结论还可以通过内存地址来验证,test()函数中内容修改如下:

    void test(){
        int a = 2;
        int b = 3;
     
        long (^myBlock)(void) = ^long() {
            return a * b;
        };
        
        struct __test_block_impl_0 *p = (__bridge struct __test_block_impl_0 *)myBlock;
     
        long r = myBlock();
    }
    

    运行后打断点:

    FuncPtr内存地址
    继续断点至return a * b;,并设置 Debug Workflow为 Always Sow Disassembly,的到如下结果:
    FuncPtr内存地址
    也就是说三个地址相互吻合。

    备注

    1. myBlock 中的isa指向变成了NSMallocBlock,这个以后会继续讲解
    2. struct __test_block_impl_0 *p = (__bridge struct __test_block_impl_0 *)myBlock;这里在myBlock前面没有加 & ,因为myBlock本身就是一个指针,其值就是它指向的内存地址,如果加了 & 意思是取myBlock的内存地址,这样就不对了~

    总结

    1. Block 的本质是一个含有 isa 指针的结构体,且继承链为 xxxBlock -> NSBlock -> NSObject。所以Block可以直接当成对象来使用,调用各种对象方法;
    2. Block 内部的代码被封装成函数由 FuncPtr 指向并调用;
      至此,Block的基本原理完毕,下一篇Block进阶~

    相关文章

      网友评论

        本文标题:iOS源码分析:Block的本质

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