Blocks

Objectvie-C Block

block 在 Objective-C开发中应用非常广泛,我们知道block会捕获外部对象,也知道使用block要防止循环引用。
“知其然不知所以然“是一件非常痛苦的。那么block这套机制在OC中是如何实现的呢?本文通过从C/C++到汇编层面分析block的实现原理。

Clang

clang 是Xcode的编译器前端,编译器前端负责语法分析、语义分析、生成中间代码。

比如当你再xcode中进行build一个.m 文件的时候,实际的编译命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
clang -x objective-c -arch x86_64
-fmessage-length=0
-fobjc-arc...
-Wno-missing-field-initializers ...
-DDEBUG=1 ...
-isysroot iPhoneSimulator10.1.sdk
-fasm-blocks ...
-I headers.hmap
-F 所需要的Framework
-iquote 所需要的Framework ...
-c ViewController.m
-o ViewController.o

Objective-C 也可以用GCC来编译,本文不做讲解。

clang除了能够进行编译以外,还有其他的一些用法。比如本文分析代码的核心命令就是这个:

1
$ clang -rewrite-objc 文件.m

通过这个命令,我们可以把Objective-C的代码用C++表示.
对于想深入理解clang命令同学,可以用明令行自带的工具来查看帮助文档

1
$ man clang

或者阅读clang官方文档.

查看汇编代码

在Xcode中,对于一个源文件,我们可以通过如下方式查看其汇编代码。这对于我们分析代码深层次的实现原理非常有用,这个在后面也可以看到。



Objective-C对象内存模型

为了本文讲解的更清楚,我们首先来看一个Objective-C对象的内存模型。我们首先常见一个类内容如下

TestClass.h

1
2
3
4
@interface TestClass : NSObject
@property (nonatomic, copy)id prop;
@end

TestClass.m

1
2
3
4
5
6
7
8
@implementation TestClass
- (void)testFuntion{
TestClass *obj = [[TestClass alloc] init];
}
@end

然后,我们用上文提到的clang命令将TestClass.m 转换成C++的表示

1
$ clang -rewrite-objc TestClass.m

当转换完毕当前目录会多一个TestClass.cpp文件,这个文件很大,接近十万行。

我们先搜索testFunction方法,以此方法作为切入

1
2
3
4
static void _I_TestClass_testFunction(TestClass * self, SEL _cmd) {
TestClass *obj = ((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("alloc")), sel_registerName("init"));
}

可以看到,转换成C++后,一个实例方法转换为一个静态方法,这个方法的内容看起来很乱,因为各种类型强制转换,去掉后就比较清楚了。

1
2
3
4
static void _I_TestClass_testFunction(TestClass * self, SEL _cmd) {
TestClass *obj = objc_msgSend(objc_msgSend(objc_getClass("TestClass"), sel_registerName("alloc")), sel_registerName("init"));
}

可以看到:

  • 转换后增加了两个参数: self 和 _cmd
  • 方法的调用换成了objc_msgSend,这是一个C函数,两个参数分别是Class 和 SEL

到这里,我们知道了一个OC的实例方法具体是怎么实现的了。
那么,一个OC对象在内存中是如何存储的呢?我们在刚刚的方法上可以找到这个类的完整实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct TestClass_IMPL {
struct NSObject_IMPL NSObject_IVARS;
id _prop;
};
// @property (nonatomic, copy)id prop;
/* @end */
// @implementation TestClass
static void _I_TestClass_testFunction(TestClass * self, SEL _cmd) {
TestClass *obj = ((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("alloc")), sel_registerName("init"));
}
static id _I_TestClass_prop(TestClass * self, SEL _cmd) { return (*(id *)((char *)self + OBJC_IVAR_$_TestClass$_prop)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_TestClass_setProp_(TestClass * self, SEL _cmd, id prop) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct TestClass, _prop), (id)prop, 0, 1); }
// @end

我们侧重来看看类对应的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct TestClass_IMPL {
struct NSObject_IMPL NSObject_IVARS;
id _prop;
};
// 依次向上查看结构体定义
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
struct objc_class {
Class isa ;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

可以看到,OC类实际上是按照以下方式存储对象的

  • isa指针。 指向objc_class 类型的结构体,这个结构体存储了方法的列表等类相关的信息,因为objc_msgSend中,发给对象的实际是一个字符串,运行时就是通过isa找到类对象,然后通过字符串找到方法的实际执行的。
  • ivar .属性背后的存储对象,到这里也可以看出来一个普通的属性就是ivar+getter+setter。

