OC中Block内存结构以及copy/持有外部变量等操作的实现原理分析

一、前言

最近对App中的埋点代码进行了改造(随着项目的增大,散落在系统各处的埋点代码实在不好管理),利用AOP的方式将散布在各个页面中的埋点代码统一起来组成一个独立的模块,从而减少埋点行为对业务代码的侵入。

熟悉iOS AOP编程的同学肯定听过Aspects这个框架,我们也使用这个框架实现Method Swizzle,通过Method Swizzle给指定的方法添加side-effect,在side-effect中做相应的埋点上报操作。

在学习Aspects源码的过程中,对Runtime以及OC的消息转发机制有了一个深入的了解,写几篇文章记录一下,文章中会穿插一些对于Aspect源码的分析内容,如有错误的地方,欢迎大家斧正。

二、Block Memory Layout

Everything hides in the source code: libclosure-67

block的本质可以看做一个函数,这点从它的定义可以看出来(Block_private.h):

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
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
// 捕获的外部变量,block将外部变量复制到结构体中,因此block内部可以访问到其外部的变量
};

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

逐条分析:

  • void *isa: oc中的所有对象都有该指针,指向其所属的类。oc对象接收到消息后,根据isa指针找到其所属的类,然后获取存储在类中的property_list、method_list等信息,然后进行下一步的操作;
  • volatile int32_t flags: 标识位,block内部操作时会用到,比如copy、dispose等,其中包含了block的reference count、是否heap block等信息;
  • int32_t reserved: 保留变量
  • void (*invoke)(void *, ...): 函数指针,指向block的实现函数地址;
  • struct Block_descriptor_1 *descriptor: 附加描述信息,存储了一些如block大小、copy/dispose函数指针、签名(用于构造方法签名)等信息。

Aspects源码截取:构造blockMethodSignature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 初始化desc指针,指向block的descriptor结构体起始地址
void *desc = layout->descriptor;
// 2. 移动指针,跳过reserved、size变量
desc += 2 * sizeof(unsigned long int);
// 3. 判断是否有copy/dispose信息,如果有,移动desc指针,跳过copy/dispose指针变量
if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
desc += 2 * sizeof(void *);
}
// 4. 移动后的指针应该指向block的signature信息,判断是否为空
if (!desc) {
NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
// 5. 指针不为空,取出signature指针,构造methodSignature
const char *signature = (*(const char **)desc);
return [NSMethodSignature signatureWithObjCTypes:signature];

在Block_private.h中还能看到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Block_private.h
// the raw data space for runtime classes for blocks
// class+meta used for stack, malloc, and collectable based blocks
BLOCK_EXPORT void * _NSConcreteMallocBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteAutoBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
// declared in Block.h
// BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
// BLOCK_EXPORT void * _NSConcreteStackBlock[32];

// Block.h
// Used by the compiler. Do not use these variables yourself.
BLOCK_EXPORT void * _NSConcreteGlobalBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
BLOCK_EXPORT void * _NSConcreteStackBlock[32]
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

从其命名方式:Global/Stack(栈)/Malloc(堆)可以推测出Block的类型:NSGlobalBlock(全局Block)、NSStackBlock(栈区Block)、NSMallocBlock(堆区Block)。下面结合代码分别分析一下这三种block。

2.1 NSGlobalBlock

我们知道,程序中的全局变量存储在内存中的数据区(.data区),这块内存中的内容在编译期就已经完全确定了。因此,这种Block无法捕捉任何变量,也无需任何运行时状态来参与运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef void(^TestBlock)(void);
static NSString *str = @"static test str"

- (void)testBlock {
TestBlock block = ^() {
NSLog(@"innner block");
};
block();
NSLog(@"block address:%@", block);
NSLog(@"static string address:%p", str);
}

//输出:
innner block
block address:<__NSGlobalBlock__: 0x10428a080>
static string address:0x104290908

可以看出,NSGlobalBlock的存储区域与静态变量的地址相近,二者都存储于全局数据区。

2.2 NSStackBlock和NSMallocBlock

  • MRC下,无论是对于作为函数临时变量的block和对象属性的block,都默认存储在栈区,其生命周期随着变量作用域结束而结束,如果想要保留block,可以显示调用copy方法将block拷贝到堆中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)testBlock {
__block int i = 0;

TestBlock stackBlock = ^{
i++;
NSLog(@"i = %ld", (long)i);
};
stackBlock();
NSLog(@"stack block address: %@", stackBlock);

TestBlock heapBlock = [^{
i++;
NSLog(@"i = %ld", (long)i);
} copy];
heapBlock();
NSLog(@"heap block address: %@", heapBlock);
}

