引言

在iOS开发中,内存管理是至关重要的一环。合理的内存管理能够提升应用的性能和稳定性,避免内存泄漏和崩溃等问题。本文将深入探索Objective - C底层的内存管理机制,涵盖Mach - O文件、TaggedPointer、对象的内存管理、copy、引用计数、weak指针、autorelease等关键知识点,并结合底层实现逻辑、底层代码和底层结构进行详细说明。

一、Mach - O文件

1.1 概念

Mach - O(Mach Object)是一种用于可执行文件、目标代码、动态库等的文件格式,它是macOS和iOS系统上的标准二进制文件格式。Mach - O文件包含了程序的代码、数据、符号表等信息,操作系统通过解析Mach - O文件来加载和执行程序。

1.2 组成部分

1.2.1 Header

Header 包含了文件的基本信息,如文件类型(可执行文件、动态库等)、CPU 架构等。在底层,Header 是一个结构体,不同的 CPU 架构可能有不同的 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 */
};

1.2.2 Load Commands

Load Commands 描述了如何加载文件内容到内存中,包括代码段、数据段的加载位置等信息。Load Commands 也是以结构体的形式存在,每个 Load Command 有一个固定的头部,包含命令类型和命令长度等信息。以下是一个简化的 Load Command 头部结构体示例:

1
2
3
4
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

1.2.3 Sections

Sections 是实际的代码和数据部分,如 .text 段存放程序的代码,.data 段存放已初始化的全局变量和静态变量,.bss 段存放未初始化的全局变量和静态变量。每个 Section 也有一个对应的结构体来描述其属性,以下是一个简化的 Section 结构体示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct section_64 {
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 */
uint32_t reserved2; /* reserved */
uint32_t reserved3; /* reserved */
};

1.3 示例

可以使用 otool 命令查看 Mach - O 文件的信息:

1
otool -h /path/to/your/app

上述命令将显示指定应用的 Mach - O 文件的头部信息。

二、TaggedPointer

2.1 背景

在 64 位系统下,如果继续使用传统的指针来存储小对象(如 NSNumber、NSDate 等),会造成内存的浪费。因为指针本身需要 8 个字节的存储空间,而小对象实际所需的存储空间可能远小于 8 个字节。为了解决这个问题,苹果引入了 TaggedPointer 技术。

2.2 原理

TaggedPointer 并不是真正的指针,而是将数据直接存储在指针的值中。它通过在指针的最高位或其他特定位置设置标记位来表明这是一个 TaggedPointer,从而可以快速判断并直接从指针中提取数据,而无需进行额外的内存访问。

在底层,TaggedPointer 的实现依赖于指针的位操作。例如,对于一个存储整数的 TaggedPointer,可能会将整数的值直接编码在指针的低几位,而高几位用于存储标记信息。以下是一个简化的示例代码,展示如何判断一个指针是否为 TaggedPointer:

1
2
3
BOOL isTaggedPointer(id pointer) {
return ((uintptr_t)pointer & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

其中,_OBJC_TAG_MASK 是一个预定义的掩码,用于提取标记位。

2.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSNumber *number1 = @1;
NSNumber *number2 = @1000;

// 判断是否为TaggedPointer
if (isTaggedPointer((__bridge_retained void *)number1)) {
NSLog(@"number1 is a TaggedPointer");
}
if (isTaggedPointer((__bridge_retained void *)number2)) {
NSLog(@"number2 is a TaggedPointer");
}
}
return 0;
}

三、对象的内存管理

3.1 手动引用计数(MRC)

在早期的 Objective - C 开发中,使用手动引用计数(MRC)来管理对象的内存。开发者需要手动调用 retainreleaseautorelease 方法来增加和减少对象的引用计数。

在底层,每个对象都有一个引用计数表,用于记录对象的引用计数。当调用 retain 方法时,引用计数加 1;当调用 release 方法时,引用计数减 1。以下是一个简化的 retainrelease 方法的底层实现示例:

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
@interface MyObject : NSObject
@end

@implementation MyObject {
int _retainCount;
}

- (instancetype)init {
self = [super init];
if (self) {
_retainCount = 1;
}
return self;
}

- (instancetype)retain {
_retainCount++;
return self;
}

- (void)release {
_retainCount--;
if (_retainCount == 0) {
[self dealloc];
}
}

- (void)dealloc {
NSLog(@"MyObject deallocated");
[super dealloc];
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject *obj = [[MyObject alloc] init]; // 引用计数为 1
[obj retain]; // 引用计数为 2
[obj release]; // 引用计数为 1
[obj release]; // 引用计数为 0,对象被释放
}
return 0;
}

3.2 自动引用计数(ARC)

为了简化内存管理,苹果引入了自动引用计数(ARC)。在 ARC 模式下,编译器会自动插入 retainreleaseautorelease 调用,开发者无需手动管理对象的引用计数。

