内存管理

内存布局

  • stack 临时变量
  • heap 通过alloc等分配的对象
  • .bss 未初始化的全局变量、静态变量
  • .data 已初始化的全局变量、静态变量
  • .text 程序代码

内存管理

不同场景的内存管理方案:

  • TaggedPointer

    • 对象如果支持使用 TaggedPointer,苹果会直接将其指针值作为引用计数返回
    • NSString\NSNumber\NSDate 支持使用TaggedPointer
  • NONPOINTER_ISA (64位架构下)

  • 散列表

TaggedPointer

  • 苹果的64位Objective-C实现中,若对象指针的最低有效位为奇数,则该指针为Tagged Pointer。
  • Tagged Pointer专门用来存储小的对象,例如NSNumber/NSDate和NSString。
  • Tagged Pointer指针的值不是地址,包含真正的值和对象类型信息。所以,实际上它不是一个对象,而是一个披着对象皮的普通变量。它的内存并不存储在堆中,也不需要malloc和free。
  • Tagged Pointer因为并不是真正的对象,没有isa 指针。
  • 内存读取和对象创建效率高。
1
2
3
4
NSNumber *number = @25; //number地址:0xb000000000000192,解释:b:NSNumber类型,25的16进制是19,2:整型
NSString *a = @"a"; // a的地址:0x10be1f340
NSString *b = [a mutableCopy]; // b的地址:0x6080002542b0
NSString *c = [b copy]; // c的地址:0xa000000000000611 为Tagged Pointer。"a"的ASCII码值为61(十六进制)
  • NSNumber类型:最高4位的“b”表示是NSNumber类型,最低4位(Int为2,long为3,float为4,double为5)表示基本数据类型,其余56位则用来存储数值本身内容。存储用的数值超过56位存储上限的时候,那么NSNumber才会用真正的64位内存地址存储数值,然后用指针指向该内存地址。
  • NSString类型:最高位表示类型,最低位表示字符串长度。而其余的56位也是用来存储数据内容。
  • NSString类型:当字符串内存长度超过了56位的时候,Tagged Pointer并没有立即用指针转向,而是用了一种算法编码,把字符串长度进行压缩存储,当这个算法压缩的数据长度超过56位了才使用指针指向。
  • NSString类型:当String的内容有中文或者特殊字符(非 ASCII 字符)时,那么就只能存储为String指针。
  • NSString类型:字面型字符串常量却从不存储为Tagged Pointer,因为字符串常量必须在不同的操作系统版本下保持二进制兼容,而Tagged Pointer在运行时总是由Apple的代码生成。

NONPOINTER_ISA

64 bit存储一个内存地址显然是种浪费。于是可以优化存储方案,用一部分额外的存储空间存储其他内容。isa是objc_object的一个私有成员,它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
union isa_t 
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1; //0 代表普通isa指针,只包含该对象的类对象的地址 1代表isa包含了内存管理的相关字段
uintptr_t has_assoc : 1; //是否包含关联对象
uintptr_t has_cxx_dtor : 1; //是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 //类的指针
uintptr_t magic : 6; //固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。
uintptr_t weakly_referenced : 1; //表示该对象是否有过 weak 对象,如果没有,则析构时更快
uintptr_t deallocating : 1; //表示该对象是否正在析构
uintptr_t has_sidetable_rc : 1; //表示该对象的引用计数值是否过大无法存储在 isa 指针
uintptr_t extra_rc : 19;//存储引用计数值减一后的结果
};
};

散列表(哈希表)

为什么不是一个SideTable?
如果只有一张SideTable,那么所有的对象都放在这张表中,这时候如果操作表是,在不同线程中,那么就需要加锁,从而引发效率问题。所以引入分离锁的。

SideTable

  • Spinlock_t (自旋锁)

    • 是“忙等”的锁
    • 适用于轻量访问
  • 引用计数表(RefcountMap)

  • 弱引用表(weak_table_t)
Spinlock_t (自旋锁)
  • 是“忙等”的锁
  • 适用于轻量访问
引用计数表(RefcountMap)

RefcountMap 结构

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

根据传入对象的指针查找到size_t(结构中存储着引用计数的值)