    也就是说,只要有isa指针,指向一个类对象,那么这个结构就能处理OC的消息机制,也就能当成OC的对象来用。

Block的本质

我们修改TestClass.m文件中的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef void(^VoildBlock)(void);
@implementation TestClass
- (void)testFunction{
NSInteger variable = 10;
VoildBlock tmp = ^{
NSLog(@"%ld",variable);
};
tmp();
}
@end

然后,重新用clang转换为C++,有关代码的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct __TestClass__testFunction_block_impl_0 {
struct __block_impl impl;
struct __TestClass__testFunction_block_desc_0* Desc;
NSInteger variable;
__TestClass__testFunction_block_impl_0(void *fp, struct __TestClass__testFunction_block_desc_0 *desc, NSInteger _variable, int flags=0) : variable(_variable) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestClass__testFunction_block_func_0(struct __TestClass__testFunction_block_impl_0 *__cself) {
NSInteger variable = __cself->variable; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_c9_vtczmvz10kq_lgl88b4xvhs80000gn_T_TestClass_d8c269_mi_0,variable);
}
static struct __TestClass__testFunction_block_desc_0 {
size_t reserved;
size_t Block_size;
} __TestClass__testFunction_block_desc_0_DATA = { 0, sizeof(struct __TestClass__testFunction_block_impl_0)};
static void _I_TestClass_testFunction(TestClass * self, SEL _cmd) {
NSInteger variable = 10;
VoildBlock tmp = ((void (*)())&__TestClass__testFunction_block_impl_0((void *)__TestClass__testFunction_block_func_0, &__TestClass__testFunction_block_desc_0_DATA, variable));
((void (*)(__block_impl *))((__block_impl *)tmp)->FuncPtr)((__block_impl *)tmp);
}
static id _I_TestClass_prop(TestClass * self, SEL _cmd) { return (*(id *)((char *)self + OBJC_IVAR_$_TestClass$_prop)); }

我们还是以方法作为切入点,看看具体是怎么实现的。_I_TestClass_testFunction是testFunction 转换后的方法。我们去掉一些强制代码,这样看起来更清楚:

1
2
3
4
5
6
static void _I_TestClass_testFunction(TestClass * self, SEL _cmd) {
NSInteger variable = 10;
VoidBlock temp = &__TestClass__testFunction_block_impl_0(__TestClass__testFunction_block_func_0, &__testClass__testFunction_block_desc_0_DATA, variable));
(temp->FuncPtr)(temp);
}

  • 初始化一个variable(也就是block捕获的变量)
  • 调用结构体testClasstestFunction_block_impl_0的构造函数来新建一个结构体,并且把地址赋值给temp变量(也就是初始化一个block)
  • 通过调用temp变量内的函数指针(C的函数指针)来执行实际的函数。

通过分析,我们大概能知道Block的实现

block背后的内存模型实际上是一个结构体,这个结构体会存储一个函数指针来指向block的实际执行代码。

接着,我们来深入的研究下block背后的结构体,也就是这个结构体TestClasstestFunction_block_impl_0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct __block_impl {
void *isa; //和上文提到的OC对象isa一样,指向的类对象,用来找到方法的实现
int Flags; //标识位
int Reserved; //保留
void *FuncPtr; //Block对应的函数指针
};
struct __TestClass__testFunction_block_impl_0 {
struct __block_impl impl;
struct __TestClass__testFunction_block_desc_0* Desc;
NSInteger variable;
__TestClass__testFunction_block_impl_0(void *fp, struct __TestClass__testFunction_block_desc_0 *desc, NSInteger _variable, int flags=0) : variable(_variable) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

我们回头看看block初始化那句代码

1
2
3
4
5
6
7
//OC
VoidBlock temp = ^{
NSLog(@"%ld",variable);
};
//C++
VoildBlock tmp = ((void (*)())&__TestClass__testFunction_block_impl_0((void *)__TestClass__testFunction_block_func_0, &__TestClass__testFunction_block_desc_0_DATA, variable));
((void (*)(__block_impl *))((__block_impl *)tmp)->FuncPtr)((__block_impl *)tmp);

在对应之前代码块的构造函数,我们可以清楚的看到,在初始化的时候三个参数一次是

  • 函数指针 TestClasstestFunction_block_func_0
  • block的描述结构体(全局静态结构体)TestClasstestFunction_block_desc_0_DATA
  • 捕获的变量variable

接着,我们看看block背后的C函数TestClasstestFunction_block_func_0

1
2
3
4
5
static void __TestClass__testFunction_block_func_0(struct __TestClass__testFunction_block_impl_0 *__cself) {
NSInteger variable = __cself->variable; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_c9_vtczmvz10kq_lgl88b4xvhs80000gn_T_TestClass_d8c269_mi_0,variable);
}

Tips:
内存中存储区域可分为几个区域:

  • TEXT 代码区
  • DATA 数据区
  • Stack 栈区
  • HEAP 堆区

