探究 block 数据结构及内存管理

本文首先将介绍一些 block 的基础, 随后着重介绍下面的内容

  • block 的数据结构
  • block 的内存管理(retain,release)

会用到下面这个
可编译的源码

基础

语法

Block 的语法比较难记, 以至于出现了 fuckingblocksyntax 这样的网站专门用于记录 block 的语法, 摘录如下:

作为变量

1
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};

作为属性

1
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);

作为函数声明参数

1
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;

作为函数调用中的参数

1
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];

作为 typedef

1
2
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};

捕获外部变量

block 可以捕获来自外部作用域的变量(id 类型, C++类型, 基础数据类型, block), 这是 block 一个很强大的特性

1
2
3
4
5
6
7
- (void)foo {
int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
testBlock();
}

正常情况下, 捕获的外部变量在 block 里做的修改,在外部是不起作用的。如果想要在外部起作用,需要使用 __block 来声明变量:

1
int __block anInteger = 42

所以,根据变量是否被 __block 修饰,可以将变量分为两类:

  • by ref: 引用类型。该类的变量被 __block 修饰,在 block 内部对其修改,外部也生效
  • by copy:拷贝类型。该类的变量不被 __block 修饰,在 block 内部对其修改,外部不生效(全局/静态 变量除外)

至于原因进阶部分会进行详细的探究

进阶

数据结构

运行下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef void(^BLK)(void);

- (void)foo
{
static int staticInt = 5;
int ret = 5;

BLK globalBlock = ^{
int a = staticInt;
};
BLK mallocBlock = ^{
int a = ret;
};

NSLog(@"global = %@", globalBlock);
NSLog(@"malloc = %@", mallocBlock);
NSLog(@"stack = %@", ^{int a = ret;});
}

打印结果为

1
2
3
global = <__NSGlobalBlock__: 0x104bb22b0>
malloc = <__NSMallocBlock__: 0x281a21bf0>
stack = <__NSStackBlock__: 0x16b8266d8>

在 iOS 平台中, 一共有三种类型的 block:

  • _NSConcreteGlobalBlock: 在 .data 区域, block 内部没有访问任何的外部非(静态变量 && 全局变量)的变量(^{;}同样是该类型)
  • _NSConcreteMallocBlock: 在堆中创建内存, 使用__strong修饰的 block
  • _NSConcreteStackBlock: 在栈中创建内存, 使用__weak修饰的 block 或者是匿名 block

优先级为 _NSConcreteGlobalBlock > _NSConcreteMallocBlock == _NSConcreteStackBlock,即满足 _NSConcreteGlobalBlock 条件的 block 就是 _NSConcreteGlobalBlock 类型的

_NSConcreteGlobalBlock 类型的 block 我不知道初始化的时候是否直接在 .data 区域创建。
_NSConcreteMallocBlock 和 _NSConcreteStackBlock类型的 block 在初始化时在栈中创建,随后如果有 __storng 强指针引用的话,
则进行 retain 操作,将其内存拷贝到堆中,后续的 retain 操作则只是增加 block 的引用计数


使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 YOUR_FILE_NAME将下面的代码转换成 C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)foo
{
static int staticInt = 5;
NSObject *commonObject = [[NSObject alloc] init];
__weak NSObject *weakObject = commonObject;
__block NSObject *byrefObject = [[NSObject alloc] init];
__block __weak NSObject *byrefWeakObject = commonObject;
BLK blockObject = ^{
NSObject *val = commonObject;
};

BLK malocBlock = ^{
staticInt++;
NSLog(@"%@", commonObject);
NSLog(@"%@", byrefWeakObject);
NSLog(@"%@", weakObject);
NSLog(@"%@", byrefObject);
NSLog(@"%@", byrefWeakObject);
NSLog(@"%@", blockObject);
};
NSLog(@"malloc = %@", malocBlock);
}

转换得到的 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
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

struct __Block_byref_byrefObject_0 {
void *__isa;
__Block_byref_byrefObject_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong byrefObject;
};
struct __Block_byref_byrefWeakObject_1 {
void *__isa;
__Block_byref_byrefWeakObject_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__weak byrefWeakObject;
};