1
2
3
4
size_t 
第一位代表是否有弱引用weakly_referenced
第二位代表是否正在dealloc(deallocating)
其他位数代表引用计数的值
弱引用表(weak_table_t)

根据传入对象的指针查找到weak_entry_t(一个数组结构,数组中的每一项存储着这个对象对应的弱引用指针)

retainCount的实现

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
uintptr_t
_objc_rootRetainCount(id obj)
{
assert(obj);

return obj->rootRetainCount();
}

inline uintptr_t
objc_object::rootRetainCount()
{
//如果是TaggedPointer类型,直接将内存地址返回作为引用计数值
if (isTaggedPointer()) return (uintptr_t)this;

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {//1 代表isa中不止包含类地址还包含内存管理相关值
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();
}

uintptr_t
objc_object::sidetable_retainCount()
{
//根据对象指针从SideTables获取SideTable
SideTable& table = SideTables()[this];

//这里初始值为1
size_t refcnt_result = 1;

//对表加锁后操作
table.lock();
//根据对象,从表中的引用计数表中查找到该对象对应的应用计数
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {//从引用计数表中查找到了this对应的it
// this is valid for SIDE_TABLE_RC_PINNED too
//size_t 的前两位不是引用计数的值,要做向右移位操作
//获取引用计数值,并在此基础上 +1 并将结果返回。
//这也就是为什么之前说引用计数表存储的值为实际引用计数减一。
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}

retain实现讲解

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
id
_objc_rootRetain(id obj)
{
assert(obj);

return obj->rootRetain();
}

inline id
objc_object::rootRetain()
{
if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}

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;
}

release实现讲解

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
void
_objc_rootRelease(id obj)
{
assert(obj);

obj->rootRelease();
}

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
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) {
//两种情况 第一种值为0 第二种 值为1
//值为0说明 引用计数为0 没有弱引用
//值为1说明 引用计数为0 且有弱引用
// 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)) {//只有引用计数大于0的时候,才可执行减一操作
it->second -= SIDE_TABLE_RC_ONE;//引用计数减一
}
table.unlock();
if (do_dealloc && performDealloc) {//引用计数为0 发送SEL_dealloc消息
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}

Core Foundation 库中也提供了增减引用计数的方法。比如在使用 Toll-Free Bridge 转换时使用的 CFBridgingRetainCFBridgingRelease 方法,其本质是使用 __bridge_retained__bridge_transfer 告诉编译器此处需要如何修改引用计数:

1
2
3
4
5
6
7
NS_INLINE CF_RETURNS_RETAINED CFTypeRef __nullable CFBridgingRetain(id __nullable X) {
return (__bridge_retained CFTypeRef)X;
}

NS_INLINE id __nullable CFBridgingRelease(CFTypeRef CF_CONSUMED __nullable X) {
return (__bridge_transfer id)X;
}

dealloc实现流程

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
void
_objc_rootDealloc(id obj)
{
assert(obj);

obj->rootDealloc();
}

inline void
objc_object::rootDealloc()
{
//如果使用了TaggedPointer就直接返回,交给栈自己处理
if (isTaggedPointer()) return; // fixme necessary?

if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}


id
object_dispose(id obj)
{
if (!obj) return nil;

objc_destructInstance(obj);
free(obj);

return nil;
}


void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
if (cxx) object_cxxDestruct(obj);

//对象在dealloc的时候自动将其关联对象删除
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}

return obj;
}

inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}

assert(!sidetable_present());
}

void
objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];

// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
table.unlock();
}

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) {
/// 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;
}


//遍历弱引用数组,将弱引用指针分别置为nil
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
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();
}
}
}

weak_entry_remove(weak_table, entry);
}

dealloc过程中,出了会将该对象的关联对象移除,还会将指向该对象的弱引用指针置为nil。

弱引用被添加到弱应用表的流程如下:

id __weak weakObj = obj;经过编译变为如下代码,这便是弱引用指针加入到弱引用表的入口

1
2
id weakObj;
objc_initWeak(&weakObj,obj);//注意这里传入的是&weakObj
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
//弱引用指针被添加到弱引用表的流程入口
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
//C++模板
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

// storeWeak中通过调用weak_register_no_lock方法将弱引用指针添加到表中
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

