美文网首页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