struct __TestObject__foo_block_impl_0 {
struct __block_impl impl;
struct __TestObject__foo_block_desc_0* Desc;
NSObject *__strong commonObject;
__TestObject__foo_block_impl_0(void *fp, struct __TestObject__foo_block_desc_0 *desc, NSObject *__strong _commonObject, int flags=0) : commonObject(_commonObject) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestObject__foo_block_func_0(struct __TestObject__foo_block_impl_0 *__cself) {
NSObject *__strong commonObject = __cself->commonObject; // bound by copy

NSObject *val = commonObject;
}
static void __TestObject__foo_block_copy_0(struct __TestObject__foo_block_impl_0*dst, struct __TestObject__foo_block_impl_0*src) {_Block_object_assign((void*)&dst->commonObject, (void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __TestObject__foo_block_dispose_0(struct __TestObject__foo_block_impl_0*src) {_Block_object_dispose((void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __TestObject__foo_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestObject__foo_block_impl_0*, struct __TestObject__foo_block_impl_0*);
void (*dispose)(struct __TestObject__foo_block_impl_0*);
} __TestObject__foo_block_desc_0_DATA = { 0, sizeof(struct __TestObject__foo_block_impl_0), __TestObject__foo_block_copy_0, __TestObject__foo_block_dispose_0};

struct __TestObject__foo_block_impl_1 {
struct __block_impl impl;
struct __TestObject__foo_block_desc_1* Desc;
int *staticInt;
NSObject *__strong commonObject;
NSObject *__weak weakObject;
__strong BLK blockObject;
__Block_byref_byrefWeakObject_1 *byrefWeakObject; // by ref
__Block_byref_byrefObject_0 *byrefObject; // by ref
__TestObject__foo_block_impl_1(void *fp, struct __TestObject__foo_block_desc_1 *desc, int *_staticInt, NSObject *__strong _commonObject, NSObject *__weak _weakObject, __strong BLK _blockObject, __Block_byref_byrefWeakObject_1 *_byrefWeakObject, __Block_byref_byrefObject_0 *_byrefObject, int flags=0) : staticInt(_staticInt), commonObject(_commonObject), weakObject(_weakObject), blockObject(_blockObject), byrefWeakObject(_byrefWeakObject->__forwarding), byrefObject(_byrefObject->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestObject__foo_block_func_1(struct __TestObject__foo_block_impl_1 *__cself) {
__Block_byref_byrefWeakObject_1 *byrefWeakObject = __cself->byrefWeakObject; // bound by ref
__Block_byref_byrefObject_0 *byrefObject = __cself->byrefObject; // bound by ref
int *staticInt = __cself->staticInt; // bound by copy
NSObject *__strong commonObject = __cself->commonObject; // bound by copy
NSObject *__weak weakObject = __cself->weakObject; // bound by copy
__strong BLK blockObject = __cself->blockObject; // bound by copy

(*staticInt)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_0, commonObject);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_1, (byrefWeakObject->__forwarding->byrefWeakObject));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_2, weakObject);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_3, (byrefObject->__forwarding->byrefObject));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_4, (byrefWeakObject->__forwarding->byrefWeakObject));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_5, blockObject);
}
static void __TestObject__foo_block_copy_1(struct __TestObject__foo_block_impl_1*dst, struct __TestObject__foo_block_impl_1*src) {_Block_object_assign((void*)&dst->commonObject, (void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->byrefWeakObject, (void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->weakObject, (void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->byrefObject, (void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->blockObject, (void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);}

static void __TestObject__foo_block_dispose_1(struct __TestObject__foo_block_impl_1*src) {_Block_object_dispose((void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);}

static struct __TestObject__foo_block_desc_1 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestObject__foo_block_impl_1*, struct __TestObject__foo_block_impl_1*);
void (*dispose)(struct __TestObject__foo_block_impl_1*);
} __TestObject__foo_block_desc_1_DATA = { 0, sizeof(struct __TestObject__foo_block_impl_1), __TestObject__foo_block_copy_1, __TestObject__foo_block_dispose_1};

static void _I_TestObject_foo(TestObject * self, SEL _cmd) {
static int staticInt = 5;
NSObject *commonObject = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
__attribute__((objc_ownership(weak))) NSObject *weakObject = commonObject;
__attribute__((__blocks__(byref))) __Block_byref_byrefObject_0 byrefObject = {(void*)0,(__Block_byref_byrefObject_0 *)&byrefObject, 33554432, sizeof(__Block_byref_byrefObject_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};
__attribute__((__blocks__(byref))) __attribute__((objc_ownership(weak))) __Block_byref_byrefWeakObject_1 byrefWeakObject = {(void*)0,(__Block_byref_byrefWeakObject_1 *)&byrefWeakObject, 33554432, sizeof(__Block_byref_byrefWeakObject_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, commonObject};
BLK blockObject = ((void (*)())&__TestObject__foo_block_impl_0((void *)__TestObject__foo_block_func_0, &__TestObject__foo_block_desc_0_DATA, commonObject, 570425344));

BLK malocBlock = ((void (*)())&__TestObject__foo_block_impl_1((void *)__TestObject__foo_block_func_1, &__TestObject__foo_block_desc_1_DATA, &staticInt, commonObject, weakObject, blockObject, (__Block_byref_byrefWeakObject_1 *)&byrefWeakObject, (__Block_byref_byrefObject_0 *)&byrefObject, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_6, malocBlock);
}

需要注意的是,上面生成的 C++ 代码中 block 的结构体,并不符合最新版本 objc4-779.1 源码里 block 的结构定义

在 objc4-779.1 里 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
#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
};

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_DESCRIPTOR 结构体,只是根据功能将其分为三部分,其实是一个整体
注意下面两个结构体在内存上是完全一样的,原因是结构体本身并不带有任何额外的附加信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct SampleA {
int a;
int b;
int c;
};

struct SampleB {
int a;
struct Part1 {
int b;
};
struct Part2 {
int c;
};
};

Block_layout 成员变量介绍:

  • void *isa: isa 指针
  • int flags: 使用位域来保存信息, 例如引用计数, 是否正在被销毁等信息
  • int reserved: 保留变量
  • void (*invoke)(void *, …): 函数指针, 指向 block 实现函数的调用地址
  • struct Block_descriptor *descriptor: block 的附加描述信息,一般来说都包含 Block_descriptor_1,但是是否包含 Block_descriptor_2 和 Block_descriptor_3 需要根据捕获外部变量的类型来判断
  • 还有一些捕获的外部变量
位域名 位置 含义
BLOCK_DEALLOCATING 0x0001 1 表示正在被销毁
BLOCK_REFCOUNT_MASK 0xfffe block 是引用计数
BLOCK_NEEDS_FREE 1 << 24 1 表示 block 已拷贝到堆中
BLOCK_HAS_COPY_DISPOSE 1 << 25 block 是否有 copy/dispos 函数,即 descriptor 是否包含 Block_descriptor_2
BLOCK_HAS_CTOR 1 << 26 copy/dispose helper 函数里面有 C++代码
BLOCK_IS_GC 1 << 27 1 表示使用 GC 管理内存,iOS 平台中不使用 GC
BLOCK_IS_GLOBAL 1 << 28 1 表示是个全局 block
BLOCK_USE_STRET 1 << 29 arm64 架构下没用,不知道干嘛的
BLOCK_HAS_SIGNATURE 1 << 30 是否有函数类型编码
BLOCK_HAS_EXTENDED_LAYOUT 1 << 31 GC 下使用

Block_descriptor 成员变量介绍:

  • unsigned long int reserved: 预留变量
  • unsigned long int size: block 结构体的 size 大小
  • void (*copy)(void *dst, void *src): copy 函数,将 block 成员变量 从栈拷贝到堆中。后面会再介绍
  • void (*dispose)(void *): dispose 函数, 对 block 成员变量内存回收
  • const char *signature:函数的类型编码
  • const char *layout: GC 下使用,不知道具体作用

让我们对照着 C++ 实现捋一遍,因为实现里的 block 结构体是老版本所以跟上面讲的可能会有出入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __TestObject__foo_block_impl_1 {
struct __block_impl impl;
struct __TestObject__foo_block_desc_1* Desc;
int *staticInt;
NSObject *__strong commonObject;
NSObject *__weak weakObject;
__strong BLK blockObject;
__Block_byref_byrefWeakObject_1 *byrefWeakObject; // by ref
__Block_byref_byrefObject_0 *byrefObject; // by ref
__TestObject__foo_block_impl_1(void *fp, struct __TestObject__foo_block_desc_1 *desc, int *_staticInt, NSObject *__strong _commonObject, NSObject *__weak _weakObject, __strong BLK _blockObject, __Block_byref_byrefWeakObject_1 *_byrefWeakObject, __Block_byref_byrefObject_0 *_byrefObject, int flags=0) : staticInt(_staticInt), commonObject(_commonObject), weakObject(_weakObject), blockObject(_blockObject), byrefWeakObject(_byrefWeakObject->__forwarding), byrefObject(_byrefObject->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

在结构体 __TestObject__foo_block_impl_1 有很多捕获的外部变量充当的成员变量,如下所示

1
2
3
4
5
6
int *staticInt;
NSObject *__strong commonObject;
NSObject *__weak weakObject;
__strong BLK blockObject;
__Block_byref_byrefWeakObject_1 *byrefWeakObject; // by ref
__Block_byref_byrefObject_0 *byrefObject; // by ref

为了尽可能的谈论多的情况,在示例代码中我在 block 加了许多不同类型的外部变量
可以看到,全局/静态 变量和 __block 变量,都是将变量的地址保存在成员变量中,这样做的目的是为了在内部修改该变量在外部也会生效。
而其它非 __block 变量则仅仅拷贝了值,类似于浅拷贝

__Block_byref_byrefWeakObject_1 是 Block_byref 类型的结构体。__block 变量在编译时变成对应的 Block_byref 实例,且实例持有该变量

Block_byref 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};

struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
void (*byref_destroy)(struct Block_byref *);
};

struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};

类似 Block_descriptor,根据功能将其分为三部分
成员变量介绍:

  • void *isa:一般指向 0x0,如果该变量还被 __weak 修饰,则指向 _NSConcreteWeakBlockVariable
  • struct Block_byref *forwarding:指向该结构体。在拷贝到堆的过程中,在堆中新建一个结构体实例,此时栈中的实例并没有被销毁,将栈中实例 forwarding 指向堆中的实例
  • int flags:引用计数使用的 bit 数目和位置与 block 相同,其它不再介绍
  • int size:Block_byref 结构体的字节长度
  • void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src):Block_byref 的 copy 函数,帮助将实例持有的变量拷贝到堆中
  • void (*byref_destroy)(struct Block_byref *):Block_byref 的 dispose 函数,帮助将持有的变量销毁
  • const char *layout:Block_byref 持有的变量

为了方便,后面将 Block_layout 的 copy/dispose 函数简称为 Block copy/dispose 函数; Block_byref 的 byref_keep/byref_destroy 函数简称为 __block copy/dispose 函数

foo() 函数中我们在 block 捕获了两个 __block 修饰的变量,下面是其中一个的 Block_byref 结构体

1
2
3
4
5
6
7
8
9
struct __Block_byref_byrefObject_0 {
void *__isa;
__Block_byref_byrefObject_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong byrefObject;
};

编译时,编译器会将 __block 修饰的变量 byrefObject,转换成对应的 Block_byref 结构体 __Block_byref_byrefObject_0 实例,然后让实例持有该变量NSObject *__strong byrefObject;


block 结构体 __TestObject__foo_block_impl_1 里面还定义了一个初始化方法:

1
2
3
4
5
6
_TestObject__foo_block_impl_1(void *fp, struct __TestObject__foo_block_desc_1 *desc, int *_staticInt, NSObject *__strong _commonObject, NSObject *__weak _weakObject, __strong BLK _blockObject, __Block_byref_byrefWeakObject_1 *_byrefWeakObject, __Block_byref_byrefObject_0 *_byrefObject, int flags=0) : staticInt(_staticInt), commonObject(_commonObject), weakObject(_weakObject), blockObject(_blockObject), byrefWeakObject(_byrefWeakObject->__forwarding), byrefObject(_byrefObject->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
  • 其中类似 : commonObject(_commonObject) 的写法是将形参NSObject *__strong _commonObject赋值给成员变量 _commonObject
  • 参数 flags 有一个默认值 0, 在本例中传入的值为 570425344, 用二进制表示为 0b00100010000000000000000000000000,即第 30 位(BLOCK_USE_STRET), 第 26 位(BLOCK_HAS_COPY_DISPOSE) bit 的值 1
  • 参数 fp 是函数指针, 在本例中它的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
static void __TestObject__foo_block_func_1(struct __TestObject__foo_block_impl_1 *__cself) {
__Block_byref_byrefWeakObject_1 *byrefWeakObject = __cself->byrefWeakObject; // bound by ref
__Block_byref_byrefObject_0 *byrefObject = __cself->byrefObject; // bound by ref
int *staticInt = __cself->staticInt; // bound by copy
NSObject *__strong commonObject = __cself->commonObject; // bound by copy
NSObject *__weak weakObject = __cself->weakObject; // bound by copy
__strong BLK blockObject = __cself->blockObject; // bound by copy

(*staticInt)++;

// 省略。。。
}

也就是 block 里面的代码。
可以看到非 __block 变量后面都写了注释 bound by copy,__block 变量后面的注释是 bound by ref,__block 变量都会转换成 Block_byref 实例保存在 block 中
为了方便,后面将 __block 修饰的变量称为引用变量,否则称为拷贝变量

  • block 初始化参数 desc 的类型是__TestObject__foo_block_desc_1 *

结构体的定义如下:

1
2
3
4
5
6
static struct __TestObject__foo_block_desc_1 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestObject__foo_block_impl_1*, struct __TestObject__foo_block_impl_1*);
void (*dispose)(struct __TestObject__foo_block_impl_1*);
} __TestObject__foo_block_desc_1_DATA = { 0, sizeof(struct __TestObject__foo_block_impl_1), __TestObject__foo_block_copy_1, __TestObject__foo_block_dispose_1};

并且初始化了一个结构体实例 __TestObject__foo_block_desc_1_DATA,其中 reserved 的值为 0, Block_size 的值为结构体 __TestObject__foo_block_impl_1 的字节长度
copy 和 dispose 两个函数指针分别指向函数 __TestObject__foo_block_copy_1 和 __TestObject__foo_block_dispose_1
实现如下:

1
2
3
static void __TestObject__foo_block_copy_1(struct __TestObject__foo_block_impl_1*dst, struct __TestObject__foo_block_impl_1*src) {_Block_object_assign((void*)&dst->commonObject, (void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->byrefWeakObject, (void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->weakObject, (void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->byrefObject, (void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->blockObject, (void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);}

static void __TestObject__foo_block_dispose_1(struct __TestObject__foo_block_impl_1*src) {_Block_object_dispose((void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);}

这两个函数很容易看懂,有多少捕获的外部变量,就调用多少次 _Block_object_assign()_Block_object_dispose() 函数。
需要注意的是 _Block_object_assign() 的第三个参数,根据变量的类型不同传入不同的标记,后面会详细讲

与其类似的是 Block_byref 的 copy/dispose 函数
在本例中它们的实现如下:

1
2
3
4
5
6
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

可以看到,Block_byref 的 copy/dispose 函数最终也是调用 _Block_object_assign/_Block_object_dispose 函数

至于参数为什么要强转成 char*, 我的理解是这样的:

举个例子,定义一个 int 类型的数组, int a[10]。我们可以使用指针来代替数组的下标, 例如用(int *)a + 1来表示数组的第二个元素, 其距离第一个元素偏移了 4 个字节长度,所以强制转换成 char* 类型是为了每次偏移 1 个字节。上面的代码表示偏移了 40 个字节

而老版本 Block_byref 的定义(因为 C++ 实现符合源码)

1
2
3
4
5
6
7
8
9
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
int flags; /* refcount; */
int size;
void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
void (*byref_destroy)(struct Block_byref *);
/* long shared[0]; */
};

Block_byref 偏移 40 个字节后的位置刚好是持有变量的首地址,所以在这里传入的参数即是引用变量(被 __block 修饰的变量)
至于后面的数字 131 后面再讲

好了,数据结构就分析到这啦。
目前我们可以知道的是,block 以及 __block 变量在编译时会生成对应的 Block_layout,Block_byref 结构体,它们都有各自的 copy/dispose 函数

验证 block 数据结构

这一节主要使用 lldb 用来验证 block 的数据结构

打个断点,使用命令 x/8xg ptr

block 内部分布

根据上一节 block 结构体内容,我们可以知道各成员变量的值

block 成员变量
void *isa 0x00000001ca53c0f0
volatile int32_t flags 0xc3000002
int32_t reserved 0x0
void (*invoke)(void *, …) 0x0000000104bcce80
struct Block_descriptor_1 *descriptor 0x000000010519e290
捕获变量 _commonObject 0x0000000281eaac10
Block_byref * _byrefObject 0x0000000281225170

验证一下:

isa 验证

isa 指针指向 __NSMallocBlock__,没问题

flags 用二进制表示为 0b11000011000000000000000000000010,即位域 BLOCK_HAS_SIGNATURE,BLOCK_HAS_EXTENDED_LAYOUT,BLOCK_HAS_COPY_DISPOSE,BLOCK_NEEDS_FREE 为 1 引用计数 BLOCK_REFCOUNT_MASK 为 1

invoke 验证
可以通过上面的方法打印出函数指针的内容

成员变量 _commonObject 的值于 commonObject 相同,均为 0x0000000281eaac10,说明是浅拷贝

下面来验证 descriptor
0x000000010519e290 为 Block_descriptor_1 结构体的首地址

descriptor 内存分布

Block_descriptor 成员变量 解释
uintptr_t reserved 0x0 预留字段
uintptr_t size 0x0000000000000030 十进制为 48,即 block 结构体的字节长度
void (*copy)(void *dst, const void *src) 0x0000000104bccee8 copy 函数指针
void (*dispose)(const void *) 0x0000000104bccf54 dispose 函数指针
const char *signature 0x000000010511847f 函数类型编码
const char *layout 0x0000000000000110 不知道干嘛的

打印下 block 函数的类型编码

函数类型编码

成员变量 const char *layout 应该是在 GC 下使用的,具体作用不明白


接下来验证 Block_byref 的结构

Block_byref 内存分布

Block_byref 成员变量 解释
void *isa 0x0 isa
struct Block_byref *forwarding 0x0000000281225170 十进制为 48,即 block 结构体的字节长度
volatile int32_t flags 0x33000004 标记,二级制表示为 0b00110011000000000000000000000100
uint32_t size 0x00000030 Block_byref 结构体长度
byref_keep 0x000000010511847f __block copy 函数指针
byref_destroy 0x0000000000000110 __block dispose 函数指针
const char *layout 0x0000000281eaac50 持有的变量 byrefObject

flags 表示引用计数为 2,因为初始化有一个,然后 block 有一个。位域 BLOCK_BYREF_LAYOUT_STRONG 为 1,表示该变量是 __strong 类型
const char *layout 表示其持有的变量

如何将 block 从栈拷贝到堆中

现在我们来探究一下 block 是如何从栈中拷贝到堆中的吧。
除 global block 类型的 block 均在栈中创建,当被强引用,即 retain block 的话,block 就会从栈拷贝到堆中,如果已经在堆中,则增加其引用计数

断点调试

step into

_Block_copy 的函数实现在 clang-800 源码 中可以看到

1
2
3
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, WANTS_ONE);
}

_Block_copy_internal 实现

我们主要看红色框框里面的代码:

  • 首先通过 malloc() 在堆中新建一个 block 结构体实例,接着使用memmove()将旧实例的数据拷贝过去
  • 重置新实例成员变量 flags 的 BLOCK_REFCOUNT_MASK(引用计数)部分
  • 将新实例成员变量 flags 的位域 BLOCK_NEEDS_FREE 设置为 1, 表示该 block 在堆中
  • 将新实例的 isa 指向 _NSConcreteMallocBlock
  • 如果存在 Block copy 函数,则调用

上一节中已经提到过,这里再贴一下它的实现:

1
2
3
4
5
6
7
8
static void __TestObject__foo_block_copy_1(struct __TestObject__foo_block_impl_1*dst, struct __TestObject__foo_block_impl_1*src) 
{
_Block_object_assign((void*)&dst->commonObject, (void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_assign((void*)&dst->byrefWeakObject, (void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->weakObject, (void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_assign((void*)&dst->byrefObject, (void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->blockObject, (void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);
}

需要注意的是,如果外部变量是 C++类型,则不会调用 _Block_object_assign()函数,而是其对应的 const 拷贝构造方法。注释如下:
In these cases helper functions are synthesized by the compiler for use in Block_copy and Block_release, called the copy and dispose helpers. The copy helper emits a call to the C++ const copy constructor for C++ stack based objects and for the rest calls into the runtime support function _Block_object_assign. The dispose helper has a call to the C++ destructor for case 1 and a call into _Block_object_dispose for the rest.

_Block_object_assign函数的实现如下:

_Block_object_assign 函数实现

参数 flags 有以下几种情况:

由 Block copy 函数调用:

  • id                   3       
    
    • (^Block)             7
      
  • __block 8
    • __weak __block       8+16
      

flags 有 4 种可能: 3, 7, 8, (8+16)

由 __block copy 函数调用:

BLOCK_BYREF_CALLER (128):表示由 __block copy 调用
此时, 传入的 flags 有 4 种可能:

  • __block id                   128+3       
    
    • __block (^Block)             128+7
      
  • __weak __block id 128+3+16
    • __weak __block (^Block)      128+7+16
      

总共有以上 8 种情况

需要注意的是,Block copy 调用该函数,第一个参数是指针的地址(void **),第二个参数传入的是指针的值(void *)
而 __block copy 调用该函数,传入的前两个参数均为指针的值(void *)

下面根据 case 条件分几步来讲解这个函数:

  • 代码块 1:

Block copy 函数调用,且该变量是 id 类型的

代码_Block_retain_object()最终会调用下面这个函数

1
2
3
static void _Block_retain_object_default(const void *ptr) {
(void)ptr;
}

代码_Block_assign()最终会调用下面这个函数

1
2
3
static void _Block_assign_default(void *value, void **destptr) {
*destptr = value;
}

不知道调用 _Block_retain_object 函数的目的是什么
这一分支仅做了浅拷贝,拷贝指针内容

  • 代码块 2:

Block copy 函数调用,且该变量也是一个 block

这里首先调用了 _Block_copy_internal() 函数, 先将 block 类型的变量拷贝到堆中
然后调用 _Block_assign 将其堆中的地址赋值给 Block 对应的成员变量

  • 代码块 3:

Block copy 函数调用,且变量被 __block 修饰

_Block_byref_assign_copy 实现

这里传入的第一个参数 void *dest 表示的是指针的地址

_Block_byref_assign_copy 实现中代码块 1 的逻辑如下

  • 声明一个布尔值 isWeak, 用来表示该变量是否还被 __weak 修饰
  • _Block_allocator 函数最终调用 malloc() 函数, 在堆上拷贝一份同样内存大小的 Block_byrefs 实例
  • 将旧实例的成员变量 flags 拷贝到新实例中, 并将新实例的引用计数设置为 2。 1 份是因为栈上有一个实例, 1 份是因为堆上也有一个, 1+1 就等于 2 了
  • 将旧实例和新实例的成员变量 forwarding 均赋值为新实例
  • 如果 isWeak 为 true, 则将 Block_byrefs 实例的 isa 指针指向 _NSConcreteWeakBlockVariable
  • 如果实例有 __block copy/dispose helpers(还是调用 _Block_object_assign 函数), 则调用它对实例持有的变量进行拷贝到堆操作; 如果没有的话则将旧实例中 size 后面的成员变量拷贝到新实例的 bit 中

函数里面有一行看起来比较让人困扰的代码

1
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);

在前面的声明中,src 被声明成了 Block_byref * 类型,所以 src + 1 的意思是从 src 的首地址偏移 sizeof(Block_byref) 个字节,即 Block_byref_2 的首地址
结构体拷贝完成后,随后将 Block_byref 持有的变量通过函数 *src2->byref_keep 也拷贝到堆中
_Block_byref_assign_copy 实现中代码块 2 的作用是如果 Block_byref 持有的变量已经拷贝到堆中了, 则增加其引用计数

  • 代码块 4:

__block copy 函数调用,且持有的变量不被 __weak 修饰

最终会调用下面那个函数

1
2
3
static void _Block_assign_default(void *value, void **destptr) {
*destptr = value;
}

浅拷贝

  • 代码块 5:

__block copy 函数调用,且持有的变量被 __weak 修饰

最终会调用下面那个函数:

1
2
3
static void _Block_assign_weak_default(const void *ptr, void *dest) {
*(void **)dest = (void *)ptr;
}

这里讲一下我的理解:
void * 是一个指针,里面保存了一个地址,但是我们不能 *(void *) 这样使用它,因为我们不知道它指向的结构是什么类型的。如果要使用的话就需要将其转换成其它类型,例如 int *,所以在这里可以仅仅把它看成是变量,保存了一个地址。
而 void **,其表示指向指针的指针,不同于 void *,我们可以 *(void **) 这样使用它,因为我们知道 void ** 指向的内容是一个指针。

在 _Block_assign_weak_default 函数中,我们先将 dest 强转成 void ** 类型,然后就可以对其进行赋值操作啦


至此,整个拷贝流程已经讲的差不多了,这里总结一下:

  • 一个 block 可能捕获多个外部变量
  • block 在栈中生成,retain 后,将栈中的内容拷贝到堆中
  • block 会调用 Block copy 函数,对其捕获的变量也进行拷贝操作
    • 如果是 C++ 类型,则调用其 const 拷贝构造函数到堆中
    • 如果是 block 类型,则将其拷贝到堆中
    • 如果是 id 类型,因为已经在堆中了,所以进行浅拷贝,仅复制指针值
    • 如果是 __block 修饰的变量,则将其对应的 Block_byref 结构体拷贝到堆中,随后调用 __block copy 函数将其持有的变量也拷贝到堆中

如何销毁 block

这一节我们将探究如何销毁 block

创建一个 malloc block 类型的 block,foo() 结束,block 会被回收。
在函数尾巴那里打个断点

step into

将断点停留在objc_release()函数
runtime 通过该函数对 block 进行 release 操作,如果其引用计数变成 0,则销毁

继续 step into

block 是特殊的类,自己重写了release()函数,所以代码 ISA()->hasCustomRR() 返回的结果是 true,将执行自己重写的 release() 函数
继续 step into

因为没有 block 的 release()源码,通过调用栈发现随后调用了 _Block_release() 函数
可编译的源码中可以找到该函数的实现(直接在文件夹中搜索)

_Block_release 实现

根据 if 的判断将函数分为三个部分

  • 判断条件 1:如果是全局 block 或者其引用计数已经为 0(表示已经在销毁了), 则返回
  • 判断条件 2:如果使用 GC 管理内存,则执行什么什么操作。因为 iOS 平台不使用 GC,所以略过
  • 判断条件 3:如果 block 已经在堆上了,则将其引用计数减 1,如果减为 0,则调用下面三个函数
1
2
3
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
_Block_deallocator(aBlock);
  • 第一个函数 _Block_call_dispose_helper

在同个文件中搜索该函数,其实现如下

1
2
3
4
5
6
7
static void _Block_call_dispose_helper(struct Block_layout *aBlock)
{
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;

(*desc->dispose)(aBlock);
}

如果存在 Block dispose 函数,则调用

1
2
3
4
5
6
7
8
9
static void __TestObject__foo_block_dispose_1(struct __TestObject__foo_block_impl_1*src) 
{
_Block_object_dispose((void*)src->byrefInt, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_dispose((void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_dispose((void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);
}

_Block_object_dispose 实现

通过判断条件将 _Block_object_dispose 分为几部分

  • 判断条件 1:变量由 __block 修饰

_Block_byref_release 实现

代码byref = byref->forwarding;,因为可能会有 byref 在栈中,而 forwarding 此时却在堆中的情况。随后判断 byref 是否在栈中,如果是的话则立即返回。
对 Block_byref 的引用计数减 1 随后判断是否为 0,如果是的话则调用其 byref_destroy 函数(也就是 _Block_object_dispose),销毁其持有的变量
最后将 Block_byref 结构体的内存释放掉

  • 判断条件 2:变量是 Block 类型

_Block_destroy 实现

调用 _Block_release 来销毁该 block

  • 判断条件 3:变量是 id 类型

_Block_release_object 最终调用的函数如下

1
2
3
static void _Block_release_object_default(const void *ptr) {
(void)ptr;
}

等于什么都没做,这是因为 id 类型的对象由 ARC 管理其内存。即不再被强指针引用时引用计数减 1

  • 判断条件 4

什么都没做,如果走到走一步说明可能系统有异常


接着讲 block 销毁时调用的第二个函数 _Block_destructInstance
调试时,调用栈里也有这个函数

接着 step into

使用该函数,主要是为了将在弱引用表中注册的使用 __weak 引用 block 的变量置为 nil,因为 block 已经要被销毁了
这里不仔细讲了


最后调用 _Block_deallocator() 函数,将 block 结构体的内存销毁


这里总结一下 block 的内存销毁流程:

  • 先将 block 捕获的外部变量进行销毁
  • 将弱引用 block 的指针置为 nil
  • 将 block 结构体的内存销毁

探究runtime碰到的类和结构体

在研究 runtime 时能遇到许多的类和结构体, 因为不可能每篇文章碰到就介绍一遍, 所以在这里统一把这些碰到的类和结构体介绍一下

SideTable

这个类的作用是存放引用计数表和弱引用表, 并使用自旋锁来防止操作表结构时可能的竞态条件
定义如下:

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 SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;

SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}

~SideTable() {
_objc_fatal("Do not delete SideTable.");
}

void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }

// Address-ordered lock discipline for a pair of side tables.

template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

有下面几个成员变量

  • spinlock_t slock: 自旋锁

  • RefcountMap refcnts: 额外引用计数存储表

  • weak_table_t weak_table: 弱引用表, 用来存储弱引用者(weak 修饰的变量)

    其中RefcountMap是类型objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable>的别名

DisguisedPtr

定义如下:

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
template <typename T>
class DisguisedPtr {
uintptr_t value;

static uintptr_t disguise(T* ptr) {
return -(uintptr_t)ptr;
}

static T* undisguise(uintptr_t val) {
return (T*)-val;
}

public:
DisguisedPtr() { }
DisguisedPtr(T* ptr)
: value(disguise(ptr)) { }
DisguisedPtr(const DisguisedPtr<T>& ptr)
: value(ptr.value) { }

DisguisedPtr<T>& operator = (T* rhs) {
value = disguise(rhs);
return *this;
}
DisguisedPtr<T>& operator = (const DisguisedPtr<T>& rhs) {
value = rhs.value;
return *this;
}

operator T* () const {
return undisguise(value);
}
T* operator -> () const {
return undisguise(value);
}
T& operator * () const {
return *undisguise(value);
}
T& operator [] (size_t i) const {
return undisguise(value)[i];
}

// pointer arithmetic operators omitted
// because we don't currently use them anywhere
};

这个类的作用是伪装指针, 避免引用计数表和弱引用表里面保存的指针被内存泄漏工具leaks当做是内存泄漏.

  • 它的成员变量value存储的是经过函数disguise()处理后的指针.
  • 你可以像操作指针一样操作DisguisedPtr实例. 为了实现这个功能, DisguisedPtr重载了许多的操作符, 例如->, *, =, ==, []. 不了解的同学可以自己查资料, 关键字 运算符重载operator

DisguisedPtr一般用来充当 Key, 保存在结构体中

RefcountMapValuePurgeable

定义如下:

1
2
3
4
5
struct RefcountMapValuePurgeable {
static inline bool isPurgeable(size_t x) {
return x == 0;
}
};

只有一个内联函数RefcountMapValuePurgeable, 用来判断这个值需要是否析构释放内存

DenseMapInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
struct DenseMapInfo<DisguisedPtr<T>> {
static inline DisguisedPtr<T> getEmptyKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-1);
}
static inline DisguisedPtr<T> getTombstoneKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-2);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const DisguisedPtr<T> &LHS, const DisguisedPtr<T> &RHS) {
return LHS == RHS;
}
};

它有两个内联函数:

  • getEmptyKey(): 生成 empty 的标记
  • getTombstoneKey(): 生成墓碑标记(即代表之前被人使用过但现在已经没人用了)

有两个静态函数:

  • getHashValue(): 根据指针返回一个哈希值
  • isEqual(): 判断两个参数是否相等

DenseMapInfo常常跟DisguisedPtr一起使用. 它的两个内联函数用来标记存储数据的DenseMapPair实例 bucket 的状态是否是 empty 或者 tombstoneKey. 它的getHashValue函数用来生成一个哈希值来确定 bucket 的序号

detail::DenseMapPair

部分定义如下:

1
2
3
4
5
6
7
8
9
10
template <typename KeyT, typename ValueT>
struct DenseMapPair : public std::pair<KeyT, ValueT> {

// FIXME: Switch to inheriting constructors when we drop support for older
// clang versions.
// NOTE: This default constructor is declared with '{}' rather than
// '= default' to work around a separate bug in clang-3.8. This can
// also go when we switch to inheriting constructors.
DenseMapPair() {}
}

它的父类std::pair是一个结构体模板, 它将两个数据组合成一个数据, 类似于我们经常用的字典.
DenseMapPair被用来在引用计数表中保存引用计数. 其中 key 的类型为DisguisedPtr

DenseMap

部分定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <typename KeyT, typename ValueT,
typename ValueInfoT = DenseMapValueInfo<ValueT>,
typename KeyInfoT = DenseMapInfo<KeyT>,
typename BucketT = detail::DenseMapPair<KeyT, ValueT>>
class DenseMap : public DenseMapBase<DenseMap<KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>,
KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT> {
friend class DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

// Lift some types from the dependent base class into this class for
// simplicity of referring to them.
using BaseT = DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

BucketT *Buckets;
unsigned NumEntries;
unsigned NumTombstones;
unsigned NumBuckets;

public:
/// Create a DenseMap wth an optional \p InitialReserve that guarantee that
/// this number of elements can be inserted in the map without grow()
explicit DenseMap(unsigned InitialReserve = 0) { init(InitialReserve); }
}

这个类就是之前提到的引用计数表, 它的成员里面有一个存储数组, 用来保存引用计数. 数组的元素类型为 detail::DenseMapPair
如果一个对象的引用计数曾经溢出保存到表中, 当对象被销毁时, 会将表中对象使用过的存储器 bucket 标记为墓碑状态

1
friend class DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

用于声明一个友元类, 这样DenseMapBase就能访问DenseMap里面的私有属性和私有方法了

1
using BaseT = DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>添加一个类型别名

1
explicit DenseMap(unsigned InitialReserve = 0) { init(InitialReserve); }

上面那行代码的作用是显示的声明一个构造方式, 这样这类就不能隐式转换了

成员变量如下:

  • Buckets: 一个 bucket 数组, 用于保存数据. 可扩容
  • NumEntries: Buckets 数组中已经被使用的数目
  • NumTombstones: Buckets 数组中 tombstone 的数目
  • NumBuckets: Buckets 数组的数目

weak_table_t

1
2
3
4
5
6
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};

这个结构体用来存储弱引用条目, 弱引用条目里面保存着对象以及它的弱引用者们.
当数组数量超过 1024 且被使用的数量占比小于 1/16 时, 数组长度会缩小为原来的 1/8

它有 4 个成员变量:

  • weak_entry_t *weak_entries: weak_entry_t 类型的数组. 弱引用条目, 用来保存弱引用者(被 weak 修饰的指针)
  • size_t num_entries: 已经被使用的条目数量
  • uintptr_t mask: 条目数组的数量
  • uintptr_t max_hash_displacement: 用来记录数组中被使用条目的 index 的最大值

weak_entry_t

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
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};

bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}

weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};

这个结构体用来保存引用对象以及它的弱引用者, 属于一对多的关系
当弱引用比较少的时候会将弱引用者保存在结构体里面, 当弱引用者数量超过 4 时会保存到外部的数组中

它有两个成员变量, 一个是DisguisedPtr<objc_object> referent;, 另一个是联合体 union.
referent 表示引用对象, 当一个弱引用者引用了一个新的对象, 那么我们需要从弱引用条目中(weak_entry_t)移除该弱引用者

weak_referrer_tDisguisedPtr<objc_object *> 类型的别名

第二个成员变量 union 里面有两个结构体, 我这里称呼它们为 s1, s2.
因为是 union, 所以 s1 里面的 out_of_line_ness 跟 s2 里面的 inline_referrers 的第二个元素的低 2 个 bit 是重合的. 数组 inline_referrers 的元素是 weak_referrer_t 类型. 在 arm64 架构下, 指针 8 字节对齐, 意味着指针低 3 位肯定都是 0, 经过DisguisedPtr的伪装后, 它的低 2 位都是 1, 也就是 s1 的 out_of_line_ness 的值为 0b11.
这个特性用来标记是否使用内部数组来保存弱引用者, 当使用外部数组时, 内部数组被清空, 将 out_of_line_ness 赋值为 0b10 来表示使用外部数组

当使用内部数组时

  • s1 的成员变量均没有意义
  • 使用 s2 的 inline_referrers 数组来保存弱引用者.

当使用外部数组时

  • s1 的成员变量referrers是指向外部数组的指针; out_of_line_ness为常量 2, 表示使用了外部数组; mask表示外部数组的长度 - 1; num_refs表示外部数组中被使用的数量; max_hash_displacement表示哈希最大偏移量
  • 此时 s2 的成员变量 inline_referrers 被清空

iOS weak弱引用底层实现

源码版本:objc4-779.1
当然还是推荐使用这个来学习:可编译的源码

weak是一个所有权修饰符, 它提供弱引用的功能, 即弱引用者(weak 修饰的变量, 后统称为弱引用者)不能持有引用对象, 当引用对象被释放时, 此弱引用者被置为 nil.
此文将探究弱引用在底层是如何实现

弱引用者如何注册到弱引用表

首先我们研究弱引用者是如何注册到弱引用表中的
调试代码如下(代码需要在开头提到的可编译版本里面才能运行, 后面不再解释), 注意里面打了个断点, 运行程序

点击 step into, 会跳转到函数objc_initWeak()

这个函数的作用是初始化弱引用者.
需要注意的是storeWeak模板里面的两个参数 DontHaveOld, DoHaveNew. 它们分别是HaveOldHaveNew枚举的变量. 枚举里面变量的含义如下

  • DontHaveOld: 表示弱引用者之前没有引用对象, 例如在用__weak id weakPtr = [[NSObject alloc] init];初始化弱引用者

  • DoHaveOld: 表示弱引用者之前有引用对象. 此时需要将弱引用者从弱引用表中注销

  • DontHaveNew: 表示弱引用者没有引用一个新的对象

  • DoHaveNew: 表示弱引用者引用了一个新的对象. 此时需要将弱引用者注册到弱引用表中

继续 step into, 跳转到storeWeak()函数里面

函数里面代码比较多, 为了方便, 我把它分成了几部分来说明

SideTable跟弱引用的实现息息相关, 在这里可以看到对这个类的详细说明.
这里我就简单的介绍一下, 它是存放引用计数表和弱引用表的结构. 它有三个成员变量:引用计数表, 弱引用表, 自旋锁. 在我们操作引用计数表或者弱引用表的时候自旋锁会加锁防止竞态条件的出现.

代码块 1 的作用:

  • 加锁, 防止在修改哈希表时可能出现的竞态条件. 按锁地址(从低到高)顺序加锁, 防止可能出现的锁的排序问题.
  • 如果弱引用者的引用对象改变了(这是因为如果并发修改弱引用者的引用对象, 在加锁之前, 可能原先的引用对象会改变), 则重复执行retry部分的代码

代码块 2 的作:

  • 保证引用对象的 isa 指针已经初始化过. 如果未初始化, 则对该对象的非元类 Class 进行初始化.

代码块 3 的作:

  • 在弱引用表上注销弱引用者, 后面会再来讲这个部分

代码块 4 的作用:

  • 在弱引用表上注册弱引用者

代码块 5 的作用:

  • 对象被弱引用者引用了, 则修改对象 isa 指针上位域的信息, 将weakly_referenced置为 YES, 表示该对象被一个弱引用者引用.

需要注意的是, 如果这个对象后续不再被弱引用者引用了, isa 指针上的weakly_referenced值仍旧是 YES

接着, 我们把断点移动到weak_register_no_lock()函数里面, 同样, 我把它分成几部分来讲解.

referent 表示引用对象, referrer 表示弱引用者

代码块 1 的作用是:

  • 保证引用对象是可用的. 如果引用对象正处于被销毁的状态, 那么程序会崩溃

代码块 2 的作用是:

  • 根据引用对象找到对应的 weak_entry_t 实例(后面统称为为弱条目), 并将该弱引用者保存到弱条目中

下面是append_referrer()函数的实现, 写了点注释就不展开讲了

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
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
if (! entry->out_of_line()) {
// 使用内部数组来保存弱引用者. 遍历内部数组, 如果有空位则用该空位保存弱引用者并返回
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}

// 如果执行到这里, 说明内部数组都被使用了. 所以需要扩容, 即创建一个外部数组来保存弱引用者
// 先初始化一个跟内部数组长度一样的数组, 并将内部数组的数据转移过去
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
// This constructed table is invalid, but grow_refs_and_insert
// will fix it and rehash it.
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
new_referrers[i] = entry->inline_referrers[i];
}
entry->referrers = new_referrers;
entry->num_refs = WEAK_INLINE_COUNT;
entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
entry->mask = WEAK_INLINE_COUNT-1;
entry->max_hash_displacement = 0;
}

ASSERT(entry->out_of_line());
// 如果外部数组中有 3/4 的元素都被使用了, 则进行扩容
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
// 扩容的长度为原来的 2 倍, 最小值为 8.
return grow_refs_and_insert(entry, new_referrer);
}
// 根据指针的哈希值来计算位置 index
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != nil) {
// 发生了哈希碰撞, 则增加下标, 查看下一个位置是否为空
hash_displacement++;
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
}
// 重新计算 entry 的最大哈希偏移量
if (hash_displacement > entry->max_hash_displacement) {
entry->max_hash_displacement = hash_displacement;
}
// 将新的弱引用者保存到弱引用条目中
weak_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
// num_refs + 1
entry->num_refs++;
}

代码块 3 的作用:

生成一个新的弱引用条目, 将弱引用者保存到弱条目中. 随后将弱条目插入到弱引用表中

  • 第一行代码的作用是初始化一个weak_entry_t的实例 new_entry
  • 第二行代码里面函数的作用是, 如果弱引用表的成员变量weak_entries数组里面有 3/4 的数量的弱条目被使用了, 则进行扩容. 长度为原来的 2 倍, 最小为 64
  • 第三行代码的作用是将新生成的弱条目插入到弱引用表中

至此, 我们已经大致了解了弱引用者是如何注册到弱引用条目的, 这里小总结一下:

  • 首先判断引用对象是否处于销毁状态, 否则是的话程序会崩溃
  • 根据引用对象获取到对应的SideTable, 它里面有成员变量弱引用表weak_table_t
  • 在弱引用表中根据引用对象来查找对应的弱引用条目, 如果存在则将该弱引用者保存在该条目中. 如果不存在则新生成一个条目, 将弱引用者保存到该条目, 随后将条目插入到弱引用表中
  • 在弱引用表中插入弱条目根据引用对象的哈希值来计算 index, 在弱条目中插入弱引用者根据弱引用者的哈希值来计算 index
  • 在此过程中, 不对任何的弱引用者进行赋值, 即不对 *referrer 进行赋值操作

弱引用者如何从弱引用表中注销

这部分用来探究弱引用者如何从弱引用表中注销
实验代码如下

点击step into跳转到objc_storeWeak()函数里面

首先弱引用者 w1 引用了对象 o1, w1 会被注册到 o1 对应的弱引用条目中. 随后我们给它设置了一个新的引用对象 o2, 那么, w1 会先从 o1 的条目中注销, 然后注册到 o2 的条目中.

继续调试, 将断点移动到storeWeak()函数里面

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
84
85
86
87
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);

Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;

// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}

SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}

// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(cls, (id)newObj);

// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;

goto retry;
}
}

// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}

// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected

// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}

// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}

SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

return (id)newObj;
}

函数里面大部分内容已经在上一节中介绍过, 这里就不重复了
这里我们要讲解的是weak_unregister_no_lock()函数. 这个函数的作用是将销弱引用者从引用对象对应的弱引用条目中注销. 函数实现如下:

referent表示原先的引用对象, referrer表示弱引用者

代码块 1 的作用是根据引用对象查找弱引用条目, 然后存在则将该弱引用者从条目中注销

函数remove_referrer()的实现如下, 写了点注释就不展开讲了

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
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
if (! entry->out_of_line()) {
//使用内部数组保存弱引用者, 此时遍历该数组, 找到对应的弱引用者, 如果找到的话则将其从数组中移除
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == old_referrer) {
entry->inline_referrers[i] = nil;
return;
}
}
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
// 此时使用外部数组来保存弱引用者
// 根据弱引用者的哈希值来得到对应元素的 index
size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != old_referrer) {
// 如果发生了哈希碰撞, 则偏移+1 往下继续查找该弱引用者.当偏移量超过最大偏移量时程序崩溃
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
hash_displacement++;
if (hash_displacement > entry->max_hash_displacement) {
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
}
// 找到对应弱引用则将其从外部数组中移除
entry->referrers[index] = nil;
entry->num_refs--;
}

代码块 2 的作用是判断弱引用条目中注册的弱引用者的数量是否为 0, 如果为 0 则将该弱引用条目从弱引用表中清除.

weak_entry_remove()函数的实现如下, 写了点注释就不展开讲了

1
2
3
4
5
6
7
8
9
10
11
12
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
// remove entry
// 如果使用的外部数组, 则将外部数组销毁. 如果使用内部数组, 则会在销毁条目时将内部数组也销毁
if (entry->out_of_line()) free(entry->referrers);
// 销毁弱引用条目
bzero(entry, sizeof(*entry));

weak_table->num_entries--;
// 当条目数量超过 1024, 并且使用了不到 1/16, 则将条目数量缩小为原来的 1/8
weak_compact_maybe(weak_table);
}

至此, 我们已经大致了解了弱引用者如何从弱引用表中注销的, 我小总结一下:

  • 根据旧引用对象获取SideTable
  • 根据就引用对象在SideTable的弱引用表中获取对应的弱引用条目
  • 在弱引用条目中查找是否注册了该弱引用者, 如果存在的话则移除
  • 若弱引用者移除后弱引用表中弱引用者的数量为 0, 则将该条目从弱引用表中移除
  • 如条目移除后, 条目的数量超过 1024, 且使用的数量少于 1/16, 则将条目的容量缩小为原先的 1/8

引用对象销毁后弱引用者如何置为 nil

我们都知道当引用对象被销毁后, 弱引用者会被置为 nil.
这部分用来粗略的讲解其实现

添加一个symbolic breakpoint, 在symbol里面输入[NSObject dealloc]

点击 continue program execution, 跳转到dealloc()函数中

对 o1 的引用计数 -1 , o1 的引用计数就变成了 0, 需要销毁. 即调用 NSObject 的 dealloc 方法

随后一直跳转到objc_object::clearDeallocating_slow()函数里面.
这个函数的作用是清除该对象在SideTable中存在于引用计数表或者弱引用表中的数据

因为研究的是对弱引用的操作, 所以这边我们只需要关注weak_clear_no_lock()函数.
它的实现如下, 写了注释就不展开讲了

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
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
// referent 表示引用对象, 即这里要被销毁的对象
objc_object *referent = (objc_object *)referent_id;
// 根据引用对象找到对应的弱引用条目
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}

// zero out references
// 数组指针
weak_referrer_t *referrers;
// 数组长度
size_t count;

if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}

for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
// 将弱引用者置为 nil
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
// 将所有弱引用者置为 nil 后, 将该条目从弱引用表中删除
weak_entry_remove(weak_table, entry);
}

至此, 我们已经大致了解了引用对象销毁后弱引用者如何置为 nil 的, 这里小总结一下:

当对象的引用计数为 0 时, 会调用 dealloc 方法用来销毁对象. 如果有弱引用者引用者该对象, 那么会从弱引用表中找到对象的弱引用条目, 将条目中所有注册的弱引用者置为 nil, 随后将该条目从弱引用表中删除.

深入了解 iOS 引用计数

iOS 通过引用计数来管理内存,简单的说就是当一个 id 类型的对象引用计数变成 0 的时候就会被销毁,回收内存。
本文将通过断点调试来探究引用计数的存储及读取在底层的实现。全程无聊,请配合源码阅读

源码版本:objc4-779.1
当然还是推荐使用这个来学习:可编译的源码

引用计数存储在哪

引用计数只适用于 id 类型的对象

有一种特殊的 id 类型对象,Tagged Pointer,如果不知道的话可以看我另一篇博客 深入了解Tagged Pointer了解一下

因为 Tagged Pointer 并不是真正的对象,所以它并不通过引用计数来管理其内存。
用下面的代码求 n1 的引用计数,输出结果为 9223372036854775807

1
2
3
// 创建一个 Tagged Pointer 对象
NSNumber *n1 = [NSNumber numberWithInt:1];
NSLog(@"n1 rc = %ld", CFGetRetainCount((__bridge CFTypeRef)(n1)));

我们知道每个 id 类型对象都有一个 isa 指针,其中只有 33 位用来保存其父类或者元类信息,剩余的31位也不能浪费啊,这其中就会一些 bit 用来保存引用计数。
下面是 arm64 架构下的 isa 结构

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
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

struct {
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
#endif
};

可以发现,isa_t 是一个联合体(union),几个不同类型的变量 clas 和 bits 及 struct 共用同一段内存。当 isa 指针的第一位nonpointer为 1 时表示它是优化的 isa 指针,即将多余的 bit 用来存储其它信息,从它的名字nonpointer也可以看出它不再是一个纯粹的指针了。当为 1 时,第一个成员变量cls是没有用的,因为 isa 不再使用它来指向父类 cls。下面是 isa 指针各位域的含义

变量名 含义
nonpointer 表示是否对 isa 指针开启优化 0:纯isa指针,1:存储了额外信息
has_assoc 表示该对象是否包含 associated object,如果没有,则析构时会更快
has_cxx_dtor 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
shiftcls 类的指针
magic 固定值 0x1a, 用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced 表示该对象是否有过 weak 对象,如果没有,则析构时更快
deallocating 表示该对象是否正在析构
has_sidetable_rc 表示该对象的引用计数值是否过大无法存储在 isa 指针
extra_rc 存储引用计数值减一后的结果

在 arm64 下, isa 使用 19 个 bit 用来存储引用计数. 当引用计数超过这个数时,将会把 RC_HALF 数量的引用计数存储在一个全局哈希表中,此时 has_sidetable_rc 变为 1

增加引用计数

下面让我们通过调试来探究具体的存储过程
因为是在模拟器中运行的,isa 最多只能使用 8 个 bit 来存储引用计数, 即在 isa 中只能存储不超过 256 的数目。所以我们只要将对象的引用计数增加到 256 以上,那么系统就会将多余的引用计数存储到哈希表。

