深入理解 Objective-C ☞ Block

0.前言

日常开发中经常会用到 Block,但如果对它的底层实现没有深入地挖掘过,就不能算是真正掌握,本篇就来探究一下 Block 的底层实现原理。

1.举个 🌰

先来看一个例子,下边是一种简单的 block 使用场景: 无参数、无返回值的 block。

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 30;
        // 创建
        MyBlock blk = ^{
            NSLog(@"My age is %d .", age);
        };
        // 执行
        blk();
    }
    return 0;
}

2.Block的实质

为了探究 Block 的本质,我们需要借助 clang 将含有 Block 语法的源代码转换成 C++ 代码

2.1 Block 的底层结构

终端执行 $ clang -rewrite-objc main.m 命令,就可以将 main.m 文件编译生成 main.cpp 文件,这里截取了 main.cpp 文件中与 block 相关的代码,并添加了部分注释:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

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)};

// Block 的结构体
struct __main_block_impl_0 {
    
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
    
    // 构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock; 
        impl.Flags = flags;
        impl.FuncPtr = fp;
        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_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}

// main() 函数
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ 
    { 
        __AtAutoreleasePool __autoreleasepool;
        
        int age = 30;

        MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

下面开始一步步讨论源码。从 main() 函数开始,关于自动释放池的代码不在此处讨论,先看一下 block 的创建过程:

MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 简化后的代码: 
MyBlock blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);

我们注意到这里实际上有两个函数:__main_block_impl_0()__main_block_func_0()

先来看后者,具体代码如下,实际是 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_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}

然后搜索前一个函数的函数名 __main_block_impl_0,发现它位于下边这个结构体里边:

struct __main_block_impl_0 {
    
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;    

    // 构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;  // 指明该 block 的类型(此处是栈上的 block)。
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

而这个结构体就是 block 经编译后得到的结构,很明显 __main_block_impl_0() 是它的构造函数,我们发现,这个构造函数里边都是在给 block 的前 3 个元素 (2 个结构体和一个 int age) 赋值。

第一个元素 impl ,它的组成是这样的:

struct __block_impl {
    void *isa;       // 用于说明 block 的类型
    int Flags;       // 标识位
    int Reserved;    // 保留字段
    void *FuncPtr;   // 指针
};
  • FuncPtr 是一个指针,根据名字推断应该是一个函数指针,结合 main() 函数中执行构造函数创建 block 的过程可以看出,FuncPtr 指向的是 block 的 { } 里边的代码构成的函数。

  • isa 指明了block 的类型,构造函数中给它赋的值是 &_NSConcreteStackBlock,说明他是栈上的 block。关于 block 的类型,下一小节就会讲到。

第二个元素 Desc__main_block_desc_0 类型的结构体,如下所示:

static struct __main_block_desc_0 {
    size_t reserved;    // 保留字段
    size_t Block_size;  // block 的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

其中 Block_size 从名字推断应该是结构体的大小,紧随其后定义了一个 __main_block_desc_0 类型的变量 __main_block_desc_0_DATA,它的第二个元素值就是当前 block 的大小 sizeof(struct __main_block_impl_0),从 main() 函数中执行 block 构造函数的语句可以看出,__main_block_desc_0_DATA 最终赋值给了 block 中的 Desc,进一步验证了 Block_size 中存放的是 block 的大小。

第三个元素 int age 是 block 捕获的一个 auto 变量,关于捕获变量的机制,后面会详细讨论。

最后回到 main() 函数的最后一行代码

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
// 简化后:
blk->FuncPtr(blk);

很明显这是执行 blk 里边的指针 FuncPtr 指向的函数,而且将 blk 自己传了进去,这样,就可以在函数内部访问到 block 捕获的变量,如前文提到的 int age。

至此,本文开头的 block 的底层结构基本介绍完了,看起来比较零散,这里绘制了一张总图做个简单小结:

Block的结构.png

2.2 Block 的类型

在此,简单说明一下 block 的类型,block 的 3 种类型及其内存分布如下:

Block的分类.png

那么这 3 种类型的 Block 有什么区别呢,为了搞清楚这个问题,我们需要先回顾一下 4 种常见的变量类型及其代码示例:

  • 自动变量(auto 变量)
  • 静态局部变量
  • 全局变量
  • 静态全局变量
// *** 4 中变量的代码示例:

// 全局变量
int global_var = 10;
// 静态全局变量
static int static_global_var = 20;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 自动变量(局部变量)
        int local_var = 30; // <==> auto int local_var = 30;
        // 静态局部变量
        static int local_static_var = 40;
        
        MyBlock blk = ^{
            NSLog(@"/n global_var: %d/n static_global_var: %d/n local_var: %d/n local_static_var: %d/n", global_var, static_global_var, local_var, local_static_var);
        };
        blk();
    }
    return 0;
}

关于各种 Block 的区别,可以简单汇总成下边的图表:

block的3种类型的区别.png

也就是说:

  • 如果 Block 里边访问了 auto 变量,那么他就是栈上的 Block;
  • 如果没有访问 auto 变量,就是全局的 Block;
  • 如果对栈上的 Block 执行了 copy 操作,就变成了堆上的 Block。

2.3 Block 的 copy

上文提到了对栈上 Block 的 copy 操作,那么为什么需要 copy 呢?原因是:设置在栈上的 Block 如果其所属的作用域结束,该 Block 就会被废弃,为了延长它的生命周期,就需要将其复制到堆上。

既然栈上的 block 经 copy 后会从栈上复制到堆上,那么另外两种 Block 执行 copy 操作又会发生什么呢? 每一种 Block 被 copy 后的结果如下:

Block的copy.png

ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如满足一下条件之一时:

  • block 作为函数返回值时;
  • 将 block 赋值给 __strong 指针时;
  • block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时;
  • block 作为 GCD API 的方法参数时。

MRC 环境下,需要手动调用 block 的 copy 操作,才能将栈上的 block 复制到堆上。

3.变量捕获

为了保证 Block 内部能够正常访问外部的变量,block有个变量捕获机制,我们以前边介绍常见变量类型的代码为例,看看 Block 是怎么捕获变量的。

执行 clang -rewrite-objc main.m 之后,转换的 block 的源码如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;

  // 捕获的变量
  int local_var;
  int *local_static_var;

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _local_var, int *_local_static_var, int flags=0) : local_var(_local_var), local_static_var(_local_static_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

从上边的源码可以看出来,对于这 4 种不同的变量,实际只捕获了自动变量 local_var静态局部标量 local_static_var,而且自动变量是捕获了值,静态局部变量捕获的是变量地址。至于为什么这么设计,推测可能的原因如下:

block捕获变量.png

3.1 捕获自动变量

实际开发中,block 捕获到的变量基本都是自动变量(局部变量),理由是:对于全局变量,任何地方都可以访问它,不安全;对于静态局部变量,它会一直存在于内存中,对内存是一种浪费。

对于基本数据类型的自动变量,前边已经讲过了,就是简单的值捕获,接下来我们重点讨论一下对象类型 auto 变量的捕获。

下边是 block 访问外部对象类型 auto 变量的简单实例。

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSObject *obj = [[NSObject alloc] init];
        
        MyBlock blk = ^{
            NSLog(@"%@", obj);
        };
        
        blk();
    }
    return 0;
}

执行 clang -rewrite-objc main.m 后,生成的源码中有这 2 点不同:

  • 捕获了 NSObject *obj;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    
  NSObject *obj;
    
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  • __main_block_desc_0 中增加了两个指针 copydispose,整个文件里新增了 2 个函数 __main_block_copy_0()__main_block_dispose_0(),结合上下问可以知道,这两个函数地址最终传给了 block 里 Desc 中的 copydispose 这 2 个指针。
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
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

这两个函数调用时机和作用如下:

  • 如果 block 从栈上拷贝到堆上
    会调用 block 内部的 copy() 函数,此函数内部会调用 _Block_object_assign() 函数,它会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成 强引用 或 弱引用。

  • 如果 block 从堆上移除
    会调用 block 内部的 dispose() 函数,此函数内部会调用 _Block_object_dispose() 函数,它会自动释放引用的 auto 变量(即 release)。

另外,如果 block 一直是在栈上,将不会对 auto 变量产生强引用。

3.2 捕获 __block 变量

前边我们只是在 block 内部使用变量,事实上,如果直接修改变量的话,比如下边这个例子,就会报错:此变量不可赋值 (错误信息见注释)。

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        MyBlock blk = ^{
            age = 20; // Error: Variable is not assignable (missing __block type specifier)
            NSLog(@"%d", age);
        };
        
        blk();
    }
    return 0;
}

按照错误信息的提示,如果给 int age = 10; 前边加上 __block,就可以解决 block 内部无法修改 auto 变量的问题,实际操作后,发现果然可以正常输出 age 的新值 20。

3.2.1 __block 变量能够被 block 修改的原因

现在来看看 __block 修饰符到底做了什么,先将上边的代码转成 C++ 源码,下边截取了其中部分关键代码

// 新出现的结构体
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
    
    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_1dfa13_mi_0, (age->__forwarding->age));
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        
        MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

先看 main() 函数,__block int age 变成了 __Block_byref_age_0 类型的 age ,也就是说编译器将 __block 修饰的变量包装成了一个新的结构:

struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};

__Block_byref_age_0 这个结构体里边有一个 int age ,用于存储 age 的值 (10)。还有一个重要成员 __Block_byref_age_0 *__forwarding; ,结合 block 的构造函数,我们知道 __forwarding 指针实际指向了它所在的结构体。

__block变量复制前.png

