详解 iOS 中的闭包(block)

block 的概念

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

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

从 C 语言 block 说起

先从一个 C 函数说起

#include 

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 也会消失,因此这时块对象就成为了所有者。注意实例对象是被共享的,不是复制的。所以不只是从块对象,从哪里都可以发送消息。


详解 iOS 中的闭包(block)_第1张图片
屏幕快照 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。注意,块句法中的实例变量为整数或实数时也是一样的(这点容易搞错)。


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

OC 中的 block 到底是什么呢?

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

#import 

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 

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

你可能感兴趣的:(详解 iOS 中的闭包(block))