iOS 的內存管理不止是 「引用計數表」。ios
iOS 開發者基本都知道 iOS 是經過「引用計數」來管理內存的,可是也許並不知道 iOS 其餘的內存管理方式,好比 「Tagged Pointer」(帶標記的指針),好比 「NONPOINTER_ISA」(非指針型 isa),這個要根據不一樣的場景進行區分。c++
咱們就這篇文章主要來談一談這三種內存管理方式。git
在說內存管理以前,咱們先來講一下關於內存的概念。程序員
內存是計算機中重要的部件之一,它是與 CPU 進行溝通的橋樑。計算機中全部的程序都是在內存中進行的。內存(Menory)也被成爲「內存儲器」和「主存儲器」,其做用是用於暫時存放 CPU 中的運算數據,以及與硬盤等外部存儲器交換的數據。只要計算機在運行中,CPU 就會把須要運算的數據調到內存中進行運算,當運算完成後 CPU 再將結果傳送出來,內存的運行也決定了計算機的穩定運行。(來自 度娘)github
在 App 啓動後,系統會把 App 程序拷貝到內存中,而後在內存中執行代碼。編程
內存的概念你們多多少少都有點了解,咱們也不說那麼多。一塊內存條,是一個從下至上、地址依次遞增結構。來看一下內存的分區:swift
上面這張圖來自 這裏。數組
大體說一下 iOS 內存分區的狀況,五大區域:安全
棧區(Stack)數據結構
堆區(Heap)
全局區
常量區
代碼區
另外說一下一些值得注意的地方:
爲了節省內存和提升執行效率,蘋果提出了
Tagged Pointer
的概念。對於 64 位程序,引入Tagged Pointer
後,相關邏輯能減小一半的內存佔用,以及 3 倍的訪問速度提高,100 倍的建立、銷燬速度提高。
(有沒有那麼牛逼咱也不知道,咱也不敢問)
咱們先看看原有的對象爲何會浪費內存,假設咱們要存儲一個 NSNumber
對象,其值是一個整數。正常狀況下,若是這個整數只是一個 NSInteger
的普通變量,那麼它所佔用的內存是與 CPU
的位數有關,在 32 位 CPU 下佔 4 個字節,在 64 位 CPU 下是佔 8 個字節的。而指針類型的大小一般也是與 CPU 位數相關的,一個指針所佔用的內存在 32 位 CPU 下爲 4 個字節,在 64 位 CPU 下也是 8 個字節。
因此一個普通的 iOS 程序,若是沒有 Tagged Pointer
對象,從 32 位機器遷移到 64 位機器中後,雖然邏輯沒有任何變化,但這種 NSNumber
、NSDate
一類的對象所佔用的內存會翻倍。
咱們再來看看效率上的問題,爲了存儲和訪問一個 NSNumber
對象,咱們須要在堆上爲其分配內存,另外還要維護它的引用計數,管理它的生命週期。這些都給程序增長了額外的邏輯,形成了運行效率上的損失。
因此爲了改進上面提到的內存佔用和效率問題,蘋果提出了 Tagged Pointer
對象,因爲 NSNumber
、NSDate
一類的變量自己的值須要佔用的內存大小經常不須要 8 個字節,拿整數來講,4 個字節所能表示的有符號整數就能夠達到 20 多億(2 ^ 31 = 2147483648,另外 1 位做爲符號位),對於絕大多數狀況都是能夠處理的。
因此咱們能夠將一個對象的指針拆分紅兩部分,一部分直接保存數據,另外一部分做爲特殊標記,表示這是一個特別的指針,不指向任何一個地址。
Tagged Pointer 特色:
Tagged Pointer
專門用來存儲小的對象,例如 NSNumber
和 NSDate
Tagged Pointer
指針的值再也不是地址了,而是真正的值。因此,實際上它再也不是一個對象了,它只是一個披着對象皮的普通變量而已。因此,它的內存並不存儲在堆中,也不須要 malloc
和 free
objc_msgSend
能識別 Tagged Pointer
,好比 NSNumber
的 intValue
方法,直接從指針提取數據Tagged Pointer
後,指針內存儲的數據變成了 Tag + Data
,也就是將數據直接存儲在了指針中蘋果將 isa
設計成了聯合體,在 isa
中存儲了與該對象相關的一些內存的信息,緣由也如上面所說,並不須要 64 個二進制位所有都用來存儲指針。
來看一下 isa
的結構:
// x86_64 架構
struct {
uintptr_t nonpointer : 1; // 0:普通指針,1:優化過,使用位域存儲更多信息
uintptr_t has_assoc : 1; // 對象是否含有或曾經含有關聯引用
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析構函數或OC的dealloc
uintptr_t shiftcls : 44; // 存放着 Class、Meta-Class 對象的內存地址信息
uintptr_t magic : 6; // 用於在調試時分辨對象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否被弱引用指向
uintptr_t deallocating : 1; // 對象是否正在釋放
uintptr_t has_sidetable_rc : 1; // 是否須要使用 sidetable 來存儲引用計數
uintptr_t extra_rc : 8; // 引用計數可以用 8 個二進制位存儲時,直接存儲在這裏
};
// arm64 架構
struct {
uintptr_t nonpointer : 1; // 0:普通指針,1:優化過,使用位域存儲更多信息
uintptr_t has_assoc : 1; // 對象是否含有或曾經含有關聯引用
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析構函數或OC的dealloc
uintptr_t shiftcls : 33; // 存放着 Class、Meta-Class 對象的內存地址信息
uintptr_t magic : 6; // 用於在調試時分辨對象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否被弱引用指向
uintptr_t deallocating : 1; // 對象是否正在釋放
uintptr_t has_sidetable_rc : 1; // 是否須要使用 sidetable 來存儲引用計數
uintptr_t extra_rc : 19; // 引用計數可以用 19 個二進制位存儲時,直接存儲在這裏
};
複製代碼
注意這裏的 has_sidetable_rc
和 extra_rc
,has_sidetable_rc
代表該指針是否引用了 sidetable 散列表,之因此有這個選項,是由於少許的引用計數是不會直接存放在 SideTables 表中的,對象的引用計數會先存放在 extra_rc
中,當其被存滿時,纔會存入相應的 SideTables 散列表中,SideTables 中有不少張 SideTable,每一個 SideTable 也都是一個散列表,而引用計數表就包含在 SideTable 之中。
引用計數要麼存放在 isa
的 extra_rc
中,要麼存放在引用計數表中,而引用計數表包含在一個叫 SideTable
的結構中,它是一個散列表,也就是哈希表。而 SideTable
又包含在一個全局的 StripeMap
的哈希映射表中,這個表的名字叫 SideTables
。
散列表(Hash table,也叫哈希表),是根據建(Key)而直接訪問在內存存儲位置的數據結構。也就是說,它經過一個關於鍵值得函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱做散列函數,存放記錄的數組稱做散列表。
來看一下 NSObject.mm
中它們對應的源碼:
// SideTables
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
// SideTable
struct SideTable {
spinlock_t slock; // 自旋鎖
RefcountMap refcnts; // 引用計數表
weak_table_t weak_table; // 弱引用表
// other code ...
};
複製代碼
它們的關係以下圖:
一個 SideTables
包含衆多 SideTable
,每一個 SideTable
中又包含了三個元素,spinlock_t
自旋鎖、RefcountMap
引用計數表、weak_table_t
弱引用表。因此既然 SideTables
是一個哈希映射的表,爲何不用 SideTables
直接包含自旋鎖,引用計數表和弱引用表呢?這是由於在衆多線程同時訪問這個 SideTable
表的時候,爲了保證數據安全,須要給其加上自旋鎖,若是隻有一張 SideTable
的表,那麼全部數據訪問都會出一個進一個,單線程進行,很是影響效率,雖然自旋鎖已是效率很是高的鎖,這會帶來很是很差的用戶體驗。針對這種狀況,將一張 SideTable
分爲多張表的 SideTables
,再各自加鎖保證數據的安全,這樣就增長了併發量,提升了數據訪問的效率,這就是爲何一個 SideTables
下涵蓋衆多 SideTable
表的緣由。
自旋鎖:計算機科學用於多線程同步的一種鎖,線程會反覆檢查鎖變量是否可用。因爲線程在這一過程當中保持執行(沒有進入休眠),所以是一種忙等。一旦獲取了自旋鎖,線程會一直保持該鎖,直至顯式釋放自旋鎖。
自旋鎖適用於小型數據、耗時不多的操做,速度很快。
弱引用表也是一張哈希表的結構,其內部包含了每一個對象對應的弱引用表 weak_entry_t
,而 weak_entry_t
是一個結構體數組,其中包含的則是每個對象弱引用的對象所對應的弱引用指針。
當須要去查找一個對象對應的 SideTable
並進行引用計數或者弱引用計數的操做時,系統又是怎樣實現的呢?
當一個對象訪問 SideTables
時:
SideTables
中 SideTable
的個數取餘,最後獲得的結果就是該對象所要訪問的 SideTable
SideTable
中的 RefcountMap
表中再進行一次哈希查找,找到該對象在引用計數表中對應的位置size_t
對象,其實就是一個 uint
類型的無符號整型引用計數(Reference Count)是一個簡單而有效的管理對象生命週期的方式。當咱們建立一個新對象的時候,它的引用計數爲 1,當有一個新的指針指向這個對象時,咱們將其引用計數加 1,當某個指針再也不指向這個對象時,咱們將其引用計數減 1,當對象的引用計數變爲 0 時,說明這個對象再也不被任何指針指向了,這個時候咱們就能夠將對象銷燬,回收內存。
上面是唐巧的 理解 iOS 的內存管理 中對引用計數的一個定義,簡單來講就是採起計數的方式對內存進行管理,內存首先須要被建立出來,而後有人用這塊內存,計數 +1
,那我的不用了,計數 -1
,若是計數爲 0
,釋放它。
固然,建立、使用、釋放是有一個規則的,來看一下 iOS 中內存管理的思考方式:
與之對應的 Objective-C 方法:
對象操做 | Objective-C 方法 |
---|---|
生成並持有對象 | alloc/new/copy/mutableCopy 等方法 |
持有對象 | retain 方法 |
釋放對象 | release 方法 |
廢棄對象 | dealloc 方法 |
這些有關 Objective-C 內存管理的方法,實際上不包括在 Objective-C 語言中,而是包含在 Cocoa 框架中用於 OS X,iOS 應用開發,swift 也採用引用計數的方式進行內存管理。Cocoa 框架中 Foundation 框架類庫的 NSObject 類擔負內存管理的職責。Objective-C 內存管理中的 alloc/retain/release/dealloc
方法分別指代 NSObject 類的 +alloc
、-retain
、-release
、-dealloc
方法。
而引用計數又分爲 MRC(Manual Reference Counting,手動引用計數) 和 ARC(Automatic Reference Counting,自動引用計數)。
咱們來看一下官方對於自動引用計數的說明:
在 Objective-C 中採用 Automatic Reference Counting(ARC)機制,讓編譯器來進行內存管理。在新一代 Apple LLVM 編譯器(LLVM 3.0 或以上)中設置 ARC 爲有效狀態,就無需再次鍵入 retain 或者 release 代碼,這在下降程序崩潰、內存泄漏等風險的同時,很大程度上減小了開發程序的工做量。編譯器徹底清楚目標對象,並能馬上釋放那些再也不被使用的對象,如此一來,應用程序將具備可預測性,且能流暢運行,速度也將大幅提高。
其實最主要的是一點:
在 LLVM 編譯器中設置 ARC 爲有效狀態,就無需再次鍵入 retain 或者是 release 代碼。
那麼咱們也就知道了 MRC 是怎麼回事了,MRC 就是須要程序員手動插入 retain
、release
等管理內存的代碼,不過如今 MRC 已經屬於遠古時代的事情了,這裏只是順便提提,咱們主要看 ARC,ARC 其實作的事情不止是自動插入管理內存的方法,還作了一些優化,咱們放到後面一點講。咱們先來看看 alloc/retain/release/dealloc
這幾個方法的大體實現,這裏有一份編譯好的 runtime 源碼,版本是 objc4-750
,或者你們能夠到 opensource.apple 去下載。
NSObject
中類方法 alloc
作的事情:
首先看看 alloc
方法的實現:
+ (id)alloc {
return _objc_rootAlloc(self);
}
複製代碼
alloc
中調用 _objc_rootAlloc()
。
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
複製代碼
_objc_rootAlloc
中調用 callAlloc()
。
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
// some code ...
id obj = class_createInstance(cls, 0);
return obj;
}
複製代碼
省略了一部分代碼,callAlloc
中會調用 class_createInstance()
。
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
複製代碼
class_createInstance()
中直接調用 _class_createInstanceFromZone
,調用 calloc
方法分配內存。
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
// some code ...
id obj;
obj = (id)calloc(1, size); // 此時分配內存
obj->initInstanceIsa(cls, hasCxxDtor);
return obj;
}
複製代碼
_class_createInstanceFromZone
中會調用 obj->initInstanceIsa()
,如下就是初始化的方法了,此時內存已經分配。
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
initIsa(cls, true, hasCxxDtor);
}
複製代碼
initInstanceIsa()
中調用 initIsa()
。
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
if (!nonpointer) {
isa.cls = cls;
} else {
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
複製代碼
這裏就是對 isa
的一個初始化。
因此關於 alloc
方法,其大概步驟以下:
alloc/allocWithZone
class_createInstance
/ initInstanceIsa
calloc
(在這一步開始分配內存)initIsa
(初始化 isa
指針裏面的內容)關於 NSObject 的源碼解析你們能夠看看如下兩篇文章:
slowpath
和 fastpath
這裏我想提一嘴 slowpath
和 fastpath
,看一下 callAlloc
的完整實現:
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())) {
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
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
複製代碼
注意到方法中使用到的 slowpath
和 fastpath
,其實這兩個都是宏定義,與代碼邏輯自己無關,定義以下:
// x 極可能不爲 0,但願編譯器進行優化
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x 極可能爲 0,但願編譯器進行優化
#define slowpath(x) (__builtin_expect(bool(x), 0))
複製代碼
其實它們是所謂的快路徑和慢路徑,爲了解釋這個,咱們來看一段代碼:
if (x)
return 1;
else
return 39;
複製代碼
因爲計算機並不是一次只讀取一條指令,而是讀取多條指令,因此在讀到 if
語句時也會把 return 1
讀取進來。若是 x
爲 0,那麼會從新讀取 return 39
,重讀指令相對來講比較耗時。
若是 x
有很是大的機率是 0,那麼 return 1
這條指令每次不可避免的會被讀取,而且實際上幾乎沒有機會執行,形成了沒必要要的指令重讀。
所以,在蘋果定義的兩個宏中,fastpath(x)
依然返回 x
,只是告訴編譯器 x
的值通常不爲 0,從而編譯能夠進行優化。同理,slowpath(x)
表示 x
的值極可能爲 0,但願編譯器進行優化。
這個例子的講解來自 bestsswifter 的 深刻理解GCD,你們感興趣能夠看看。
因此如下代碼的解釋就出來了:
// 極可能 cls 是有值的,編譯器能夠不用每次都讀取 return nil 指令
if (slowpath(checkNil && !cls)) return nil;
複製代碼
fastpath
也是一樣的機制,可是你們要知道,當 checkNil && !cls
判斷成立的時候,return nil
指令仍是會被讀取,而後執行的。
還有一個就是 #if __OBJ2__
、#endif
,若是查看源碼的話,還會碰到 #if !__LP64__
、#elif 1
、#else
這類的宏判斷,這是由於蘋果針對不一樣的版本作了不一樣的實現,好比 32 位架構下和 64 位架構下的實現,有一些代碼在不一樣的狀況下是不須要參與編譯的,其實也跟咱們平時的 if-else
是同樣的概念。
retain
方法用於增長引用計數,release
用於減小引用計數。那麼引用計數存儲在哪裏?其實有兩個地方,一個是 NONPOINTER_ISA
,也就是非指針型 isa
中,isa
有個 extra_rc
屬性,就是用於存放引用計數的,在 ARM 64 下,extra_rc
佔 19 位。
extra_rc
只會保存額外的自動引用計數,對象的實際的引用計數會在這個基礎上 +1。當 isa
的 extra_rc
中存不下的時候,會使用 SideTable
來存儲,SideTable
中包含了咱們你們都知道的引用計數表。
經過引用計數表管理引用計數的好處在於:
第二點在調試時有着舉足輕重的做用,即便出現故障致使對象佔用的內存塊損壞,但只要引用計數表沒有被破壞,就可以確認各內存塊的位置。另外,在利用工具檢測內存泄漏時,引用計數表的記錄也有助於檢測各個對象的持有者是否存在。
若是想了解 retain
和 release
的底層實現,能夠看一下 黑箱中的 retain 和 release。
顧名思義,autorelease
就是自動釋放。這看上去很像 ARC,但實際上它更相似於 C 語言中自動變量(局部變量)的特性。
在計算機編程領域,自動變量(Automatic Variable)指的是局部做用域變量,具體來講便是在控制流進入變量做用域時系統自動爲其分配存儲空間,並在離開做用域時釋放空間的一類變量。
程序執行時,若某自動變量超出其做用域,該自動變量將被自動廢棄。
autorelease
會像 C 語言的自動變量那樣來對待對象實例,當超出其做用域(至關於變量做用域)時,對象實例的 release
實例方法被調用。另外,同 C 語言的自動變量不一樣的是,編程人員能夠設定變量的做用域。
須要被自動釋放的對象會被添加到離它最近的自動釋放池中(AutoreleasePool),咱們先明確什麼對象會自動加入自動釋放池:
autorelease
纔會入池,ARC 下能夠經過 __autoreleasing
修飾符,不然的話看方法名,經過 alloc/new/copy/mutablecopy
之外的方法取得的對象,編譯器幫咱們自動加入 autoreleasepool。(使用 alloc/new/copy/mutablecopy
方法進行初始化時,由系統管理對象,在適當的位置 release
,不加入 autoreleasepool)array
會自動將返回對象註冊到 autoreleasepool__weak
修飾的對象,爲了保證在引用時不被廢棄,會被註冊到 autoreleasepool 中id
的指針或對象的指針,在沒有顯式指定時會被註冊到 autoreleasepool 中那 Autorelease 的對象何時釋放?
在沒有手動添加 AutoreleasePool 的狀況下,Autorelease 對象是在當前的 runloop
迭代結束時釋放的,而它可以釋放的緣由是系統在每一個 runloop 迭代中都加入了自動釋放池的 Push 和 Pop。
App 啓動後,蘋果在主線程 runLoop
裏註冊了兩個 Observer
,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()
。
第一個 Observer
監視的事件是 Entry(即將進入 loop)
,其回調會調用 _objc_autoreleasePoolPush()
建立自動釋放吃。其 order
是 -2147483647
,優先級最高,保證建立釋放池發生在其餘全部回調以前。
第二個 Observer
監視了兩個事件:BeforeWaiting(準備進入休眠)
時調用 _objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
釋放舊的池並建立新池;Exit(即將退出 Loop)
時調用 _objc_autoreleasePoopPop()
來釋放自動釋放池,這個 Observer
的 order
是 2147483647
,優先級最低,保證釋放池釋放發生在其餘全部回調以後。
在主線程執行的代碼,一般是寫在諸如事件回調、Timer 回調內的。這些回調會被 RunLoop
建立好的 AutoreleasePool
環繞着,因此不會出現內存泄漏,開發者也沒必要顯式建立 Pool。
autorelease
的具體使用方法以下:
NSAutoreleasePool
對象autorelease
實例方法NSAutoreleasePool
對象NSAutoreleasePool
對象的生存週期至關於 C 語言變量的做用域,對於全部調用過 autorelease
實例方法的對象,在廢棄 NSAutoreleasePool
對象時,都將調用 release
實例方法。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; // 等同於 [obj release]
複製代碼
在 Cocoa 框架中,至關於程序主循環的 NSRunLoop
或者在其餘程序可運行的地方,對 NSAutoreleasePool
對象進行生成、持有和廢棄處理。所以,開發者通常不須要使用手動建立釋放池。Objective-C 的 main.m
的 UIApplicationMain
方法就是被一個自動釋放池環繞着的,也就是說,整個 iOS 應用都是包含在一個自動釋放池 block
中:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製代碼
不過,在大量產生 autorelease
的對象時,只要不廢棄 NSAutoreleasePool
對象,那麼生成的對象就不能被釋放,所以有時會因爲內存不足而到達內存峯值。典型的例子是讀入大量圖片的同時改變其尺寸,圖像文件讀入到 NSData
對象,並從中生成 UIImage
對象,改變該對象尺寸後生成新的 UIImage
對象。這種狀況下,就會大量產生 autorelease
的對象:
for (int i = 0; i < 圖像數 ; ++i) {
/* 讀入圖像 * 大量產生 autorelease 的對象 * 因爲沒有廢棄 NSAutoreleasePool 對象 * 最終致使內存不足! */
}
複製代碼
在這種狀況下,有必要在適當的地方生成、持有或廢棄 NSAutoreleasePool
對象:
for (int i = 0; i < 圖像數; ++i) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/* * 讀入圖像 * 大量產生 autorelease 的對象 */
[pool drain];
/* * 經過 [pool drain], * autorelease 的對象被一塊兒 release。 */
}
複製代碼
在 ARC 下咱們使用 @autoreleasepool{}
將代碼環繞便可。
那麼系統是如何實現 Autorelease 的,在 ARC 下,咱們使用 @autoreleasepool{}
來使用一個 AutoreleasePool
,隨後編譯器將其改寫成下面的樣子:
void *context = objc_autoreleasePoolPush();
// {} 中的代碼
objc_autoreleasePoolPop(context);
複製代碼
這兩個函數都是對 AutoreleasePoolPage
的簡單封裝,因此自動釋放機制的核心就在於這個類。
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
// other code ...
}
複製代碼
AutoreleasePoolPage
是一個 C++ 實現的類。
AutoreleasePool
並無單獨的結構,而是由若干個 AutoreleasePoolPage
以雙向鏈表的形式組合而成(分別對應結構中的 parent
指針和 child
指針)AutoreleasePool
是按線程一一對應的(結構中的 thread
指針指向當前線程)AutoreleasePoolPage
每一個對象會開闢 4096
字節內存(也就是虛擬內存一頁的大小),除了上面的實例變量所佔空間,剩下的空間所有用來儲存 autorelease
對象的地址id *next
指針做爲遊標(哨兵對象)指向棧頂最新 add
進來的 autorelease
對象的下一個位置AutoreleasePoolPage
的內存被佔滿時,會新建一個 AutoreleasePoolPage
對象,鏈接鏈表,後來的 autorelease
對象會被添加到新的 page
中因此,若當前線程中只有一個 AutoreleasePoolPage
對象,並記錄了不少 autorelease
對象地址,以下圖:
圖中的狀況,這一頁再加入一個 autorelease
對象就要滿了(也就是 next
指針立刻指向棧頂),這時就要執行上面說的操做,創建下一頁 page
對象,與這一頁鏈表鏈接完成後,新的 page
的 next
指針被初始化在棧底(begin
的位置),而後繼續向棧頂添加新對象。
因此,向一個對象發送 autorelease
消息,就是將這個對象加入到當前的 AutoreleasePoolPage
的棧頂 next
指針指向的位置。
每當進行一次 objc_autoreleasePoolPush
調用時,runtime
向當前的 AutoreleasePoolpage
中 add
進一個 哨兵對象
,值爲 0(也就是個 nil
),那麼這一個 page
就變成了下面的樣子:
objc_autoreleasePoolPush
的返回值正是這個哨兵對象的地址,被 objc_autoreleasePoolPop(哨兵對象)
做爲入參,因此:
page
page
中,將晚於哨兵對象插入的全部 autorelease
對象都發送一次 -release
消息,並向回移動 next
指針到正確位置,從最新加入的對象一直向前清理,能夠向前跨越若干個 page
,直到哨兵對象所在的 page
剛纔的 objc_autoreleasePoopPop
執行後,最終變成了下面的樣子:
知道了上面的原理,嵌套的 AutoreleasePool
就很是簡單了,pop
的時候總會釋放到上次 push
的位置,多層的 pool
就是多個哨兵對象而已,就像剝洋蔥同樣,每次一層,互不影響。
在對象的引用計數歸零時,會調用 dealloc
方法回收對象。
原理部分的講解來自於孫源大神的 黑幕背後的Autorelease,講的很是好,你們能夠看看。
另外說一下 ARC 中對 autorelease
和 retain
的一些優化:
若是 ARC 在運行時檢測到類函數中的 autorelease
後緊跟着一個 retain
操做,此時不直接調用對象的 autorelease
方法,而是改成調用 objc_autoreleaseReturnValue
。objc_autoreleaseReturnValue
會檢視當前方法返回以後將要執行的那段代碼,若那段代碼要在返回對象上執行 retain
操做,則設置全局數據結構中的一個標誌位,而不執行 autorelease
操做,與之類似,若是方法返回了一個自動釋放的對象,而調用方法的代碼要保留此對象,那麼此時不直接執行 retain
,而是改成執行 objc_retainAutoreleasedReturnValue
函數。此函數要檢測剛纔提到的標誌位,若已經置位,則不執行 retain
操做,設置並檢測標誌位,要比調用 autorelease
和 retain
更快。
當對象的引用計數爲 0 時,也就是對象的全部者都不持有該對象,該對象被廢棄時,無論 ARC 是否有效,都會調用對象的 dealloc
方法,對對象進行析構。
簡單列舉一下 dealooc
的調用流程,你們能夠結合 runtime 源碼來看:
dealloc
調用流程
_objc_rootDealloc()
rootDealloc()
NONPointer_ISA
// 是不是非指針類型 isaweakly_reference
// 是否有若引用has_assoc
// 是否有關聯對象has_cxx_dtor
// 是否有 c++ 相關內容has_sidetable_rc
// 是否使用到 sidetablefree()
objc_dispose()
調用流程
objc_destructInstance()
free()
objc_destructInstance()
調用流程
hasCxxDtor
,若是有 c++
相關內容,要調用 object_cxxDestruct()
,銷燬 c++ 相關內容hasAssociatedObjects
,若是有關聯對象,要調用 object_remove_associations()
,銷燬關聯對象的一系列操做clearDeallocating()
clearDeallocating()
調用流程
sideTable_clearDeallocating()
waek_clear_no_lock
,將指向該對象的弱引用指針置爲 nil
table.refcnts.eraser()
,從引用計數表中擦除該對象的引用計數dealloc
的執行流程結束來作一個小總結吧。
內存分區:
內存管理方式:
這篇文章講了內存分區、內存管理方式、SideTables
原理、引用計數、alloc/retain/release/autorelease/dealloc
內存相關方法的介紹,以及自動釋放池。
神經病院 Objective-C Runtime 入院第一天