上文的字符串@”%ld”,对应C++代码是)&NSConstantStringImplvar_folders_c9_vtczmvz10kq_lgl88b4xvhs80000gn_T_TestClassd8c269
mi_0,是存储在数据区的。这样即使程序中有多个@”%ld”,也不会创建多个实例。

可以看到。这个C函数的参数
TestClasstestFunction_block_impl_0,也就是一个block类型。然后在方法体内部,使用这个block 参数。

最后,我们分析block的描述信息,也就是这段代码

1
2
3
4
static struct __TestClass__testFunction_block_desc_0 {
size_t reserved;
size_t Block_size;
} __TestClass__testFunction_block_desc_0_DATA = { 0, sizeof(struct __TestClass__testFunction_block_impl_0)}

这段代码不难理解,就是声明一个描述信息的结构体,然后初始化这个结构体的全局静态变量。

分析到这里,上面代码的大多数内容我们都理解了,但是有一点我们还没有搞清楚,就是isa指向的内容_NSConcreteStackBlock

1
impl.isa = &_NSConcreteStackBlock;

但是,到这里我们知道了为什么Block可以当作OC对象来用的原因:就是这个指向类对象的isa指针。

Block 类型

上文提到了_NSConcreteStackBlock是Block一种,block一共有三种类型

  • NSConcreteStackBlock 栈上分配,作用域结束后自动释放
  • NSConcreteGlobalBlock 全局分配,类似全局变量,存储在数据段,内存中只有一份
  • NSConcreteHeapBlock 堆上分配

我们仍然尝试用Clang转换的方式,来验证我们的理论。将TestClass.m内容修修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef void(^VoildBlock)(void);
@interface TestClass ()
@property (nonatomic, copy) VoildBlock headBlock;
@end
@implementation TestClass
- (void)testFunction{
VoildBlock stackBlock = ^{};
stackBlock();
_headBlock = ^{};
}
@end

然后转成C++之后,分别对应如下

全局globalBlock

1
impl.isa = &_NSConcreteGlobalBlock;

栈上stackBlock

1
impl.isa = &_NSConcreteStackBlock;

属性Block

1
impl.isa = &_NSConcreteStackBlock;

What the fuck! 怎么属性的block是栈类型的,难道不该是堆类型的吗?

到这里,C/C++层面的代码已经无法满足我们的需求了。我们试着把代码转成汇编,一探究竟:

方便分析属性block究竟是怎么回事,我们修改.m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import "TestClass.h"
typedef void(^VoildBlock)(void);
@interface TestClass ()
@property (nonatomic, copy) VoildBlock headBlock;
@end
@implementation TestClass
- (void)testFunction{
_headBlock = ^{};
}
@end

