美文网首页
三年后再看block

三年后再看block

作者: 花生儿 | 来源:发表于2019-09-29 09:57 被阅读0次

    三年前,开始接触使用block的时候,觉得block的语法很怪异,也不理解block的原理,只是觉得block比代理更高级,会用block的人就牛逼。那时候看唐巧大神的博客,跟着他的博客学习。

    时光荏苒,现在在看唐巧大神的博客,已经很少看到他在更新技术相关的文章了,基本上都是在更新一些看过的书籍之类的,很明显他转管理了,应该还很成功。唐巧好多事情,都是走在了绝大多数程序员的前面,很有前瞻性。

    再看看巧大的block文章,当时觉得看起来特别费劲的。

    唐巧的文章主要是将block是怎么实现的,现在看来高级编程里面讲的要更详细,细节更多一些。但是在那个时候,大概6,7年前,他能总结到这个水平,还是很厉害,很超前的。

    现在回过头来,反思自己当初学习的时候,觉得当时的学习方法和心态都有很大的问题,当时学的费劲,很大程度上,是这两方面的问题。

    问题

    1. 没有看清楚他整体文章思路,因为有好多c语言的复杂代码都是通过clang编译的,并不需要完全记住,给当时看文章的时候的增加了很大的难度
    2. 站在了一个读者的角度,他这篇文章其实讲述的是一个实践的过程,所以,应该站在他的角度,最好能动手实践。

    方案

    1. 这次在重新开一遍文章
    2. 根据他提供的参考资料,看看自己是否能写出一遍跟他的文章类似的高质量block文章

    block和delegate的区别

    1. 首先,我想说的是blcok 和 delegate的区别,其实在iOS 开发中,使用block 和 delegate 的目的,基本都是为了实现回调。
      block 的语法特点是代码集中,是一个集中的代码块。基于它的这一个特点,block 比较适合作为api设计的一部分,比如网络请求,需要有一个异步回调的操作,去把异步下载下来的数据,发送给对应的接受者。
      delegate 的声明部分和实现部分是分开的,比如UITableViewDelegate的声明和实现分别在UITableView中和某一个UIViewController中,是分散代码块。
      delegate 这样设计适用于公共接口较多的情况,这样做也更容易解耦
      这个就是iOS 开发中,block和delegate最明显的一个区别了。
      2.也有另外一个区别,从性能上来说,block的运行成本,要比delegate的运行成本高。
      block出栈时,需要将使用的数据从栈内存赋值到堆内存。delegate只保存一个对象指针,直接回调,并没有额外消耗,想比C语言的函数指针,只多了一个查表动作。

    我觉得而面试的时候,基本上也就问道这里就结束了。因为后面的分析,真的挺麻烦的。

    block的实现原理

    1. block 是什么
      block 是含有自定义变量匿名函数
    2. 什么是匿名函数
      无论是c语言还是oc语言,我们正常声明一个函数的时候,都是需要定义函数名的。block可以声明匿名函数,用^来作为标识,其实在block的数据结构定义里面,后续还是会把匿名函数转换为正常的c语言函数
    3. 什么是自定义变量
      block 具有截获自动变量的功能,block的代码块里面如果有使用了外部的变量,block结构体中,会自动保存使用的外部变量。
      但是保存的代码块中的自动变量,不能修改,如果想修改,需要使用__block修饰外部变量。
    4. block 的结构定义
      block的结构定义这块的代码还有细节非常多,当时也是我看的非常乱的,而且现在也没有完全记住。我看巧大的博客,也是只记录了block的结构定义的关键点,然后通过clang将oc的源码改写成c语言进行分析的。

    下面我也来按巧神发现问题解决问题的思路分析一下

    • block的数据结构介绍
    • block的三种类型及相关的内存管理方式
    • block如何通过capture方式来访问函数外部的变量
    1. block的数据结构介绍
    image.png

    其实这个block的结构体相对来说是比较比较好理解的,

    struct Block_layout {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        struct Block_descriptor *descriptor;
        /* Imported variables. */
    };
    struct Block_descriptor {
        unsigned long int reserved;
        unsigned long int size;
        void (*copy)(void *dst, void *src);
        void (*dispose)(void *);
    };
    
    

    从图中我们可以看出来,block 的结构体有6个成员变量,但是这个跟clang 将objective-c转换过来的还是有区别的。clang装换的里面还有构造函数,block的实例是通过构造函数生成的。不过这个Block_layout看起来更通俗易懂。

    • isa
      任何对象都有isa指针,isa指针指向的是一个类对象,runtime中有详细的定义
    • flags
      表示block的附加信息,block copy 的实现代码中有对它的应用
    • reserved
      保留版本号,没有特别的用处
    • invoeke
      函数指针,指向具体的block函数实现的地址
    • Block-descriptor
      block的函数附加信息,包括大小,copy,dispose的函数指针。
    • variables
      capture 过来的变量,block能够访问它的外部局部变量,就是因为将这些变量复制到了结构体中。
      其实这还存在一个问题,为什么block已经把外部局部变量复制到了结构体中,但是在block代码块中修改外部局部变量,仍然要将外部局部变量用__block去修饰。
    用clong工具分析得出来的代码,这个地方巧大做了特别说明,就是为什么clang分析出来的代码和上面图中的代码有些不一样,clong里面的代码是嵌套的,而上图的代码却不是嵌套的。

    这个问题我以前也没有注意过,我当时以为是作者为了表述清楚,故意简化了,原来并不是这样。

    巧神给的代码例子,我copy过来了

    struct SampleA {
        int a;
        int b;
        int c;
    };
    struct SampleB {
        int a;
        struct Part1 {
            int b;
        };
        struct Part2 {
            int c;
        };
    };
    

    原因就是结构体本身,不带有任何的附加信息
    敲黑板,重要的事情,说三遍

    1. block 的三种类型以及相关的内存管理方式
      现在看,三种类型无非就是block的实例放到哪里,可以放到,堆上,栈上,还有全局代变量区。
      _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
      _NSConcreteStackBlock 保存到栈区的block,当函数返回时会被销毁。
      _NSConcreteMallocBlock 保存到堆区的block,当引用计数为0时被销毁。

    细节实现

    • 全局的静态block如何实现的
      建一个名为 block1.c 的源文件:
    #include <stdio.h>
    int main()
    {
        ^{ printf("Hello, World!\n"); } ();
        return 0;
    }
    

    然后在命令行中输入clang -rewrite-objc block1.c即可在目录中看到 clang 输出了一个名为 block1.cpp 的文件。该文件就是 block 在 c 语言实现,我将 block1.cpp 中一些无关的代码去掉,将关键代码引用如下:

    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;
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("Hello, World!\n");
    }
    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) };
    int main()
    {
        (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
        return 0;
    }
    

    下面我们就具体看一下是如何实现的。__main_block_impl_0 就是该 block 的实现,从中我们可以看出:

    1. 一个 block 实际是一个对象,它主要由一个 isa 和 一个 impl 和 一个 descriptor 组成。
    2. 在本例中,isa 指向 _NSConcreteGlobalBlock, 主要是为了实现对象的所有特性,在此我们就不展开讨论了。
    3. 由于 clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC。所以这里我们看到 isa 指向的还是_NSConcreteStackBlock。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类型,具体可以看 《objective-c-blocks-quiz》 第二题的解释。
    4. impl 是实际的函数指针,本例中,它指向 __main_block_func_0。这里的 impl 相当于之前提到的 invoke 变量,只是 clang 编译器对变量的命名不一样而已。
    5. descriptor 是用于描述当前这个 block 的附加信息的,包括结构体的大小,需要 capture 和 dispose 的变量列表等。结构体大小需要保存是因为,每个 block 因为会 capture 一些变量,这些变量会加到 __main_block_impl_0 这个结构体中,使其体积变大。在该例子中我们还看不到相关 capture 的代码,后面将会看到。

    这块巧大说的很细节了。几年前是真没看懂。现在感觉还可以。

    • 存在栈上的block的实现
      我们另外新建一个名为 block2.c 的文件,输入以下内容:
    #include <stdio.h>
    int main() {
        int a = 100;
        void (^block2)(void) = ^{
            printf("%d\n", a);
        };
        block2();
        return 0;
    }
    

    用之前提到的 clang 工具,转换后的关键代码如下:

    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;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        int a = __cself->a; // bound by copy
        printf("%d\n", a);
    }
    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)};
    int main()
    {
        int a = 100;
        void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
        ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
        return 0;
    }
    

    1.isa指针指向了_NSConcreteStackBlock,说明这是一个分配到栈上的实例。
    2.main_block_impl_0 中增加了一个变量 a,在 block 中引用的变量 a 实际是在申明 block 时,被复制到 main_block_impl_0 结构体中的那个变量 a。因为这样,我们就能理解,在 block 内部修改变量 a 的内容,不会影响外部的实际变量 a。
    3.main_block_impl_0 中由于增加了一个变量 a,所以结构体的大小变大了,该结构体大小被写在了 main_block_desc_0 中。
    修改上面的源码,在变量前面增加 __block 关键字:

    #include <stdio.h>
    int main()
    {
        __block int i = 1024;
        void (^block1)(void) = ^{
            printf("%d\n", i);
            i = 1023;
        };
        block1();
        return 0;
    }
    

    查看转换后的代码

    struct __Block_byref_i_0 {
        void *__isa;
        __Block_byref_i_0 *__forwarding;
        int __flags;
        int __size;
        int i;
    };
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        __Block_byref_i_0 *i; // by ref
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        __Block_byref_i_0 *i = __cself->i; // bound by ref
        printf("%d\n", (i->__forwarding->i));
        (i->__forwarding->i) = 1023;
    }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
    static struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;
        void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
        void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    int main()
    {
        __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
        void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344);
        ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
        return 0;
    }
    

    转换后的代码,明显比之前增加了好多代码

    1. 源码中增加了Block_byref_i_0 的结构体,这个结构体是用来保存截获并且要修改的变量i的。
      注意,当不用__block修饰的时候,__main_block_impl_0结构体中只是增加一个变量,而用__block修饰的时候,__main_block_impl_0结构体中又增加了一个结构体Block_byref_i_0
    2. __Block_byref_i_0 结构体中带有 isa,说明它也是一个对象。
    3. main_block_impl_0 中引用的是 Block_byref_i_0 的结构体指针,这样就可以达到修改外部变量的作用。
    4. 我们需要负责 Block_byref_i_0 结构体相关的内存管理,所以 main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改相应变量的引用计数。
    • NSConcreteMallocBlock 类型的 block 的实现
      NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,因为默认它是当一个 block 被 copy 的时候,才会将这个 block 复制到堆中。以下是一个 block 被 copy 时的示例代码,可以看到,在第 8 步,目标的 block 类型被修改为 _NSConcreteMallocBlock。
    static void *_Block_copy_internal(const void *arg, const int flags) {
        struct Block_layout *aBlock;
        const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
        // 1
        if (!arg) return NULL;
        // 2
        aBlock = (struct Block_layout *)arg;
        // 3
        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            // latches on high
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
        // 4
        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            return aBlock;
        }
        // 5
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
        // 6
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // 7
        result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 1;
        // 8
        result->isa = _NSConcreteMallocBlock;
        // 9
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
    

    变量的复制

    1. 对于block 外部变量的引用,block默认是将其复制到block的结构体中来实现访问。
    image.png

    2.对于用__block修饰的外部变量引用,block是复制其应用地址来实现访问的。


    image.png

    ARC对block的影响

    在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

    原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。证明方式是以下代码在 XCode 中,会输出 <__NSMallocBlock__: 0x100109960>。在苹果的 官方文档 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。

    #import <Foundation/Foundation.h>
    int main(int argc, const char * argv[])
    {
        @autoreleasepool {
            int i = 1024;
            void (^block1)(void) = ^{
                printf("%d\n", i);
            };
            block1();
            NSLog(@"%@", block1);
        }
        return 0;
    }
    

    唐巧认为这么做的原因是,由于 ARC 已经能很好地处理对象的生命周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。

    花了一下午时间,又重新梳理了block,目前已经基本都可以看懂了,以后还要更加深入的了解block的底层实现。

    相关文章

      网友评论

          本文标题:三年后再看block

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