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 的路径:

shell 复制代码
clang -isysroot `xcrun --show-sdk-path` -rewrite-objc file.m

💡 如果想要在 ARC 模式下转译的话,需要加上 -fobjc-arc 参数:

shell 复制代码
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 函数中调用:

cpp 复制代码
#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 实现:

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 实现:

objectivec 复制代码
#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 实现:

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++ 代码:

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

转译结果:

objc 复制代码
#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 类型的。其结构体定义如下:

c 复制代码
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 时直接使用。

    cpp 复制代码
    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)};
  • __main_block_impl_0 结构体的构造器。

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

cpp 复制代码
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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

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

cpp 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

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

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

c 复制代码
__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 的实例。

调用这个构造器的代码:

c 复制代码
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(); ,这行代码对应于转译后的代码:

c 复制代码
((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 第一弹|基础知识

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

c 复制代码
(*blk->impl.FuncPtr)(blk);

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

isa = &_NSConcreteStackBlock 的含义

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

cpp 复制代码
impl.isa = &_NSConcreteStackBlock;

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

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

cpp 复制代码
// 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 的定义为:

cpp 复制代码
/// 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++ 实现。

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

转换结果:

objectivec 复制代码
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 中使用到的局部变量 fmtval__main_block_impl_0 中有了对应的成员变量,而 blk 没有使用的局部变量 dmy 则没有处理。

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

objectivec 复制代码
__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 函数。

c 复制代码
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); 
}

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

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

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 的逻辑函数里也能对全局变量和静态全局变量直接访问,不需要生成镜像变量来存储全局变量/静态全局变量的值。

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

objectivec 复制代码
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++ 实现:

objectivec 复制代码
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 的地址:

objectivec 复制代码
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++ 实现:

objectivec 复制代码
int main() {
    __block int val = 10;
    void (^blk)(void) = ^ {
        val = 1;
    };
}

转译后:

objectivec 复制代码
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; ,在标准实现里面变成了:

objectivec 复制代码
__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

objectivec 复制代码
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 类型的结构体地址:

objectivec 复制代码
__block int val = 10;
void (^blk)(void) = ^ {
    val = 1;
};
void (^blk1)(void) = ^ {
    val = 2;
};

转译后:

objectivec 复制代码
__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:

    cpp 复制代码
    void (^blk)(void) = ^{printf("Global Block\n");};
    int main() {
    	...
    }
  • Block 内部没有捕获局部变量,逻辑与外界独立:

    cpp 复制代码
    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

c 复制代码
impl.isa = &_NSConcreteMallocBlock;

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

自动/手动拷贝 Block

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

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

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

objectivec 复制代码
#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 执行结果:

    objectivec 复制代码
    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 执行结果:

    objectivec 复制代码
    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 方法。

objectivec 复制代码
- (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];
}

输出为:

objectivec 复制代码
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>
  • 通过 NSArrayinitWithObjects 方法传入的 Block 都变成了 NSMallocBlock,触发了编译器的自动拷贝。这一点和原书中讲述的不一致:原书中说这种作为参数传递给函数或方法的 Block 是 NSStackBlock,且不会出发自动拷贝,会在函数结束后释放,导致后面在数组里取 Block 元素并执行的时候报错
  • Block 类型的变量强持有的 Block 是堆区 Block。由于数组也是强持有 Block,所以数组里面的 Block 元素被拷贝到了堆区
  • Block 变量弱持有的 Block 是栈区 Block

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

objectivec 复制代码
- (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]);
}

输出:

objectivec 复制代码
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 函数里,有这样的一段代码:

objectivec 复制代码
__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 函数的实现:

objectivec 复制代码
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 的实现。源码可以在 ‣ 中的 runtime.c 文件找到。

objectivec 复制代码
//
// 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 函数源码:

objectivec 复制代码
// 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 申请了内存空间。比较关键的是这两行代码:

objectivec 复制代码
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 肯定是堆区的

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

objectivec 复制代码
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 成员属性也变成堆区的:

objectivec 复制代码
*dest = _Block_byref_copy(object);

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

相关推荐
开心就好202516 小时前
iOS App 安全加固流程记录,代码、资源与安装包保护
后端·ios
开心就好202516 小时前
iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成
后端·ios
zhongjiahao2 天前
面试常问的 RunLoop,到底在Loop什么?
ios
wvy3 天前
iOS 26手势返回到根页面时TabBar的动效问题
ios
RickeyBoy3 天前
iOS 图片取色完全指南:从像素格式到工程实践
ios
aiopencode3 天前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
二流小码农4 天前
鸿蒙开发:路由组件升级,支持页面一键创建
android·ios·harmonyos
iceiceiceice4 天前
iOS PDF阅读器段评实现:如何从 PDFSelection 精准还原一个自然段
前端·人工智能·ios
ssshooter6 天前
Tauri 踩坑 appLink 修改后闪退
前端·ios·rust
二流小码农6 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos