iOS Block 底层实现详解

Blocks 是 C 语言的扩充功能,Apple 在 OS X Snow Leopard 和 iOS 4 中引入了这个新功能。

一句话来形容 Blocks:带有自动变量(局部变量)的匿名函数。

Block 相关源码:https://opensource.apple.com/source/clang/clang-800.0.42.1/src/projects/compiler-rt/lib/BlocksRuntime/

研究工具:clang
为了研究编译器的实现原理,我们需要使用 clang 命令。clang 命令可以将 Objetive-C 的源码改写成 C / C++ 语言的,借此可以研究 block 中各个特性的源码实现方式。
命令一:clang -rewrite-objc main.m -o main.c
命令二:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.c

Block 的实现

Block 在 OC 中的实现如下:

/* Revised new layout. */
struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};
iOS Block 底层实现详解_第1张图片

从结构图中很容易看到 isa,所以 OC 处理 Block 是按照对象来处理的。在 iOS 中,isa 常见的有 3 种:

  • _NSConcreteStackBlock:只用到外部局部变量、成员属性变量,且没有强指针引用的 block 都是 StackBlock。StackBlock 的生命周期由系统控制的,一旦返回之后,就被系统销毁了。
  • _NSConcreteMallocBlock:有强指针引用或 copy 修饰的成员属性引用的 block 会被复制一份到堆中成为 MallocBlock,没有强指针引用即销毁,生命周期由程序员控制。
  • _NSConcreteGlobalBlock:没有用到外界变量或只用到全局变量、静态变量的 block 为 _NSConcreteGlobalBlock,生命周期从创建到应用程序结束。

另外在 GC 环境下还有 3 种:

  • _NSConcreteFinalizingBlock
  • _NSConcreteAutoBlock
  • _NSConcreteWeakBlockVariable

Block 捕获外部变量

说到外部变量,先了解一下 C 语言中的几种类型变量:自动变量、静态变量、静态全局变量、全局变量。

写出 Block 的测试代码:

#import 

int global_i = 1;
static int static_global_j = 2;

int main(int argc, const char * argv[]) {
    static int static_k = 3;
    int val = 4;
    void (^myBlock)(void) = ^{
        global_i++;
        static_global_j++;
        static_k++;
        val++;
        NSLog(@"Block中 global_i = %d, static_global_j = %d, static_k = %d, val = %d", global_i, static_global_j, static_k, val);
    };
    
    global_i++;
    static_global_j++;
    static_k++;
    val++;
    NSLog(@"Block外 global_i = %d, static_global_j = %d, static_k = %d, val = %d", global_i, static_global_j, static_k, val);
    
    myBlock();
    return 0;
}

上面代码在编译器中会报一个错误,变量 val 在 Block 中是不可赋值的,需要指定 __block。故先将 val++ 注释起来,先测试其他类型的变量。

运行结果如下:

2019*** Block外 global_i = 2, static_global_j = 3, static_k = 4, val = 5
2019*** Block中 global_i = 3, static_global_j = 4, static_k = 5, val = 4

可以看到自动变量的值没有增加,而其他几个变量的值增加了。自动变量是什么时候被 Block 捕获进去的呢?

为了弄清楚,我们用 clang 先将代码转换为 C 代码再进行分析:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.c

转换后的代码如下:

#pragma clang assume_nonnull end

int global_i = 1;
static int static_global_j = 2;


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_k;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_k = __cself->static_k; // bound by copy
  int val = __cself->val; // bound by copy

        global_i++;
        static_global_j++;
        (*static_k)++;

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kd_qrsf4xd10_qc1sx9sddh096w0000gn_T_main_538daf_mi_0, global_i, static_global_j, (*static_k), val);
    }

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[]) {
    static int static_k = 3;
    int val = 4;
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));

    global_i++;
    static_global_j++;
    static_k++;
    val++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_kd_qrsf4xd10_qc1sx9sddh096w0000gn_T_main_538daf_mi_1, global_i, static_global_j, static_k, val);

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

