专栏首页ShaoYLiOS Block的本质(一)

iOS Block的本质(一)

iOS Block的本质(一)

1.对block有一个基本的认识

  • block本质上也是一个oc对象,他内部也有一个isa指针。block是封装了函数调用以及函数调用环境的OC对象。

2.探寻block的本质

  • 首先写一个简单的block
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void (^block)(int, int) =  ^(int a , int b){
            NSLog(@"this is a block! -- %d", age);
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
        };

        block(10, 10);
    }
    return 0;
}

3.查看其内部结构

  1. 使用命令行将代码转化为c++与OC代码进行比较 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m命令代码 // 编译后代码 int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int age = 10; void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10); } return 0; }
  2. 从以上c++代码中block的声明和定义分别与oc代码中相对应显示。将c++中block的声明和调用分别取出来查看其内部实现。
  3. 定义block变量代码 // 定义block变量代码 void(*block)(int ,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); // 可以简化为下列 // void (*block)(int, int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);
  4. 上述定义代码中,可以发现,block定义中调用了__main_block_impl_0函数,并且将__main_block_impl_0函数的地址赋值给了block。那么我们来看一下__main_block_impl_0函数内部结构。
  5. __main_block_imp_0结构体 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
  6. __main_block_imp_0结构体内有一个同名构造函数__main_block_imp_0,构造函数中对一些变量进行了赋值最终会返回一个结构体。
    • 那么也就是说最终将一个__main_block_imp_0结构体的地址赋值给了block变量
    • __main_block_impl_0结构体内可以发现__main_block_impl_0构造函数中传入了四个参数。(void *)__main_block_func_0、&__main_block_desc_0_DATA、age、flags。其中flage有默认值,也就说flage参数在调用的时候可以省略不传。而最后的 age(_age)则表示传入的_age参数会自动赋值给age成员,相当于age = _age。
    • 接下来着重看一下前面三个参数分别代表什么。
    1. (void *)__main_block_func_0参数 ```C++ static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) { int age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_0, age); NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_1); NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_2); NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_3); } ```
      • 在__main_block_func_0函数中首先取出block中age的值,紧接着可以看到四个熟悉的NSLog,可以发现这段代码恰恰是我们在block块中写下的代码。
      • 那么__main_block_func_0函数中其实存储着我们block中写下的代码。而__main_block_impl_0函数中传入的是(void *)__main_block_func_0,也就说将我们写在block块中的代码封装成__main_block_func_0函数,并将__main_block_func_0函数的地址传入了__main_block_impl_0的构造函数中保存在结构体内。
    2. &__main_block_desc_0_DATA参数 C++ 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)};
      • 我们可以看到__main_block_desc_0中存储着两个参数,reserved和Block_size,并且reserved赋值为0而Block_size则存储着__main_block_impl_0的占用空间大小。最终将__main_block_desc_0结构体的地址传入__main_block_func_0中赋值给Desc。
    3. age参数
      • age也就是我们定义的局部变量。因为在block块中使用到age局部变量,所以在block声明的时候这里才会将age作为参数传入,也就说block会捕获age,如果没有在block中使用age,这里将只会传入(void *)__main_block_func_0,&__main_block_desc_0_DATA两个参数。 这里可以根据源码思考一下为什么当我们在定义block之后修改局部变量age的值,在block调用的时候无法生效。
      • 因为block在定义的之后已经将age的值传入存储在__main_block_imp_0结构体中并在调用的时候将age从block中取出来使用,因此在block定义之后对局部变量进行改变是无法被block捕获的。
  7. 此时回过头来查看__main_block_impl_0结构体 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp;// block 内部代码块地址 Desc = desc;// 存储block 对象占用的内存大小 } };
  8. 首先我们看一下__block_impl第一个变量就是__block_impl结构体。 // __block_impl结构体内部 struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };
  9. 我们可以发现__block_impl结构体内部就有一个isa指针。因此可以证明block本质上就是一个oc对象。而在构造函数中将函数中传入的值分别存储在__main_block_impl_0结构体实例中,最终将结构体的地址赋值给block。
  10. 接着通过上面对__main_block_impl_0结构体构造函数三个参数的分析我们可以得出结论:
    1. __block_impl结构体中isa指针存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。
    2. block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址。
    3. Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存。
  11. 调用block执行内部代码 ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
  12. 通过上述代码可以发现调用block是通过block找到FunPtr直接调用,通过上面分析我们知道block指向的是__main_block_impl_0类型结构体,但是我们发现__main_block_impl_0结构体中并不直接就可以找到FunPtr,而FunPtr是存储在__block_impl中的,为什么block可以直接调用__block_impl中的FunPtr呢?
  13. 重新查看上述源代码可以发现,(__block_impl *)block将block强制转化为__block_impl类型的,因为__block_impl是__main_block_impl_0结构体的第一个成员,相当于将__block_impl结构体的成员直接拿出来放在__main_block_impl_0中,那么也就说明__block_impl的内存地址就是__main_block_impl_0结构体的内存地址开头。所以可以转化成功。并找到FunPtr成员。
  14. 上面我们知道,FunPtr中存储着通过代码块封装的函数地址,那么调用此函数,也就是会执行代码块中的代码。并且回头查看__main_block_func_0函数,可以发现第一个参数就是__main_block_impl_0类型的指针。也就是说将block传入__main_block_func_0函数中,便于重中取出block捕获的值。

