美文网首页
iOS开发总结-Block(二)

iOS开发总结-Block(二)

作者: 云逸枫林 | 来源:发表于2017-09-21 18:16 被阅读64次

    前言

    上一篇文章主要是介绍在iOS开发过程中遇到的Block的具体结构和类型,以及相关操作的影响。
    这一篇文章主要是探究 Block 的实现,以及 __block 的原理。
    注意:本文中实例都是在ARC环境下运行的。

    作者使用的环境是:

    • Xcode 9正式版。
    • 真机调试 iPhone 6Plus 10.3.3
    • MacBook Pro 10.12.6

    Block 的实现

    关于block的结构,上一篇文章已经说过了,但是我们并没有具体的使用。
    这次我将使用 clang 工具查看 Objective-C 代码用c语言的实现,并对block的具体实现进行分析。

    需要注意的是clang转化的文件只能作为研究的参考,在实际运行中,还是和源文件有所区别的。

    还是熟悉的代码:

    typedef void(^TestBlockExample)(void);
    
        TestBlockExample block1 = ^{
            printf("Hello, World!\n");
        };
    

    补充一下使用clang工具的命令(后面的BlockTest.m是具体的文件名):

    clang -rewrite-objc BlockTest.m
    
    image.png

    代码太多,这里只取出关键部分进行分析。

    从下面的选中部分可以看出来 block 主要分成三个部分:
    __BlockTest__test_block_impl_0、
    __BlockTest__test_block_func_0、
    __BlockTest__test_block_desc_0_DATA

    下面我们一个个来分析:
    先看最简单的

    • __BlockTest__test_block_desc_0_DATA
    static struct __BlockTest__test_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0)};
    

    从这里就可以看出其实这个结构体就包含两个 size_t 信息。一个是保留数 reserved ,默认就是0,另一个是 Block_size 就是 __BlockTest__test_block_impl_0 结构体的大小。这个地方没啥好说的,我们看下一个:

    • __BlockTest__test_block_func_0
    static void __BlockTest__test_block_func_0(struct __BlockTest__test_block_impl_0 *__cself) {
            printf("Hello, World!\n");
        }
    

    这一部分就相当于临时创建了一个函数,供block实际执行的时候调用。

    • __BlockTest__test_block_impl_0
    struct __BlockTest__test_block_impl_0 {
      struct __block_impl impl;
      struct __BlockTest__test_block_desc_0* Desc;
      __BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    这里面包含了一个 impl 是 __block_impl 结构体,定义如下:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    

    还有个 Desc 其实就是上面说的 __BlockTest__test_block_desc_0_Data,是block的size信息,下面 __BlockTest__test_block_impl_0 这也是个block结构,就相当于 __BlockTest__test_block_impl_0 的构造函数了。根据传进来的参数对 impl 和 Desc 进行赋值。

    下面我们更改一下代码,让block捕获外部变量,根据上一篇的内容,我们已经预见了 block将会是 _ NSMallocBlock 类型,但是这里面的block实际上还是在栈中,所以isa指向是_NSConcreteStackBlock:

        int a = 6;
        TestBlockExample block1 = ^{
            printf("Hello, World! %d\n", a);
        };
    
    image.png

    我们看到其实多出来的也就是标注的几个地方,中间还贴心的给了注释 “bound by copy” 这里可以简单理解为“值拷贝”,实际上这个地方已经将变量a存到了block的结构体中,外部对变量a以后的修改都不会影响到block结构体中的a。下面讲到 __block 的时候还会探究这个地方。
    言归正传,__BlockTest__test_block_impl_0 里面多出了 a 变量,其实这样看来,捕获一个没有被 __block 修饰的变量和普通的block区别不是很大。好,下面我们看一下 __block 的影响。

    - (void)test {
        int a = 6;
        TestBlockExample block1 = ^{
            printf("Hello, World! %d\n", a);
        };
        a = 8;
        block1();
    }
    

    这段代码的运行结果就是

    Hello, World! 6
    

    下面我们改一下代码:

    - (void)test {
        __block int a = 6;
        TestBlockExample block1 = ^{
            printf("Hello, World! %d\n", a);
        };
        a = 8;
        block1();
    }
    

    运行结果

    Hello, World! 8
    

    我们都知道这就是 __block 的功劳。那具体 __block 做了什么呢,我们用clang看一下:

    image.png

    简直了,没想到一个__block做了这么多改变,我做了个文件比对:

    image.png

    使用__block以后大致改变的就是:

    • 变量a的类型变了
    __block 前 :int a;
    __block 后 :__Block_byref_a_0 *a; // by ref
    

    这里面的__Block_byref_a_0就是下面的这个结构体:

    • 增加了新的结构体
    struct __Block_byref_a_0 {
      void *__isa;
    __Block_byref_a_0 *__forwarding;
     int __flags;
     int __size;
     int a;
    };
    

    新增的结构体也可以看成是一个对象。
    在 __Block_byref_a_0 结构体中我们可以看到成员变量__forwarding,它持有指向该实例自身的指针;
    另外还有变量a,变量a在这里只是一个成员了;
    这就相当于比之前不加__block修饰的时候多了4个成员变量。

    • 构造的时候 入参变了
    __block 前 :__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    __block 后 :__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    

    主要是 传入的变量值由 _a 变成了 _a->__forwarding,这有什么意义呢,上面说了__forwarding持有指向__Block_byref_a_0实例的指针,这为访问和修改外部变量创造了条件。

    • 生成函数体的时候(__BlockTest__test_block_func_0)
    __block 前 :
    int a = __cself->a; // bound by copy 值拷贝
            printf("Hello, World! %d\n", a); // 使用的时候直接使用值
    
    __block 后 :
    __Block_byref_a_0 *a = __cself->a; // bound by ref (引用)地址拷贝
            printf("Hello, World! %d\n", (a->__forwarding->a));// 使用的时候需要通过__forwarding指针访问变量
    

    这个地方一开始我觉得多此一举,因为直接访问地址不行么,为啥还要加个__forwarding指针“中转”一下,后来也是查阅了一些资料:
    其实这样做的目的是防止栈中的block在出栈的时候,其所持有的变量也会被回收,这样在栈中所做的修改也不会保存到堆里,__block也就没了意义。所以在block被copy到堆中的时候,其实栈中的__forwarding的指向也随之改变,和堆中的__forwarding同时指向了堆中的实例(Block_byref_a_0)。

    这样无论是栈中的 block 或者堆中的 block,各自使用 __forwarding 来修改变量,就都可以保留下来了。

    在栈中的时候大致是这样的情况:

    image.png

    copy到堆中的时候的情况:

    image.png
    • 增加了两个函数 copy 和 dispose
    static void __BlockTest__test_block_copy_0(struct __BlockTest__test_block_impl_0*dst, struct __BlockTest__test_block_impl_0*src) {
    _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static void __BlockTest__test_block_dispose_0(struct __BlockTest__test_block_impl_0*src) {
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    

    这两个函数类似于MRC 中retain和release的效果。

    • __BlockTest__test_block_desc_0_DATA 的sizeof 计算block的成员增加了
    __block 前 :
    __BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0)};
    
    __block 后 :
    __BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0), __BlockTest__test_block_copy_0, __BlockTest__test_block_dispose_0};
    
    • 变量声明时的变化
    __block 前 :int a = 6;
    
    __block 后 :__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 6};
    
    

    后面增加的这些就是为了给__Block_byref_a_0填充数据,以备后续修改。

    • 初始化block的变化
    __block 前 :
    TestBlockExample block1 = ((void (*)())&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, a));
    
    __block 后 :
    TestBlockExample block1 = ((void (*)())&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    

    真正的变化就是 最后一个参数 ‘a’ 变成了 ‘(__Block_byref_a_0 *)&a, 570425344’
    不过这后面的 ‘570425344’ 我一直不明白是什么意思。

    • 修改变量的变化
    __block 前 :a = 8;
    __block 后 :(a.__forwarding->a) = 8;
    

    a.__forwarding指向了自身。
    在block构造的时候我们用到了a.__forwarding,
    在使用的时候(__BlockTest__test_block_func_0)也是通过(a->__forwarding->a)这样的方式找到最终修改过的数值‘8’。
    能做到这样的原因上面已经说过了,这里复述一下:
    栈上的__block变量复制到堆上时,会将成员变量__forwarding的值替换为复制到堆上的__block变量用结构体实例的地址。所以“不管__block变量配置在栈上还是堆上,都能够正确的访问该变量”,这也是成员变量__forwarding存在的理由。参照上面的图。。。

    好了,到这里__block的作用已经讲的差不多了,剩下的更深层次的东西,暂时不知道怎么入手,其实关于block的知识还有很多,比如在MRC的情况下是怎么样的,还有静态变量和全局变量之类的,本文都没有讨论,下次有机会在研究吧,另外下面参考的几篇文章中也有相关的介绍。


    参考的相关资料

    Block源码解析和深入理解
    关于Block再啰嗦几句
    深入理解Block之Block的类型
    深入研究Block捕获外部变量和__block实现原理
    iOS Block源码分析系列(二)————局部变量的截获以及__block的作用和理解

    相关文章

      网友评论

          本文标题:iOS开发总结-Block(二)

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