三年后再看block

三年前,开始接触使用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的底层实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容