转换成汇编后,在方法部分我们看到类似汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bl _objc_retainBlock
movw r1, :lower16:(_OBJC_IVAR_$_TestClass._headBlock-(LPC0_1+4))
movt r1, :upper16:(_OBJC_IVAR_$_TestClass._headBlock-(LPC0_1+4))
LPC0_1:
add r1, pc
.loc 1 0 0 @ /Users/apple/git/git_code/Demo1/Demo1/TestClass.m:0:0
ldr r2, [sp, #4]
.loc 1 22 5 @ /Users/apple/git/git_code/Demo1/Demo1/TestClass.m:22:5
ldr r1, [r1]
add r1, r2
.loc 1 22 16 is_stmt 0 @ /Users/apple/git/git_code/Demo1/Demo1/TestClass.m:22:16
ldr r2, [r1]
str r0, [r1]
.loc 1 22 16 discriminator 1 @ /Users/apple/git/git_code/Demo1/Demo1/TestClass.m:22:16
mov r0, r2
bl _objc_release

也就是说,在返回方法之前,一次调用了

1
2
_objc_retainBlock
_objc_release

那么 _objc_retainBlock 就是block从栈到堆得黑魔法。

我们通过runtime的源码来分析这个方法的实现:

1
2
3
4
5
6
7
8
9
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

到这里我们就清楚了,编译器为我们自动插入了 _objc_retainBlock ,而这个函数就是把栈上的block拷贝到堆上。

Tips: 通常在写属性的时候,block都会声明为copy。这是显式的表示,即使block是栈上的,也会拷贝到堆上。其实在赋值的时候,编译器已经自动帮我们做了这些,所以其实使用strong也可以。

那么,一个临时变量的block会被拷贝到堆上么?

修改testFunction :

1
2
3
4
5
6
- (void)testFunction{
VoidBlock stackBlock = ^{};
}

继续查看汇编:

1
2
3
4
5
6
7
8
9
10
11
12
Ltmp0:
.loc 1 22 15 prologue_end @ /Users/apple/git/git_code/Demo1/Demo1/TestClass.m:22:15
mov r0, r2
bl _objc_retainBlock
movs r1, #0
mov r2, sp
str r0, [sp]
.loc 1 24 1 @ /Users/apple/git/git_code/Demo1/Demo1/TestClass.m:24:1
mov r0, r2
bl _objc_storeStrong
add sp, #12
pop {r7, pc}

我们仍然看到了 _objc_retainBlock ,也就是即使一个函数中的block,在ARC开启的情况下,仍然会拷贝到堆上。

__block

通过之前的研究,我们知道了block如何捕获外部变量,也知道了block的几种类型。那么block如何修改外部变量的呢?

block 是不可以直接修改外部变量的,比如:

1
2
3
4
NSInteger variable = 0;
_heapBlock = ^{
variable = 1;
};

直接那么写,编译器是不会通过的,想想也简单,因为变量可能在block之前就被释放掉了,直接那么赋值会导致野指针。
在OC层面,我们可以通过__block关键字,那么加了这个关键字之后,实际上C++层面的代码时什么样的呢?

1
2
3
4
5
6
7
- (void)testFunction{
__block NSInteger variable = 0;
VoidBlock stackBlock = ^{
variable = 1;
};
}

在转换成C++代码后,如下:

1
2
3
4
5
static void _I_TestClass_testFunction(TestClass * self, SEL _cmd) {
__attribute__((__blocks__(byref))) __Block_byref_variable_0 variable = {(void*)0,(__Block_byref_variable_0 *)&variable, 0, sizeof(__Block_byref_variable_0), 0};
VoidBlock stackBlock = ((void (*)())&__TestClass__testFunction_block_impl_0((void *)__TestClass__testFunction_block_func_0, &__TestClass__testFunction_block_desc_0_DATA, (__Block_byref_variable_0 *)&variable, 570425344));
}

可以看到,__block NSInteger variable = 0转换成了一个结构体

1
__attribute__((__blocks__(byref))) __Block_byref_variable_0 variable = {(void*)0,(__Block_byref_variable_0 *)&variable, 0, sizeof(__Block_byref_variable_0), 0};

这个结构体的定义如下:

1
2
3
4
5
6
7
struct __Block_byref_variable_0 {
void *__isa;
__Block_byref_variable_0 *__forwarding;
int __flags;
int __size;
NSInteger variable; //这个是要修改的变量
};

通过初始化我们可以看到

  • __isa指向0
  • forwarding 指向Block_byref_variable_0自身
  • __flags为0
  • __size就是结构题的大小
  • variable是我们定义的原始值0
    到这里,我们有一点疑惑

  • 为什么要存在一个__forwarding来指向自身呢?
    我们来看看block的方法体,也就是这部分

    1
    2
    3
    ^{
    variable = 1;
    }

转换成C++后:

1
2
3
4
static void __DemoClass__demoFunction_block_func_0(struct __DemoClass__demoFunction_block_impl_0 *__cself) {
__Block_byref_variable_0 *variable = __cself->variable; // bound by ref
variable->__forwarding->variable) = 1;
}

也就是说__forwarding存在的意义就是通过它来访问到变量的地址,如果这个指针一直指向自身,那么它也就没有存在的意义,也就是在将来的某一个时间点,它一定会指向另外一个数据结构。
我们在上文中讲到,ARC开启的时候,栈上的block会被复制到堆上。

在没有复制之前




复制之后


这样,我们就清楚原因了:

即使发生了复制,只要修改__forwarding的指向,我们就能够保证栈上和堆上的block都访问同一个对象。

Block 对象捕获

到这里,我们分析的block都是捕获一个外部值,并不是对象。值和对象最大的区别就是对象有生命周期,对象我们需要考虑引用计数。

修改testFunction:

1
2
3
4
5
6
7
- (void)testFunction{
NSObject * obj = [[NSObject alloc] init];
VoidBlock stackBlock = ^{
[obj description];
};
stackBlock();
}

再转换成C++后,我们对比之前捕获NSInteger,发现多了两个生命周期管理函数

1
2
3
static void __TestClass__testFunction_block_copy_0(struct __TestClass__testFunction_block_impl_0*dst, struct __TestClass__testFunction_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __TestClass__testFunction_block_dispose_0(struct __TestClass__testFunction_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

我们再查看下Block_object_assign和Block_object_dispose的定义

1
2
3
4
// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int);
// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int);

也就是说,编译器通过这两个函数来管理Block捕获对象的生命周期。其中

  • _Block_object_assign相当于ARC中的reatain,在block从栈上拷贝到堆上的时候调用
  • _Block_object_dispose相当于ARC中的release,在block堆上废弃的时候调用

总结

  • block在C语言层面就是结构体,结构体存储了函数指针和捕获的变量列表
  • block分为全局,栈上,堆上三种,ARC开启的时候,会自动把栈上的block拷贝到堆上
  • __block变量在C语言层面也是一个结构体
  • block捕获对象的时候会增加对象的引用计数。