// 输出
i = 1
stack block address: <__NSStackBlock__: 0x7ffeeeeeb970>
i = 2
heap block address: <__NSMallocBlock__: 0x6000017bbfc0>
  • ARC下,由于编译期会隐式地为非全局block添加copy操作,因此所有的非全局block都存储于堆区。这个copy操作属于深拷贝,将block拷贝到堆中,这个拷贝的block对象强引用它捕获的变量,因此ARC下要注意block的隐式copy引起的retain cycle问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testBlock {
__block int i = 0;

TestBlock block = ^{
i++;
NSLog(@"i = %ld", (long)i);
};
block();
NSLog(@"heap block address: %@", block);
}

// 输出
i = 1
heap block address: <__NSMallocBlock__: 0x60000221eb50>

因此,MRC下block有三种:NSGlobalBlock、NSStackBlock、NSMallocBlock,
而ARC下由于编译器隐式添加copy操作,只有两种block:NSGlobalBlock、NSMallocBlock。

三、Block持有外部变量分析

上一节中说过block结构体中有专门存放捕获的变量的区域,那么block是如何捕获到外部变量的呢?分几种情况分析。

3.1 block持有非__block修饰的基本类型变量

先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)testBlock {
int j = 0;
NSLog(@"before block, j address: %p", &j);

TestBlock block = ^{
NSLog(@"inner block, j address: %p", &j);
};
block();
NSLog(@"after block, j address: %p", &j);
}

// 输出
before block, j address: 0x7ffee08849bc
inner block, j address: 0x600000dac080
after block, j address: 0x7ffee08849bc

从输出可以看出,打印变量j的地址,before block和after block的地址相同,且二者与block内部的地址不同。(ARC和MRC下,输出的结果一样)

分析原因

使用clang -rewrite-objc命令重写.m文件,只选取关键代码

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
33
34
35
36
37
38
39
#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
...
struct __TestObject__testBlock_block_impl_0 {
struct __block_impl impl;
struct __TestObject__testBlock_block_desc_0* Desc;
int j;
__TestObject__testBlock_block_impl_0(void *fp, struct __TestObject__testBlock_block_desc_0 *desc, int _j, int flags=0) : j(_j) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestObject__testBlock_block_func_0(struct __TestObject__testBlock_block_impl_0 *__cself) {
int j = __cself->j; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_3e4a38_mi_1, &j);
}

static struct __TestObject__testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
} __TestObject__testBlock_block_desc_0_DATA = { 0, sizeof(struct __TestObject__testBlock_block_impl_0)};

static void _I_TestObject_testBlock(TestObject * self, SEL _cmd) {
int j = 0;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_3e4a38_mi_0, &j);

TestBlock block = ((void (*)())&__TestObject__testBlock_block_impl_0((void *)__TestObject__testBlock_block_func_0, &__TestObject__testBlock_block_desc_0_DATA, j));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_3e4a38_mi_2, &j);
}

其中__TestObject__testBlock_block_impl_0是testBlock的实现,包含了一个isa指针(本例中它指向NSConcreteStackBlock, 说明这个block是分配在栈上的)、一个impl函数指针(本例中它指向__TestObject__testBlock_block_func_0)、一个Desc结构体指针,从block的实现来看,捕获到的外部变量会追加到Desc指针后面,使得__TestObject__testBlock_block_impl_0结构体变大。

__TestObject__testBlock_block_func_0中可以看到:int j = __cself->j; // bound by copy这样一句代码,block将捕获的外部变量复制一份到其内部,这也说明了为什么block内部打印的变量j的地址与外部不一致。

3.2 block持有__block修饰的基本类型变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)testBlock {
__block int j = 0;
NSLog(@"before block, j address: %p", &j);

TestBlock block = ^{
NSLog(@"inner block, j address: %p", &j);
};
block();
NSLog(@"after block, j address: %p", &j);
}

// 输出
// MRC
before block, j address: 0x7ffeec8969a8
inner block, j address: 0x7ffeec8969a8
after block, j address: 0x7ffeec8969a8
// ARC
before block, j address: 0x7ffeeea309a8
inner block, j address: 0x600000c54558
after block, j address: 0x600000c54558

从输出可以看到,MRC下,block前后和block内部打印的变量j的地址都相同,而且都是存在于栈中;ARC下,由于block的隐式copy操作,block内部和block执行后打印变量j的地址是在堆中,而block之前的地址是在栈中。

分析原因

使用clang重写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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
...
struct __Block_byref_j_0 {
void *__isa;
__Block_byref_j_0 *__forwarding;
int __flags;
int __size;
int j;
};

struct __TestObject__testBlock_block_impl_0 {
struct __block_impl impl;
struct __TestObject__testBlock_block_desc_0* Desc;
__Block_byref_j_0 *j; // by ref
__TestObject__testBlock_block_impl_0(void *fp, struct __TestObject__testBlock_block_desc_0 *desc, __Block_byref_j_0 *_j, int flags=0) : j(_j->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestObject__testBlock_block_func_0(struct __TestObject__testBlock_block_impl_0 *__cself) {
__Block_byref_j_0 *j = __cself->j; // bound by ref

NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_f67065_mi_1, &(j->__forwarding->j));
}
static void __TestObject__testBlock_block_copy_0(struct __TestObject__testBlock_block_impl_0*dst, struct __TestObject__testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->j, (void*)src->j, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __TestObject__testBlock_block_dispose_0(struct __TestObject__testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->j, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __TestObject__testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestObject__testBlock_block_impl_0*, struct __TestObject__testBlock_block_impl_0*);
void (*dispose)(struct __TestObject__testBlock_block_impl_0*);
} __TestObject__testBlock_block_desc_0_DATA = { 0, sizeof(struct __TestObject__testBlock_block_impl_0), __TestObject__testBlock_block_copy_0, __TestObject__testBlock_block_dispose_0};

static void _I_TestObject_testBlock(TestObject * self, SEL _cmd) {
__attribute__((__blocks__(byref))) __Block_byref_j_0 j = {(void*)0,(__Block_byref_j_0 *)&j, 0, sizeof(__Block_byref_j_0), 10};
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_f67065_mi_0, &(j.__forwarding->j));

TestBlock block = ((void (*)())&__TestObject__testBlock_block_impl_0((void *)__TestObject__testBlock_block_func_0, &__TestObject__testBlock_block_desc_0_DATA, (__Block_byref_j_0 *)&j, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_f67065_mi_2, &(j.__forwarding->j));
}

可以看到,block内部除了isa指针、impl函数指针、Desc指针外,增加了一个__Block_byref_j_0类型的结构体指针,block捕获的外部变量由该结构体管理,block通过持有该结构体指针实现了对外部变量修改的目的。而且__TestObject__testBlock_block_desc_0中新增了copy和dispose两个函数指针,用于实现对__Block_byref_j_0结构体的内存管理。

__Block_byref_j_0结构体中包含了:

  • isa指针: 指向该__block变量的类型,本例中是基本数据类型,因此,isa指针为(void *)0
  • forwarding指针: 指向该结构体,用于取值;
  • flags: 标识位,对于基本数据类型该值为0,对于对象类型该值为3554432;
  • size: 该结构体的大小;
  • j: 不同的捕获变量,该值命名类型不同,本例中用于储存捕获的整型变量的值;

在block内部和block前后读写变量j的值,都是读取或修改j->__forwarding->j或者j.__forwarding->j的值,由于block内外获取到的__forwarding指针指向同一结构体地址,因此使得block内部修改变量影响到了block外部。

3.3 block持有__block修饰的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)testBlock {
__block ObjectA *blockObj = [[ObjectA alloc] init];
NSLog(@"before block, blockObj address: %p, obj pointer address: %p", blockObj, &blockObj);
TestBlock block = ^ {
NSLog(@"inner block, blockObj address: %p, obj pointer address: %p", blockObj, &blockObj);
};
block();
NSLog(@"after block, blockObj address: %p, obj pointer address: %p", blockObj, &blockObj);
}

// 输出
// MRC
before block, blockObj address: 0x60000356ae50, obj pointer address: 0x7ffee823b9a8
inner block, blockObj address: 0x60000356ae50, obj pointer address: 0x7ffee823b9a8
after block, blockObj address: 0x60000356ae50, obj pointer address: 0x7ffee823b9a8

// ARC
before block, blockObj address: 0x6000028667c0, obj pointer address: 0x7ffee4d8e9a8
inner block, blockObj address: 0x6000028667c0, obj pointer address: 0x600002431318
after block, blockObj address: 0x6000028667c0, obj pointer address: 0x600002431318

可以看到,无论是MRC或者ARC下,blockObj对象的地址都不变,在堆中;MRC下,blockObj对象的指针地址不变,在栈中;而ARC下,blockObj对象的指针地址在copy之后发生了变化,指针从栈中拷贝到了堆中。

这也说明了block对其持有的对象的copy操作只是浅拷贝,拷贝的是指针,而指针指向的对象始终存在于堆中的某个区域

分析原因

使用clang重写

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
33
34
35
36
37
38
39
40
41
42
43
44
struct __Block_byref_blockObj_0 {
void *__isa;
__Block_byref_blockObj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
ObjectA *blockObj;
};

struct __TestObject__testBlock_block_impl_0 {
struct __block_impl impl;
struct __TestObject__testBlock_block_desc_0* Desc;
__Block_byref_blockObj_0 *blockObj; // by ref
__TestObject__testBlock_block_impl_0(void *fp, struct __TestObject__testBlock_block_desc_0 *desc, __Block_byref_blockObj_0 *_blockObj, int flags=0) : blockObj(_blockObj->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestObject__testBlock_block_func_0(struct __TestObject__testBlock_block_impl_0 *__cself) {
__Block_byref_blockObj_0 *blockObj = __cself->blockObj; // bound by ref

NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_ba14cb_mi_1, &(blockObj->__forwarding->blockObj));
}
static void __TestObject__testBlock_block_copy_0(struct __TestObject__testBlock_block_impl_0*dst, struct __TestObject__testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->blockObj, (void*)src->blockObj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __TestObject__testBlock_block_dispose_0(struct __TestObject__testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->blockObj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __TestObject__testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestObject__testBlock_block_impl_0*, struct __TestObject__testBlock_block_impl_0*);
void (*dispose)(struct __TestObject__testBlock_block_impl_0*);
} __TestObject__testBlock_block_desc_0_DATA = { 0, sizeof(struct __TestObject__testBlock_block_impl_0), __TestObject__testBlock_block_copy_0, __TestObject__testBlock_block_dispose_0};

static void _I_TestObject_testBlock(TestObject * self, SEL _cmd) {
__attribute__((__blocks__(byref))) __Block_byref_blockObj_0 blockObj = {(void*)0,(__Block_byref_blockObj_0 *)&blockObj, 33554432, sizeof(__Block_byref_blockObj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((ObjectA *(*)(id, SEL))(void *)objc_msgSend)((id)((ObjectA *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ObjectA"), sel_registerName("alloc")), sel_registerName("init"))};
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_ba14cb_mi_0, &(blockObj.__forwarding->blockObj));
TestBlock block = ((void (*)())&__TestObject__testBlock_block_impl_0((void *)__TestObject__testBlock_block_func_0, &__TestObject__testBlock_block_desc_0_DATA, (__Block_byref_blockObj_0 *)&blockObj, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_ba14cb_mi_2, &(blockObj.__forwarding->blockObj));
}

与上例的结构基本相同,不同的是__Block_byref_blockObj_0结构体中增加了copy、dispose两个函数指针用于实现block持有对象的内存管理,__Block_byref_blockObj_0持有的是捕获对象的指针。

3.4 block持有类的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)testBlock {

NSLog(@"before block, str value: %@", _str);
NSLog(@"before block, str address: %p", &_str);
TestBlock block = ^ {
_str = @"block str";
NSLog(@"inner block, str value: %@", _str);
NSLog(@"inner block, str address: %p", &_str);
};
block();
NSLog(@"after block, str value: %@", _str);
NSLog(@"after block, str address: %p", &_str);
}

// 输出
// MRC和ARC
before block, str value: init string.
before block, str address: 0x6000004c23b8
inner block, str value: block str
inner block, str address: 0x6000004c23b8
after block, str value: block str
after block, str address: 0x6000004c23b8

MRC和ARC下,block内外打印的对象地址相同,且不需要__block修饰,block也能捕获并修改类的属性。

分析原因
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
33
34
35
36
37
38
struct __TestObject__testBlock_block_impl_0 {
struct __block_impl impl;
struct __TestObject__testBlock_block_desc_0* Desc;
TestObject *self;
__TestObject__testBlock_block_impl_0(void *fp, struct __TestObject__testBlock_block_desc_0 *desc, TestObject *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestObject__testBlock_block_func_0(struct __TestObject__testBlock_block_impl_0 *__cself) {
TestObject *self = __cself->self; // bound by copy

(*(NSString **)((char *)self + OBJC_IVAR_$_TestObject$_str)) = (NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_4b2b3a_mi_3;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_4b2b3a_mi_4, (*(NSString **)((char *)self + OBJC_IVAR_$_TestObject$_str)));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_4b2b3a_mi_5, &(*(NSString **)((char *)self + OBJC_IVAR_$_TestObject$_str)));
}
static void __TestObject__testBlock_block_copy_0(struct __TestObject__testBlock_block_impl_0*dst, struct __TestObject__testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __TestObject__testBlock_block_dispose_0(struct __TestObject__testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __TestObject__testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestObject__testBlock_block_impl_0*, struct __TestObject__testBlock_block_impl_0*);
void (*dispose)(struct __TestObject__testBlock_block_impl_0*);
} __TestObject__testBlock_block_desc_0_DATA = { 0, sizeof(struct __TestObject__testBlock_block_impl_0), __TestObject__testBlock_block_copy_0, __TestObject__testBlock_block_dispose_0};

static void _I_TestObject_testBlock(TestObject * self, SEL _cmd) {

NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_4b2b3a_mi_1, (*(NSString **)((char *)self + OBJC_IVAR_$_TestObject$_str)));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_4b2b3a_mi_2, &(*(NSString **)((char *)self + OBJC_IVAR_$_TestObject$_str)));
TestBlock block = ((void (*)())&__TestObject__testBlock_block_impl_0((void *)__TestObject__testBlock_block_func_0, &__TestObject__testBlock_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_4b2b3a_mi_6, (*(NSString **)((char *)self + OBJC_IVAR_$_TestObject$_str)));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b5_rsv7dqn103b02trb8h2l2dx00000gn_T_TestObject_4b2b3a_mi_7, &(*(NSString **)((char *)self + OBJC_IVAR_$_TestObject$_str)));
}

从代码中可以看到,构建block结构体时,其内部持有的是该类的实例对象:TestObject *self。而在创建block时,传入的是类的当前的实例对象self:

1
TestBlock block = ((void (*)())&__TestObject__testBlock_block_impl_0((void *)__TestObject__testBlock_block_func_0, &__TestObject__testBlock_block_desc_0_DATA, self, 570425344));

因此,block内部持有的实际上是self,在读取和修改类的属性时,使用的实际上是

1
(*(NSString **)((char *)self + OBJC_IVAR_$_TestObject$_str))

所以,无需__block修饰,block也能修改类的属性,而且无论在MRC还是ARC下,打印的属性地址都是堆中的某个区域(基本类型的属性不同,是在栈中)。

四、Block copy过程以及导致retain cycle的原因分析

ARC下,为了延长分配在栈中block的生命周期,编译期会对非全局block默认加copy操作,将其copy到堆中。对于基本数据类型,copy一份到堆中,对于对象类型变量,copy其指针到堆中。而我们常说的block的循环引用就是这个copy操作导致的,那么为什么block的copy操作会导致某些情况下的循环引用呢?下面通过源码分析一下。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// libclosure-67/Block_private.h
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};

// libclosure-67/runtime.c
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;

if (!arg) return NULL;

// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
while (1) {
int32_t old_value = aBlock->flags;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
break;
}
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, &aBlock->flags)) {
break;
}
}
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1

struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;
(*desc->copy)(result, aBlock); // do fixup

// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}

首先是在Block_private.h头文件中的一些枚举值,包含了用于描述block对象的一些flags;
核心方法是_Block_copy函数,这个函数的流程如下:

  1. 声明一个Block_layout结构体类型的指针aBlock;
  2. 检查传入的参数arg是否为空,为空则return NULL;
  3. 将aBlock指针指向arg;
  4. 判断block的flags是否包含BLOCK_NEEDS_FREE,如果包含,说明这是一个堆block,将其引用计数+1;
  5. 判断是否global block,如果是,直接返回相同的block;
  6. 如果是一个栈block,执行以下操作:根据传入的block中的size信息创建一块同样大小的内存空间,并使用result指针指向其起始地址;判断result是否为空,如果为空返回NULL;将aBlock按位拷贝(memmove)到result指向的内存空间中;更新块标识,初始化引用计数为0;设置拷贝的block引用计数为1;如果有辅助copy函数,调用辅助函数;设置result的isa指针指向_NSConcreteMallocBlock,即说明这是一个堆block。

block辅助copy/dispose函数

在上一节的3.2、3.3、3.4例子中可以看到,编译期自动生成了copy、dispose函数并添加到Block_layout

1
2
3
static void __TestObject__testBlock_block_copy_0(struct __TestObject__testBlock_block_impl_0*dst, struct __TestObject__testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __TestObject__testBlock_block_dispose_0(struct __TestObject__testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

这里我们重点关注copy函数,__TestObject__testBlock_block_copy_0函数调用了_Block_object_assign函数进行辅助的拷贝操作(主要是对block持有的变量的copy、内存管理等)。
源码如下:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// Block_private.h
// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};

// 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 (os_assumes(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;
}
}

流程分析:

  1. 首先是block field有关的一些枚举值,列举了要copy的变量的类型:object/block/byref/caller等;
  2. 判断block field,如果是object类型,调用_Block_retain_object函数对object进行一次retain操作;
  3. 如果是_Block_byref_copy类型,调用_Block_byref_copy函数进行__Block_byref_结构体的copy操作
  4. 其他case,比如__weak __block__weak __block id或者__weak __block void (^object)(void)类型,不进行copy和retain操作,只是进行指针赋值。

看到这里,应该就明白block中循环引用是怎么造成的了吧。
结合第三节的例子3.4和上述流程2可以看出,block持有实例对象的属性(无论是self.xxx或_xxx)时,实际上持有的是当前的实例对象self,而这种情况下在进行copy操作时,调用block的辅助copy函数时,会对self进行一次retain操作,使self的引用计数+1,如果此时实例对象再强引用block的话,就会出现retain cycle,导致对象和block相互引用而无法释放。

五、总结及参考

通过上述分析,对于block的内存结构和MRC、ARC下block的行为以及block如何持有外部变量、如何copy等有了一个大致的了解。如果对于block的dispose等操作感兴趣的同学,可以去下载官方完整源码阅读:

libclosure-67