iOS Blocks 第二弹|底层实现

揭秘 Block 的底层实现

iOS Blocks 第二弹|底层实现

上一篇关于 Block 基础知识的文章里,提到 Block 是对标准 C 的拓展,它的底层还是依赖标准 C/C++ 实现的。这篇文章就来揭秘一下 Block 的底层实现。

利用 clang 转译 Objc 源码

利用 clang 的 -rewrite-objc 参数可以将 Objc 源码转译为标准 C 代码(输出文件格式为 .cpp )。虽说输出是 C++ 文件,但其实内部主要还是用标准 C 写的,只不过某些用到了 struct 构造器的地方是 C++ 的特性。

在终端直接使用 clang -rewrite-objc file.m 指令转译 Objc 代码会出现找不到系统库头文件的问题。这时候需要在指令里加上 -isysroot `xcrun --show-sdk-path` 指定 SDK 的路径:clang -isysroot `xcrun --show-sdk-path` -rewrite-objc file.m

💡
如果想要在 ARC 模式下转译的话,需要加上 -fobjc-arc 参数。 clang -isysroot `xcrun --show-sdk-path` -rewrite-objc -fobjc-arc block_auto_copy.m

C++ 中的 this 和 Objective-C 中的 self

C++ 源码也能借助 clang 转成标准 C 实现,我们借此先了解一下 thisself

C++ 中的 this

在 C++ 中定义一个实例方法,并在 main 函数中调用:

#include <stdio.h>

class MyClass {
public:
    void method(int param);
};

void MyClass::method(int param) {
    printf("%p %d\n", this, param);
}

int main() {
    MyClass obj; 
    obj.method(10);
}

将上面代码转译为标准 C 实现:

#include <stdio.h>

struct MyClass {
    void (*method)(struct MyClass* self, int param);
};

void method(struct MyClass* self, int param) {
    printf("%p %d\n", self, param);
}

int main() {
    struct MyClass obj;
    obj.method = method;
    obj.method(&obj, 10);
}

可以看到 C++ 的类在标准 C 里面是使用 struct 实现的。实例方法 method 转为标准 C 实现后,变成了一个单独的函数,并且参数里多了一个 struct Myclass* self,即实例本身(实际为 struct)。Myclass 结构体里面存储了 method 的指针,调用实例方法时通过这个指针找到对应的函数。

实例在调用自己的实例方法时,也是将自己的地址作为参数传递了进去:cls.method(&obj, 10); 。这就是 C++ 中的 this

Objective-C 中的 self

同样使用 clang 将下面这段代码转译为标准 C 实现:

#include <Foundation/Foundation.h>

@interface MyClass : NSObject

@end

@implementation MyClass

- (void)method:(int)param {
    NSLog(@"%p %d\n", self, param);
}

@end

int main(int argc, const char * argv[]) {
    MyClass *obj = [[MyClass alloc] init];
    [obj method:10];
    return 0;
}

标准 C 实现:

#ifndef _REWRITER_typedef_MyClass
#define _REWRITER_typedef_MyClass
typedef struct objc_object MyClass;
typedef struct {} _objc_exc_MyClass;
#endif

struct MyClass_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};

/* @end */

// @implementation MyClass

