详解 iOS 中的闭包(block)

block 的概念

这篇文章我打算来深究一下 OC 中的 block 到底是何方神圣。后面会介绍用可爱的 clang 指令来看看 block 底层的实现。

块对象 (block Object)是在 mac OS 10.6 及 iOS 4.0 平台下可以使用的功能,他不是 OC 而是 C 语言的功能实现。苹果公司的文档中将其称为块对象或 Block,在其他编程语言中,他与闭包(closure)的功能基本相同。

从 C 语言 block 说起

先从一个 C 函数说起

#include <stdio.h>

void myfunc(int m, void (^b)(void)) {
    printf("%d: ", m);
    b();
}

int global = 1000; // 外部变量(全局静态变量)

int main(int argc, const char * argv[]) {
    
    void (^block)(void);
    static int s = 20; // 局部静态变量
    int a = 20; // 自动变量(局部变量)
    
    block = ^{     // ============ 1
        printf("%d, %d, %d\n", global, s, a);
    };
    myfunc(1, block);
    
    s = 0;
    a = 0;
    global = 5000;
    myfunc(2, block);
    
    block = ^{       // ============ 2
        printf("%d, %d, %d\n", global, s, a);
    };
    myfunc(3, block);
    
    return 0;
}

仔细读代码,想想输出结果是什么
输出结果是

1: 1000, 20, 20
2: 5000, 0, 20
3: 5000, 0, 0

上面结果中,第一行没有问题,第二行是为什么呢?可以发现,变量 global 和 s 的值都改变了,但是局部变量 a 的值没有改变。第三行显示的是在代码 2 处代入块对象后的变量值,此处的变量 a 的值已经改变了。
综上,块对象貌似只在块句法中保存自动变量的值。(我们所说的自动变量其实就是函数内的局部变量,通常不用 static 关键字修饰)
块对象就是把可以执行的代码和代码中可访问的变量封装起来,使得之后可以进一步处理的包。
综上,总结一下

  • block 内部可以直接访问全局变量(外部变量)和静态变量,也可以直接改变其值
  • 但是对于局部变量,块句法会将其从 栈区 copy 一份到 堆区,所以即使最初的变量发生了变化,块内部在使用的时候也不知道。而且变量的值只可以被读取不能被改变。自动变量在运行时就相当于 const 修饰的变量。


    屏幕快照 2017-03-01 下午4.11.12.png

可以通过 __block 来完成在 block 内部对局部变量的修改。
注意:

__block 变量不是静态变量,它在块句法每次执行块句法时获取变量的内存区域。也就是说,__block 变量在同一个变量作用域中被多个 块对象 访问的时候,其实访问的是同一块内存区域。

OC 中 block 的注意点解析

块句法中使用其他任意实例对象

前面已经讲了块句法中有外部变量或自动变量时这些变量的行为,现在我们来介绍一下块句法内使用对象时的行为,特别是引用计数器的处理。

void (^cp)(void); // 可以保存块的静态变量

- (void)someMethod {
    id obj = ...; // 引用任意实例对象
    int n = 10;
    void (^block)(void) = ^{
        [obj calc: n];
    };
    // ...
    cp = [block copy];
}

如上代码,块对象在栈上生成,变量 obj 引用任何实例变量时,块对象内使用的变量 obj 也会访问同一个对象,这时实例变量的引用计数不会发生改变。接着块对象复制到堆区,实例对象的引用计数加 1,由于方法执行结束后自动变量 obj 也会消失,因此这时块对象就成为了所有者。注意实例对象是被共享的,不是复制的。所以不只是从块对象,从哪里都可以发送消息。


屏幕快照 2017-03-01 下午6.02.10.png
块句法中使用同一类的实例变量

先上代码

void (^cp)(void); // 可以保存块的静态变量

- (void)someMethod {
    int n = 10;
    void (^block)(void) = ^{
        [ivar calc: n]; // 注:ivar 为该类实例变量
    };
    // ...
    cp = [block copy];
}

这种情况下,当对象呗复制时,self 的引用计数会加 1,而非 ivar。注意,块句法中的实例变量为整数或实数时也是一样的(这点容易搞错)。


屏幕快照 2017-03-01 下午6.02.29.png
综上总结
  • 方法定义内的块句法中存在实例变量时,可以直接访问实例变量,也可以修改其值。(因为是指向同一块内存区域)
  • 方法定义内的块句法中存在实例变量时,如果被 copy 到堆区,self 引用计数会加 1。实例变量不一定是对象。
  • 块句法中存在非实例变量的实例对象时,被 copy 后,这个对象的引用计数会加 1。
  • 已经复制后,堆区中某个块对象即使再次收到 copy 方法,结果也只是块对象自身的引用计数 1。包含的对象的引用计数不变。
  • 复制的块对象在被释放时,也会向包含的对象发送 release。

OC 中的 block 到底是什么呢?

本着刨根问底的精神,就来一探究竟,block 到底是何方神圣。
我们创建一个纯净的 Command Line Tool 项目,在 main.m 中书写一下简单的代码:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        void (^block)() = ^{
            NSLog(@"======>%d", age);
        };
        age = 20;
        block();
    }
    return 0;
}

然后打开终端,cd 该目录下,键入

ZK$ clang -rewrite-objc main.m 