在底层,ARC 依赖于编译器的静态分析和代码插桩技术。编译器会在合适的位置插入 retainrelease 调用,以确保对象的引用计数正确。例如,在一个方法返回一个对象时,编译器会自动插入 retain 调用,以保证对象在返回后仍然有效;在对象超出作用域时,编译器会自动插入 release 调用,以释放对象的内存。

四、copy

4.1 概念

在 Objective - C 中,copy 方法用于创建对象的副本。根据对象是否遵循 NSCopying 协议,copy 方法的行为有所不同。

4.2 浅拷贝和深拷贝

  • 浅拷贝:只复制对象本身,而不复制对象所引用的内容。新对象和原对象共享相同的内容。
  • 深拷贝:不仅复制对象本身,还复制对象所引用的内容。新对象和原对象拥有各自独立的内容。

4.3 底层实现

要实现 copy 功能,对象需要遵循 NSCopying 协议,并实现 copyWithZone: 方法。以下是一个示例代码,展示了如何实现 copyWithZone: 方法:

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
#import <Foundation/Foundation.h>

@interface MyClass : NSObject <NSCopying>
@property (nonatomic, strong) NSArray *array;
@end

@implementation MyClass

- (id)copyWithZone:(NSZone *)zone {
MyClass *copy = [[[self class] allocWithZone:zone] init];
copy.array = [self.array copy]; // 这里的 copy 是浅拷贝
return copy;
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *original = [[MyClass alloc] init];
original.array = @[@1, @2, @3];

MyClass *copy = [original copy];

NSLog(@"Original array: %@", original.array);
NSLog(@"Copy array: %@", copy.array);
}
return 0;
}

在底层,copyWithZone: 方法的实现通常涉及到对象的属性复制。对于基本数据类型的属性,可以直接复制;对于对象类型的属性,需要根据具体需求进行浅拷贝或深拷贝。

五、引用计数

5.1 底层结构

在Objective - C中,对象的引用计数管理有不同的实现方式,这取决于对象的类型和系统的优化策略。

5.1.1 散列表(SideTables)

在64位系统中,为了避免多个线程同时操作同一个对象的引用计数时产生数据竞争问题,Objective - C使用了多个散列表(SideTables)来存储对象的引用计数。散列表本质上是一个全局的哈希表数组,每个散列表都有自己的锁,这样不同的对象可以在不同的散列表中操作,减少锁竞争。

1
2
3
4
5
6
7
8
9
10
11
// 简化的SideTables结构示意
static StripedMap<SideTable>& SideTables() {
static StripedMap<SideTable> tables;
return tables;
}

struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
};
  • spinlock_t slock:用于保护散列表的自旋锁,确保同一时间只有一个线程可以访问该散列表。
  • RefcountMap refcnts:一个哈希表,用于存储对象的引用计数。键是对象的指针,值是对象的引用计数。
  • weak_table_t weak_table:用于管理对象的弱引用,后续在weak指针部分会详细介绍。

5.1.2 引用计数的存储

对象的引用计数存储在RefcountMap中,它是一个std::unordered_map类型的哈希表。在实际存储时,引用计数并不是简单的整数值,而是采用了位域的方式,将不同的信息存储在不同的位上。例如,除了引用计数本身,还会存储一些标记位,如是否正在析构、是否使用了弱引用等。

5.2 底层原理

5.2.1 retain操作

当调用对象的retain方法时,底层会进行以下操作:

  1. 根据对象的地址找到对应的散列表。
  2. 加锁,确保线程安全。
  3. RefcountMap中查找该对象的引用计数。
  4. 如果找到了,将引用计数加1;如果没找到,说明该对象是第一次被引用,将引用计数初始化为1。
  5. 解锁。
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
id objc_object::retain() {
assert(!isTaggedPointer());
return rootRetain();
}

id objc_object::rootRetain() {
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;

do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
sidetable_unlock();
}
return sidetable_retain();
}
// ... 其他逻辑
newisa.retain();
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

if (slowpath(transcribeToSideTable)) {
sidetable_addExtraRC_nolock(RC_ONE);
}

if (slowpath(sideTableLocked)) sidetable_unlock();
return (id)this;
}

5.2.2 release操作

当调用对象的release方法时,底层会进行以下操作:

  1. 根据对象的地址找到对应的散列表。
  2. 加锁,确保线程安全。
  3. RefcountMap中查找该对象的引用计数。
  4. 如果找到了,将引用计数减1;如果引用计数减为0,调用对象的dealloc方法释放对象。
  5. 解锁。
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
void objc_object::release() {
assert(!isTaggedPointer());
rootRelease();
}

bool objc_object::rootRelease() {
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;

do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
sidetable_unlock();
}
return sidetable_release(true);
}
// ... 其他逻辑
uintptr_t carry;
newisa.release(&carry);
// ... 其他逻辑
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

if (slowpath(sideTableLocked)) sidetable_unlock();
return true;
}

六、weak指针

6.1 底层结构