static void _I_MyClass_method_(MyClass * self, SEL _cmd, int param) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_kk_801wlmp11577d334vqtg_vgr0000gn_T_main_4bc26d_mii_0, self, param);
}
// @end
int main(int argc, const char * argv[]) {
    MyClass *obj = ((MyClass *(*)(id, SEL))(void *)objc_msgSend)((id)((MyClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyClass"), sel_registerName("alloc")), sel_registerName("init"));
    ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)obj, sel_registerName("method:"), 10);
    return 0;
}

和 C++ 一样,self 也是被作为了一个参数传递给了 _I_MyClass_method_ 函数。而 _I_MyClass_method_ 函数对应着源码 method 函数,且变成了一个静态函数。静态函数的好处之一就是其他文件中可以定义相同名字的函数,不会发生冲突。

在调用 method 函数时,标准 C 实现里使用了 objc_msgSend 函数。这也是我们常说的,Objc 的方法调用其实就是向对象发送消息。objc_msgSend 函数会通过对象名和方法名检索 _I_MyClass_method_ 的函数指针,通过函数指针调用函数,并将实例本身 obj 作为第一个参数传递进去。

Blocks 的底层实现

简易 Block 的标准 C/C++ 实现

利用 clang 将下面代码转译为标准 C/C++ 代码:

int main(int argc, const char * argv[]) {
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    blk();
    return 0;
}

转译结果:

#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
// Runtime copy/destroy helper functions (from Block_private.h)
#ifdef __OBJC_EXPORT_BLOCKS
extern "C" __declspec(dllexport) void _Block_object_assign(void *, const void *, const int);
extern "C" __declspec(dllexport) void _Block_object_dispose(const void *, const int);
extern "C" __declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" __declspec(dllexport) void *_NSConcreteStackBlock[32];
#else
__OBJC_RW_DLLIMPORT void _Block_object_assign(void *, const void *, const int);
__OBJC_RW_DLLIMPORT void _Block_object_dispose(const void *, const int);
__OBJC_RW_DLLIMPORT void *_NSConcreteGlobalBlock[32];
__OBJC_RW_DLLIMPORT void *_NSConcreteStackBlock[32];
#endif
#endif
#define __block
#define __weak

// 省略了一些无关的代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("Block\n");
    }

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[]) {
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

从标准 C/C++ 代码里可以看出来,Block 在底层是利用一个主逻辑函数和一个负责存储数据的结构体实现的。函数 __main_block_func_0 负责处理 Block 的主要计算逻辑,而存储数据的结构体 __main_block_impl_0 负责存储前者的指针(函数指针),以及需要给函数传递的参数等。在后面的章节里面,我们还会发现,Block 外部被捕获的变量也会存储在 __main_block_impl_0 里。

函数 __main_block_func_0 函数接受一个 _cself 参数,这个参数就类似于 C++ 中的 this,以及 Objc 中的 self。只不过 _cself 代表着 Block 自己,相当于把 Block 当成一个对象了。

__main_block_func_0 以及 __main_block_impl_0 这俩名字是根据原始 Objc 代码中 Block 声明所在的函数的名字,以及是函数中第几个 Block 来定下的。

这就是一个简单的 Block 的底层实现,接下来我们会更深入地分析底层实现的源码。

__cself 结构体的结构

在上面转译之后的 C 代码中,__main_block_func_0 函数接受的 _cself 参数是 __main_block_impl_0 类型的。其结构体定义如下:

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

这是一个比较简单的结构体,还没有捕获外界的变量。里面包含了三个要素:

  • __block_impl 类型的 impl 。是一个结构体,存储 Block 的基本实现信息,比如对应的函数指针。
  • __main_block_desc_0 类型的 Desc 。也是一个结构体,存储 Block 的 size 等信息。定义如下,里面直接定义了一个 __main_block_desc_0 类型的静态全局变量 __main_block_desc_0_DATA,方便创建 __main_block_impl_0 时直接使用。
  • __main_block_impl_0 结构体的构造器。

其中 __block_impl 结构体是 Blocks 通用的,并不是专门为某个 Block 定义的。定义如下:

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

里面存储了 Block 对应的实现函数的指针,标志,isa 等信息,可以称作 Block 的「基类」。

简化一下__main_block_impl_0 结构体,去掉构造函数:

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

这个结构体整体可以作为 Block 的身份信息,也可以说是 Block 的类似于 class 的声明,这就是 __cself 的意义。

__main_block_impl_0 内部还定义了一个构造器:

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

这个构造器接收上面提到的几个要素作为参数,来创建 __main_block_impl_0 的实例。

调用这个构造器的代码:

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

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

第一行代码调用了 __main_block_impl_0 的构造函数,将生成的 __main_block_impl_0 实例的地址赋给了变量 blk。也就是说 blk 目前是 __main_block_impl_0 类型的指针,并且其指向的实例是一个在栈上创建的局部变量(MRC 环境下)。

__main_block_func_0 的调用逻辑

未转译时的 Objc 源码在执行 Block 时的代码是 blk(); ,这行代码对应于转译后的代码:

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

因为 blk 的类型是 __main_block_impl_0,其结构体中第一个元素是 __block_impl 类型的 impl。从内存结构上来说,可以通过成员 impl 的偏移量(是 0)来直接获取成员的地址并访问成员。因此 (__block_impl *)blk 这块代码直接将 blk 转成了 __block_impl 类型。

关于 struct 的内存结构,可以参考我之前的 post:

iOS Blocks 第一弹|基础知识
Block 是在 iOS 开发过程中经常出现的角色。它是由 Apple 在 OS X Snow Leopard / iOS 4 上引入的,属于对标准 C 的拓展。Block 可以视为是「带有局部变量的匿名函数(anonymous functions together with automatic variables)」

去掉各种类型转换,可以简化为:

(*blk->impl.FuncPtr)(blk);

简化后的代码一目了然,调用了 FuncPtr 指向的函数,并将 blk 自己作为 __cself 传递了进去。

isa = &_NSConcreteStackBlock 的含义

__main_block_impl_0 的构造器中,有这样一句代码:

impl.isa = &_NSConcreteStackBlock;

这句代码将_NSConcreteStackBlock 的地址赋给了 isa。为了理解 isa 是什么,我们首先得知道 Objective-C 中的 class 和 object 是怎么实现的。

在 Objective-C 中我们经常将一个对象的指针赋值给一个 id 类型的变量,它就像 C 语言中的 void * 。其实 id 也是在 C 语言中声明和实现的:

// objc.h
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

id 是一个指向 objc_object 类型的指针。而 Class 的定义为:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

struct objc_class : objc_object {
	Class superclass;
}

objc_object 和 objc_class 是 Objective-C 中对象和类最基础的结构体。objc_object 指代类的实例,而 objc_class 指代实例的类型。其实不难发现,objc_class 其实构成了类似于链表的结构,内部装了同一类型的另一个变量。逐个遍历这个链表,就能一层一层拿到父级、父级的父级等的类型信息。

💡
objc_object 和 objc_class 的源码可以在 Apple 的 objc 开源仓库里找到。

isa = &_NSConcreteStackBlock 意思是当 Block 被视为一个 object 时,我们能从 _NSConcreteStackBlock 中溯源到我们想要的关于 Block 类型的信息。

至于 _NSConcreteStackBlock ,我们后面的章节会具体讲到它以及和它相关的另外几种类型。

Block 对局部变量的捕获及修改

接下里的篇章就是重头戏了。Block 最重要的能力就是能捕获局部变量,关联上下文信息。

捕获变量的底层实现

将下面这段代码转换成标准 C/C++ 实现。

int main() {
	int dmy = 256;
	int val = 10;
	const char *fmt = "val = %d\n";
	void (^blk)(void) = ^{ printf(fmt, val); };
	blk();
	return 0;
}

转换结果:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), 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) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
 printf(fmt, 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 dmy = 256;
 int val = 10;
 const char *fmt = "val = %d\n";
 void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
 ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
 return 0;
}