if (!referent || referent->isTaggedPointer()) return referent_id;

// ensure that the referenced object is viable
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}

if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}

// now remember it and where it is being stored
weak_entry_t *entry;
//根据原对象从弱引用表中找到对应的弱引用数组结构
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
//创建weak_entry_t
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
//将weak_entry_t插入到弱引用表中
weak_entry_insert(weak_table, &new_entry);
}

// Do not set *referrer. objc_storeWeak() requires that the
// value not change.

return referent_id;
}

autoreleasepool

autoreleasepool 是没有单独的内存结构的,它是通过以 AutoreleasePoolPage 为结点的双向链表来实现的。我们打开 runtime 的源码工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的实现源码。通过阅读源码,我们可以知道:

  • 每一个线程的 autoreleasepool 其实就是一个指针的堆栈;
  • 每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵对象,代表一个 autoreleasepool 的边界);
  • 一个 pool token 就是这个 pool 所对应的 POOL_SENTINEL 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release ;
  • 这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除;
  • Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。

一个空的 AutoreleasePoolPage 的内存结构如下图所示:

1
2
3
4
5
6
7
magic_t const magic;
id *next;//栈结构中的下一个位置的指针;指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
pthread_t const thread;//与线程一一对应;指向当前线程
AutoreleasePoolPage * const parent;//父节点
AutoreleasePoolPage *child;//子节点
uint32_t const depth;//代表深度,从 0 开始,往后递增 1;
uint32_t hiwat;//代表 high water mark

当 next == begin() 时,表示 AutoreleasePoolPage 为空;当 next == end() 时,表示 AutoreleasePoolPage 已满。

这段代码

1
2
3
4
@autoreleasepool{
NSMutableArray *arr = [NSMutableArray array];
NSLog(@"%@",arr);
}

经过clang编译后

1
2
3
4
5
6
7
8
9
10
11
12
13
/* @autoreleasepool */{ __AtAutoreleasePool __autoreleasepool; 
NSMutableArray *arr = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_cf_2w9p9n251x9b1gvcyt1p8tbw0000gp_T_Test_fb1412_mi_0,arr);
}

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

不得不说,苹果对 @autoreleasepool {}的实现真的是非常巧妙,真正可以称得上是代码的艺术。苹果通过声明一个 __AtAutoreleasePool 类型的局部变量 __autoreleasepool 来实现 @autoreleasepool {} 。当声明 __autoreleasepool 变量时,构造函数 __AtAutoreleasePool() 被调用,即执行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;当出了当前作用域时,析构函数~__AtAutoreleasePool()被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj);。也就是说 @autoreleasepool {}的实现代码可以进一步简化如下:

1
2
3
4
5
/* @autoreleasepool */ {
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
// 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中
objc_autoreleasePoolPop(atautoreleasepoolobj);
}

因此,单个autoreleasepool 的运行过程可以简单地理解为 objc_autoreleasePoolPush()[对象 autorelease]objc_autoreleasePoolPop(void *) 三个过程。

push 操作

提到的 objc_autoreleasePoolPush()函数本质上就是调用的 AutoreleasePoolPagepush 函数。

1
2
3
4
5
6
7
8
9
10
11
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}

一个 push 操作其实就是创建一个新的 autoreleasepool,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。这个地址也就是我们前面提到的 pool token ,在执行 pop 操作的时候作为函数的入参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static inline void *push()
{
id *dest = autoreleaseFast(POOL_SENTINEL);
assert(*dest == POOL_SENTINEL);
return dest;
}

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);
}
}

autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:

  • 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;
  • 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;
  • 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中。

每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。

autorelease 操作

AutoreleasePoolPage 的 autorelease 函数的实现对我们来说就比较容量理解了,它跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_SENTINEL ,而 autorelease 操作插入的是一个具体的 autoreleased 对象。

1
2
3
4
5
6
7
8
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || *dest == obj);
return obj;
}

pop 操作

pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址,即 pool token 。当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的 next 指向 pool token 为止。

每一个 autoreleasepool 只对应一个线程

参考

Objective-C 引用计数原理

黑幕背后的Autorelease

iOS内存管理-TaggedPointer

内存管理