1
2
3
4
5
6
void foo(void) {
NSObject *o1 = [[NSObject alloc] init];
for (int i = 0; i < 500; i++) {
_objc_rootRetain(o1);
}
}

_objc_rootRetain()方法最终会调用objc_object::rootRetain(bool tryRetain, bool handleOverflow), 这个方法的代码如下:

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
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;

bool sideTableLocked = false;
bool transcribeToSideTable = false;

isa_t oldisa;
isa_t newisa;

do {
// 是否将引用计数转移到索引表中
transcribeToSideTable = false;
// LoadExclusive 的作用是让读取操作原子化
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
// if (slowpath) else (),表示大概率执行 else 后面的语句,用来提高效率。
// fastpath 则表示大概率执行 if 后面的语句
if (slowpath(!newisa.nonpointer)) {
// arm64 架构下,一般的对象 nonpointer 为YES,但也有特例,例如 block 的 nonpointer 为 False,即不使用 isa 保存额外信息
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
// 用来表示 isa 指针的值是否溢出。因为引用计数存储在指针的高位,当引用计数增加到一定程度,会超过最高位的数字,此时 carry 的值不等于 0,表示溢出
uintptr_t carry;
// addc() 是一个内置函数,作用是将 isa 里面的引用计数 + 1 后保存
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++

if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
// 移除的话则需要对多余的引用计数保存到索引表中。
// 刚开始进入的时候 handleOverflow == false
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// 加锁
if (!tryRetain && !sideTableLocked) sidetable_lock();
// 此时 isa 指针存储的引用计数应该是满的, extra_rc 的 19 位都是 1,将 extra_rc 赋值为原来的一半,也就是将 isa 的最高位赋值为 0,这样做的目的是下次引用计数增加的时候可以直接存储在 isa 中,而不需要调用索引表来存储。将 has_sidetable_rc 赋值为 1,标记有额外的引用计数存储在索引表中
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
// StoreExclusive(), 如果&isa.bits和oldisa.bits相等,那么就把newisa.bits的值赋给&isa.bits,并且返回true。
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
// 如果溢出的话,则将 isa 存储的引用计数赋值为最大值的一半,即 RC_HALF,那么减少的一半则转移到索引表中
sidetable_addExtraRC_nolock(RC_HALF);
}

if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}

写了点注释帮助你们理解. 现在我们在sidetable_addExtraRC_nolock(RC_HALF);那一行打个断点,启动程序

step into进入这个方法

因为用的模拟器,所以 RC_HALF == delta_rc == 128,符合预期。

代码SideTable& table = SideTables()[this],通过计算指针值得到哈希值获取到对应的 sidetable
让我们点开方法SideTables()

1
2
3
4
5
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}

class ExplicitInit 的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename Type>
class ExplicitInit {
alignas(Type) uint8_t _storage[sizeof(Type)];

public:
template <typename... Ts>
void init(Ts &&... Args) {
new (_storage) Type(std::forward<Ts>(Args)...);
}

Type &get() {
return *reinterpret_cast<Type *>(_storage);
}
};

ExplicitInit的作用是生成一个模板类型 Type 的实例。alignas(Type) uint8_t _storage[sizeof(Type)];成员变量_storage是一个 sizeof(Type) 长度的 uint8_t 数组,而 uint8_t 占用一个字节,所以实际上_storage的长度跟一个 Type 实例所占用的内存是一样多的。成员函数 Type &get() 将 _storage 数组指针用reinterpret_cast<Type *>强转成了 Type * 类型指针,前面再加一个 *,说明返回的实际上是 Type 实例内存的首地址。而另一个成员函数init用来初始化生成的 Type 实例。

所以static StripedMap<SideTable>& SideTables()函数实际上返回了一个全局静态StripedMap<SideTable>的实例。
下面是class StripedMap的部分定义

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
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif

struct PaddedT {
T value alignas(CacheLineSize);
};

PaddedT array[StripeCount];

static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
// ...省略
}
  • 局部结构体PaddedT的作用是用来内存对齐,常量CacheLineSize是固定值 64。在 arm64 架构下,成员变量array是一个长度为 8,元素类型为 T,每个元素占 64 字节的数组,而在模拟器下是一个长度为 64,每个元素占 64 字节的数组。
  • 成员函数indexForPointer用来根据指针来计算哈希值,确定对应于array里面第几个元素。
  • T& operator[] (const void *p)使用 operator 关键字重载了[]符号。举个例子,StripedMap 有一个实例 sm,有一个 int 类型的变量 a,当执行sm[&a]的代码时,就会跳转到T& operator[] (const void *p)这个方法里面。而在上面的重载[]函数中,传入的参数为指针类型,然后计算出其数组 index,返回对应的元素 T,也就是 SideTable

继续调试,断点此时在SideTable& table = SideTables()[this];这一行。点击 step into

_storage的数组长度为 4096,也就是 64 * 64,符合预期。继续step into,最终会跳转到下面这里

跳转到了重载的[]方法里面了。

目前已知道的情报就是,有一个全局的StripedMap条目索引,里面包含了若干个SideTable索引表。在使用时,根据指针得到对应的 sidetable。SideTable 的部分定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;

SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// ...省略
};

SideTable 的成员对象里面有一个自旋锁,引用映射表RefcountMap refcnts,弱引用表weak_table_t weak_table。在上面我们提到了每个 sidetable 占用 64 个字节, 那是因为 spinlock_t 占用 4, RefcountMap 占用 24, weak_table_t 占用 32, 加起来就是 64.
在这里我们只了解下 RefcountMap, 因为它跟我们引用计数相关.
RefcountMap是一个类型别名,它的定义如下:

1
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;

先别急着查看DenseMap的定义,我们先看看DisguisedPtr是个什么类。它的定义如下:

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
template <typename T>
class DisguisedPtr {
uintptr_t value;

static uintptr_t disguise(T* ptr) {
return -(uintptr_t)ptr;
}

static T* undisguise(uintptr_t val) {
return (T*)-val;
}

public:
DisguisedPtr() { }
DisguisedPtr(T* ptr)
: value(disguise(ptr)) { }
DisguisedPtr(const DisguisedPtr<T>& ptr)
: value(ptr.value) { }

DisguisedPtr<T>& operator = (T* rhs) {
value = disguise(rhs);
return *this;
}
DisguisedPtr<T>& operator = (const DisguisedPtr<T>& rhs) {
value = rhs.value;
return *this;
}

operator T* () const {
return undisguise(value);
}
T* operator -> () const {
return undisguise(value);
}
T& operator * () const {
return *undisguise(value);
}
T& operator [] (size_t i) const {
return undisguise(value)[i];
}
};
  • 简单的说, 你可以像指针Type *一样的使用DisguisedPtr,但是其成员变量value保存的是伪装后的指针值。伪装函数是disguise(),0 和 0x800…00 经过伪装之后还是本身,但指针值也不可能是这两个值,所以无所谓的。
  • 为了能像指针Type *一样的使用DisguisedPtr,在DisguisedPtr内部,重载了许多符号,例如 [], =, *, ()。具体就不分析了。
  • 该类的作用是为了避免直接引用指针, 防止被 leaks 内存检测工具检测出内存泄漏

class RefcountMapValuePurgeable的定义如下:

1
2
3
4
5
struct RefcountMapValuePurgeable {
static inline bool isPurgeable(size_t x) {
return x == 0;
}
};

它只有一个内联函数,根据 x 是否为 0 返回一个 bool 值。它的作用就是根据参数判断是否需要析构这个实例
好了,了解完了DisguisedPtrRefcountMapValuePurgeable,接下来我们来看DenseMap,它的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename KeyT, typename ValueT,
typename ValueInfoT = DenseMapValueInfo<ValueT>,
typename KeyInfoT = DenseMapInfo<KeyT>,
typename BucketT = detail::DenseMapPair<KeyT, ValueT>>
class DenseMap : public DenseMapBase<DenseMap<KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>,
KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT> {
friend class DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

using BaseT = DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

BucketT *Buckets;
unsigned NumEntries;
unsigned NumTombstones;
unsigned NumBuckets;

public:
explicit DenseMap(unsigned InitialReserve = 0) { init(InitialReserve); }
}
  • 代码friend class DenseMapBase的作用是声明一个友元类DenseMapBase,这样DenseMapBase在内存可以直接访问DenseMap private 的成员函数。
  • 代码using BaseT = DenseMapBase的作用是为DenseMapBase指定一个别名BaseT
  • 函数explicit DenseMap(), explicit的作用是用来声明类构造函数是显示调用的,而非隐式调用。

DenseMap里面有 4 个成员变量,Buckets 和 NumEntries,NumTombstones, NumBuckets。

  • Buckets, 存储引用计数的数组, 元素类似于字典, key 为指针, value 为引用计数. 数组可扩容
  • NumEntries 表示数组中用来存储引用计数的元素有几个
  • NumTombstones 数组中元素被用来存储一个对象的引用计数, 当这个对象被销毁后, 该元素被标记为墓碑. 该变量用来表示有多少个这种元素
  • NumBuckets 数组 Buckets 的长度

再来看一下模板里的几个类型 DenseMapInfo<KeyT>, detail::DenseMapPair<KeyT, ValueT>

DenseMapInfo 有很多的模板定义,可以适用于各种类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
struct DenseMapInfo<DisguisedPtr<T>> {
static inline DisguisedPtr<T> getEmptyKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-1);
}
static inline DisguisedPtr<T> getTombstoneKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-2);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const DisguisedPtr<T> &LHS, const DisguisedPtr<T> &RHS) {
return LHS == RHS;
}
};

内联函数getEmptyKeygetTombstoneKey均返回一个特征值,用来标记 bucket 的状态 Empty 和 Tombstone。
静态函数getHashValue根据指针计算出哈希值。
ptr_hash()的实现如下:

1
2
3
4
5
6
7
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}

看不懂,知道是计算哈希值的就好了。

最后一个静态函数isEqual用来比较两个参数是否相等。

接着看detail::DenseMapPair<KeyT, ValueT> 它的定义如下

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
template <typename KeyT, typename ValueT>
struct DenseMapPair : public std::pair<KeyT, ValueT> {
DenseMapPair() {}

DenseMapPair(const KeyT &Key, const ValueT &Value)
: std::pair<KeyT, ValueT>(Key, Value) {}

DenseMapPair(KeyT &&Key, ValueT &&Value)
: std::pair<KeyT, ValueT>(std::move(Key), std::move(Value)) {}

template <typename AltKeyT, typename AltValueT>
DenseMapPair(AltKeyT &&AltKey, AltValueT &&AltValue,
typename std::enable_if<
std::is_convertible<AltKeyT, KeyT>::value &&
std::is_convertible<AltValueT, ValueT>::value>::type * = 0)
: std::pair<KeyT, ValueT>(std::forward<AltKeyT>(AltKey),
std::forward<AltValueT>(AltValue)) {}

template <typename AltPairT>
DenseMapPair(AltPairT &&AltPair,
typename std::enable_if<std::is_convertible<
AltPairT, std::pair<KeyT, ValueT>>::value>::type * = 0)
: std::pair<KeyT, ValueT>(std::forward<AltPairT>(AltPair)) {}

KeyT &getFirst() { return std::pair<KeyT, ValueT>::first; }
const KeyT &getFirst() const { return std::pair<KeyT, ValueT>::first; }
ValueT &getSecond() { return std::pair<KeyT, ValueT>::second; }
const ValueT &getSecond() const { return std::pair<KeyT, ValueT>::second; }
};

看起来很复杂的样子,但其实只要知道它的父类std::pair是干嘛的就好了。
std::pair的作用是将两个数据组合成一个数据,两个数据可以是同一类型或者不同类型。它是一个模板结构体,主要的两个成员变量是first和second,这两个变量可以直接使用。
再根据注释,可以推测出DenseMapPair作用是将 keyT 和 ValueT 两个类型的值保存在一起

继续之前的调试,在代码size_t& refcntStorage = table.refcnts[this];打个断点,然后点击 continue program execution 跳转过去。

注意这里的size_t&符号不是取内存地址意思,& 在这里表示引用的意思。举个例子

1
2
3
int a = 1;
int& b = a;
b = 10;

对 b 赋值 10 后,a 的值也变成了 10。好了,继续

继续 step into

首先会跳转到类DisguisedPtr的一个初始化方法里面,这里对指针进行伪装并赋值给了成员对象 value。
继续 step into

跳转到了这里。可以看出来DenseMap的父类DenseMapBase也重载了[]方法。
参数类型为KeyT &&,也就是DisguisedPtr<objc_object> &&类型。这里的&&符号以及下面的std::move(Key)函数都是为了确定参数是右值引用,实现移动语义,减少不必要的参数拷贝。

继续 step into,跳转到了下面这里

之前我们讲DenseMapPair<KeyT, ValueT>的时候说到过,这个类的作用是将两个类型的值的组合成一个整体,类似于我们平常使用的字典。key 对应 first, value 对应 second。在FindAndConstruct方法中,首先会通过LookupBucketFor()查找是否已经有存在的 bucket,如果没有则通过InsertIntoBucket函数生成并返回对应的 bucket。

下面是LookupBucketFor()函数的实现

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
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
// 返回成员变量 Buckets
const BucketT *BucketsPtr = getBuckets();
// 返回成员变量 NumBuckets
const unsigned NumBuckets = getNumBuckets();

if (NumBuckets == 0) {
FoundBucket = nullptr;
return false;
}

// 保存在查找过程中碰到的墓碑
const BucketT *FoundTombstone = nullptr;
// DenseMapInfo 的内联函数 getEmptyKey
const KeyT EmptyKey = getEmptyKey();
// DenseMapInfo 的内联函数 getTombstoneKey
const KeyT TombstoneKey = getTombstoneKey();
assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
!KeyInfoT::isEqual(Val, TombstoneKey) &&
"Empty/Tombstone value shouldn't be inserted into map!");

//DenseMapInfo 的静态函数 getHashValue 计算哈希值,并与 NumBuckets-1 进行与运算,得到对应 BucketT 实例的 index
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
// 查找次数
unsigned ProbeAmt = 1;
while (true) {
const BucketT *ThisBucket = BucketsPtr + BucketNo;
// Found Val's bucket? If so, return it.
// 找到了对应的 BucketT 实例
if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
FoundBucket = ThisBucket;
return true;
}

// If we found an empty bucket, the key doesn't exist in the set.
// Insert it and return the default value.
// 如果对应的 bucket 是空的。如果是第一次查找的话则在这个 bucket 里面插入 key 和 value(在InsertIntoBucket函数中执行)。如果不是第一次并且之前找到了墓碑,则使用墓碑bucket插入key和value
if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
// If we've already seen a tombstone while probing, fill it in instead
// of the empty bucket we eventually probed to.
FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
return false;
}

// If this is a tombstone, remember it. If Val ends up not in the map, we
// prefer to return it than something that would require more probing.
// Ditto for zero values.
// 如果找到一个墓碑,则使用 FoundTombstone 将这个墓碑记录下来
if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) &&
!FoundTombstone)
FoundTombstone = ThisBucket; // Remember the first tombstone found.
// 如果 FoundTombstone 为空并且 对应的 bucket 的 second 是 0,则用FoundTombstone保存下这个bucket
if (ValueInfoT::isPurgeable(ThisBucket->getSecond()) && !FoundTombstone)
FoundTombstone = ThisBucket;

// Otherwise, it's a hash collision or a tombstone, continue quadratic
// probing.
// 如果查找次数多余 NumBuckets ,则报错
if (ProbeAmt > NumBuckets) {
FatalCorruptHashTables(BucketsPtr, NumBuckets);
}
// 重新计算 BucketNo,重新查找
BucketNo += ProbeAmt++;
BucketNo &= (NumBuckets-1);
}
}

在上面的代码里写了点注释帮助理解。
bucket 有两种状态,一种是 Empty,这个表示当前这个 bucket 还没有被使用的标记;另一个状态是 TombstoneKey,当一个 bucket 被使用了,当对象释放后该 bucket 被定义为这种状态

通过FindAndConstruct函数的分析我们知道,如果已经存在 bucket,则返回 YES,并将指针 TheBucket 指向这个 bucket。如果存在一个墓碑或者空的 bucket,则返回 false,并将指针 TheBucket 指向这个 bucket。

接着分析InsertIntoBucket(),下面是它的实现:

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
template <typename KeyArg, typename... ValueArgs>
BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
ValueArgs &&... Values) {
TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);

// 保存 key 到 bucket 的成员变量 first 中
TheBucket->getFirst() = std::forward<KeyArg>(Key);
// 初始化 ValueT 的实例并保存到 bucket 的成员变量 second 中
::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
return TheBucket;
}

template <typename LookupKeyT>
BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,
BucketT *TheBucket) {
// If the load of the hash table is more than 3/4, or if fewer than 1/8 of
// the buckets are empty (meaning that many are filled with tombstones),
// grow the table.
//
// The later case is tricky. For example, if we had one empty bucket with
// tons of tombstones, failing lookups (e.g. for insertion) would have to
// probe almost the entire table until it found the empty bucket. If the
// table completely filled with tombstones, no lookup would ever succeed,
// causing infinite loops in lookup.
unsigned NewNumEntries = getNumEntries() + 1;
unsigned NumBuckets = getNumBuckets();
// 当 buckets 数组已经使用了超过 3/4 或者 空bucket的数量小于 1/8,则增加 buckets 数组的长度。
if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
// 扩增原来的 NumBuckets,新的 buckets 长度为旧的两倍,长度至少为 4。新的 buckets 生成后将旧数组里面的数据转移过去,并且旧数组删除
this->grow(NumBuckets * 2);
// 因为重新生成了 buckets 数组,所以需要使用LookupBucketFor重新查找对应bucket,并用指针 TheBucket 指向
LookupBucketFor(Lookup, TheBucket);
NumBuckets = getNumBuckets();
} else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
NumBuckets/8)) {
this->grow(NumBuckets);
LookupBucketFor(Lookup, TheBucket);
}
ASSERT(TheBucket);

// Only update the state after we've grown our bucket space appropriately
// so that when growing buckets we have self-consistent entry count.
// If we are writing over a tombstone or zero value, remember this.
if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {
// Replacing an empty bucket.
// 使用空bucket,NumEntries + 1
incrementNumEntries();
} else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {
// Replacing a tombstone.
// 使用墓碑 bucket,NumEntries + 1, NumTombstones - 1
incrementNumEntries();
decrementNumTombstones();
} else {
// we should be purging a zero. No accounting changes.
// 如果 second 等于 0,则析构 second
ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));
TheBucket->getSecond().~ValueT();
}

return TheBucket;
}

通过InsertIntoBucket函数我们了解到,当 buckets 数组已经使用了超过 3/4 或者 空bucket的数量小于 1/8,则会生成一个新的 buckets,长度为原来的2倍(最小为4),并将旧数组的数据转移到新数组中,并重新查找 TheBucket 的 index。最后将伪装后的指针保存到bucket的first中,将引用计数保存到bucket的second中。

理论分析到这里。这几个函数的调试我就不贴出来了,比较长,你们可以自己调试下。

接下来在代码size_t oldRefcnt = refcntStorage;中打个断点,点击 continue program execution 跳转过去。

让我们回到objc_object::sidetable_addExtraRC_nolock函数中。因为我们刚刚在 sidetable 索引表里面插入了 bucket,所以这里 oldRefcnt 的值 为 0。
接下来会对 oldRefcnt 进行校验,这说明 oldRefcnt 并没有把所有的 bit 都用来保存引用计数。

  • 低 1 位(从低位往高位数第一位)用来标记是否是弱引用
  • 低 2 位用来标记该变量是否在析构
  • 高 1 位(从高位往低位数第一位)用来标记保存的引用计数是否已经溢出。

下面的代码我们好像看到过了,对,在将引用计数保存在 isa 指针那里

1
2
3
4
5
6
7
8
9
10
11
12
uintptr_t carry;
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
if (carry) {
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {
refcntStorage = newRefcnt;
return false;
}

这几句的代码的作用是将 newRefcnt = oldRefcnt + delta_rc,如果值太大了溢出了则将 carry 赋值为1,并将 refcntStorage 的高1位赋值为1,并返回true。如果没有溢出的话则将 newRefcnt 赋值到 refcntStorage,并返回 false。

到这里,我们探究了当引用计数增加时,如何保存多余的引用计数。简单的归纳下:

  • 有一个静态全局变量 SideTablesMap,实际上是 StripedMap 的实例。以便于在程序初始化的时候就能用到
  • 变量 SideTablesMap 里面有一个长度为 8,类型为 SideTable 的数组成员array,每个元素占用的内存为64(spinlock_t内存4, RefcountMap内存24,weak_table_t内存32)。在保存引用计数的时候,根据指针地址来计算出相应 SideTable 的 index。
  • 获取到相应的 SideTable 后,根据指针地址计算出相应的哈希值,然后查找 bucket。如果 bucket 不存在,则使用空 bucket 或者 墓碑 bucket。如果 bucket 的数量不够则扩容。将指针为 key 和 引用计数 为 value 保存到 bucket 当中。

注意:当一个对象创建后它的引用计数为 1,但是 isa 指针里的引用计数还是 0。因为初始化的时候并没有对引用计数进行操作。

减少引用计数

这里我们只讨论有额外引用计数存储在 sidetable,且 isa 指针里面存储的引用计数为 0 时的临界情况。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
void aoo(void) {
NSObject *o1 = [[NSObject alloc] init];
for (int i = 0; i < 512; i++) {
_objc_rootRetain(o1);
}
for (int i = 0; i < 129; i++) {
if (i == 128) {
int a = 1;
}
_objc_rootRelease(o1);
}
}

在代码int a = 1;处打个断点,此时 isa 保存的引用计数为 0。一直 step over,直到跳转到objc_object::rootRelease(bool performDealloc, bool handleUnderflow)函数。


一直点击 step over,直到停在newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);那一行

用调试命令打印看一下此时 isa 指针里面的内容

因为使用的是模拟器,所以 isa 里面只有最高的 8 个bit用来保存引用计数,之前我们减少它的引用计数 128。所以此时它 isa 指针里面保存的引用计数为 0。调试输出结果extra_rc也为0,符合预期(0b是二进制符号)

接下来系统要将引用保存在 isa 指针的引用计数 -1,并判断是否溢流。此刻它的引用计数已经为 0 了,那么对它进行 -1 操作会发生什么呢?我们接着调试,点击 step over


可以看到变量carry == 1,表示出现溢流。让我们再看一下 isa 指针里面发生了什么。


我们可以看到保存引用计数的 8 个 bit 都变成了 1,这其实就是 -1 在无符号整数中的表现形式,不懂的同学可以去了解一下补码。

接着一直 step over,我们可以看到因为溢流所以跳转到了underflow部分。一直 step over 直到跳转到如下的位置

rootRelease_underflow最终调用的还是objc_object::rootRelease()函数,只不过参数 handleUnderflow 变成了 YES。
进入该函数后,又会执行之前的那些代码,过程也是一样的,就不重复说了。接下来会来到这个位置

是的,因为没上锁,所以又会回到retry部分的代码。注释里写着是因为

Need to start over to avoid a race against the nonpointer -> raw pointer transition.

不是很明白,但是过了。继续 step over

之前引用计数溢出时,我们将RC_HALF数目的引用计数存储到了sidetable当中,此时因为溢流,所以我们从sidetable中取回同样数目的引用计数。

step into 进入objc_object::sidetable_subExtraRC_nolock()函数

在最开始的代码中,我们增加了变量o1 512 的引用计数, 在 isa 指针中保存了 128, 还剩余 384. 但是因为最低两位没有用来保存数据, 所以在 bucket 中存储的数字应该是 384 << 2, 即 1536, 符合预期

接下来的代码作用就是判断保存在 bucket 中的引用计数是否合理, 如何合理的话则将原有的数据减去RC_HALF并重新保存. 点击 step out 返回之前的函数

  • 代码块1的作用是将从 sidetable 取回了保存着的一部分引用计数, -1 保存到 isa 指针里面
  • 如果代码块1保存失败, 则用代码块2重新保存一次
  • 如果代码块2保存失败了, 则将从 sidetable 取出的引用计数重新放回去. 然后又跳回到 retry 部分
  • 保存成功则解锁, 并返回 false. 返回 true 表示这个对象需要 dealloc 了

另一种情况, 如果一个对象 isa 引用计数为 0, 且没有额外的引用计数. 那么它从 sidetable ‘借来的’ borrowed 为 0, 那么会将 isa 的 deallocating 赋值为 1, 并随后对其发送 delloc 消息进行内存消耗

最后

以上就是此次对引用计数的探究, 欢迎留言告诉我错的或者不明白的地方~

深入了解Tagged Pointer

objc 源码版本:779.1
当然还是推荐使用这个来学习:可编译的源码

在 2013 年苹果推出了首个使用 64 位架构的双核处理器的手机 iphone 5s。为了节约内存以及提高执行效率,苹果使用了一种叫做 ‘Tagged Pointer’ 的技术,现在跟着我来了解一下它吧。

Tagged Pointer

从 5s 开始,iPhone 均使用 arm64 指令集的处理器。在 64 位系统上,一个指针占 8 个字节,而指针指向的实例变量至少需要 16 个字节,并且还需要执行额外的一些操作,例如:申请内存,销毁内存。为了达到优化的目的,苹果将一些存储数据的类,例如 NSString,NSNumber,当它们需要保存的数据不需要占用那么多的字节时,直接将数据保存在“指针”里面。

下面让我们用代码来证实Tagged Pointer的存在

1
NSNumber *a = [NSNumber numberWithInt:1];

然后打个断点,使用 lldb 的命令调试,x/8xg a,该命令的意思是从a的起始地址开始,打印 8 个 16进制的 8字节长度的值
输出结果

说明指针 a 并不是指向 NSNumber 实例的指针。

或者下面这样更加直观一点

可以看到 a 并没有 isa 指针,所以它并不是一个 NSNumber 实例指针。

Tagged Pointer 如何存储数据

这里你最好打开源码对照着看。

LSB

在非 arm64 架构中,将最低位即 LSB 设置为 1,与正常的指针进行区分。
这样做的原因是,OC 类在创建实例最终调用的是 C 标准库中的 calloc 函数,它所返回的内存地址是 16 字节对齐的,参考 Aligned memory management?。这样的结果就是指针地址低 4 位都是 0,用最低位来表示也合理的

非 arm64 架构下标记位的设定

MSB

在 arm64 架构中,将最高位即 MSB 设置为 1,与正常的指针进行区分。
这样做的原因的是因为在 arm64 架构中,指针只用了低位的 48 位,高 16 位都是空着的,原因可以看下这个 为什么64位机指针只用48个位?

objc-internal.h 372 行以及 384 行,我们 我们可以看到掩码 _OBJC_TAG_MASK 的定义:

1
2
3
4
5
6
7
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
# define _OBJC_TAG_INDEX_SHIFT 60
#else
# define _OBJC_TAG_MASK 1UL
# define _OBJC_TAG_INDEX_SHIFT 1
#endif

内置类型和扩展类型

系统启动时生成两个全局的数组 objc_debug_taggedpointer_classes 和 objc_debug_taggedpointer_ext_classes。一个用来存储系统内置的Tagged Pointer类型,而另一个数组存储扩展的Tagged Pointer类型。

1
2
3
4
extern "C" { 
extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT];
extern Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
}
  • 内置类型数组的数目为 8,使用高 2 - 高 4 的 3 个 bit 来存储数据
  • 扩展类型数组的数目为 256,,使用高 5 - 高 12 的 8 个 bit 来存储数据

是否剩余的 bit 都用来保存数据了呢?答案是否定的,拿 NSUmber 举个例子,它属于内置类型,其 tagged pointer 额外使用 低 1 - 低 4 的 5 个 bit 用来保存数字的类型信息,即使用 56 个 bit 来保存数据。

内置类型如下:

1
2
3
4
5
6
7
8
9
10
11
enum
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
}

而当指针的高 1 - 高 4 的 4 个 bit 位均为 1 时,表示这是一个扩展类型。系统自带的扩展类型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum
{
// ...
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
// ...
}

数据混淆

运行下面的代码

1
2
3
4
5
6
7
8
9
10
11
- (void)boo
{
NSNumber *a = [NSNumber numberWithInt:1];
NSNumber *b = [NSNumber numberWithInt:2];
NSNumber *c = [NSNumber numberWithInt:16];

NSLog(@"pointer a is %lx", a);
NSLog(@"pointer b is %lx", b);
NSLog(@"pointer c is %lx", c);
NSLog(@"pointer d is %lx", d);
}

输出结果:

1
2
3
pointer a is ef59c3d36981ed4b
pointer b is ef59c3d36981ed7b
pointer c is ef59c3d36981ec5b

等等,不是说 Tagged Pointer 最高 4 位用来保存类型信息,剩下的几位都只用来保存数据嘛,为什么输出结果看起来这么复杂呢?
原因是从 iOS12 开始,为了系统安全,将数据与一个随机数进行 异或(^) 操作进行混淆,混淆函数如下:

1
2
3
4
5
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

objc_debug_taggedpointer_obfuscator是一个extern关键字的常量,因为被 extern 声明,所以我们可以用下面的代码来解码,得到真正的值

1
2
3
4
5
6
7
8
9
10
11
12
extern uintptr_t objc_debug_taggedpointer_obfuscator;

- (void)foo
{
NSNumber *a = [NSNumber numberWithInt:1];
NSNumber *b = [NSNumber numberWithInt:2];
NSNumber *c = [NSNumber numberWithInt:16];

NSLog(@"pointer a real value is %lx", ((uintptr_t)a ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer b real value is %lx", ((uintptr_t)b ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer c real value is %lx", ((uintptr_t)c ^ objc_debug_taggedpointer_obfuscator));
}

输出结果:

1
2
3
pointer a real value is b000000000000012
pointer b real value is b000000000000022
pointer c real value is b000000000000102

从输出结果可以看出,这几个值都是 0Xb 开头,16进制的 b 用 二级制表示为 1011,最高1用来表示这是一个 Tagged Pointer,而剩余 3 位的10进制数为 3,符合之前的定义OBJC_TAG_NSNumber = 3
至于为什么结尾都是 0x2,这个后面再解释。
下面让我们测试下 NSNumber 的 Tagged Pointer 使用多少位来保存数据。从之前的探究我们知道内置类型用 60 位来保存数据,而经过上面的实验我们可以看到还有 4 位用来做别的事了,那么是否剩余的 56 位都用来保存数据了呢?

1
2
3
4
5
- (void)foo
{
NSNumber *d = [NSNumber numberWithLongLong:-0x7FFFFFFFFFFFFF];
NSLog(@"pointer a real value is %lx", ((uintptr_t)d ^ objc_debug_taggedpointer_obfuscator));
}

输出结果:

1
pointer d real value is b800000000000013

结果符合预期。至于为什么最高位 b 后面的数字是 8,是因为高位 5 的位置变成了 1,用来表示这个数是负数(0则表示正数), 而 7 的二进制表示为 ob111,高位 5-8 连起来就是 0b1111,也就是16进制的 8 了。
还一个值得注意的是低位第一位的数字变成了 3,而不是之前的正整数 2,由此我们可以推测最低位的 4 位是用来表示存储数据类型的数据,例如 int,float,bool 这几个类型生成的 Tagged Pointer 最低4位数字应该是不同的。用下面的代表再来验证下:

1
2
3
4
5
6
7
8
9
NSNumber *a = [NSNumber numberWithInt:1];
NSNumber *b = [NSNumber numberWithShort:2];
NSNumber *c = [NSNumber numberWithFloat:1.];
NSNumber *d = [NSNumber numberWithLongLong:-0x7FFFFFFFFFFFFF];

NSLog(@"pointer a real value is %lx", ((uintptr_t)a ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer b real value is %lx", ((uintptr_t)b ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer c real value is %lx", ((uintptr_t)c ^ objc_debug_taggedpointer_obfuscator));
NSLog(@"pointer d real value is %lx", ((uintptr_t)d ^ objc_debug_taggedpointer_obfuscator));

输出结果

1
2
3
4
pointer a real value is b000000000000012
pointer b real value is b000000000000021
pointer c real value is b000000000000014
pointer d real value is b800000000000013

结果符合预期,说明我们的推测是正确的

参考

NSNumber 与 Tagged Pointer
深入解构 objc_msgSend 函数的实现

希望大家看了有所收获吧。

iOS应用 main 执行前发生的事情

这篇是对 iOS 应用启动时,main 函数执行前发生的事的一点总结,限于水平,如有错误请指正~

FAT 二进制

FAT 二进制文件,将多种架构的 Mach-O 文件合并而成。它通过 Fat Header 来记录不同架构在文件中的偏移量,Fat Header 占一页(64位16kb,32位4kb)的空间。
按分页来存储这些 segement 和 header 会浪费空间,但这有利于虚拟内存的实现。

Mach-O 文件

Mach-O为 Mach Object 文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。
在 Mac OS X 系统中使用 Mach-O 作为其可执行文件类型。
它的组成结构如下图所示:

Mach-O 文件结构

每个 Mach-O 文件包括一个 Mach-O Header,然后是一系列的载入命令 load commands,再是一个或多个段(segment),每个段包括0到255个节(section)。Mach-O使用REL再定位格式控制对符号的引用。Mach-O在两级命名空间中将每个符号编码成“对象-符号名”对(所以需要保持 selector 的唯一),在查找符号时则采用线性搜索法。

Mach-O包含了几个 segment,每个 segment 又包含几个 section。segment的名字都是大写的,例如__DATA;section的名字都是小写的, 例如 __text。在 Mach-O 的类型不为MH_OBJECT时,空间大小为页的整数倍。页的大小跟硬件有关,在 arm64 架构一页是16kb,其余为4kb。
section 虽然没有整数倍页大小的限制,但是 section 之间不会有重叠。

Mach-O Header

推荐使用MachOView这个软件查看 Mach-O 的文件结构。注意需要手动关闭 processing,不然会闪退。下面是用 MachOView 查看自己的应用结构:
使用MachOView查看文件结构

东西有点多就没有截取全部。我们查看一下Mach-O Header部分
Header结构

下面是64位架构下header的数据结构:

1
2
3
4
5
6
7
8
9
10
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};

image

Mach-O 全部的 filetype 和 flags 可以在loader.h中找到。


除了MH_OBJECT以外的所有类型,段(Segment)的空间大小均为页的整数倍。页的大小跟硬件有关系,在 arm64 架构下一页为16kb,其它为4kb。

Load commands

Load commands紧跟在头部之后, 当加载过 header 之后,会通过解析Load commands来加载剩下的数据,确定其内存的分布。
下面是 load commands 的结构定义:

1
2
3
4
struct load_command {
uint32_t cmd; /* 载入命令类型 */
uint32_t cmdsize; /* total size of command in bytes */
};

所有load commands的大小即为 Header->sizeofcmds, 共有 Header->ncmds 条load command
load command 以LC开头,不同的加载命令有不同的专有的结构体,cmd 和 cmdsize 是都有的,分别为命令类型(即命令名称),这条命令的长度。这些加载命令告诉系统应该如何处理后面的二进制数据,对系统内核加载器和动态链接器起指导作用。如果当前 LC_SEGMENT 包含 section,那么 section 的结构体紧跟在 LC_SEGMENT 的结构体之后,所占字节数由 SEGMENT 的 cmdsize 字段给出。

cmd(命令名称) 作用
LC_SEGMENT_64 将对应的段中的数据加载并映射到进程的内存空间去
LC_SYMTAB 符号表信息
LC_DYSYMTAB 动态符号表信息
LC_LOAD_DYLINKER 启动动态加载连接器/usr/lib/dyld程序
LC_UUID 唯一的 UUID,标示该二进制文件,128bit
LC_VERSION_MIN_IPHONEOS/MACOSX 要求的最低系统版本(Xcode中的Deployment Target)
LC_MAIN 设置程序主线程的入口地址和栈大小
LC_ENCRYPTION_INFO 加密信息
LC_LOAD_DYLIB 加载的动态库,包括动态库地址、名称、版本号等
LC_FUNCTION_STARTS 函数地址起始表
LC_CODE_SIGNATURE 代码签名信息

注意:不同类型的 segment 会使用不同的函数来加载

Segment

Mach-O 文件中由许多个段(Segment),每个段都有不同的功能,每个段包含了许多个小的Section。LC_SEGMENT意味着这部分文件需要映射到进程的地址空间去,几乎所有 Mach-O 都包含这三个段:

  1. __TEXT:包含了执行代码和其它只读数据(如C 字符串)。权限:只读(VM_PROT_READ),可执行(VM_PROT_EXECUTE)
  2. __DATA:程序数据,包含全局变量,静态变量等。权限:可读写(VM_PROT_WRITE/READ) 可执行(VM_PROT_EXECUTE)
  3. __LINKEDIT:包含了加载程序的”元数据”,比如函数的名称和地址。权限:只读(VM_PROT_READ)

除了上面三个,还有一个常见的 segment:

  • __PAGEZERO:空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对 NULL 指针的引用

LC_SEGMENT_64 的结构定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};

可以看到这里大部分的成员变量都是帮助内核将 segment 映射到虚拟内存的。nsects即表明该段中包含多少个 section,section是具体数据存放的地方。cmdsize表示当前 segment 结构体以及它所包含的所有 section 结构体的总大小。

文件映射的起始位置由fileoff给出,映射到地址空间的vmaddr处。

Section

section 的名字均为小写。section 是具体数据存放的地方,它的结构体跟随在 LC_SEGMENT 结构体之后。在64位环境中它的结构定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};

其中flasg字段储存了两个属性的值:type 和 attributes。type 只能有一个值,而 attributes 的值可以有多个。如果 segment 中任何一个 section 拥有属性 S_ATTR_DEBUG,那么该段所有的 section 都会拥有这个属性。属性详情可以参考loader.h

section name 作用
__text 主程序代码
__stub_helper 用于动态链接的存根
__symbolstub1 用于动态链接的存根
__objc_methname Objective-C 的方法名
__objc_classname Objective-C 的类名
__cstring 硬编码的字符串
__lazy_symbol 懒加载,延迟加载节,通过 dyld_stub_binder 辅助链接
_got 存储引用符号的实际地址,类似于动态符号表
__nl_symbol_ptr 非延迟加载节
__mod_init_func 初始化的全局函数地址,在 main 之前被调用
__mod_term_func 结束函数地址
__cfstring Core Foundation 用到的字符串(OC字符串)
__objc_clsslist Objective-C 的类列表
__objc_nlclslist Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行
__objc_const Objective-C 的常量
__data 初始化的可变的变量
__bss 未初始化的静态变量

虚拟内存

在 segment 的结构体中,我们可以看到vmaddrvmsize两个成员变量,它们分别代表 segment 在虚拟内存中的地址以及大小。

虚拟内存就是一层间接寻址(indirection)。软件工程中有句格言就是任何问题都能通过添加一个间接层来解决。虚拟内存解决的是管理所有进程使用物理 RAM 的问题。通过添加间接层来让每个进程使用逻辑地址空间,它可以映射到 RAM 上的某个物理页上。这种映射不是一对一的,逻辑地址可能映射不到 RAM 上,也可能有多个逻辑地址映射到同一个物理 RAM 上。针对第一种情况,当进程要存储逻辑地址内容时会触发 page fault;第二种情况就是多进程共享内存。

对于文件可以不用一次性读入整个文件,可以使用分页映射(mmap())的方式读取。也就是把文件某个片段映射到进程逻辑内存的某个页上。当某个想要读取的页没有在内存中,就会触发 page fault,内核只会读入那一页,实现文件的懒加载。

也就是说 Mach-O 文件中的__TEXT段可以映射到多个进程,并可以懒加载,且进程之间共享内存。__DATA段是可读写的。这里使用到了Copy-On-Write技术,简称 COW。也就是多个进程共享一页内存空间时,一旦有进程要做写操作,它会先将这页内存内容复制一份出来,然后重新映射逻辑地址到新的 RAM 页上。也就是这个进程自己拥有了那页内存的拷贝。这就涉及到了 clean/dirty page 的概念。dirty page 含有进程自己的信息,而 clean page 可以被内核重新生成(重新读磁盘)。所以 dirty page 的代价大于 clean page。

在多个进程加载 Mach-O 文件时__TEXT__LINKEDIT因为只读,都是可以共享内存的。而__DATA因为可读写,就会产生 dirty page。当 dyld 执行结束后,__LINKEDIT就没用了,对应的内存页会被回收。

dyld

dyld

dyld(the dynamic link editor),Apple 的动态链接器。在内核完成映射进程的工作后会启动dyld,负责加载应用依赖的所有动态链接库,准备好运行所需的一切。
在 App 启动的时候,首先会加载 App 的 mach-o 文件,从 load commands 中得到 dyld 的路径,并且运行。随后 dyld 做的事情顺序概括如下:

  1. 初始化运行环境
  2. 加载主程序执行文件 生成 image, 将image添加到一个全局容器中
  3. 加载共享缓存
  4. 根据依赖链递归加载动态链接库 dylib,如果在缓存中有加载好的 image 则直接拿出来,否则生成一个新的 image,将image添加到一个全局容器中
  5. link 主执行文件
  6. link dylib
    • 根据依赖链递归修正指针 Rebase
    • 根据依赖链递归符号绑定 Bind
  7. 初始化 dylib(runtime 的初始化就在这个时候)

在加载完所有的 dylib 之后,它们处于互相独立的状态,所以还需要将它们绑定起来。代码签名让我们不能修改指令,所以不能直接让一个 dylib 调用另一个 dylib,这时需要很多间接层。
这个时候需要 dyld 来修正指针(rebasing)和绑定符号(binding)。

详细可以查看 dyld 的源码中的_main函数。
下面会分析上述的其中几个步骤。

ImageLoader

ImageLoader是一个将 mach-o 文件里面二进制数据(编译过的代码、符号等)加载到内存的基类,它负责将 mach-o 中的二进制数据映射到内存,它的实例就是我们熟悉的 image。
每一个 mach-o 文件都会有一个对应的 image,实例的类型根据 mach-o 格式的不同也会不同。

  • ImageLoaderMachOClassic: 用于加载__LINKEDIT段为传统格式的 mach-o 文件
  • ImageLoaderMachOCompressed: 用于加载__LINKEDIT段为压缩格式的 mach-o 文件

因为dylib之间有依赖关系,所以系统会沿着依赖链递归加载 image。

Rebasing

dylib的二进制数据会随机的映射到内存的一个随机地址ASLR(Address space layout randomization,)中,这个随机的地址跟代码和数据指向的旧地址(preferred_address)会有一定的偏差,dyld需要修正这个偏差(slide),做法就是将dylib内部的指针地址都加上这个偏移值,偏移值的计算方法如下:

slide = actual_address - preferred_address

随后就是不断的将__DATA段中需要修正的指针加上这个偏移值。
注意:每次程序启动后的地址都会变化,所以指针的地址都需要重新修正。

在 mach-o 的一个载入命令LC_DYLD_INFO_ONLY可以查看到rebase, bind, week_bind,lazy_bind的偏移量和大小。

Binding

binding处理那些指向dylib外部的指针,它们实际上被符号名称(symbol)绑定,也就是个字符串。比如我们 objc 代码中需要使用到 NSObject, 即符号_OBJC_CLASS_$_NSObject,但是这个符号不存在当前的 image 中,而是在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起。

Lazy Binding

lazyBinding就是在加载动态库的时候不会立即 binding, 当时当第一次调用这个方法的时候再实施 binding。 做到的方法也很简单: 通过dyld_stub_binder这个符号来做。lazy binding 的方法第一次会调用到 dyld_stub_binder, 然后 dyld_stub_binder负责找到真实的方法,并且将地址bind到桩上,下一次就不用再bind了。
多数符号都是 lazy binding 的

Runtime

每一个dylib都有自己的初始化方法,当相应的 image 被加载到内存后,就会调用初始化方法。当然这不是调用名为initialize方法,而是C++静态对象初始化构造器,__attribute__((constructor))标记的方法以及Initializer方法。你可以在程序中设置环境变量DYLD_PRINT_INITIALIZERS来打印dylib的初始化方法。

打印信息

我们可以看到程序首先调用了libSystem这个dylib的初始化方法。libSystem是很多系统的lib的集合,包括 libdispatch(GCD), libsystem_c(c语言库), libsystem_blocks(block)。
libSystem的源码init.c中我们可以看到,它的初始化方法libSystem_initializer会调用libdispatch_init();, 然后逐步调用到_objc_init,也就是 objc 和 runtime 的入口。
添加一个符号断点_objc_init,下面是方法调用栈:

断点调试

注意:runtime 和 objc 属于libobjc


下面是_objc_init的实现:

1
2
3
4
5
void _objc_init(void)
{
// 省略...
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

上面的map_images不是将 image 添加到内存中的意思,在这个方法被调用的时候,已经完成了 image 的映射以及指针修正,绑定符号的工作了。
这个函数实际上是将 image 中 OBJC 相关的信息进行初始化,具体实现可以查看_read_image的源码,因为代码太多所以这里就不贴出来了,下面是具体做的事情:

  • 会将所有的 Class 存放在一张映射类名与 Class 的全局表中gdb_objc_realized_classes
  • 随后调用readClass函数将 每一个 Class 添加到gdb_objc_realized_classes表中。
  • 确定 selector 是唯一的
  • read protocols: 读取protocol
  • realizeClasses:这一步的意义就是动态链接好class, 让class处于可用状态,主要操作如下:
    • 检查ro是否已经替换为rw,没有就替换一下。
    • 检查类的父类和metaClass是否已经realize,没有就先把它们先realize
    • 重新layout ivar. 因为只有加载好了所有父类,才能确定ivar layout
    • 把一些flags从ro拷贝到rw
    • 链接class的 nextSiblingClass 链表
    • attach categories: 合并categories的method list、 properties、protocols到 class_rw_t 里面
  • read categories:读取类目

map_images结束会调用load_images函数。这一步做的事情比较少,就是调用我们熟悉的+(load)函数。父类会先调用,除了 Class,每个类目的+(load)方法也会被调用一次,但顺序就不一定了。

总结

在这里对 main 函数之前的操作做一个小总结吧:

  1. 将 App 的 mach-o header 读取到内存中
  2. 根据 load commands 获取 dyld 的路径,运行 dyld
  3. 初始化运行环境,加载 dylib,如果缓存中存在则从缓存中拿出加载过的 image,否则新建一个 image,加载到内存中
  4. 修正指针(rebase),绑定符号(bind)
  5. 初始化 dylib,运行 runtime
  6. runtime 将 image 中有关 OBJC 的数据进行初始化
  7. 调用 +(load) 方法
  8. dyld 调用 main 函数

花了一周的时间用来研究这部分的内容,终于填完坑了很舒服
最大的感受就是学习完后,看 clang 编译后的 C++ 代码能看懂的更多了。比如添加完一个类目之后,会将这个这个类目添加到__DATA的section __objc_catlist中,以前不知道啥意思现在就明白了。也明白 xcode 的许多设置是用来干嘛的,总之好处多多

学习也是一个递归的过程,加油跳出这个递归吧!

引用

iOS 程序 main 函数之前发生了什么
优化 App 的启动时间
dyld源码分析-动态加载load
dyld与ObjC

Objective-C类和对象的内存分布

之前在别人博客下面看到了一个问题,觉得挺有意思的。但是自己想回答的时候又发现好像有一些知识点还不是很熟悉,觉得有点迷糊,所以准备再研究一下底层再来回答问题。现在把这个坑填上吧。

OC对象的指针类型

Objective-C 是一门动态语言,而动态语言是在运行时确定数据类型,变量使用之前不需要类型声明。但是我们在写代码的时候还是要给对象一个类型或者使用id的,我自己觉得这么做是为了通过编译(例如声明了类型为NSObject的实例sark,却调用了方法foo,那么编译就通不过了)。
实际上动态语言的一个特性多态就是这么实现的,即用父类的指针指向子类的实例。

对象的内存分布

还是举个例子会明白一点。需要注意的是需要在模拟器上调试,在真机调试会有问题的。

1
2
3
4
5
6
7
8
9
10
11
@interface Father : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation ViewController
- (void)foo {
Father *father1 = [Father new];
father1.name = @"001";
id father2 = [Father new];
}
@end

调试之前,我们要明白几点常识。在计算机中每个字节都是有一个地址的,每个字节有8个bit,每个bit可以存储1或者0,这8个bit就是这个字节的值。在小端系统中,低位的值存储在低地址上。
使用 x 命令调试。格式:x/<n/f/u> <addr>

  • x 显示内存
  • n 正整数,表示需要显示的内存单元的个数
  • f 表示addr指向的内存内容的输出格式
    • s: 对应输出字符串
    • x: 按十六进制格式显示变量
    • d: 按十进制格式显示变量
    • c: 按字符格式显示变量
  • u 以多少个字节作为一个内存单元
    • b: 1 byte
    • h: 2 bytes
    • w: 4 bytes
    • g: 8 bytes

打断点,然后输入命令: x/8xg father1, 即:以8个字节为一个单元,从 father1 指针的地址开始起8个单元的值

1
2
3
4
5
6
7
8
9
10
11
(lldb) x/8xg father1
0x6000000128f0: 0x000000010be34050(Class) 0x000000010bdcc058(name)
0x600000012900: 0x00006000000128a0 0x0000000100000002
0x600000012910: 0x000000010f8f8e58 0x0000000000000000
0x600000012920: 0x0000000000000000 0x0000000000000000

(lldb) x/8xg father2
0x600000012490: 0x000000010be34050(Class) 0x0000000000000000(name)
0x6000000124a0: 0xbadd2dcdc19dbead 0x00006000000124f0
0x6000000124b0: 0x0000000000000000 0x0000000000000000
0x6000000124c0: 0x00007f8ae3c140c0 0x00006080000092b0

这里我提前将这些地址代表的意思标注好了。
father2虽然是id类型的,但是它跟father1第一个8字节所存储的地址是相同的,都是0x000000010be34050。其实这个地址就是 Father类的地址。我们可以使用下面的方法验证:

1
2
(lldb) po (Class)0x000000010be34050
Father

所以一个实例对象第一个8字节存储的是这个类的指针,那么后面的字节存储的是什么呢?答案是这个实例的成员变量,在上面的例子中我们给实例father1的成员变量name赋值了001, 现在让我们验证一下:

1
2
(lldb) po (id)0x000000010bdcc058
001

因为我们没有对father2的成员变量 name 赋值,所以这8个字节的值是空的。


打开 runtime 750版本源码,查看 id 和 Class 的定义

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
typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_object {
private:
isa_t isa;
}

union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

class_rw_t *data() {
return bits.data();
}
}
  1. id的定义很简单,是一个指向 objc_object 的指针,而 objc_object 只有一个私有成员变量 isa。objc_class 继承于 objc_object,所以你也可以用 id 来声明 Class 的变量,例如id foo = [NSObject class];
  2. isa是一个联合体,里面的 struct 在不同架构的CPU中定义是不同的。在 64 位CPU中,isa 可以用来存储更多的信息,例如引用计数,是否有关联对象等,可以看我的这篇博客Objective-C引用计数原理

使用clang rewrite-objc ViewController.m将代码转化成C++实现,可以看到 Father 这个类变成了如下的结构体

1
2
3
4
5
6
7
struct Father_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
struct NSObject_IMPL {
Class isa;
};

看到这个结构体你是不是就明白了为什么对象的内存分布是下图这个样子的?
需要注意的是,NSObject 的实例虽然理论上只有8个字节,但是它的实例实际上有 16 个字节,后面8个字节是空的。

实例内存分布图

研究到这里,我们就可以回答开头的那个问题了。

  1. 指针的类型是id类型,而指针指向的类型可以是别的类。因为 OC 是动态语言,变量的类型需要在运行时才能够确定。
  2. 指针保存的是对象内存的首地址
  3. 64位平台中,对象首地址开始的8个字节存储的是类的指针。也就是通过这个才能确定该类的类型

是不是很简单!下面继续让我们研究下 Class 的内存分布问题

Class的内存分布

让我们继续回到之前的代码调试。上一节中我们已经知道了Father类的地址了

1
2
3
4
5
6
7
8
9
(lldb) x/16xg 0x000000010be34050
0x10be34050: 0x000000010be34028(meta-class) 0x000000010f8f8e58(superClass)
0x10be34060: 0x00006000000972f0(bucket_t *_buckets) 0x0000000200000003(_mask & _occupied )
0x10be34070: 0x0000600000074302 0x000000010f8f8e08
0x10be34080: 0x000000010f8f8e08 0x000000010f548520
0x10be34090: 0x0000000000000000 0x000000010bdd7df0
0x10be340a0: 0x000000010be34078 0x000000010f8f8e58
0x10be340b0: 0x000000010f548520 0x0000000000000000
0x10be340c0: 0x000000010bdd7e38 0x000000010f8f8e08

PS: 注意不要使用真机来调试,因为我调试的时候发现跳不到那个内存地址中,但在模拟器中没这个问题…

配套的我们把 objc_class 的定义放到下面。

1
2
3
4
5
6
7
8
9
10
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

class_rw_t *data() {
return bits.data();
}
}

因为 objc_class 继承于 objc_object,所以 Class 的第一个8字节还是 isa 指针,也就是一个指向元类(meta-Class)的指针。如果你不知道元类是什么意思的话就去百度,我也懒得讲了。第2个8字节储存的是指向父类的指针。先让我们验证一下

1
2
3
4
5
lldb) po (Class)0x000000010be34028
Father

(lldb) po (Class)0x000000010f8f8e58
NSObject

结论正确。让我们接着看cache_t的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
}

cache_t关系到方法查找的缓存。当对实例发送消息后,会先到Class的缓存中查找有没有该方法的缓存,如果有则直接调用方法的实现,提高效率。
大致可以看出,bucket_t是一个哈希表,根据_key找到其映射的方法实现_imp,而_key就是 SEL(方法的名字 const char *)。cache_t是中的_mask_occupied是两个4字节的变量,应该代表的是缓存的数量。所以,Class 第三个8字节存储的是bucket_t *类型的指针,第4个8字节保存的是 _mask 和 _occupied。因为是小端,低位地址存储低位的数据,所以 _mask 的值是0x00000003,而 _occupied 的值是0x00000002

接下来看 Class 的第3个成员变量class_data_bits_t bits;

1
2
3
4
5
6
7
8
9
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;

public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}

在64位下,uintptr_t 为8个字节。class_data_bits_t 的公共方法有很多,主要是配合掩码进行一些读写操作。
继续看class_rw_t的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;

#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
};

在结构体中,你可以看到有一个成员变量的类型是class_ro_t,是不是很像class_rw_t。从字面意思上可以猜测,一个是readwriite,一个是readonly。因为 OC 是动态语言,可以在运行时添加方法和成员变量,运行时添加的方法或者成员变量就是添加到class_rw_t上的,而class_ro_t存储的是一些编译后Class的信息。
class_data_bits_t的定义中,我们知道了需要掩码FAST_DATA_MASK才能得到 class_rw_t 的地址。下面是 class_rw_t的内存分布