在 blk 中使用到的局部变量 fmt 和 val 在 __main_block_impl_0 中有了对应的成员变量,而 blk 没有使用的局部变量 dmy 则没有处理。

__main_block_impl_0 的构造函数中也多了对 fmt 和 val 的初始化操作。在对 blk 进行初始化时,将局部变量 fmt 和 val 作为参数传递给了 __main_block_impl_0 的构造函数。

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val)

这不仅印证了上面提到的,__main_block_impl_0 是专门存储数据的,包括了上下文信息;也解释了为什么 Block 里面的 self 要用 __weak 来处理——__main_block_impl_0 里面又(隐式)强持有了 self。

为什么 Block 不能捕获 C 数组

源码 ^{ printf(fmt, val); } 对应的标准 C/C++ 实现代码对应着 __main_block_func_0 函数。

变量 fmt 和 val 现在是从 __cself 中取出的,也就是从 __main_block_impl_0 里面取出来的。

前面章节中说过,Blocks 是无法捕获 C 数组的。至于为什么不能这么干,我们可以先写个简短的代码实验。

char a[10] = "hello";
char b[10] = a;

上面这段代码编译时会报错:Array initializer must be an initializer list or string literal.

意思是必须用字面意义上的数组或者字符串初始化数组,不能用另外一个数组直接初始化。这就是为啥 Block 无法捕获 C 数组的原因。如果 Block 捕获了 C 数组,那么之后在执行 Block 内部代码时,肯定会有类似 int val = __cself->val; 的操作;如果把这个操作放在 C 数组上,那就是用一个 现有的 C 数组来初始化一个局部变量,会报编译错误。

