筆記-更深層次的瞭解iOS記憶體管理
在研究Hash表的過程中,想看iOS當中有哪些場景應用,最為大家所知的應該就是weak關鍵字的底層原理,利用網上的資料深究了一下,同時更進一步瞭解到了iOS記憶體管理方面的知識,所以希望自己能夠保留這份記憶,就記錄一下。
Hash
Hash或者說散列表,它是一種基礎資料結構,這裡為什麼會說到它,因為我感覺理解了Hash對weak關鍵字底層的理解有很大的幫助。
Hash表是一種特殊的資料結構,它同陣列、連結串列以及二叉樹等相比有很明顯的區別,但是它又是在陣列和連結串列的基礎上演化而來。
Hash表的本質是一個數組,陣列中每一個元素稱為一個箱子,箱子中存放元素。
儲存過程如下:
- 根據key計算出它的雜湊值h。
- 假設箱子的個數為n,那麼這個鍵值對應該放在第(h % n)個箱子中。
- 如果該箱子中已經有了鍵值對,就使用方法解決衝突(這裡值說分離連結法解決衝突,還有一個方法是開放定址法)。
Hash表採用一個對映函式f:key->address將關鍵字對映到該記錄在表中儲存位置,從而想要查詢該記錄時,可以直接根據關鍵字和對映關係計算出該記錄在表中的儲存位置,通常情況下,這種對映關係稱作Hash函式,而通過Hash函式和關鍵字計算出來的儲存位置( 這裡的儲存位置只是表中的儲存位置,並不是實際的實體地址 )稱作Hash地址。
先看一個列子: 假如聯絡人資訊採用Hash表儲存,當想要找到“lisi”的資訊時,直接根據“lisi”和Hash函式計算出Hash地址即可。 因為我們是用陣列大小對雜湊值進行取模,有可能不同的鍵值產生的索引值相同,這就是所謂的衝突。

顯然這裡“sizhang”元素和“zhangsi”元素產生了衝突,解決該衝突的方法就是改變資料結構,將陣列內的元素改變為一個連結串列,這樣就能容下足夠多的元素。
在使用分離連結法解決雜湊衝突時,每個箱子其實是一個連結串列,將屬於同一個箱子裡的元素儲存在一張線性表中,而每張表的表頭的序號即為計算得到的Hash地址,如下圖最左邊是陣列結構,陣列內的元素為連結串列結構。