3.验证block的本质

  • 验证block的本质是__main_block_impl_0结构体类型。
  1. 通过代码证明一下上述内容: #import <Foundation/Foundation.h> struct __main_block_desc_0 { size_t reserved; size_t Block_size; }; struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; }; int main(int argc, const char * argv[]) { @autoreleasepool { int age = 10; void (^block)(int, int) = ^(int a , int b){ NSLog(@"this is a block! -- %d", age); NSLog(@"this is a block!"); NSLog(@"this is a block!"); NSLog(@"this is a block!"); }; // 将底层的结构体强制转化为我们自己写的结构体,通过我们自定义的结构体探寻block底层结构体 struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block; block(10, 10); } return 0; }
  2. 通过打断点可以看出我们自定义的结构体可以被赋值成功,以及里面的值。
  1. 接下来断点来到block代码块中,看一下堆栈信息中的函数调用地址。Debuf workflow -> always show Disassembly
  1. 通过上图可以看到地址确实和FuncPtr中的代码块地址一样。

总结

  • block的原理是怎样的?本质是什么?
    • block本质上也是一个OC对象,它内部也有个isa指针
    • block是封装了函数调用以及函数调用环境的OC对象
    • block的底层结构如下图所示

引用

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • iOS---小经验分享

    用户1941540
  • OC语言Block 续

    用户1941540
  • # iOS Block的本质(三)

    struct __main_block_impl_0 { struct __block_impl impl; struct __main_blo...

    用户1941540
  • OC - Block 详解

    通过 Clang 将以下 Block 代码转换为 C++ 代码,来分析 Block 的底层实现。

    师大小海腾
  • 编译器 bug 系列(1)

    作为客户端开发者,我们每天都在接触编译器带来的便利,避免了手写机器码的麻烦,但是,某些情况下,编译器也会代码很多负面的作用。

    酷酷的哀殿
  • 5.Block的内存管理 内存管理内存管理block的循环引用

    陈雨尘
  • iOS block探究(一): 基础详解你要知道的block都在这里

    你要知道的block都在这里 转载请注明出处 https://cloud.tencent.com/developer/user/1605429 本文大纲 blo...

    WWWWDotPNG
  • 【iOS】block小记

    VV木公子
  • iOS底层原理总结 - 探寻block的本质(一)

    xx_Cc
  • Block 小结block  的 储存位置block  的循环引用

    陈雨尘

扫码关注云+社区

领取腾讯云代金券