修改被捕获的局部变量的值

一般情况下,局部变量(非全局/静态)被捕获后,在 struct 内部生成的与之对应的变量(__main_block_impl_0 里面的 val,下称「镜像变量」)是不允许修改值的。因为编译器是把被捕获变量的值赋给了镜像变量,并不是让镜像变量指向被捕获变量,他们是两个独立的变量。如果镜像变量能修改值的话,那其意义就不存在了。

尽管如此,我们还是有一些场景需要修改被捕获变量的值,比如在 Block 中修改计次数。

使用静态变量或者全局变量

前面说到被捕获的变量和镜像变量是相互独立的,但也有特例,比如静态变量或全局变量。在 C 语言中,我们经常使用以下几种变量做持久存储或共享信息:

  • 静态变量
  • 静态全局变量
  • 全局变量

全局变量和静态全局变量能够在同一文件中任意地方访问。因此在将源码转成标准 C/C++ 实现后,Block 的逻辑函数里也能对全局变量和静态全局变量直接访问,不需要生成镜像变量来存储全局变量/静态全局变量的值。

用下面这段代码做个实验:

int global_val = 1;
static int static_global_val = 2;

int main() {

    static int static_val = 3;
    void (^blk)(void) = ^ {
        global_val *= 1;
        static_global_val *= 2;
        static_val *= 3;
    };
    blk();

    return 0;
}

转换成标准 C/C++ 实现:

int global_val = 1;
static int static_global_val = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_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_val = __cself->static_val; // bound by copy

        global_val *= 1;
        static_global_val *= 2;
        (*static_val) *= 3;
    }

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() {
    static int static_val = 3;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

可以发现,全局变量 global_val 和静态全局变量 static_global_val 在 block 的实现函数 __main_block_impl_0 里并没有生成对应的镜像变量,在逻辑函数 __main_block_func_0 里也是直接访问的这两个变量。

但静态变量有些不同,因为静态变量尽管生命周期比普通的局部变量长,但是其作用域还是被限制在了函数内部,无法在 main 函数外访问,也无法在 __main_block_func_0 里访问。

静态变量毕竟是和普通变量有很大差别的,如果按照普通变量的方式给静态变量生成一个镜像变量,那绝对是不能的。标准 C/C++ 实现中解决这一问题的方式是使用指针,让指针指向静态变量。在 __main_block_impl_0 的构造过程中传入了 static_val 的地址:

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

使用 __block 关键字

在 C 语言中,有下面几个存储类型关键字,表明变量存储的位置:

  • typedef 为现有类型添加一个同义字
  • extern 声明一个已经在别处定义了的变量
  • static 静态变量
  • auto 变量具有自动存储时期,一般是存储在栈区的局部变量
  • register 将一个变量归入寄存器存储类,具备更快的读写速度

__block 关键字和上面这几个关键字类似,也是说明了变量的存储类型,下面我们来具体看一下。

老规矩,还是看下 __block 的标准 C/C++ 实现:

int main() {
    __block int val = 10;
    void (^blk)(void) = ^ {
        val = 1;
    };
}

转译后:

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;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : 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

        (val->__forwarding->val) = 1;
    }
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() {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
}

原始代码里面简单的 __block int val = 10; ,在标准实现里面变成了:

__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};

val 在标准实现里面变成了 __Block_byref_val_0 类型的结构体,不再是一个单一的变量。在这个结构体里面,又有一个 int 类型的成员变量 val ,存储着原始的 val ,它是可以更改值的。

在 Block 里面修改局部变量 val 的值,对应着修改 __Block_byref_val_0 里面的 val

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	__Block_byref_val_0 *val = __cself->val; // bound by ref

  (val->__forwarding->val) = 1;
}

每个被 __block 修饰的变量都会在标准 C/C++ 实现里生成一个类似 __Block_byref_val_0 的结构体。如果有多个 Block 内部都要修改同一个被 __block 修饰的变量的话,这些 Block 被初始化时会接受这个变量对应的 __Block_byref_val_0 类型的结构体地址:

__block int val = 10;
void (^blk)(void) = ^ {
    val = 1;
};
void (^blk1)(void) = ^ {
    val = 2;
};

转译后:

__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
void (*blk1)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_val_0 *)&val, 570425344));

__Block_byref_val_0 这种类型的结构体单独拆出来,是为了方便多个 Block 捕获同一个 __block 变量。

看到这里,我们应该大致理解了 __block 关键字的原理:被 __block 标记的变量会在底层实现里变成一个结构体,这个结构体可以在 Block 之间共享,修改变量其实是修改这个结构体里面的变量。

但是我们还有个疑惑,__Block_byref_val_0 这个结构体是在栈上创建的,而 Block 是要从栈上拷贝到堆上的(生命周期比函数/方法长),__Block_byref_val_0 是怎么跟随 Block 一起被拷贝到堆上的?

Block 的内存结构

Block 的存储区域

前面的章节里面提到了 _NSConcreteStackBlock ,与之同类的有以下几个:

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

它们代表了 Block 不同的存储区域。

图源:Pro Multithreading and Memory Management for iOS and OS X with ARC, Grand Central Dispatch, and Blocks (Kazuki Sakamoto, Tomohiko Furumoto)

存储在 Data 区的 Block —— _NSConcreteGlobalBlock

有两种 Block 会被存放在 Data 区:

  • 像全局变量一样,声明在函数外面的 Block:
void (^blk)(void) = ^{printf("Global Block\n");};
int main() {
	// ...
}
  • Block 内部没有捕获局部变量,逻辑与外界独立:
typedef int (^blk_t)(int);
for (int rate = 0; rate < 10; ++rate) { 
	blk_t blk = ^(int count){
		return count;
	};
}

Data 区的内存在编译时就已确定好,在程序整个运行期间都存在。把数据存储在 Data 区能够减少数据的创建和释放,加快执行以及访问速度。

存储在堆区的 Block —— _NSConcreteMallocBlock

在 Data 区存储的 Block 有着和程序一样长的生命周期,在栈区存储的 Block 会在超出作用域后释放,而存储在堆区的 Block 具备更灵活的生命周期。

Block 提供了从栈区复制到堆区的能力。从栈区复制到堆区后,Block 的 isa 属性值会从 &_NSConcreteMallocBlock 变成 &_NSConcreteMallocBlock

impl.isa = &_NSConcreteMallocBlock;

Block 被拷贝到堆区后,可以通过 __forwarding 属性获取到堆上的 Block。

自动/手动拷贝 Block

在 ARC 环境下,编译器会自动检测并把 Block 从栈区拷贝到堆区,甚至是在创建时就直接把 Block 拷贝到了堆区(因为隐式强持有)。

💡
前面的代码转译大部分是在 MRC 环境下进行的,因为没有加 -fobjc-arc 参数。

将下面的代码片段通过 clang 编译执行(注意不是转译,是直接编译执行):

#include <Foundation/Foundation.h>

typedef int (^blk_t)(int);
blk_t func(int rate) {
    blk_t blk = ^(int count){
        return rate * count;
    };
    NSLog(@"blk: %@", blk);

    blk_t blk1 = ^(int count){
        return rate * count;
    };
    NSLog(@"blk1: %@", blk1);
    return blk1;
}

int main(int argc, const char * argv[]) {
    blk_t blk = func(10);
    NSLog(@"blk main: %@", blk);

    return 0;
}
💡
使用 clang -isysroot `xcrun --show-sdk-path` -fno-objc-arc -framework Foundation block_copy.m -o block_copy 编译单个 .m 文件。-fno-objc-arcfobjc-arc 分别控制是否在 ARC 环境下执行。
  • ARC 执行结果:
2023-10-08 19:15:32.227 block_copy[18722:5025896] blk: <__NSMallocBlock__: 0x6000026f8660>
2023-10-08 19:15:32.227 block_copy[18722:5025896] blk1: <__NSMallocBlock__: 0x6000026fc000>
2023-10-08 19:15:32.228 block_copy[18722:5025896] blk main: <__NSMallocBlock__: 0x6000026fc000>
  • 非 ARC 执行结果:
2023-10-08 18:57:25.187 block_copy[18054:5000955] blk: <NSStackBlock: 0x16b75f128>
2023-10-08 18:57:25.187 block_copy[18054:5000955] blk1: <NSStackBlock: 0x16b75f0f8>
[1]    18054 segmentation fault  ./block_copy

