探索iOS block的实现原理

初探block的实现

Clang提供了中间代码展示的选项供我们进一步了解block的原理。
以一段block0.c文件中的很简单的代码为例:

#include <stdio.h>

int main()
{	
	return 0;
}
复制代码

使用clang的-rewrite-objc选项生成中间代码:

clang -rewrite-objc block0.c

# 或者使用命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc block0.c -o block0.cpp

# 或者使用命令
clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations  block0.c -o block0.cpp

复制代码

查看编译后的block0.cppblock的实现如下:

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

从命名可以看出这是block的实现,并且得知block在Clang编译器前端得到实现,可以生成C中间代码。很多语言都可以只实现编译器前端,生成C中间代码,然后利用现有的很多C编译器后端。

从结构体的4个成员可以看出:

  • Flags可以先略过
  • Reserved可以先略过
  • isa指针表明了block可以是一个NSObject
  • FuncPtr指针显然是block对应的函数指针。

由此,揭开了block的神秘面纱。

不过,block相关的变量放哪里呢?上面提到block可以capture词法范围内(或者说是外层上下文、作用域)的状态,即便是出了该范围,仍然可以修改这些状态。这是如何做到的呢?

一个简单的block实现

先看一个只输出一句话的block是怎么样的。

#include <stdio.h>

int main()
{	
	void (^blk)(void) = ^{printf("hello block!\n");};
	return 0;
}
	
复制代码

使用clang -rewrite-objc block1.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;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("hello 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()
{
 void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
 return 0;
}
复制代码

首先出现的结构体就是__main_block_impl_0,可以看出是根据所在函数(main函数)以及出现序列(第0个)进行命名的。如果是全局block,就根据变量名和出现序列进行命名。
__main_block_impl_0中包含了两个成员变量和一个构造函数,成员变量分别是__block_impl结构体和描述信息Desc,之后在构造函数中初始化block的类型信息和函数指针等信息。

接着出现的是__main_block_func_0函数,即block对应的函数体。该函数接受一个__cself参数,即对应的block自身。

再下面是__main_block_desc_0结构体,其中比较有价值的信息是block大小。

最后就是main函数中对block的创建和调用,可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。

这里,block的类型用_NSConcreteStackBlock来表示,表明这个block位于栈中。同样地,还有_NSConcreteMallocBlock和_NSConcreteGlobalBlock。

由于block也是NSObject,我们可以对其进行retain操作。不过在将block作为回调函数传递给底层框架时,底层框架需要对其copy一份。比方说,如果将回调block作为属性,不能用retain,而要用copy。我们通常会将block写在栈中,而需要回调时,往往回调block已经不在栈中了,使用copy属性可以将block放到堆中。或者使用Block_copy()和Block_release()。

捕获局部变量的block

再看一个访问局部变量的block是怎样的。

#include <stdio.h>

int main()
{	
	int i = 1024;
	void (^blk)(void) = ^{printf("hello block, index = %d!\n",i);};
	return 0;
}
	
复制代码

生成中间代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int i;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
{
    int i = __cself->i; // bound by copy
    printf("hello block, index = %d!\n",i);
}

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 i = 1024;
 void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, i));
 return 0;
}
复制代码

可以看出这次的block结构体__main_block_impl_0多了个成员变量i,用来存储使用到的局部变量i(值为1024);并且此时可以看到__cself参数的作用,类似C++中的this和Objective-C的self。

如果我们尝试修改局部变量i,则会得到如下错误:

$ clang -rewrite-objc block1.c                                                                                        
/var/folders/l3/6z9vzk6j31dgt27r3n4j8r780000gn/T/block1-fd3e93.i:442:11: error: variable is not assignable (missing __block type specifier)
        i = 0;
        ~ ^
1 error generated.
复制代码

错误信息很详细,既告诉我们变量不可赋值,也提醒我们要使用__block类型标识符。

为什么不能给变量i赋值呢?

因为main函数中的局部变量i和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量i还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了(block此时在哪里?),再用指针访问就非法了。

所以,对于auto类型的局部变量,不允许block进行修改是合理的。

block修改本地变量

那么,__block类型变量是如何支持修改的呢?

#include <stdio.h>

int main()
{	
 	__block int i = 1024;
	void (^blk)(void) = ^{
		i = 1025;
		printf("hello block, index = %d!\n",i);
	};
	blk();
	return 0;
}
	

复制代码

我们为int类型变量加上__block指示符,使得变量i可以在block函数体中被修改。

此时再看中间代码,会多出很多信息。首先是__block变量对应的结构体:

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};
复制代码

由第一个成员__isa指针也可以知道__Block_byref_i_0也可以是NSObject。

第二个成员__forwarding指向自己,为什么要指向自己?指向自己是没有意义的,只能说有时候需要指向另一个__Block_byref_i_0结构。

最后一个成员是目标存储变量i。

此时,__main_block_impl_0结构如下:


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
复制代码

__main_block_impl_0的成员变量i变成了__Block_byref_i_0 *类型。

对应的函数__main_block_func_0如下:

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

  (i->__forwarding->i) = 1025;
  printf("hello block, index = %d!\n",(i->__forwarding->i));
 }
复制代码

亮点是__Block_byref_i_0指针类型变量i,通过其成员变量__forwarding指针来操作另一个成员变量。 🙂

而main函数如下:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->i, 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_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
  
 void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
 
 return 0;
}
复制代码