1
2
3
4
5
6
7
8
9
// 得到class_rw_t的内存地址
0x0000600000074302 & 0x00007ffffffffff8 = 0x600000074300;

(lldb) x/16xg 0x600000074300
0x600000074300: 0x00000000800a0000(flags & version) 0x000000010bdd7da8(ro)
0x600000074310: 0x000000010bdd7d18(methods) 0x000000010bdd7d90(properties)
0x600000074320: 0x0000000000000000(protocols) 0x000000010be33f60(firstSubclass)
0x600000074330: 0x000000010ee88c68(nextSiblingClass) 0x0000000000000000(demangledName)
0x600000074340: 0xbadd2dcdc19dbead 0x0000600000074240

因为在代码中我还声明了一个 Father 的子类 Son,没想到在这里出现,没错,就是这个 firstSubclass。至于如果有多个子类,确定哪个是 firstSubclass 我就不清楚了。。。

1
2
(lldb) po (Class)0x000000010be33f60
Son

再来看一下class_ro_t的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}

然后是它的内存分布:

1
2
3
4
5
6
(lldb) x/16xg 0x000000010bdd7da8
0x10bdd7da8: 0x0000000800000184(flags & instanceStart) 0x0000000000000010(instanceSize & reserved)
0x10bdd7db8: 0x000000010bd3ea79(ivarLayout) 0x000000010bd3eafc(name)
0x10bdd7dc8: 0x000000010bdd7d18(baseMethodList) 0x0000000000000000(baseProtocols)
0x10bdd7dd8: 0x000000010bdd7d68(ivars) 0x0000000000000000(weakIvarLayout)
0x10bdd7de8: 0x000000010bdd7d90(baseProperties) 0x0000002800000081
  1. 可以看到 ro 的成员变量中有instanceStartinstanceSize。这两个值的作用是非脆弱成员变量。即如果基类如果增加了成员变量,不需要重新编译,只需要在初始化系统自动修改instanceStartinstanceSize的值,就能够继续使用子类。具体你可以看我的这篇博客 谈Objective-C类成员变量
  2. ivarLayout 记录了那些是 storng 的ivar
  3. name 存储的是这个类的名字,你可以使用po (char *)0x000000010bd3eafc打印该名字
  4. ivars 存储的是该类的成员变量(不包括关联对象)
  5. weakIvarLayout 记录了哪一些是 weak 的ivar

还可以看到 ro 的baseMethodList和rw的methods的地址都是0x000000010bdd7d18,ro 的baseProperties和rw的properties的地址都是0x000000010bdd7d90

实际上 rw 的三个成员变量,methods, properties, protocols的类型都继承于list_array_tt,这个列表可能有以下3中值:1. 空值 2. 指向列表的指针 3. 指向列表的指针的数组。所以这就是为什么Class可以在类目中添加方法和协议,只需要在这个列表数组中再添加一个指向类目中方法和协议列表的指针就好了。
因为在这个实例中没有使用类目添加方法,所以rw中methods数组仅有一个值,这个值等于ro的baseMethodList。

先来研究methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
}

struct method_list_t {
uint32_t entsizeAndFlags;
uint32_t count;
method_t first;
}

(lldb) x/16xg 0x000000010bdd7d18
0x10bdd7d18: 0x000000030000001a(entsizeAndFlags & count) 0x000000010f547965(name)
0x10bdd7d28: 0x000000010bd41271(types) 0x000000010b7e01e0(imp)
0x10bdd7d38: 0x000000010fd3a28e(name) 0x000000010bd41284(types)
0x10bdd7d48: 0x000000010b7e0180(imp) 0x0000000112f11912(name)
0x10bdd7d58: 0x000000010bd4128c(types) 0x000000010b7e01a0(imp)
0x10bdd7d68: 0x0000000100000020 0x000000010be30c50
0x10bdd7d78: 0x000000010bd19fc8 0x000000010bd4130b
0x10bdd7d88: 0x0000000800000003 0x0000000100000010

entsizeAndFlags 第一个4字节保存的是 entsize 和标记, entsize 我的理解好像是method_t的长度。第二个4字节保存的是方法的数量,在上面的例子中我们可以知道一共保存了3个方法。后面保存了3个method_t的实例,每个实例占用了24个字节。每个 method_t 实例,第一个8字节为 sel,即方法名字;第二个8自己保存了方法的参数类型;第3个8字节是方法的函数指针。我们把上面保存的3个方法的信息按顺序打印出来

  • .cxx_destruct v16@0:8
  • name @16@0:8
  • setName: v24@0:8@16

第2和第3个方法比较好理解,系统为我们自动生成了属性 name 的 getter 和 setter 方法。
第1个方法cxx_destruct 的作用是在delloc时释放该类的成员变量的,具体你可以看这篇博客 探究ARC下dealloc实现

properties 与 methods 类似,因为继承与同一个结构体。这里简单分析一下,内存分布为 entsizeAndFlags(4字节), count(4字节),property_t数组。property_t里面有两个成员变量,一个是属性的名字,一个是属性的属性。。。


大致上这就是 Class 的内存分布了,下面这张图能够简要的概括了:

类的内存分布

引用

ObjectC对象内存布局分析

Objective-C引用计数原理

在 Objective-C 2.0 中,我们无需手动进行内存管理,因为ARC会自动帮我们在编译的时候,在合适的地方对对象进行retainrelease操作。
本文将结合runtime 750版本源码 探究 ARC 环境下引用计数的实现原理。

如何存储引用计数

从 5S 开始,iPhone 都采用了64位架构的处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念,专门用来存储小的对象,例如 NSNumber 和 NSDate。这一类变量本身的值需要占用的内存大小常常不需要8字节,拿整数来说,4个字节所能表示的有符号整数可以达到20多亿(2^31=2147483648,另外 1 位作为符号位),基本可以处理大多数情况。所以我们将一个对象的指针(64位下8字节)拆成两部分,一部分用来存储数据,另一部分作为特殊标记,表示这是一个 Tagged Pointer, 不指向任何一个地址。也就是当某些类使用 Tagged Pointer 来存储数据后,它就不是一个对象了,因为它并没有指向任何地址,变成了一个披着对象皮的普通变量而已,而对于这一类的‘对象’,它的内存是分配在中,由系统分配以及释放,所以它的引用计数也没有意义了,当然你仍然可以使用CFGetRetainCount方法去获取它的引用计数,返回的是它的指针地址。

而在某些平台中(比如arm64),isa 实例的一部分空间也会被用来存储引用计数,当引用计数超过一定值之后,runtime 会使用一张散列表(哈希表)来管理其引用计数;如果不使用 isa 存储引用计数则会直接存储到散列表中。

isa 指针

用64位(8字节)来存储一个内存地址显然是种浪费,于是可以将一部分的空间用来存储引用计数。当 isa 指针第一位为1时即表示使用优化的 isa 指针,这里列出64位环境下的 isa 结构:

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
union isa_t 
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

#if SUPPORT_NONPOINTER_ISA

# if __arm64__
# define ISA_MASK 0x00000001fffffff8ULL
# define ISA_MAGIC_MASK 0x000003fe00000001ULL
# define ISA_MAGIC_VALUE 0x000001a400000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
uintptr_t magic : 9;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};

// SUPPORT_NONPOINTER_ISA
#endif

};

SUPPORT_NONPOINTER_ISA表示是否支持在 isa 指针内添加额外的信息,例如引用计数,析构状态,被__weak变量引用的情况等。目前仅支持 arm64架构的设备支持。

变量名 含义
indexed 0 表示普通的 isa 指针,1 表示可以存储引用计数
has_assoc 表示该对象是否包含 associated object(关联对象)
has_cxx_dtor 表示该对象是否有 C++ 的析构函数
shiftcls 类的指针
magic 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化
weakly_referenced 表示该对象是否有过 weak 对象,如果没有,则析构时更快
deallocating 表示该对象是否正在析构
has_sidetable_rc 表示该对象的引用计数值是否过大无法存储在 isa 指针
extra_rc 存储引用计数值减一后的结果

在64位环境下,isa 会存储引用计数,当 has_sidetable_rc 的值为1时,那么溢出的引用计数将会存储在一张全局散列表中,也就是引用计数 = isa保存的引用计数 + 哈希表保存的引用计数 + 1。后面会详细讲到。

哈希表 DenseMap

1
2
3
4
5
6
7
8
9
10
11
12
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

template<typename KeyT, typename ValueT,
bool ZeroValuesArePurgeable = false,
typename KeyInfoT = DenseMapInfo<KeyT> >
class DenseMap
: public DenseMapBase<DenseMap<KeyT, ValueT, ZeroValuesArePurgeable, KeyInfoT>,
KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable>
{
// ...省略
}

runtime 使用 DenseMap 哈希表(也叫散列表,类似NSDictionary)的别名RefcountMap来存储引用计数。DenseMap 继承于 DenseMapBase 这个 C++ 类,通过观察 DenseMapBase 的内部实现我们可以发现以下几点:

  • 键 KeyT 的类型为DisguisedPtr<objc_object>,这个类是对objc_object *指针及其一些操作进行的封装,目的是不受内存泄漏工具leaks的检测
  • 值 ValueT 的类型为 size_t, size_t在64位环境下等同于 unsigned long。保存的值等于引用计数减一
  • 模板的 KeyInfoT 类型为 DenseMapInfo,在这里等同于DenseMapInfo<DisguisedPtr>。DenseMapInfo 封装了比较重要的方法,用于在哈希表中查找 key 映射的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
struct DenseMapInfo<T*> {
static inline T* getEmptyKey() {
uintptr_t Val = static_cast<uintptr_t>(-1);
return reinterpret_cast<T*>(Val);
}
static inline T* getTombstoneKey() {
uintptr_t Val = static_cast<uintptr_t>(-2);
return reinterpret_cast<T*>(Val);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const T *LHS, const T *RHS) { return LHS == RHS; }
};

指针哈希算法实现:

1
2
3
4
5
6
7
8
9
#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}
#endif

虽然不完美,但是速度很快(注释说的。。。)


简单来讲,DenseMap 通过对象的指针地址来映射其引用计数

SideTable

1
2
3
4
5
6
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
// ...省略
}

介绍完存储引用计数的哈希表,那么这个哈希表是存储在哪里的呢?
答案是保存在一个叫做SideTable的结构体中,通过观察它的结构组成,我们可以可以看到有三个成员变量slock, refcntsweak_table

  • slock是一个自旋锁,保证线程安全
  • refcnts的类型是 RefcountMap,也就是上一节提到过的 DenseMap 类型的别名。用来保存引用计数
  • weak_table用来保存__weak修饰的指针。当一个对象 delloc 时,通过这个表将这些指向要释放对象的用__weak修饰的指针置为nil,避免野指针的情况出现。

StripedMap

知道引用计数的哈希表是保存在SideTable中,那么SideTable实例保存在哪里呢?
答案是在一个全局的StripedMap<SideTable *>类型的静态变量SideTableBuf

1
2
3
4
5
6
7
8
9
alignas(StripedMap<SideTable>) static uint8_t 
SideTableBuf[sizeof(StripedMap<SideTable>)];

static void SideTableInit() {
new (SideTableBuf) StripedMap<SideTable>();
}
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

之所以在初始化时将 SideTableBuf 定义成 uint8_t 是因为方便计算内存大小,在SideTables()方法中我们可以看到SideTableBuf会被强制转换成StripedMap<SideTable>*类型。实际上 SideTableBuf 也是哈希表,根据指针地址映射到相应的SideTable类型的变量。下面是StripedMap这个类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];

static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
// ...省略
}

StripedMap中有一个PaddedT类型的数组array,在模拟器中容量为64,在真机中为8。PaddedT结构体大小为64个字节,其成员变量 value 的类型实际是我们之前传入 SideTable。当系统调用SideTable& table = SideTables()[]时首先会执行SideTables()得到SideTableBuf, 然后在StripedMap中执行T& operator[] (const void *p)方法获取相应的SideTable。

1
2
3
4
5
6
7
8
T& operator[] (const void *p) { 
return array[indexForPointer(p)].value;
}

static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

indexForPointer()函数中返回相应 SideTable 的index。(addr >> 4) ^ (addr >> 9)这一步我也不是很懂,应该是类似于产生一个随机数,后面的% StripeCount返回一个 [0, StripeCount)的数,也就是相应 SideTable 的index。所以一个 SideTable 应该是对应许多的对象的。


保存引用计数的哈希表保存在SideTable结构体中,而SideTable保存在一个全局的静态变量StripedMap<SideTable> SideTableBuf中。在真机下,SideTableBuf能够储存8个SideTable实例。StripedMap的方法indexForPointer()通过对象的指针计算出相应 SideTable 的 index。一个 SideTable 对应多个对象

获取引用计数

在 ARC 环境下我们可以使用方法CFGetRetainCount得到对象的引用计数。在 runtime 中,通过调用objc_objectrootRetainCount()获取引用计数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
inline uintptr_t 
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}

sidetable_unlock();
return sidetable_retainCount();
}
  1. isTaggedPointer在前面我们已经分析过了如果是Tagged Pointer类型的对象时是怎么样的。此时对象在栈中分配,由系统自动销毁内存(先进后出),所以此时对它求引用计数返回其地址。
    下面让我们重点看一下sidetable_retainCount()这个方法
  2. 当 isa 的 nonpointer = 1 的情况我们开头也分析过了,此时 isa 指针也用来存储引用计数,如果引用计数溢出则将溢出部分存储在哈希表中
  3. 下面让我们研究一下不使用isa优化是怎么从哈希表中获取引用计数的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];

size_t refcnt_result = 1;

table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
  1. 首先得到 SideTable 实例。
  2. 成员变量 refcnts 就是之前说的保存引用计数的哈希表,在哈希表中根据指针值查找引用计数。
  3. it->second >> SIDE_TABLE_RC_SHIFT 注意result从第三位才开始保存数据,所以需要将数据向右移动2位才能取到引用计数。第1位用来保存该对象是否被用__weak修饰的变量引用,第2位用来表示该对象是否正在析构
  4. 将右移后得到的数+1(refcnt_result)后返回。这也是为什么之前说哈希表保存的引用计数是实际值 -1 之后的值的原因。

Retain

在非 ARC 环境中可以使用retainrelease方法对引用计数进行加减操作,在 ARC 环境中我们无需也无法使用这两个方法操作引用计数,但是你可以使用CFRetain()对对象进行 retain 操作。最终会调用 objc_objectrootRetain方法

1
2
3
4
5
6
7
8
9
inline id 
objc_object::rootRetain()
{
assert(!UseGC);

if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}

类似于上一节中获取引用计数的方法,当对象属于Tagged Pointer时则返回该对象。所以我们接着看sidetable_retain()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();

return (id)this;
}

首先得到 SideTable 实例。从实例中得到存储引用技术的哈希表refcnts,在哈希表中根据对象的地址找到对应的引用计数refcntStorage,判断引用计数的值是否有溢出,如果没有则对引用计数 + 1,返回对象。
上一节我们讲过 refcntStorage 中第三位才开始用来存储引用计数,所以读数时需要先往右边移动两位,那为什么这里的代码没有呢?

1
2
3
4
5
6
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2

注意观察SIDE_TABLE_RC_ONE的定义,是一个8字节的 unsigned long 类型,值为1,向左偏移了两位。refcntStorage += SIDE_TABLE_RC_ONE两者相加的话则直接从第三位开始相加了,所以可以使用 SIDE_TABLE_RC_ONE 对引用计数进行 +1 和 -1 操作。
同样的,在上面的代码中, SIDE_TABLE_RC_PINNED用来判断引用计数值是否有溢出。

Release

release 最终会调用 objc_object的方法rootRelease()

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
inline bool 
objc_object::rootRelease()
{
if (isTaggedPointer()) return false;
return sidetable_release(true);
}

uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

bool do_dealloc = false;

table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}

在这个方法你可以知道为什么哈希表中保存的引用计数是实际值 -1 之后的值。
it->second < SIDE_TABLE_DEALLOCATING用来判断保存的引用计数值是否小于1,如果小于1的话则对该值标记为正在析构:it->second |= SIDE_TABLE_DEALLOCATING;,并且在随后对该对象发送 delloc 消息。
举个例子,一个对象 sark,实际的引用计数为1,在哈希表中保存的值为0,当这个对象进行release操作后,sark 的引用计数变成了0,也就是需要进行销毁操作了。而到了该方法中,会判断保存的引用计数的值是否小于1,如果是的话则进行 delloc 操作,并且将哈希表中存储的值标记为正在析构状态。而 sark 原先保存着的引用计数值就是 =0,这样设计避免了在哈希表存储的引用计数出现负数的情况。

alloc,new, copy 和 mutableCopy

copy 以及 mutableCopyNSCopyingNSMutableCopying 协议上的方法,需要在各类上自己去实现copyWithZone: mutableCopyWithZone:方法。无论是深拷贝还是浅拷贝都会增加引用计数。

1
2
3
4
5
6
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
+ (id)alloc {
return _objc_rootAlloc(self);
}

[cls alloc]以及[cls allocWithZone:nil]方法最终会调用callAlloc()方法,所以 alloc 和 new 这两个方法后面都会调用callAlloc()这个方法,因为 Objective-C 2.0 忽视垃圾回收和 NSZone,那么后续的调用顺序依次是为:

1
2
3
4
callAlloc()
class_createInstance()
_class_createInstanceFromZone
calloc()

calloc()函数相比于malloc()函数的优点是它将分配的内存区域初始化为0,相当于malloc()后再用memset()方法初始化一遍。

单例

其实这一节是对上一节内容的补充。
记得我刚出来工作的时候,单例是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation Son
+ (instancetype)shareManager
{
static Son *son;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
son = [super allocWithZone:nil];
});
return son;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
return [self shareManager];
}
@end

当时组长问我为什么要这样子写(因为跟他们写的方式不一样),我也答不上来,因为这种代码都是直接google的。但是看了callAlloc()实现之后我明白为什么了。
在上一节我们已经知道了 alloc 和 new 都会接着调用callAlloc()

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
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif

// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}

如果类重载了allocWithZone方法,那么cls->ISA()->hasCustomAWZ()将会返回YES,也就是说当我们用alloc或者new创建实例的时候,就不会走系统的方法,而会走重载的allocWithZone方法了。我们在重载allocWithZone方法时返回[self shareManager](注意此时的self代表Son类), 因为shareManager方法返回的是一个静态变量。