在 ARC 环境下,因为局部变量对 Block 是强持有的,所以Blocks 被自动拷贝到了堆区。被拷贝到堆区的 Block 不会在函数执行完毕后释放,因此在 main 函数里赋值没有问题。

而非 ARC 环境下,Blocks 没有被自动拷贝到堆区,还是在栈区。而且由于栈区的 Block 在函数结束后被释放了,因此在 main 函数里接受的返回值是空的,导致程序执行出错。

在 ARC 环境下,大部分 Block 的都被编译器在堆区或者 Data 区初始化。可以说,只要有变量(不管是局部变量还是其他变量)强持有了 Block,那 Block 就不大可能是存在栈区的。而且在 ARC 环境下,变量默认是强持有的,除非用 weak__weak 指定弱持有。

以下面代码为例,调用其中的 testCopy 方法。

- (void)testCopy {
    NSArray *arr = [self createBlocks];
    typedef void (^blk_t)(void);
    blk_t arr_blk = (blk_t)[arr objectAtIndex:0];
    NSLog(@"arr_blk: %@", arr_blk);
    arr_blk();
    
    __weak blk_t weak_arr_blk = (blk_t)[arr objectAtIndex:1];
    NSLog(@"weak_arr_blk: %@", weak_arr_blk);
    weak_arr_blk();
    
    int val = 0;
    __weak blk_t weak_blk = ^{ NSLog(@"val: %d", val); };
    NSLog(@"weak_blk: %@", weak_blk);
    
    blk_t strong_blk = ^{ NSLog(@"val: %d", val); };
    NSLog(@"strong_blk: %@", strong_blk);
}

- (id)createBlocks {
    int stackValue = 1;
    NSLog(@"stack value address: %p", &stackValue);
//    __block int val = 10;
    int val = 10;
    NSLog(@"val address: %p", &val);
    return [[NSArray alloc] initWithObjects:^{ NSLog(@"blk0: %d, val pointer: %p", val, &val); }, ^{ NSLog(@"blk1: %d", val); }, nil];
}

输出为:

stack value address: 0x16fdff0dc
val address: 0x16fdff0d8
arr_blk: <__NSMallocBlock__: 0x6000012a0000>
blk0: 10, val pointer: 0x6000012a0020
weak_arr_blk: <__NSMallocBlock__: 0x6000012a0030>
blk1: 10
weak_blk: <__NSStackBlock__: 0x16fdff1a0>
strong_blk: <__NSMallocBlock__: 0x6000012a8330>
  • 通过 NSArray 的 initWithObjects 方法传入的 Block 都变成了 NSMallocBlock,触发了编译器的自动拷贝。这一点和原书中讲述的不一致:原书中说这种作为参数传递给函数或方法的 Block 是 NSStackBlock,且不会出发自动拷贝,会在函数结束后释放,导致后面在数组里取 Block 元素并执行的时候报错
  • Block 类型的变量强持有的 Block 是堆区 Block。由于数组也是强持有 Block,所以数组里面的 Block 元素被拷贝到了堆区
  • Block 变量弱持有的 Block 是栈区 Block

另外也可以通过 copy 方法主动将栈区的 Block 拷贝到堆区:

- (void)testStackCopytoHeap {
    int val = 0;
    typedef void (^blk_t)(void);
    __weak blk_t weak_blk = ^{ NSLog(@"val: %d", val); };
    NSLog(@"stack_blk: %@", weak_blk);
    
    NSLog(@"heap_blk: %@", [weak_blk copy]);
}

输出:

stack_blk: <__NSStackBlock__: 0x16fdff1d8>
heap_blk: <__NSMallocBlock__: 0x600002b243f0>

调用 copy 方法后,栈区的 Block 被拷贝到了堆区。

__block 变量的内存区域

前面的部分总结了 Block 由栈区拷贝到堆区的规则。这一小节会总结下由 __block 修饰的变量的内存拷贝原则。

先记住总的原则:当 Block 由栈区拷贝到堆区后,其捕获的 __block 变量也会由栈区拷贝到堆区,拷贝完成后 Block 依旧对 __block 变量有所属权

拷贝流程:

  1. 起初假设 Block 和 __block 变量都在栈区。
  2. 当 Block 被拷贝到堆区后,其持有的 __block 变量也会被拷贝到堆区。
  3. 如果是多个 Block 都对 __block 变量拥有所属权,__block 变量仅会经历一次从栈区拷贝到堆区的操作,不会有从堆区拷贝到堆区的操作。
  1. Block 释放后,如果不再有 Block 持有 __block 变量,那么 __block 变量将随之释放。

__block 变量拷贝到堆区前后,Block 内部使用这个变量时都是通过 __forwarding 来获取当前实际的变量。

__forwarding

__Block_byref_val_0 结构体里面的 __forwarding 属性是为了追踪 __block 变量的实际位置的。如果 __block 变量处于栈区,那么 __forwarding 将是一个指向栈区变量的指针;如果 __block 变量被拷贝到了堆区,那么 __forwarding 将同步变成指向堆区变量的指针。

在上面转译后的源码的 main 函数里,有这样的一段代码:

__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

这段代码在栈区创建了一个 __Block_byref_val_0 类型的 val,并将其地址作为参数传递给了 __main_block_impl_0 类型的 blk 变量。虽然说 Block 能通过 (val->__forwarding->val) 来获取到实际的 val,但是当 Block 被拷贝到堆区,函数执行完毕后,栈区的 val 应该会被释放掉,这时候再调用 (val->__forwarding->val) 是如何保证不出现崩溃问题的?

在转译代码中,有 Block 的 copy 和 dispose 函数的实现:

static void __main_block_copy_1(struct __main_block_impl_1*dst, struct __main_block_impl_1*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_1(struct __main_block_impl_1*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

其中用到了 _Block_object_assign_Block_object_dispose 函数,这两个函数接受的并不是 __main_block_impl_1 类型的参数,而是 __Block_byref_val_0 类型的参数,也就是 __block 变量的底层结构体。

clang 转译的代码中没有说 Block 以及 __block 变量具体是怎么被拷贝的,需要查看关键函数 _Block_object_assign 的实现。源码可以在 libclosure 中的 runtime.c 文件中找到。

//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
//
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (flags & BLOCK_ALL_COPY_DISPOSE_FLAGS) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/

        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      default:
        break;
    }
}

函数里面根据不同的 flag 参数走到了不同的分支,转译代码中用的 flag 是 8/*BLOCK_FIELD_IS_BYREF*/ 。那么接下来会调用 _Block_byref_copy 函数,传递进去的参数是拷贝前的 __Block_byref_val_0

_Block_byref_copy 函数源码:

// Runtime entry points for maintaining the sharing knowledge of byref data blocks.

// A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
// Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
// We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
// Otherwise we need to copy it and update the stack forwarding pointer
static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (BLOCK_BYREF_LAYOUT(src) == BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

因为 val (这里指的是 __block 变量的底层结构体)在拷贝前是在栈区,所以会走到 if 分支里。如果走到 else if 里面,就说明 val 已经被拷贝到堆区了。随后调用了 malloc 函数,在堆区给新的 val 申请了内存空间。比较关键的是这两行代码:

copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy;  // patch stack to point to heap copy

这两行代码让堆区新拷贝的和原来栈区没释放的 val__forwarding 都指向了堆区新拷贝的 val 。也就是说,拷贝完成后,通过 __forwarding 获取的 val 肯定是堆区的

另外还有一个需要注意的地方:

static void __main_block_copy_1(struct __main_block_impl_1*dst, struct __main_block_impl_1*src) {
		_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

Block 的这个拷贝函数 __main_block_copy_1 调用了 _Block_object_assign 函数,表示 Block 被拷贝到堆区后,会触发将 __block 变量也拷贝到堆区。仔细看这个 _Block_object_assign 函数,里面 dst 取的是地址,src 取的是指针。dst 之所以要取地址,目的就是在 Block 拷贝到堆区后,要把他的 val 成员属性也变成堆区的:

*dest = _Block_byref_copy(object);

到这里,前面的疑惑也就解开了。拷贝到堆区的 Block,它的 val 成员变量也会赋值为拷贝到堆区的 val 。所以不存在当栈区变量释放后,堆区的 Block 调用 val 会引起崩溃的问题。