首先全局变量 global_i 和静态全局变量 static_global_j 的值增加了,这一点很好理解,因为是全局的,作用域很广,所以可以在 Block 里面进行 ++ 操作,Block 结束之后,它们的值依旧可以保存下来。

接下来看看自动变量和静态变量,在 __main_block_impl_0 中,可以看到静态变量 static_k 和自动变量 val,被 Block 从外面捕获进来,成为 __main_block_impl_0 这个结构体的成员变量。并且在构造了函数中,自动变量和静态变量被捕获为成员变量并追加到了构造函数中。到此,__main_block_impl_0 结构体就是这样把自动变量捕获进来的。也就是说,在执行 Block 语法的时候,Block 语法表达式所使用的自动变量的值是被保存进了 Block 的结构体实例中,也就是 Block 自身中。

Block 捕获外部变量仅仅只捕获 Block 闭包里面会用到的值,其他用不到的值,它并不会去捕获。

再研究一下源码,注意到 __main_block_func_0 这个函数的实现。可以发现,系统自动给我们加上的注释 bound by copy,自动变量 val 虽然被捕获进来了,但是是用 __cself->val 来访问的。Block 仅仅捕获了 val 的值,并没有捕获 val 的内存地址,所以在 __main_block_func_0 这个函数中即使我们重写这个自动变量 val 的值,依旧没法去改变 Block 外面自动变量 val 的值。

OC 可能是基于这一点,在编译层面就防止开发者可能犯的错误,因为自动变量没法在 Block 中改变外部变量的值,所以编译过程中就直接报编译错误。

小结一下:自动变量是以值传递方式传递到 Block 的构造函数里面去的,只捕获会用到的变量。由于只捕获自动变量的值,并非内存地址,所以 Block 内部不能改变自动变量的值。而局部的静态变量,Block 捕获的则是指针,因此可以改变静态变量的值。

根据官方文档我们可以了解到,苹果要求我们在自动变量前加入
__block 关键字(__block storage-class-specifier 存储域类说明符),就可以在 Block 里面改变外部自动变量的值了。

加上 __block 的改变

测试代码中取消 val++ 的注释,并添加 __block

运行输出的结果变为:

2019*** Block外 global_i = 2, static_global_j = 3, static_k = 4, val = 5
2019*** Block中 global_i = 3, static_global_j = 4, static_k = 5, val = 6

看到结果是成功改变了自动变量的值了,用 clang 转换一下源码:

#pragma clang assume_nonnull end

int global_i = 1;
static int static_global_j = 2;

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_k;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, __Block_byref_val_0 *_val, int flags=0) : static_k(_static_k), val(_val->__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_val_0 *val = __cself->val; // bound by ref
  int *static_k = __cself->static_k; // bound by copy

        global_i++;
        static_global_j++;
        (*static_k)++;
        (val->__forwarding->val)++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kd_qrsf4xd10_qc1sx9sddh096w0000gn_T_main_72a7da_mi_0, global_i, static_global_j, (*static_k), (val->__forwarding->val));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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[]) {
    static int static_k = 3;
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 4};
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, (__Block_byref_val_0 *)&val, 570425344));

    global_i++;
    static_global_j++;
    static_k++;
    (val.__forwarding->val)++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_kd_qrsf4xd10_qc1sx9sddh096w0000gn_T_main_72a7da_mi_1, global_i, static_global_j, static_k, (val.__forwarding->val));

    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

__main_block_func_0 里面可以看到传递的是指针,并且在 __main_block_impl_0 中含有 val 的指针,最终是通过 (val->__forwarding->val)++; 来修改自动变量的值。

Block 的 copy

为什么 Block 用 copy 属性修饰?这个问题会关系到 Block 存储域的问题。

属性 Block 是 _NSConcreteMallocBlock,内存分配在堆上,而赋值的 Block 通常可能是在栈上,因此需要使用 copy 拷贝到推上,来改变存储的位置。

  • Block的本质与使用

  • block详解

  • 关于block(二)----为什么使用copy,为什么使用__block

  • Block原理,为什么block能捕获变量,为什么需要加__block

你可能感兴趣的:(iOS Block 底层实现详解)