6.1.1 弱引用表(weak_table_t)

弱引用表是一个全局的哈希表,用于存储对象的弱引用信息。它存储在每个散列表(SideTable)中。

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;
};
  • weak_entry_t *weak_entries:一个数组,存储所有的弱引用条目。
  • size_t num_entries:当前弱引用条目的数量。
  • uintptr_t mask:用于哈希计算的掩码。
  • uintptr_t max_hash_displacement:最大哈希冲突位移量。

6.1.2 弱引用条目(weak_entry_t)

每个弱引用条目对应一个对象,存储了所有指向该对象的弱引用指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
};
  • DisguisedPtr<objc_object> referent:被引用的对象。
  • weak_referrer_t *referrers:指向弱引用指针的数组。
  • uintptr_t num_refs:当前弱引用指针的数量。

6.2 底层原理

6.2.1 弱引用的创建

当创建一个weak指针时,底层会进行以下操作:

  1. 根据对象的地址找到对应的散列表。
  2. 在弱引用表中查找该对象的弱引用条目。
  3. 如果找到了,将weak指针添加到该条目的弱引用指针数组中;如果没找到,创建一个新的弱引用条目,并将weak指针添加到其中。
1
2
3
4
5
6
7
8
id objc_initWeak(id *location, id newObj) {
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

6.2.2 弱引用的清理

当对象被释放时,底层会进行以下操作:

  1. 根据对象的地址找到对应的散列表。
  2. 在弱引用表中查找该对象的弱引用条目。
  3. 如果找到了,遍历该条目的弱引用指针数组,将所有的弱引用指针置为nil
  4. 从弱引用表中移除该对象的弱引用条目。
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
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) {
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
return;
}
// 清除所有弱引用指针
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) {
if (*referrer == referent) {
*referrer = nil;
}
}
}
// 移除弱引用条目
weak_entry_remove(weak_table, entry);
}

七、autorelease

7.1 底层结构

7.1.1 自动释放池页(AutoreleasePoolPage)

自动释放池是由一系列的自动释放池页(AutoreleasePoolPage)组成的,这些页以双向链表的形式连接起来。每个自动释放池页是一个栈结构,用于存储自动释放的对象。

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
class AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
public:
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_SIZE; // size and alignment, power of 2
#endif
explicit AutoreleasePoolPage(AutoreleasePoolPage *newParent)
: magic(), next(begin()), thread(pthread_self()),
parent(newParent), child(nil),
depth(parent ? 1+parent->depth : 0),
hiwat(parent ? parent->hiwat : 0)
{
if (parent) {
parent->check();
assert(!parent->child);
parent->child = this;
}
}
// ... 其他方法
};
  • magic_t const magic:用于验证自动释放池页的有效性。
  • id *next:指向栈顶的指针,用于记录下一个可存储对象的位置。
  • pthread_t const thread:当前线程的ID,确保每个线程有自己独立的自动释放池。
  • AutoreleasePoolPage * const parent:指向前一个自动释放池页的指针。
  • AutoreleasePoolPage *child:指向后一个自动释放池页的指针。

7.2 底层原理

7.2.1 autorelease操作

当调用对象的autorelease方法时,底层会进行以下操作:

  1. 获取当前线程的自动释放池页。
  2. 如果当前页还有空间,将对象压入栈中;如果当前页已满,创建一个新的自动释放池页,并将对象压入新页的栈中。
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
id objc_object::autorelease() {
assert(!isTaggedPointer());
return rootAutorelease();
}

id objc_object::rootAutorelease() {
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}

id objc_object::rootAutorelease2() {
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((objc_object *)this);
}

static inline id autorelease(id obj) {
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}

static inline id *autoreleaseFast(id obj) {
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}

7.2.2 自动释放池的销毁

当自动释放池被销毁时,底层会进行以下操作:

  1. 从当前自动释放池页的栈顶开始,依次弹出对象并调用它们的release方法。
  2. 如果当前页的所有对象都被释放,且有父页,将当前页销毁,并继续处理父页。
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 AutoreleasePoolPage::pop(void *token) {
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// 处理空池占位符
page = hotPage();
if (!page) {
// 没有自动释放池页,直接返回
return setHotPage(nil);
}
// 找到最老的页
while (page->parent) {
page = page->parent;
}
page->fastPop(page->begin());
return;
}
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// 单个页的情况
} else {
// 错误处理
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->fastPop(stop);
// 释放多余的页
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}

八、总结

Objective - C 的内存管理机制是一个复杂而重要的主题。通过深入理解 Mach - O 文件、TaggedPointer、对象的内存管理、copy、引用计数、weak 指针和 autorelease 等知识点,以及它们的底层实现逻辑、底层代码和底层结构,开发者可以更好地掌握内存管理的技巧,编写出高效、稳定的 iOS 应用。同时,合理使用 ARC 和相关的内存管理技术,可以避免常见的内存问题,提升应用的性能和用户体验。

参考资料