一、內(nèi)存管理五大區(qū)
在iOS中內(nèi)存主要分為五大區(qū)域:棧區(qū)、堆區(qū)、靜態(tài)區(qū)、常量區(qū)、代碼段。

1.棧區(qū)
棧區(qū)是一段連續(xù)的內(nèi)存區(qū)域,從高地址向低地址存儲,遵循先進(jìn)后出(FILO)原則。
在 x86 架構(gòu)下,棧的地址一般為 0X7 開頭。
一般在運(yùn)行時進(jìn)行分配,內(nèi)存空間由系統(tǒng)管理,變量過了作用域范圍后內(nèi)存便會自動釋放。
參數(shù)、函數(shù)、局部變量都放在棧區(qū)
參數(shù)入棧是從前往后入棧。而結(jié)構(gòu)體入棧是從后往前入棧。
通過 sp 寄存器直接定位。
2.堆區(qū)
堆區(qū)是不連續(xù)的內(nèi)存從低地址向高地址存儲,遵循先進(jìn)先出(FIFO)原則。
堆的地址空間 iOS x86 架構(gòu)下以 0X6 開頭,空間的分配是動態(tài)的。
需要關(guān)注變量的生命周期,不及時釋放會造成內(nèi)存泄露。
OC 中使用 alloc、new 開辟空間創(chuàng)建的對象內(nèi)存放在堆區(qū)(而指向內(nèi)存的指針還是在棧里)。
C語言中使用 malloc、calloc、realloc 分配的空間,需要 free 釋放。
通過 sp 寄存器來定位到棧內(nèi)存地址,通過該地址定位堆內(nèi)存地址,所以說棧定位比堆定位速度快。
3.棧區(qū)與堆區(qū)對比
- 棧是一段連續(xù)的內(nèi)存區(qū)域,堆是不連續(xù)的內(nèi)存。
- 棧系統(tǒng)自動回收內(nèi)存,堆需要開發(fā)人員手動釋放。
- 棧內(nèi)存大小有限制,內(nèi)存空間小,堆內(nèi)存空間大。
4.全局靜態(tài)區(qū)
全局靜態(tài)區(qū)是編譯時分配的內(nèi)存空間,在 iOS 中一般以 0x1 開頭,程序運(yùn)行過程中,此內(nèi)存中的數(shù)據(jù)一直存在,程序結(jié)束后由系統(tǒng)釋放。
未初始化的全局變量和靜態(tài)變量,在 BSS 區(qū),即未初始化區(qū),.bss。
已初始化的全局變量和靜態(tài)變量,在數(shù)據(jù)區(qū),即初始化區(qū),.data。
5.常量區(qū)
常量區(qū)是編譯時分配的內(nèi)存空間,在程序運(yùn)行過程中,此內(nèi)存中的數(shù)據(jù)一直存在,程序結(jié)束后由系統(tǒng)釋放。
存放常量:整型、字符型、浮點(diǎn)、字符串等。
6.代碼區(qū)
代碼區(qū)編譯時分配的內(nèi)存空間,在程序運(yùn)行過程中,此內(nèi)存中的數(shù)據(jù)一直存在,程序結(jié)束后由系統(tǒng)釋放。
程序運(yùn)行時的代碼會被編譯成二進(jìn)制,存進(jìn)內(nèi)存的代碼區(qū)域。
二、內(nèi)存管理機(jī)制
隨著各個平臺的發(fā)展,現(xiàn)在被廣泛采用的內(nèi)存管理機(jī)制主要有 GC 和 RC 兩種。
GC (Garbage Collection):垃圾回收機(jī)制,定期查找不再使用的對象,釋放對象占用的內(nèi)存。
RC (Reference Counting):引用計(jì)數(shù)機(jī)制。采用引用計(jì)數(shù)來管理對象的內(nèi)存,當(dāng)需要持有一個對象時,使它的引用計(jì)數(shù) +1;當(dāng)不需要持有一個對象的時候,使它的引用計(jì)數(shù) -1;當(dāng)一個對象的引用計(jì)數(shù)為 0,該對象就會被銷毀。
Objective-C 支持三種內(nèi)存管理機(jī)制:ARC、MRC 和 GC,但 Objective-C 的 GC 機(jī)制有平臺局限性,僅限于 MacOS 開發(fā)中,iOS 開發(fā)用的是 RC 機(jī)制,從 MRC 到現(xiàn)在的 ARC。
引用計(jì)數(shù)的存儲
以上我們對 “引用計(jì)數(shù)” 這一概念做了初步了解,Objective-C 中的 “對象” 通過引用計(jì)數(shù)功能來管理它的內(nèi)存生命周期。那么,對象的引用計(jì)數(shù)是如何存儲的呢?它存儲在哪個數(shù)據(jù)結(jié)構(gòu)里?
首先,不得不提一下 isa。
isa 指針用來維護(hù) “對象” 和 “類” 之間的關(guān)系,并確保對象和類能夠通過 isa 指針找到對應(yīng)的方法、實(shí)例變量、屬性、協(xié)議等;
在 arm64 架構(gòu)之前,isa 就是一個普通的指針,直接指向 objc_class,存儲著Class、Meta-Class 對象的內(nèi)存地址。instance 對象的 isa 指向 class 對象,class 對象的 isa 指向 meta-class 對象;
從 arm64 架構(gòu)開始,對 isa 進(jìn)行了優(yōu)化,用 nonpointer 表示,變成了一個共用體(union)結(jié)構(gòu),還使用位域來存儲更多的信息。將 64 位的內(nèi)存數(shù)據(jù)分開來存儲著很多的東西,其中的 33 位才是拿來存儲 class、meta-class 對象的內(nèi)存地址信息。要通過位運(yùn)算將 isa 的值 & ISA_MASK 掩碼,才能得到 class、meta-class 對象的內(nèi)存地址。
// objc.h
struct objc_object {
Class isa; // 在 arm64 架構(gòu)之前
};
// objc-private.h
struct objc_object {
private:
isa_t isa; // 在 arm64 架構(gòu)開始
};
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
// extra_rc must be the MSB-most field (so it matches carry/overflow flags)
// nonpointer must be the LSB (fixme or get rid of it)
// shiftcls must occupy the same bits that a real class pointer would
// bits + RC_ONE is equivalent to extra_rc + 1
// RC_HALF is the high bit of extra_rc (i.e. half of its range)
// future expansion:
// uintptr_t fast_rr : 1; // no r/r overrides
// uintptr_t lock : 2; // lock for atomic property, @synch
// uintptr_t extraBytes : 1; // allocated with extra bytes
# if __arm64__ // 在 __arm64__ 架構(gòu)下
# define ISA_MASK 0x0000000ffffffff8ULL // 用來取出 Class、Meta-Class 對象的內(nèi)存地址
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; // 0:代表普通的指針,存儲著 Class、Meta-Class 對象的內(nèi)存地址
// 1:代表優(yōu)化過,使用位域存儲更多的信息
uintptr_t has_assoc : 1; // 是否有設(shè)置過關(guān)聯(lián)對象,如果沒有,釋放時會更快
uintptr_t has_cxx_dtor : 1; // 是否有C++的析構(gòu)函數(shù)(.cxx_destruct),如果沒有,釋放時會更快
uintptr_t shiftcls : 33; // 存儲著 Class、Meta-Class 對象的內(nèi)存地址信息
uintptr_t magic : 6; // 用于在調(diào)試時分辨對象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否有被弱引用指向過,如果沒有,釋放時會更快
uintptr_t deallocating : 1; // 對象是否正在釋放
uintptr_t has_sidetable_rc : 1; // 如果為1,代表引用計(jì)數(shù)過大無法存儲在 isa 中,那么超出的引用計(jì)數(shù)會存儲在一個叫 SideTable 結(jié)構(gòu)體的 RefCountMap(引用計(jì)數(shù)表)散列表中
uintptr_t extra_rc : 19; // 里面存儲的值是對象本身之外的引用計(jì)數(shù)的數(shù)量,retainCount - 1
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
...... // 在 __x86_64__ 架構(gòu)下
};
如果 isa 非 nonpointer,即 arm64 架構(gòu)之前的 isa 指針。由于它只是一個普通的指針,所以它本身不能存儲引用計(jì)數(shù),所以以前對象的引用計(jì)數(shù)都存儲在一個叫 SideTable 結(jié)構(gòu)體的 RefCountMap(引用計(jì)數(shù)表)散列表中。
如果 isa 是 nonpointer,則它本身可以存儲一些引用計(jì)數(shù)。從以上 union isa_t 的定義中我們可以得知,isa_t 中存儲了兩個引用計(jì)數(shù)相關(guān)的東西:extra_rc 和has_sidetable_rc。
extra_rc:里面存儲的值是對象本身之外的引用計(jì)數(shù)的數(shù)量,這 19 位如果不夠存儲,has_sidetable_rc 的值就會變?yōu)?1;
has_sidetable_rc:如果為 1,代表引用計(jì)數(shù)過大無法存儲在 isa 中,那么超出的引用計(jì)數(shù)會存儲 SideTable 的 RefCountMap 中。
所以,如果 isa 是 nonpointer,則對象的引用計(jì)數(shù)存儲在它的 isa_t 的 extra_rc 中以及 SideTable 的 RefCountMap 中。
SideTable
以上提到了一個數(shù)據(jù)結(jié)構(gòu) SideTable,我們進(jìn)入 objc4 源碼查看它的定義。
// NSObject.mm
struct SideTable {
spinlock_t slock; // 自旋鎖
RefcountMap refcnts; // 引用計(jì)數(shù)表(散列表)
weak_table_t weak_table; // 弱引用表(散列表)
......
}
SideTable 存儲在 SideTables() 中,SideTables() 本質(zhì)也是一個散列表,可以通過對象指針來獲取它對應(yīng)的(引用計(jì)數(shù)表或者弱引用表)在哪一個 SideTable中。在非嵌入式系統(tǒng)下,SideTables() 中有 64 個 SideTable。以下是SideTables() 的定義:
// NSObject.mm
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
所以,查找對象的引用計(jì)數(shù)表需要經(jīng)過兩次哈希查找:
(1)第一次根據(jù)當(dāng)前對象的內(nèi)存地址,經(jīng)過哈希查找從 SideTables()中取出它所在的 SideTable;
(2)第二次根據(jù)當(dāng)前對象的內(nèi)存地址,經(jīng)過哈希查找從 SideTable 中的 refcnts 中取出它的引用計(jì)數(shù)表。
為什么不是一個 SideTable,而是使用多個 SideTable 組成 SideTables() 結(jié)構(gòu)?
如果只有一個 SideTable,那我們在內(nèi)存中分配的所有對象的引用計(jì)數(shù)或者弱引用都放在這個 SideTable 中,那我們對對象的引用計(jì)數(shù)進(jìn)行操作時,為了多線程安全就要加鎖,就存在效率問題。
系統(tǒng)為了解決這個問題,就引入 “分離鎖” 技術(shù)方案,提高訪問效率。把對象的引用計(jì)數(shù)表分拆多個部分,對每個部分分別加鎖,那么當(dāng)所屬不同部分的對象進(jìn)行引用操作的時候,在多線程下就可以并發(fā)操作。所以,使用多個 SideTable 組成SideTables() 結(jié)構(gòu)。