通过这样看起来有点复杂的改变,我们可以修改变量i的值。但是问题同样存在:__Block_byref_i_0类型变量i仍然处于栈上,当block被回调执行时,变量i所在的栈已经被展开,怎么办?

在这种关键时刻,__main_block_desc_0站出来了,此时,__main_block_desc_0多了两个成员函数:copy和dispose,分别指向__main_block_copy_0和__main_block_dispose_0。

当block从栈上被copy到堆上时,会调用__main_block_copy_0将__block类型的成员变量i从栈上复制到堆上;而当block被释放时,相应地会调用__main_block_dispose_0来释放__block类型的成员变量i。

一会在栈上,一会在堆上,那如果栈上和堆上同时对该变量进行操作,怎么办?

这时候,__forwarding的作用就体现出来了:当一个__block变量从栈上被复制到堆上时,栈上的那个__Block_byref_i_0结构体中的__forwarding指针也会指向堆上的结构。

日常开发中的block实现探究

参考如下MCBlock.m实现代码:

#import "MCBlock.h"

@implementation MCBlock

// 全局变量
int global_var = 4;

- (void)method
{
    int var1 = 1;
    
    __unsafe_unretained id unsafe_obj = nil;
    __strong id strong_obj = nil;
    
    static int static_var = 3;
    
    void (^my_Block)(void) = ^{
        NSLog(@"局部变量<基本数据类型> var %d",var1);
        
        NSLog(@"局部变量<__unsafe_unretained对象类型> var %@",unsafe_obj);
        
        NSLog(@"局部变量<strong对象类型> var %@",strong_obj);
        
        NSLog(@"静态变量 %d",static_var);
        
    };
    
    my_Block();
}
@end
复制代码

使用clang -rewrite-objc MCBLock.m生成中间代码,得到的block部分的如下的实现代码片段:

int global_var = 4;

// block实现的结构体
struct __MCBlock__method_block_impl_0 {
  struct __block_impl impl;
  struct __MCBlock__method_block_desc_0* Desc;
  int var1;
  id unsafe_obj;
  id strong_obj;
  int *static_var;
  __MCBlock__method_block_impl_0(void *fp, struct __MCBlock__method_block_desc_0 *desc, int _var1, id _unsafe_obj, id _strong_obj, int *_static_var, int flags=0) : var1(_var1), unsafe_obj(_unsafe_obj), strong_obj(_strong_obj), static_var(_static_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __MCBlock__method_block_func_0(struct __MCBlock__method_block_impl_0 *__cself) {
  int var1 = __cself->var1; // bound by copy
  id unsafe_obj = __cself->unsafe_obj; // bound by copy
  id strong_obj = __cself->strong_obj; // bound by copy
  int *static_var = __cself->static_var; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_l3_6z9vzk6j31dgt27r3n4j8r780000gn_T_MCBlock_27310d_mi_0,var1);

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_l3_6z9vzk6j31dgt27r3n4j8r780000gn_T_MCBlock_27310d_mi_1,unsafe_obj);

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_l3_6z9vzk6j31dgt27r3n4j8r780000gn_T_MCBlock_27310d_mi_2,strong_obj);

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_l3_6z9vzk6j31dgt27r3n4j8r780000gn_T_MCBlock_27310d_mi_3,(*static_var));

    }
static void __MCBlock__method_block_copy_0(struct __MCBlock__method_block_impl_0*dst, struct __MCBlock__method_block_impl_0*src) {_Block_object_assign((void*)&dst->unsafe_obj, (void*)src->unsafe_obj, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->strong_obj, (void*)src->strong_obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __MCBlock__method_block_dispose_0(struct __MCBlock__method_block_impl_0*src) {_Block_object_dispose((void*)src->unsafe_obj, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->strong_obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __MCBlock__method_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __MCBlock__method_block_impl_0*, struct __MCBlock__method_block_impl_0*);
  void (*dispose)(struct __MCBlock__method_block_impl_0*);
} __MCBlock__method_block_desc_0_DATA = { 0, sizeof(struct __MCBlock__method_block_impl_0), __MCBlock__method_block_copy_0, __MCBlock__method_block_dispose_0};

static void _I_MCBlock_method(MCBlock * self, SEL _cmd) {
    int var1 = 1;

    __attribute__((objc_ownership(none))) id unsafe_obj = __null;
    __attribute__((objc_ownership(strong))) id strong_obj = __null;

    static int static_var = 3;

    void (*my_Block)(void) = ((void (*)())&__MCBlock__method_block_impl_0((void *)__MCBlock__method_block_func_0, &__MCBlock__method_block_desc_0_DATA, var1, unsafe_obj, strong_obj, &static_var, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)my_Block)->FuncPtr)((__block_impl *)my_Block);
}
复制代码

block在内存中的位置

在ARC环境下,block只会保存在两个地址:全局区和堆区

在非ARC环境下,才会保存在全局区(全局block,不捕获变量的局部block),栈区(捕获变量的局部block)和堆区三种地方。

所以为什么对象的property声明的时候,需要保证是使用copy,而不是strong,就是这个原因,保证在ARC和非ARC下进行统一。

参考

QA

基本类型的变量和对象类型的变量被__block修饰与否各有什么区别?

 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
复制代码

小结

当我们创建一个block,并调用之,编译器为我们做的事情如下:

  1. 创建block所有的Struct代码:一个构造函数,一个真正的执行代码函数,一个描述信息(假如使用了局部变量,则会有每个局部变量的定义)。
  2. 将我们的创建代码转码为block_impl的构造语句。
  3. 将我们的执行语句转码为对block的执行函数的调用。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享