美文网首页
详解 iOS 中的闭包(block)

详解 iOS 中的闭包(block)

作者: 念念不忘一个丫头的容 | 来源:发表于2020-07-16 09:07 被阅读0次

    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 修饰的变量。

      image

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

    image
    块句法中使用同一类的实例变量

    先上代码

    void (^cp)(void); // 可以保存块的静态变量
    
    - (void)someMethod {
        int n = 10;
        void (^block)(void) = ^{
            [ivar calc: n]; // 注:ivar 为该类实例变量
        };
        // ...
        cp = [block copy];
    }
    
    

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

    image
    综上总结
    • 方法定义内的块句法中存在实例变量时,可以直接访问实例变量,也可以修改其值。(因为是指向同一块内存区域)
    • 方法定义内的块句法中存在实例变量时,如果被 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 结构体等变化

    相关文章

      网友评论

          本文标题:详解 iOS 中的闭包(block)

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