前言
??前面我們已經學習了解了sideTable,今天來看看在OC中,sideTable是如何在我們使用weak時工作的。在OC中,weak修飾符是一種用于聲明“弱引用”的關鍵字,其核心特性是不參與對象的引用計數管理,而且當被引用的對象被釋放時,weak指針會自動置為nil(避免野指針)。為探究weak的工作原理和底層邏輯,筆者特寫此篇來記錄對weak的學習。
引用計數與sideTable
??OC的內存管理是基于引用計數的(ARC 下由編譯器自動生成引用計數增減代碼)。對象的isa指針指向其類信息,而對象的內存分布中通常包含:
- isa指針:指向對象的所屬類或元類。
- 引用計數相關數據:早期直接存儲在對象內存頭部,后來為了減少內存碎片,現將引用計數和弱引用信息分離到sideTable中。
sideTable是什么
??sideTable是一個輔助數據結構(本質是哈希表),用于存儲與對象關聯的元數據,包括引用計數表、弱引用表和其它元數據(如關聯對象、關聯引用等)。
引用計數表(RefcountMap):記錄對象的引用計數值(分散存儲,避免每個對象都占用額外空間)。
弱引用表(WeakMap):記錄所有指向該對象的 weak指針地址(鍵為對象地址,值為 weak指針的集合)。
沒個對象可能共享一個sideTable(通過哈希計算映射),因此sideTable的內存開銷備份談到多個對象上。
weak修飾符的底層實現原理
首先,我們來看看調用weak時的底層調用。
weak工作流程第二階段
我們在調用weak的地方打上斷點,然后進行匯編代碼調試,然后我們能發現,weak調用了一個objc_storeWeak方法:
然后我們在obj4-906-main源碼中找到了這部分方法的底層實現:
這其實是weak的賦值階段,用于將weak指針的地址注冊到目標對象的弱引用表中。
這個函數中的參數含義如下:
- location:指向 weak變量的指針(即存儲 weak指針的內存地址)。例如,有一個 __weak NSObject *obj;,則 &obj就是 location的值。
- newObj:要賦值給 weak變量的新對象(可能為 nil)。
- 返回值:返回舊值(即 location原來的對象,若未修改則為 nil)。
返回值中,有三個參數控制 storeWeak函數的行為邏輯:
DoHaveOld:是否處理舊值
若為 true,函數會先檢查 location原有的舊值(即之前指向的對象),并從該舊值的弱引用表中移除當前 location地址(避免舊對象釋放時錯誤地清理已失效的 weak指針);
若為 false,則跳過舊值處理(適用于首次賦值或無需清理舊值的場景)。
DoHaveNew:是否處理新值
若為 true,函數會將 newObj的地址注冊到其對應的弱引用表中(即把location地址添加到 newObj的弱引用表 referrers數組中);
若為 false,則跳過新值處理(適用于清空 weak指針的場景,如 obj = nil)。
DoCrashIfDeallocating:對象釋放時是否崩潰
若為 true,當 newObj正在被釋放(deallocating狀態)時,函數會觸發崩潰(避免向已釋放對象注冊弱引用);
若為 false,則允許向正在釋放的對象注冊弱引用(但后續對象釋放時會清理該 weak指針)。
storeWeak函數源碼:
storeWeak(id *location, objc_object *newObj)
{ASSERT(haveOld || haveNew);if (!haveNew) ASSERT(newObj == nil);Class previouslyInitializedClass = nil;id oldObj;SideTable *oldTable;SideTable *newTable;// 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:if (haveOld) {oldObj = *location;oldTable = &SideTables()[oldObj];} else {oldTable = nil;}if (haveNew) {newTable = &SideTables()[newObj];} else {newTable = nil;}SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);if (haveOld && *location != oldObj) {SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);goto retry;}// Prevent a deadlock between the weak reference machinery// and the +initialize machinery by ensuring that no // weakly-referenced object has an un-+initialized isa.if (haveNew && newObj) {Class cls = newObj->getIsa();if (cls != previouslyInitializedClass && !((objc_class *)cls)->isInitialized()) {SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);class_initialize(cls, (id)newObj);// If this class is finished with +initialize then we're good.// If this class is still running +initialize on this thread // (i.e. +initialize called storeWeak on an instance of itself)// then we may proceed but it will appear initializing and // not yet initialized to the check above.// Instead set previouslyInitializedClass to recognize it on retry.previouslyInitializedClass = cls;goto retry;}}// Clean up old value, if any.if (haveOld) {weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);}// Assign new value, if any.if (haveNew) {newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);// weak_register_no_lock returns nil if weak store should be rejected// Set is-weakly-referenced bit in refcount table.if (!_objc_isTaggedPointerOrNil(newObj)) {newObj->setWeaklyReferenced_nolock();}// Do not set *location anywhere else. That would introduce a race.*location = (id)newObj;}else {// No new value. The storage is not changed.}SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);// This must be called without the locks held, as it can invoke// arbitrary code. In particular, even if _setWeaklyReferenced// is not implemented, resolveInstanceMethod: may be, and may// call back into the weak reference machinery.callSetWeaklyReferenced((id)newObj);return (id)newObj;
}
weak工作流程第一階段
剛剛說objc_storeWeak方法是weak的賦值和持有階段,這是調用weak的第二階段,在這之前還有一個聲明階段即初始化階段。
初始化時,runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址。
源碼如下:
weak工作流程第三階段
weak工作流程的第三階段就是對象釋放階段(清楚弱引用表并置nil):
objc_object::clearDeallocating()函數,它是一個內聯函數,內部直接調用了sidetable_clearDeallocating()。這說明clearDeallocating()是對外暴露的接口,而sidetable_clearDeallocating()是具體的實現細節,負責實際的清理操作。
這個函數的主要操作是處理SideTable中的弱引用表和相關元數據。步驟包括:
- 獲取SideTable:通過SideTables()[this]獲取當前對象對應的SideTable實例。
- 加鎖:使用table.lock()確保線程安全,避免多線程同時修改SideTable導致數據競爭。
- 查找引用計數項:在table.refcnts(引用計數表)中查找當前對象的迭代器it。
- 檢查弱引用標記:如果找到迭代器且其值包含SIDE_TABLE_WEAKLY_REFERENCED標志(表示該對象有弱引用需要處理),則調用weak_clear_no_lock函數清理弱引用表。
- 清理引用計數項:從refcnts中刪除當前對象的條目。
- 解鎖:釋放SideTable的鎖,確保其他線程可以繼續操作。
weak的本質
- 運行時維護的弱引用跟蹤機制
weak的本質是運行時通過SideTable動態跟蹤對象與weak指針的關聯關系。其核心特性(不參與引用計數、自動置nil)均由以下機制支撐:
- 不參與引用計數:weak賦值時不調用retain,對象釋放時不依賴weak指針的計數。
- 自動置nil:通過SideTable中的弱引用表,在對象釋放時主動遍歷并清理所有關聯的weak指針地址。
- 弱引用表的集中管理
Weak(即__weak修飾的指針)的本質是運行時在SideTable中維護的一張弱引用表(weak_table_t)。該表存儲了所有指向當前對象的weak指針地址(referrers數組),是對象釋放時定位并清理weak指針的核心依據。
weak置nil
weak指針置nil的關鍵操作發生在對象釋放階段,由sidetable_clearDeallocating
函數觸發:
- 對象調用
dealloc
后,執行objc_object::clearDeallocating
(內聯函數),調用sidetable_clearDeallocating
。 sidetable_clearDeallocating
獲取對象的SideTable并加鎖,檢查引用計數映射(refcnts)中是否存在SIDE_TABLE_WEAKLY_REFERENCED
標記(表示被weak引用過)。- 若存在,調用
weak_clear_no_lock
函數,遍歷該對象的弱引用表(weak_table_t.referrers
),將每個weak指針的地址(entry->referrers)處的值置為nil(本質是修改內存為0)。 - 清理完成后,刪除引用計數映射項并解鎖SideTable,完成
weak
指針的置nil操作。
總結
??weak的實現以SideTable和弱引用表為核心,通過運行時動態跟蹤對象與weak指針的關聯關系,在對象釋放時主動清理所有weak指針并置nil,既避免了循環引用導致的內存泄漏,又保證了內存訪問的安全性,其本質是運行時維護的弱引用跟蹤機制。