這裡的Hash表我們只做簡單的瞭解,想要詳細瞭解的請參考:
記憶體管理的思考
ARC的核心思想:
- 自己生成的物件,自己持有
- 非自己生成的物件,自己也可以持有
- 自己持有的物件不需要時,需要對其進行釋放
- 非自己持有的物件無法釋放
其實不論ARC還是MRC都遵循該方式,只是在ARC模式下這些工作被編譯器做了
引用計數
retain、release、etainCount
蘋果的實現:(這部分內容是根據 《Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理》 來的)
- retainCount __CFDoExternRefOperation CFBasicHashGetCountOfKey 複製程式碼
- retain __CFDoExternRefOperation CFBasicHashAddValue 複製程式碼
- release __CFDoExternRefOperation CFBasicHashRemoveValue (CFBasicHashRemoveValue返回0時,-release呼叫dealloc) 複製程式碼
各個方法都通過同一個呼叫來 __CFDoExternRefOperation
函式,呼叫來一系列名稱相似的函式。如這些函式名的字首“CF”所示,它們包含於 Core Foundation
框架原始碼中,即是 CFRuntime.c
的 __CFDoExternRefOperation
函式。
__CFDoExternRefOperation
函式按 retainCount/retain/release
操作進行分發,呼叫不同的函式,NSObject類的 retainCount/retain/release
例項方法也許如下面程式碼所示:
- (NSUInteger)retainCount{ return (NSUInteger)__CFDoExternRefOperation(OPERATION_retainCount,self); } - (id)retain{ return (id)__CFDoExternRefOperation(OPERATION_retain,self); } - (void)release{ return __CFDoExternRefOperation(OPERATION_release,self); } 複製程式碼
int __CFDoExternRefOperation(uintptr_r op,id obj) { CFBasicHashRef table = 取得物件對應的散列表(obj); int count; switch(op) { case OPERATION_retainCount: count = CFBasicHashGetCountOfKey(table,obj); return count; case OPERATION_retain: CFBasicHashAddValue(table,obj); return obj; case OPERATION_release: count = CFBasicHashRemoveValue(table,obj): return 0 == count; } } 複製程式碼
從上面程式碼可以看出,蘋果大概就是採用散列表(引用計數表)來管理引用計數,當我們在呼叫 retain、retainCount、release
時,先呼叫 _CFDoExternRefOperation()
從而獲取到引用計數表的記憶體地址以及本物件的記憶體地址,然後根據物件的記憶體地址在表中查詢獲取到引用計數值。
若是 retain
則加1,若是 retainCount
就直接返回值,若是 release
則減1。(在 CFBasechashRemoveValue
中將引用計數減少到0時會呼叫 dealloc
廢棄物件)
Autorelease
作用: autorelease
作用是將物件放入自動釋放池中,當自從釋放池銷燬時對自動釋放池中的物件都進行一次release操作。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; id obj = [[NSObject alloc] init]; [obj autorelease]; [pool drain]; 複製程式碼
原理:ARC下,使用 @autoreleasepool{}
來使用一個 AutoreleasePool
,隨後編譯器會改成下面的樣子:
void *context = objc_autoreleasePoolPush(); // 執行的程式碼 objc_autoreleasePoolPop(context); 複製程式碼
而這兩個函式都是對 AutoreleasePoolPage
的簡單的封裝,所以自動釋放機制的核心就在於這個類。 AutoreleasePoolPage
是一個C++實現的類

-
AutoreleasePool
並沒有單獨的結構,而是由若干個AutoreleasePoolPage
以雙鏈表的形式組合而成(分別對應結構中的parent
指標和child
指標) -
AutoreleasePool
是按執行緒一一對應的(結構中的thread
指標指向當前執行緒) -
AutoreleasePoolPage
每個物件開闢一個虛擬記憶體一頁的大小,除了上面例項變數所佔空間,剩下的空間全部用來儲存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_autoreleasePoolPop
執行後,最終變成了下面樣子:

關鍵字
__strong
__strong
表示強引用,指向並持有該物件。該物件只要引用計數不為0,就不會被銷燬。如果在宣告引用時,不加修飾符,那麼引用將預設為強引用。
- 物件通過
alloc、new、copy、mutableCopy
來分配記憶體的
id __strong obj = [[NSObject alloc] init]; 複製程式碼
編譯器會轉換成下面程式碼:
id obj = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(obj, @selector(init)); // ... objc_release(obj); 複製程式碼
當使用 alloc、new、copy、mutableCopy
進行物件記憶體分配時,強指標直接指向一個引用計數為1的物件
- 物件不是自身生成,但是自身持有
id __strong obj = [NSMutableArray array]; 複製程式碼
在這種情況下, obj
也指向一個引用計數為1的物件記憶體。編譯器會轉換成下面程式碼:
id obj = objc_msgSend(NSMutableArray, @selector(array)); //替代我們呼叫retain方法,是obj持有該物件 objc_retainAutoreleaseReturnValue(obj); objc_release(obj); 複製程式碼
從而使得obj指向了一個引用計數為1的物件,不過, objc_retainAutoreleaseReturnValue
有一個成對的函式 objc_autoreleaseReturnValue
,這兩個函式可以用於最優化程式的執行,程式碼如下:
+ (id)array { return [[NSMutableArray alloc] init]; } 複製程式碼
編譯器轉換如下:
+ (id)array { id obj = objc_msgSend(NSMutableArray,@selector(alloc)); objc_msgSend(obj,@selector(init)); // 代替我們呼叫autorelease方法 return objc_autoreleaseReturnValue(obj); } 複製程式碼
其實 autorelease
這個開銷不小, runtime
機制解決了這個問題。
優化
Thread Local Storage(TLS)
執行緒區域性儲存,目的很簡單,將一塊記憶體作為某個執行緒專有的儲存,以 key-value
的形式進行讀寫,比如在非arm架構下,使用 pthread
提供的方法實現:
void *pthread_getspecific(pthread_key_t); int pthread_setspecific(pthread_key_t, const void *); 複製程式碼
在返回值身上呼叫 objc_autoreleaseReturnValue
方法時, runtime
將這個返回值 object
儲存在 TLS
中,然後直接返回這個 object
(不呼叫 autorelease
),同時,在外部接收這個返回值的 objc_retainAutoreleaseReturnValue
裡,發現 TLS
中正好存在這個物件,那麼直接返回這個 object
(不呼叫 retain
)。 於是乎,呼叫方和被呼叫利用 TLS
做中轉,很有默契的免去了對返回值的記憶體管理。
關係圖如下:

__weak
__weak
表示弱引用,弱引用不會影響物件的釋放,而當物件被釋放時,所有指向它的弱引用都會自動被置為 nil
,這樣可以防止野指標。
id __weak obj = [[NSObject alloc] init]; 複製程式碼
根據我們的瞭解,可以知道 obj
物件在生成之後立馬就會被釋放,主要原因是因為 __weak
修飾的指標沒有引起物件內部的引用計數發生變化。
__weak
的幾個使用場景:
- 在Delegate關係中防止迴圈引用
- 在Block中防止迴圈引用
- 用來修飾指向有Interface Builder建立的控制元件
weak實現原理的概括:
Runtime
維護了一個 weak
表,用於儲存指向某個物件的所有 weak
指標。 weak
表其實是一個Hash(雜湊)表(這就是為什麼在本文開始我要簡單介紹一下Hash表的原因), Key
是所指物件的地址, Value
是 weak
指標的地址(這個地址的值是所指物件的地址)陣列。
weak
的實現原理可以概括成三步:
- 初始化時,
runtime
會呼叫objc_initWeak
函式,初始化一個新的weak
指標指向物件的地址。 - 新增引用時,
objc_initWeak
函式會呼叫objc_storeWeak()
函式,objc_storeWeak()
的作用是更新指標指向,建立對應的弱引用表。 - 釋放時,呼叫
clearDeallocating
函式。clearDeallocating
函式首先根據物件地址獲取所有weak
指標地址的陣列,然後遍歷這個陣列把其中的資料設為ni
l,最後把這個entry
從weak
表中刪除,最後清理物件的記錄。
weak表
weak
表是一個弱引用表,實現為一個 weak_table
結構體
struct weak_table_t { weak_entry_t *weak_entries;// 儲存來所有指向指定物件的weak指標weak_entries的物件 size_t num_entries;// weak物件的儲存空間 uintptr_t mask;// 參與判斷引用計數輔助量 uintptr_t max_hash_displacement;// hash key 最大偏移值 }; 複製程式碼
這是一個全域性弱引用Hash表。使用不定型別物件的地址作為 key
,用 weak_entry_t
型別結構體物件作為 value
,其中的 weak_entries
成員,從字面意思上看,即為弱引用表的入口。
weak
全域性表中的儲存 weak
定義的物件的表結構 weak_entry_t
, weak_entry_t
是儲存在弱引用表中的一個內部結構體,它負責維護和儲存指向一個物件的所有弱引用Hash表。定義如下:
typedef objc_object ** weak_referrer_t; struct weak_entry_t { DisguisedPtr<objc_object> referent;//範型 union { struct { weak_referrer_t *referrers; uintptr_tout_of_line : 1; uintptr_tnum_refs : PTR_MINUS_1; uintptr_tmask; uintptr_tmax_hash_displacement; }; struct { // out_of_line=0 is LSB of one of these (don't care which) weak_referrer_tinline_referrers[WEAK_INLINE_COUNT]; }; } }; 複製程式碼
即:
-
weak_table_t
(weak
全域性表):採用Hash表的方式把所有weak
引用的物件,儲存所有引用weak
物件。 -
weak_entry_t
(weak_table_t
表中Hash表的value
值,weak
物件體):用於記錄Hash表中weak
物件。 -
objc_objct
(weak_entry_t
物件中的範型物件,用於標記物件weak
物件):用於標示weak
引用物件。
下面詳細看下 weak
底層實現原理:
id __weak obj = [[NSObject alloc] init]; 複製程式碼
編譯器轉換後代碼如下:
id obj; id tmp = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(tmp,@selector(init)); objc_initWeak(&obj,tmp); objc_release(tmp); objc_destroyWeak(&obj); 複製程式碼
對於 objc_initWeak()
的實現:
id objc_initWeak(id *location, id newObj) { // 檢視物件例項是否有效,無效物件直接導致指標釋放 if (!newObj) { *location = nil; return nil; } // 儲存weak物件 return storeWeak(location, newObj); } 複製程式碼
儲存 weak
物件的方法:
/** * This function stores a new value into a __weak variable. It would * be used anywhere a __weak variable is the target of an assignment. * * @param location The address of the weak pointer itself * @param newObj The new object this weak ptr should now point to * * @return \e newObj */ id objc_storeWeak(id *location, id newObj) { // 更新弱引用指標的指向 id oldObj; SideTable *oldTable; SideTable *newTable; spinlock_t *lock1; #if SIDE_TABLE_STRIPE > 1 spinlock_t *lock2; #endif // 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: // 更改指標,獲得以oldObj為索引所儲存的值地址 oldObj = *location; oldTable = SideTable::tableForPointer(oldObj); // 更改新值指標,獲得以newObj為索引所儲存的值地址 newTable = SideTable::tableForPointer(newObj); // 加鎖操作,防止多執行緒中競爭衝突 lock1 = &newTable->slock; #if SIDE_TABLE_STRIPE > 1 lock2 = &oldTable->slock; if (lock1 > lock2) { spinlock_t *temp = lock1; lock1 = lock2; lock2 = temp; } if (lock1 != lock2) spinlock_lock(lock2); #endif spinlock_lock(lock1); if (*location != oldObj) { spinlock_unlock(lock1); #if SIDE_TABLE_STRIPE > 1 if (lock1 != lock2) spinlock_unlock(lock2); #endif goto retry; } // 舊物件解除註冊操作 weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); // 新物件添加註冊操作 newObj = weak_register_no_lock(&newTable->weak_table, newObj, location); // weak_register_no_lock returns nil if weak store should be rejected // Set is-weakly-referenced bit in refcount table. if (newObj&&!newObj->isTaggedPointer()) { // 弱引用位初始化操作 // 引用計數那張散列表的weak引用物件的引用計數中標識為weak的引用 newObj->setWeaklyReferenced_nolock(); } // Do not set *location anywhere else. That would introduce a race. // 前面不要設定location物件,這裡需要更改指標指向 *location = newObj; spinlock_unlock(lock1); #if SIDE_TABLE_STRIPE > 1 if (lock1 != lock2) spinlock_unlock(lock2); #endif return newObj; } 複製程式碼
這裡同樣引用一個比較直觀的初始化弱引用物件流程圖:

總之根據以上對weak進行的儲存過程,可以通過下面流程圖幫助理解:

weak釋放為nil的過程
釋放物件基本流程如下:
- 呼叫
objc_release
- 因為物件的引用計數為0,所以執行
dealloc
- 在
dealloc
中,呼叫來_objc_rootDealloc
函式 - 在
_objc_rootDealloc
中,呼叫來object_dispose
函式 - 呼叫
objc_destructInstance
- 最後呼叫
objc_clear_deallocating
clearDeallocating
函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為 nil
,最後把這個 entry
從 weak
表中刪除,最後清理物件的記錄。
void objc_clear_deallocating(id obj) { assert(obj); assert(!UseGC); if (obj->isTaggedPointer()) return; obj->clearDeallocating(); } //執行 clearDeallocating方法 inline void objc_object::clearDeallocating() { sidetable_clearDeallocating(); } // 執行sidetable_clearDeallocating,找到weak表中的value值 voidobjc_object::sidetable_clearDeallocating() { SideTable *table = SideTable::tableForPointer(this); // clear any weak table items // clear extra retain count and deallocating bit // (fixme warn or abort if extra retain count == 0 ?) spinlock_lock(&table->slock); 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); } spinlock_unlock(&table->slock); } 複製程式碼
最終通過呼叫 weak_clear_no_lock
方法,將 weak
指標置空,函式實現如下:
/** * Called by dealloc; nils out all weak pointers that point to the * provided object so that they can no longer be used. * * @param weak_table * @param referent The object being deallocated. */ 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 should not 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) { 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); } 複製程式碼
objc_clear_deallocating
函式的操作如下:
- 從
weak
表中獲取廢棄物件的地址為鍵值的記錄 - 將包含在記錄中的所有附有
weak
修飾符變數的地址,置為nil
- 將
weak
表中該記錄刪除 - 從引用計數表中刪除廢棄物件的地址為鍵值的記錄
說了這麼多,還是為了說明一開始說的那句話:
Runtime
維護了一個weak表,用於儲存指向某個物件的所有weak指標。weak表其實是一個Hash(雜湊)表,Key是所指物件的地址,Value是weak指標的地址(這個地址的值是所指物件的地址)陣列。
__unsafe_unretained
__unsafe_unretained
作用需要和weak對比,它不會引起物件的內部引用計數的變化,但是,當其指向的物件被銷燬是 __unsafe_unretained
修飾的指標不會置為nil。是不安全的所有權修飾符,它不納入ARC的記憶體管理。
__autoreleasing
將物件賦值給附有 __autoreleasing
修飾符的變數等同於MRC時呼叫物件的 autorelease
方法。
@autoeleasepool { // 如果看了上面__strong的原理,就知道實際上物件已經註冊到自動釋放池裡面了 id __autoreleasing obj = [[NSObject alloc] init]; } 複製程式碼
編譯器轉換如下程式碼:
id pool = objc_autoreleasePoolPush(); id obj = objc_msgSend(NSObject,@selector(alloc)); objc_msgSend(obj,@selector(init)); objc_autorelease(obj); objc_autoreleasePoolPop(pool); @autoreleasepool { id __autoreleasing obj = [NSMutableArray array]; } 複製程式碼
編譯器轉換上述程式碼如下:
id pool = objc_autoreleasePoolPush(); id obj = objc_msgSend(NSMutableArray,@selector(array)); objc_retainAutoreleasedReturnValue(obj); objc_autorelease(obj); objc_autoreleasePoolPop(pool); 複製程式碼
上面兩種方式,雖然第二種持有物件的方法從 alloc
方法變為了 objc_retainAutoreleasedReturnValue
函式,都是通過 objc_autorelease
,註冊到 autoreleasePool
中。
篇幅太長了,很多底層上面的東西,網上都有相關的資料,以前看不是很懂,現在回過頭來細細研讀,感覺還是能理解的,所以參考了網路上的資料整理出來了,增加自己的印象,也希望我的理解能夠幫助到小夥伴們,如有錯誤,希望指出,共同進步,謝謝
參考資料:
《Objective-C高階程式設計 iOS於OS X多執行緒和記憶體管理》
iOS 底層解析weak的實現原理(包含weak物件的初始化,引用,釋放的分析
黑幕後的Autorelease