还有一个需要注意的点就是在shareManager中,我们使用son = [super allocWithZone:nil];初始化实例,为什么不使用son = [[super alloc] init];来初始化呢?
代码中的[super alloc];在编译后会变成objc_msgSendSuper(objc_super super, @selector(alloc))(大致意思是这样)。其中objc_super是一个结构体,只有两个成员变量id receiverClass class,receiver 仍是 self(Son类), class 为 Father类。当我们想通过[super alloc]创建实例的时候,会从 Father类中查找 +alloc 方法,如果没有实现则在 NSObject 中查找 +alloc 方法。而方法里面的参数 self 仍旧为 Son 类而不是 Father 类,所以还是会去调用重载的allocWithZone方法,导致死循环。

引用

Objective-C 引用计数原理

谈Objective-C关联对象

#前言

前不久刚写了 谈Objective-C类成员变量 ,分析了成员变量的实现原理以及不能动态添加的原因,在这篇文章里我们来根据 objc4-646.tar.gz版本 源码来谈一下 Objective-C 关联对象的实现原理。

关联对象(Associated Objects)是 Objective-C 2.0运行时的一个特性,起始于OS X Snow Leopard和iOS 4。它允许开发者对已经存在的类在扩展中添加自定义的属性。相关参考可以查看 <objc/runtime.h> 中定义的三个允许你将任何键值在运行时关联到对象上的函数:

  • void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) 用于给对象添加关联属性,传入nil则移除已有的关联对象
  • id objc_getAssociatedObject(id object, const void *key) 用于获取关联属性
  • void objc_removeAssociatedObjects(id object) 移除一个对象所有的关联属性,但不建议手动调用这个函数,因为这可能会导致其它人对其添加的属性也被移除了。你可以调用objc_setAssociatedObject方法并传入nil来指定移除某个关联

下面分析一下 objc_setAssociatedObject 两个参数 keypolicy

#key

通常来说该属性应该是常量、唯一的,在getter和setter方法中都可以访问到。这里有两种常见的添加方式:

第一种是添加 static char 类型的变量,当然更推荐是指针型的。

1
2
3
4
5
static char kAssociatedObjectKey;
- (void)setMenber:(NSString *)menber {
objc_setAssociatedObject(self, &kAssociatedObjectKey, menber, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

当然更推荐的是使用更简单的方式实现:用 selector(getter方法):

1
2
3
- (void)setMenber:(NSString *)menber {
objc_setAssociatedObject(self, @selector(menber), menber, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

#关联策略 policy

关联策略跟属性修饰符的使用方法差不多,属性可以根据定义在 objc_AssociationPolicy 上的类型被关联到对象上:

关联策略 等价属性 说明
OBJC_ASSOCIATION_ASSIGN @property (assign)或 @property (unsafe_unretained) 弱引用关联对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 强引用关联对象,且为非原子操作
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 复制关联对象,且为非原子操作
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 强引用关联对象,且为原子操作
OBJC_ASSOCIATION_COPY @property (atomic, copy) 复制关联对象,且为原子操作

#关联对象实现

下面让我们具体来分析一下这几个函数的具体实现吧!

分析objc_setAssociatedObject实现

objc_setAssociatedObject的实现被定义在objc-auto.mm文件 467 行

1
2
3
4
5
6
7
8
9
10
GC_RESOLVER(objc_setAssociatedObject)

#define GC_RESOLVER(name) \
OBJC_EXPORT void *name##_resolver(void) __asm__("_" #name); \
void *name##_resolver(void) \
{ \
__asm__(".symbol_resolver _" #name); \
if (UseGC) return (void*)name##_gc; \
else return (void*)name##_non_gc; \
}
  • ## 符号: 连接宏。举个例子:#define COMMAND(A, B) A##B , int COMMAND(temp, Int) = 10 等同于 int tempInt = 10
  • UseGC 是否使用垃圾回收,在 iPhone 平台上被定义为 NO
    所以这个宏展开来为下面的代码
1
2
3
4
void GC_RESOLVER(name)                                 
{
return (void*)objc_setAssociatedObject_non_gc();
}

objc_setAssociatedObject_non_gc的实现在objc-runtime.m文件,再经过一些跳转,可以发现 objc_setAssociatedObject 最终会调用 _object_set_associative_reference方法 (objc-runtime.m 268行)

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
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
  • AssociationsManager manager;, 会创建一个AssociationsManager结构体的变量 manager,在调用它的构造函数时会上锁,调用析构函数时解锁。结构体内有一个静态变量 AssociationsHashMap, 懒加载该变量。
  • DISGUISE(object) 用来获取 object 的指针地址
  • AssociationsHashMap是一个无序的哈希表,维护了从对象地址到 ObjectAssociationMap 的映射
  • ObjectAssociationMap 是一个map,维护了从 key 到 ObjcAssociation 的映射
  • ObjcAssociation 是一个 C++ 类, 主要包括两个成员变量:uintptr_t _policy(关联策略) id _value(关联对象的值)

简单的讲解上面那个函数的流程:

  1. 新建一个 AssociationsManager 实例 manager,同时上锁。通过 manager 得到 AssociationsHashMap 关联哈希表 associations,通过 DISGUISE()函数得到 object 的指针 disguised_object。在哈希表 associations 中 根据 disguised_object 查找 ObjectAssociationMap,如果没有则新建一个 refs。
  2. 新建一个 ObjcAssociation 实例 new_association,存储在 refs 中
  3. 如果传入的value是nil,则在 refs 移除该映射关系
  4. 释放掉旧的 old_association
  5. 作用域结束释放掉 manager,解锁

添加关联对象流程图

分析objc_getAssociatedObject实现

按照上一节的流程,我们首先找到 objc_getAssociatedObject 的最终实现源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
}
return value;
}

代码量比上一节少了还挺多哈,过程也类似,就不讲的很细了

  1. 先得到 AssociationsHashMap 实例 associations(静态变量)。根据 object 的指针地址,在 associations 得到映射的 ObjectAssociationMap refs。
  2. 在 refs 根据 key 得到映射的 ObjcAssociation 实例 entry,在 entry 中可以得到成员变量 _value,也就是我们所关联属性的值。
  3. 根据关联策略 policy 进行相应的操作(autorelease, retain)后返回 value

分析objc_removeAssociatedObjects实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}

其实不看代码应该也能够猜出个大概了吧.

  1. 根据 object地址 找到映射的 refs,遍历 refs,将保存着的 value 保存在 vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements
  2. 删除 refs, 然后一个个的释放 elements 里面的值

#给类对象关联对象

看完源代码后,我们知道实例对象地址与 ObjectAssociationMap map是一一对应的。那么是否可以给类对象添加关联对象呢?
答案是可以,因为Class也是一个对象,我们完全可以用同样的方式给类对象添加关联对象,只不过我们一般情况下不会这样做,因为更多时候可以通过 static 变量来实现类级别的变量。

你可以通过下面的代码这样操作

1
2
3
4
5
6
7
8
9
10
11
12
@implementation NSObject (AssociatedObject)
+ (NSString *)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}
+ (void)setAssociatedObject:(NSString *)associatedObject {
objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

- (void) foo {
NSObject.associatedObject = @"associatedObject";
}

#何时释放关联对象

探究ARC下dealloc实现 中我们研究过,当对象引用计数变为0时会调用 dealloc 方法,然后最终调用 objc_destructInstance 方法来执行释放所有__weak修饰的指向该对象的指针,释放关联对象,释放该对象成员变量的操作

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
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;

// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (dealloc) obj->clearDeallocating();
}
return obj;
}

void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}

是不是有点熟悉呢,在上上节中我们刚刚分析过这个方法。当对象 dealloc 时,会自动调用 objc_removeAssociatedObjects 方法来释放所有的关联对象。

#总结一下

  • 类实例跟关联对象(关联的属性)并没有直接的存储关系,关联对象在创建时后存储在一个静态哈希表中,根据类实例的指针映射到该关联对象
  • 当类实例 dealloc 后,会从哈希表中释放该实例的所有的关联对象
  • 关联对象的关联策略跟属性的修饰符非常的相似,要合理使用避免 crash
  • 比起其他解决问题的方法,关联对象应该被视为最后的选择

#引用

谈Objective-C类成员变量

#我是前言

Objective-C 是一门动态语言,所以它总是将一些决定工作从编译延迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统来执行编译后的代码。这就是 runtime 存在的意义,它是 Objective-C 框架的一块基石。
runtime 有两个版本:modeen 和 leagcy,我们现在使用的是 modern 版的。
本文 runtime 源码为objc4-646.tar.gz版本

在老版本的 runtime 中,如果修改了基类的成员变量布局(比如增加成员变量),子类需要重新编译。

父类NSObject,子类MyObject成员变量布局

如果苹果发布了新的 iOS SDK,NSObject 增加了几个成员变量,那么我们原先的代码将无法运行。因为 MyObject 成员变量布局在编译时就确定了,父类新增的成员变量的地址跟子类成员变量的内存区域重叠了。此时,我们只能重新编译 MyObject 的代码,程序才能在新版本系统上运行。如果 MyObject 存在于别人编写的静态库,那我们只能希望作者快点发布新版本了。

新版本后NSObject,MyObject的成员变量布局

非脆弱[Non-fragile]实例变量是新版 Objective-C 的一个新功能,应用于iPhone和64位Mac上。它们提供给框架开发者更多的灵活性,且不会失去二进制的兼容性

非脆弱成员变量

#如何寻址成员变量

点开 runtime 的源码,让我们找到 ivar 的定义:

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
typedef struct objc_class *Class;
typedef struct objc_object *id;

// 类实例
struct objc_object {
private:
isa_t isa;
// ...省略
}

union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
// ...省略
}

// 类定义
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
// ...省略
}

struct class_data_bits_t {

// ...省略
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// ...省略
}

struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;
// ...省略
}

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
const method_list_t * baseMethods;
const protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
const property_list_t *baseProperties;
};

image

  • 每个 OC 类实例实际上都是一个内存上指向objc_object结构体的指针,成员变量 isa 有指向objc_class结构体的指针Class cls;
  • class_ro_t结构体中可以找到成员变量 const ivar_list_t * ivars,这个就是存储类所有成员变量的列表
  • class_ro_t结构体中成员变量const uint8_t * ivarLayout;const uint8_t * weakIvarLayout;的作用可以看一下孙源的这篇博客
1
2
3
4
@interface MyObject : NSObject {
NSString *_age;
}
@end

使用 clang -rewrite-objc MyObject.h 将代码转化成 C++ 实现,你可以看到编译后的 MyObject 实例的内存布局:

1
2
3
4
5
6
7
struct MyObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *__strong _age;
};
struct NSObject_IMPL {
__unsafe_unretained Class isa;
};

类实例内存布局

ivar_list_t 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct ivar_list_t {
uint32_t entsize;
uint32_t count;
ivar_t first;
};

struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// ...省略
};

我们可以看到ivar_t有名为offset的成员变量,这个就是成员变量在对象中的位置偏移量。在应用启动时,如果父类size变大时,runtime 会通过修改 offset,更新成员变量的偏移量,来正确的找到成员变量的地址。


1
2
3
4
5
6
7
8
9
10
11
@interface MyObject : NSObject {
NSString *_age;
}
@end

@implementation MyObject
- (void)test
{
self -> _age = @"hhh";
}
@end

使用命令行clang -F -cc1 -S -emit-llvm -fblocks MyObject.m,将代码编译成 IR(intermediate representation)。
注意要加*-F*,好多人的博客里面都少了这个标志,会报错。在 stackoverflow 找到答案。
下面是编译后的代码:

1
2
3
4
5
6
7
8
@"OBJC_IVAR_$_MyObject._age" = hidden global i64 8, section "__DATA, __objc_ivar", align 8
// ...
%6 = load i64, i64* @"OBJC_IVAR_$_MyObject._age", align 8, !invariant.load !8
%7 = bitcast %0* %5 to i8*
%8 = getelementptr inbounds i8, i8* %7, i64 %6
%9 = bitcast i8* %8 to %1**
store %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to %1*), %1** %9, align 8

可以简化成如下的代码

1
2
3
int32_t g_ivar_MyClass_age = 8;  // 全局变量
*(NSString *)((uint8_t *)obj + g_ivar_MyObject_age) = @"hhh";

  • 编译时,LLVM 为每各类的每一个成员变量定义一个全局变量,用于存储该成员变量的偏移量
  • 根据成员变量的偏移量,可以直接找到成员变量的地址并赋值

这也是为什么结构体ivar_t的成员变量offsetint32_t *类型,因为保存的是该全局变量的地址。

#Non Fragile ivars

在前面部分我们已经知道该如何寻址成员变量,那么当基类的size变化时,runtime 是如何更新子类成员变量的offset呢?

在应用程序启动后,main 函数执行之前,runtime 在加载类的时候,会使用static Class realizeClass(Class cls)函数对类进行初始化,分配其读写数据的内存,返回类的真实结构

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
/* realizeClass
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
*/
static Class realizeClass(Class cls) {
class_rw_t *rw = cls->data();
//...省略
if (ro->instanceStart < super_ro->instanceSize) {
// Superclass has changed size. This class's ivars must move.
// Also slide layout bits in parallel.
// This code is incapable of compacting the subclass to
// compensate for a superclass that shrunk, so don't do that.
class_ro_t *ro_w = make_ro_writeable(rw);
ro = rw->ro;
moveIvars(ro_w, super_ro->instanceSize,
mergeLayouts ? &ivarBitmap : nil,
mergeLayouts ? &weakBitmap : nil);
gdb_objc_class_changed(cls, OBJC_CLASS_IVARS_CHANGED, ro->name);
layoutsChanged = YES;
}
// ...省略
}

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
const ivar_list_t * ivars;
// ...省略
};

  • rw 是当前类的可读数据,ro 是类的 Ivar Layout,ro 的结构体定义在上面
  • 在初始化类时,如果父类 ro 的instanceSize比子类的instanceStart大的话,那么会调用moveIvars函数更新子类的instanceSize以及子类成员变量的偏移量

再让我们看一下 moveIvars 的源码:

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
/***********************************************************************
* moveIvars
* Slides a class's ivars to accommodate the given superclass size.
* Also slides ivar and weak GC layouts if provided.
* Ivars are NOT compacted to compensate for a superclass that shrunk.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
static void moveIvars(class_ro_t *ro, uint32_t superSize,
layout_bitmap *ivarBitmap, layout_bitmap *weakBitmap)
{
rwlock_assert_writing(&runtimeLock);

uint32_t diff;
uint32_t i;

assert(superSize > ro->instanceStart);
diff = superSize - ro->instanceStart;

if (ro->ivars) {
// Find maximum alignment in this class's ivars
uint32_t maxAlignment = 1;
for (i = 0; i < ro->ivars->count; i++) {
ivar_t *ivar = ivar_list_nth(ro->ivars, i);
if (!ivar->offset) continue; // anonymous bitfield

uint32_t alignment = ivar->alignment();
if (alignment > maxAlignment) maxAlignment = alignment;
}

// Compute a slide value that preserves that alignment
uint32_t alignMask = maxAlignment - 1;
if (diff & alignMask) diff = (diff + alignMask) & ~alignMask;

// Slide all of this class's ivars en masse
for (i = 0; i < ro->ivars->count; i++) {
ivar_t *ivar = ivar_list_nth(ro->ivars, i);
if (!ivar->offset) continue; // anonymous bitfield

uint32_t oldOffset = (uint32_t)*ivar->offset;
uint32_t newOffset = oldOffset + diff;
*ivar->offset = newOffset;

if (PrintIvars) {
_objc_inform("IVARS: offset %u -> %u for %s (size %u, align %u)",
oldOffset, newOffset, ivar->name,
ivar->size, ivar->alignment());
}
}

// Slide GC layouts
uint32_t oldOffset = ro->instanceStart;
uint32_t newOffset = ro->instanceStart + diff;

if (ivarBitmap) {
layout_bitmap_slide(ivarBitmap,
oldOffset >> WORD_SHIFT,
newOffset >> WORD_SHIFT);
}
if (weakBitmap) {
layout_bitmap_slide(weakBitmap,
oldOffset >> WORD_SHIFT,
newOffset >> WORD_SHIFT);
}
}

*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;

if (!ro->ivars) {
// No ivars slid, but superclass changed size.
// Expand bitmap in preparation for layout_bitmap_splat().
if (ivarBitmap) layout_bitmap_grow(ivarBitmap, ro->instanceSize >> WORD_SHIFT);
if (weakBitmap) layout_bitmap_grow(weakBitmap, ro->instanceSize >> WORD_SHIFT);
}
}
  • 首先计算 superSize 与 instanceStart 之间的差值 diff
  • 得到结构体中最大的成员变量的size:maxAlignment, 然后赋值:alignMask = maxAlignment - 1
  • 比较 diff 和 alignMask,通过算法 if (diff & alignMask) diff = (diff + alignMask) & ~alignMask; 对diff重新赋值

编译器在给结构体开辟空间时,首先找到结构体中最大的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数。
为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。了解更多可以看这篇博客

  • 更新成员变量的 offset,ivar.newOffset = diff + ivar.oldOffset
  • 更新子类 ro 的 instanceStart 和 instanceSize,ro.newinstanceStart = ro.oldinstanceStart + diff,ro.newinstanceSize = ro.oldinstanceSize + diff
  • 当父类变大时会调用该函数来移动子类ivar,当父类变小时则子类ivar不变化

通过这个函数,即使父类size变大了,我们还是可以通过子类的 ro.instanceStart + ivar.offset 访问到成员变量

#不能动态添加成员变量

在 runtime 中有一个函数 class_addIvar()可以为类添加成员变量, 下面是该方法的一部分注释:

his function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.
The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.

上面的大致意思是该函数只能在类注册之前使用,且不能为元类添加成员变量。

让我们设想一下如果 OC 允许动态增加成员变量:

1
2
3
4
5
6
7
8
9
@interface Father : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@end

@interface Son : Father
@property (nonatomic, copy) NSArray *toys;
@end

当Father初始化之后,instanceStart,instanceSize,offset已经确定
为 Father 添加新的成员变量 sex,则使用 Son 的实例对象 son 会出错误,因为 son.instanceStart < Father.instanceSize,即 father 成员变量的 sex 的内存区域会跟 son 的一部分重合

我们有时会在类目中动态的为类添加关联对象(添加对象),为什么可以添加关联对象呢?
具体的你可以看一下我的另一篇博客 谈Objective-C关联对象
这里我简单解释一下:关联对象被保存在一个静态的 map 中,以类实例的指针地址为映射,而不是保存在类实例的结构体中,不影响对象中成员变量的使用,所以可以在运行时添加成员变量。

#引用

Objective-C类成员变量深度剖析