美文网首页
三年后再看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

    三年前,开始接触使用block的时候,觉得block的语法很怪异,也不理解block的原理,只是觉得block比代...

  • 半年后再看

    第一,有了新的朋友交际圈;第二,锻炼好了身体;第三,HK拿到了棒的录取;第四,coding,DA/DS业务能力提升...

  • iOS block 为什么官方文档建议用 copy 修饰

    一、block 的三种类型block 三种类型:全局 block,堆 block、栈 block。全局 block...

  • 十五年后再看

    我现在是一个帅气迷人玉树临风才高八斗颜值超纲身材魁梧成绩好学习好不早恋不猥亵等的四十好少年。我目前芳龄12,...

  • 多年后再看《金锁记》

    最近仔细地把十几年前拍得《金锁记》看了一遍,曾经看不懂的,现在似乎懂了一些。 还是以前的剧拍的有韵味和格调,整部剧...

  • Block

    一、Block本质 二、 BlocK截获变量 三、__block 修饰变量 四、Block内存管理 五、Block...

  • 关于block--你想了解的几乎都在这里了

    一.block定义二.block的本质三.block变量捕获(Capture)四.block的类型五.block的...

  • 三年后再看“清单”思维

    好久就看过《清单革命》和《为什么精英都是清单控》,昨天再次复读这两本书籍的简报,对清单的使用进行总结和反思:...

  • iOS Block

    Block的分类 Block有三种类型:全局Block,堆区Block,栈区Block 全局Block 当Bloc...

  • block使用及其底层原理

    一 block基本使用 二 block底层结构 三 block变量捕获 四 block的类型 五 block对象类...

网友评论

      本文标题:三年后再看block

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