之所以这么做是为了当 block 被拷贝到堆上以后,无论访问栈上的 block 还是 堆上的 block,最终都是访问的堆上的同一个 block(拷贝后,堆上的 __forwarding 指向自己所在的 __block 变量,栈上的 __forwarding 指向堆上的 __block 变量),如下图所示。

__block变量复制后.png

接下来,看看 block 的结构 __main_block_impl_0,里边多了一个变量 __Block_byref_age_0 *age;,即 block 捕获了这个新的结构体 __Block_byref_age_0 的地址,所以 block 里边就可以通过地址访问这个结构体,进而修改里边 int age 的值。

__block 修饰的对象类型的 auto 变量与此类似,差别仅在于新生成的结构体:

image.png

从上图可知,__block 修饰的对象类型转换后的结构体里边多了两个函数指针,他们分别指向下面 2 个函数,负责内存管理的相关操作,下边就会讲到。

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

3.2.2 被 __block 修饰的对象类型的内存管理

对于 被 __block 修饰的对象类型,内存管理分以下 3 种情况:

  • 1.当 __block 变量 在栈上时,不会对指向的对象产生强引用。

  • 2.当 __block 变量 被 copy 到堆时,分两种情况:

    • ARC 环境下,会调用 __block 变量内部 的 copy 函数,它会调用 _Block_object_assign() 函数,此函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。
ARC环境.png
  • MRC 环境下,不会形成强引用(retain)。
typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block NSObject *obj = [[NSObject alloc] init];
        
        MyBlock block = [^{
            NSLog(@"%p", obj);
        } copy];
        
        block();
        
        [obj release];
    }
    return 0;
}

如上所示,在 MRC 环境下,对象前加了 __block,不会对 block 形成强引用, 即当执行完 [obj release]; 之后,person 就被释放了。

MRC环境.png
  • 3.如果 __block 变量从堆上移除,会调用 __block 变量内部dispose() 函数,它会调用 _Block_object_dispose() 函数,此函数会自动释放指向的对象(release)

3.2.3 对象类型的auto变量 和 __block 变量

  • 当block在栈上时,对它们都不会产生强引用

  • 当 block 拷贝到堆上时,都会通过 copy 函数来处理它们

    • 对于 __block变量(假设变量名叫做a),最终会执行 _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

    • 对于对象类型的 auto 变量(假设变量名叫做p),最终会执行 _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

      以上两者最终调用的方法是相同的,只不过最后一个参数有差别,前者是 8(表示引用类型),后者是 3 (表示对象),下面对 _Block_object_dispose() 函数的调用与之类似。

  • 当 block 从堆上移除时,都会通过 dispose 函数来释放它们

    • 对于 __block变量(假设变量名叫做a),最终会执行 _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

    • 对于对象类型的auto变量(假设变量名叫做p),最终会在执行 _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

4.循环引用

关于使用 block 可能遇到的循环引用问题,我们分 ARC 和 MRC 两种情况进行讨论。

ARC 环境

在 ARC 环境下,目前大概有 3 种常见的解决循环引用的方式:

  • __weak

此方式是最常见也是推荐使用的一种方式,基本原理是,self 对 block 的引用维持强引用,不过将 block 对 self 的引用改成了弱引用。

    __weak typeof(self) weakSelf = self;
    self.block = ^{
        printf("%p", weakSelf);
    };
  • __unsafe_unreturned

这种方式与上边的方式类似,不过当 weakSelf 指向的对象销毁后,指针已然指向那块已经被回收的内存,可能发生野指针错误,所以是不安全的。

    __unsafe_unretained typeof(self) weakSelf = self;
    self.block = ^{
        printf("%p", weakSelf);
    };
  • __block
    __block typeof(self) weakSelf = self;
    self.block = ^{
        printf("%p", weakSelf);
        weakSelf = nil;
    };
    self.block();

我们知道,当在变量前边加了 __block 之后就多了一个 __blcok 变量,于是里边的引用关系就变成了:

__block解决循环引用A.png

为了打破这个循环引用的关系,需要在 block 里边将对象置为 nil,而且必须执行 block 才能断开 __block 变量对对象的强引用。

__block解决循环引用B.png

MRC 环境

MRC 环境下解决循环引用的方式与 ARC 环境类似,只是由于 MRC 环境下不可以使用 weak,所以只有 __unsafe_unreturned__block 2 种解决方式。对于 __block 的方式,在MRC中,__block 变量 不会对 weakSelf 产生强引用,也就不需要将其置为 nil 并执行 block 了。

    __block typeof(self) weakSelf = self;
    self.block = ^{
        printf("%p", weakSelf);
    };

5.小结

以上就是对 Block 底层实现的一个简单讨论,受自己的知识积累所限,难免有理解不到位的地方,后期会及时修正。

# 参考

版权声明:
作者:dingding
链接:https://www.techfm.club/p/42413.html
来源:TechFM
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>