然后在该路径下生成 main.cpp 文件,打开后惊奇发现短短几句 OC 代码,竟然生成了 九万多行 C++ 代码,别怕,我们写的核心 block 代码其实也没多少行。拉到最下面,就是我们重写出来的 block C++ 代码,为了阅读方便,我对这些代码进行了稍微处理,比如去掉类型强转等干扰性代码,就得到了下面这一片精美的 C++ 代码,我还贴心地加了一些注释。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
    // 下面这些代码值这个结构体的构造函数
    // `int flags=0` 是默认值
    // `: age(_age)` C++ 语法,将 _age 传给 age 属性,可知在没有 __block 情况下,从外部传进来的 age 直接就赋值给这个结构体的 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 最终生成的一个函数体
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d0_jq4b136d16j6t2ktg954pmcw0000gn_T_main_0f9b1b_mi_0, age);
        }

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 argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 10;
        // 从下面这句代码得知,block 就是指向一个结构体的指针。
        // 参1:block 生成的函数
        // 参2:`__main_block_desc_0_DATA` 结构体的指针
        // 参3:将上面的自动变量直接传递进去
        void (*block)() = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age)); // 在这里是直接将 10 传递进去
        age = 20; // 该处的 age 赋值在 block 里面根本无法感知
        // 调用 block,`(block)->FuncPtr` 就是 `__main_block_impl_0` 函数
        ((block)->FuncPtr)(block);
    }
    return 0;
}

必要的说明已经在上面代码的注释中说的很明白,我来总结一下,定义 block 的时候,首先会生成一个结构体 __main_block_impl_0,他有三个参数,参1是 block 生成的函数__main_block_func_0,参2是结构体 __main_block_desc_0_DATA 的地址。参3 就是我们直接传递进去的自动变量。三个参数传递进去 __main_block_impl_0 后会直接出发其构造函数,上面注释说明很明确。
那么,目光转回 __main_block_func_0 函数,int age = __cself->age; 这句代码是将 age 属性直接取出来,而这个 age 就是我们刚一开始上面提到的参3传递进去的自动变量的值 10,固然打印出来的是 10,不是 20。

还不过瘾?那么我们 __block 修饰一下自动变量,看看有什么神奇的地方
注意啦,OC 代码改成如下

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block int age = 10;
        void (^block)() = ^{
            NSLog(@"======>%d", age);
        };
        age = 20;
        block();
    }
    return 0;
}

运行 clang 指令,让我们看看有哪些变化。

// 这个结构体用来修饰 __block 的自动变量,竟然发现了我们熟悉的老面孔 `isa`!说明他也是一个对象。
struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_d0_jq4b136d16j6t2ktg954pmcw0000gn_T_main_cff06d_mi_0, (age->__forwarding->age));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        // 变化1:age 不是用 int 修饰了,而是增加一个名为 `__Block_byref_age_0` 的结构体,详见上面这个结构体的定义有注释。
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {0, &age, 0, sizeof(__Block_byref_age_0), 10};
        // 变化2:注意,下面的参3的 age 多了个 `&` 符号取地址,说明 `__main_block_impl_0` 引用的是结构体 `__Block_byref_age_0`的指针,不向之前直接将自动变量的值传递进去了,这也就是为什么 定义 block 后外部自动变量修改了,block 内部依然可以读到最新值。同时,这样我们也可以在 block 内部修改外部自动变量的值。
        void (*block)() = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &age, 570425344));
        (age.__forwarding->age) = 20;
        ((block)->FuncPtr)(block);
    }
    return 0;
}

上面的主要变化已经在注释说明了,我再总结一下重要的变化:

  • 变化1:age 不是用 int 修饰了,而是增加一个名为 __Block_byref_age_0 的结构体,这个结构体用来修饰 __block 的自动变量,竟然发现了我们熟悉的老面孔 isa!说明他也是一个对象。
  • 变化2:__main_block_impl_0 的参3的 age 多了个 & 符号取地址,说明 __main_block_impl_0 引用的是结构体 __Block_byref_age_0的指针,不向之前直接将自动变量的值传递进去了,这也就是为什么 定义 block 后外部自动变量修改了,block 内部依然可以读到最新值。同时,这样我们也可以在 block 内部修改外部自动变量的值。
  • 变化3:还有像添加了 __main_block_copy_0__main_block_dispose_0 结构体等变化,先说到这吧。

总结一下

上面说了那么多,对 block 就不会那么陌生了吧,平常开发中当然我们很少遇到需要剖析源码才能解决的问题,但是凡事本着刨根问底的精神,保持一颗对底层的好奇和敬畏之心,探讨其中的乐趣,我想这才是一个合格的开发人员对待问题的正确打开方式吧。

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

推荐阅读更多精彩内容

  • 《Objective-C高级编程》这本书就讲了三个东西:自动引用计数、block、GCD,偏向于从原理上对这些内容...
    WeiHing阅读 9,728评论 10 69
  • iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,B...
    smile刺客阅读 2,245评论 2 26
  • 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这...
    小人不才阅读 3,729评论 0 23
  • Block基础回顾 1.什么是Block? 带有局部变量的匿名函数(名字不重要,知道怎么用就行),差不多就与C语言...
    YeeChain阅读 6,683评论 5 61
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,292评论 18 399