文章目錄
- 前言
- 1??Side Table 的核心作用:擴展對象元數據存儲
- 1.1 傳統對象的內存限制
- 1.2 Side Table 的定位:集中式元數據倉庫
- 2??Side Table 的底層結構與關聯
- 2.1 Side Table 與 isa 指針的關系
- 2.2 Side Table 的存儲結構
- 2.3 SideTable 的工作流程
- 3??SideTable 的典型應用場景
- 3.1 弱引用(`__weak`)的實現
- 3.2 關聯對象(Associated Objects)
- 3.3 KVO(鍵值觀察)的實現
- 3.4 引用計數的優化存儲
- 4??SideTable特點
- 總結
前言
在 iOS/macOS 的內存管理中,Side Table(邊表) 是蘋果為優化對象元數據存儲而設計的 輔助數據結構。它與對象的 isa 指針緊密關聯,用于存儲對象的額外信息(如引用計數、弱引用指針、關聯對象等),解決了傳統對象內存布局無法高效擴展的問題。它的設計與使用是OC運行時實現弱引用的基礎,使得ARC能夠正確地處理弱引用的生命周期。
1??Side Table 的核心作用:擴展對象元數據存儲
1.1 傳統對象的內存限制
OC 對象的內存布局以 isa 指針為核心(指向類對象),但僅靠 isa 無法存儲所有元數據:
- 普通對象的 isa 指針僅指向類對象,無法直接存儲引用計數、弱引用等動態信息。
- 若為每個對象單獨分配元數據內存,會導致內存碎片和性能下降。
1.2 Side Table 的定位:集中式元數據倉庫
Side Table 是 全局共享的哈希表(或數組),為需要額外元數據的對象提供統一的存儲空間。它的核心作用是:
- 存儲對象的擴展元數據(如引用計數、弱引用指針、關聯對象鍵值對)。
- 減少內存碎片:多個對象共享同一 Side Table 的存儲空間,避免為每個對象單獨分配內存。
2??Side Table 的底層結構與關聯
2.1 Side Table 與 isa 指針的關系
OC 對象的 isa 指針(Class 類型)在 64 位系統中被擴展為 uintptr_t
類型(無符號長整型),通過 位域(Bit Field) 存儲額外信息:
- 低 48 位:指向類對象或 Side Table 的地址(具體取決于對象類型)。
- 高 16 位:存儲標記位(如是否為 weak 對象、是否需要 Side Table 等)。
/*_arm64(對應ios移動端)*/
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \uintptr_t nonpointer : 1; /*是否使用非指針格式*/ \uintptr_t has_assoc : 1; /*是否有關聯對象*/ \uintptr_t has_cxx_dtor : 1; /*是否有C++析構函數*/ \uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ /*存儲類指針的核心字段(加密后)*/ \uintptr_t magic : 6; /*??驗證指針合法性?? 調試器將提取的 magic_value 與預定義的合法值(ISA_MAGIC_VALUE)比較*/ \uintptr_t weakly_referenced : 1; /*是否為弱飲用*/ \uintptr_t unused : 1; /*保留位(數據結構或協議中預留的未使用位)*/ \uintptr_t has_sidetable_rc : 1; /*是否使用弱引用表*/ \uintptr_t extra_rc : 19 /*內聯引用計數(最大2^19-1)*/# elif __x86_64__ //__x86_64__(對應macos)(與上面一樣)
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \uintptr_t nonpointer : 1; \uintptr_t has_assoc : 1; \uintptr_t has_cxx_dtor : 1; \uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \uintptr_t magic : 6; \uintptr_t weakly_referenced : 1; \uintptr_t unused : 1; \uintptr_t has_sidetable_rc : 1; \uintptr_t extra_rc : 8 /*(最大為2^8-1)*/
在上述isa的結構體代碼中,extra_rc
和 has_sidetable_rc
,這兩者共同記錄引用計數器。
關鍵邏輯:
當對象需要存儲額外元數據時(如弱引用),isa 指針的高位會被標記為“需要 Side Table”,并通過低 48 位指向對應的 Side Table 條目。
2.2 Side Table 的存儲結構
Side Table 的底層實現是一個 全局的哈希表(或數組),每個條目對應一個對象的內存地址,存儲其擴展元數據。不同平臺(iOS/macOS)的實現略有差異,但核心結構相似:
字段 | 描述 |
---|---|
refcount | 引用計數(用于 ARC 管理)。 |
weak_refs | 弱引用指針數組(存儲指向該對象的弱引用變量地址)。 |
associated_objects | 關聯對象鍵值對(存儲通過 objc_setAssociatedObject 設置的關聯數據)。 |
flags | 標記位(如是否被標記為待釋放、是否為類對象等)。 |
在obj4-906中,sideTable部分的結構如下:
struct SideTable {spinlock_t slock; //自旋鎖RefcountMap refcnts; //引用計數映射表weak_table_t weak_table; //弱引用表SideTable() {memset(&weak_table, 0, sizeof(weak_table));} //SideTable():構造函數,使用memeset將weak_table初始化為0,確保弱引用表的初始狀態安全。~SideTable() {_objc_fatal("Do not delete SideTable.");} //~SideTable():析構函數,調用 _objc_fatal("Do not delete SideTable.")防止手動釋放。SideTable的生命周期由 Objective-C 運行時管理(如對象銷毀時自動釋放),禁止用戶主動刪除。void lock() { slock.lock(); }void unlock() { slock.unlock(); }//直接操作自旋鎖 slock,用于保護對 refcnts和 weak_table的修改。void reset() { slock.reset(); } //重置自旋鎖狀態(具體實現依賴 spinlock_t的底層邏輯,通常用于釋放鎖或重置為未鎖定狀態)。// Address-ordered lock discipline for a pair of side tables.template<HaveOld, HaveNew>static void lockTwo(SideTable *lock1, SideTable *lock2);template<HaveOld, HaveNew>static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
slock是一個自旋鎖:保證對 SideTable中數據(如引用計數、弱引用表)的線程安全訪問。自旋鎖適用于臨界區較小的場景(引用計數修改通常很快),避免線程阻塞開銷。
refcnts是引用計數映射表:存儲當前對象的引用計數值。RefcountMap本質是一個輕量級的哈希表或鍵值對結構,鍵為對象的內存地址(或唯一標識),值為對應的引用計數值。
weak_table是弱引用表:存儲所有指向當前對象的弱引用指針(__weak修飾的指針)。弱引用表的核心功能是:當對象被釋放時,自動將所有弱引用置為 nil,避免野指針。
weak_table_t源碼結構如下:
/*** The global weak references table. Stores object ids as keys,* and weak_entry_t structs as their values.*/
struct weak_table_t {weak_entry_t *weak_entries; //弱引用條目數組:指向 weak_entry_t結構體的指針數組。每個 weak_entry_t對應一個指向當前對象的 __weak指針,存儲弱引用的具體信息(如指針地址、對象狀態等)size_t num_entries; //弱引用條目數量:記錄當前 weak_table_t中有效弱引用條目的數量(即 weak_entries數組中實際使用的元素個數)uintptr_t mask; //哈希掩碼:用于計算弱引用的哈希索引,優化哈希表的存儲和查找效率//弱引用的哈希值通常通過 hash ^ mask計算(hash是弱引用指針的哈希值),結果對齊到 weak_entries數組的大小。mask的值通常為數組大小減一(如數組大小為 2^n,則 mask = (1 << n) - 1),確保索引在數組范圍內uintptr_t max_hash_displacement; //最大哈希位移
};
關于weak_entry_t,,其源碼在obj4_906中是這樣的:
#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2
struct weak_entry_t {DisguisedPtr<objc_object> referent;union {struct {weak_referrer_t *referrers; // 外聯弱引用指針數組(動態分配)uintptr_t out_of_line_ness : 2; // 標記為外聯存儲(固定為 1)uintptr_t num_refs : PTR_MINUS_2; // 弱引用數量(減去 2 位用于其他標記)uintptr_t mask; // 哈希掩碼(用于快速查找)uintptr_t max_hash_displacement; // 最大哈希位移(解決哈希沖突)};struct {// out_of_line_ness field is low bits of inline_referrers[1]weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];};bool out_of_line() {return (out_of_line_ness == REFERRERS_OUT_OF_LINE);}weak_entry_t& operator=(const weak_entry_t& other) {memcpy(this, &other, sizeof(other));return *this;}weak_entry_t(objc_object *newReferent, objc_object **newReferrer): referent(newReferent){inline_referrers[0] = newReferrer;for (int i = 1; i < WEAK_INLINE_COUNT; i++) {inline_referrers[i] = nil;}}
};
2.3 SideTable 的工作流程
由上述源碼,我們可以略微推斷出sideTable的工作流程:
- 對象創建:當對象大小超過小對象閾值時,運行時為其分配
SideTable
,并將引用計數初始化為 1(retainCount
)。 - 引用計數修改:調用
retain
/release
時,運行時通過對象的isa
指針找到對應的SideTable
,加鎖后修改refcnts
中的計數值。 - 弱引用注冊:當對象被
__weak
指針指向時,運行時將其弱引用指針注冊到SideTable
的weak_table
中。 - 對象釋放:當引用計數減至 0 時,運行時觸發
dealloc
,遍歷weak_table
將所有弱引用置為nil
,然后釋放SideTable
關聯的資源(如refcnts
和weak_table
內存)。
3??SideTable 的典型應用場景
3.1 弱引用(__weak
)的實現
__weak
修飾的變量不會增加對象的引用計數,但需在對象釋放時自動置空。Side Table 是其核心實現載體:
流程:
- 當對象被
__weak
變量引用時,系統會在 Side Table 中為該對象創建條目。 __weak
變量的值存儲為 Side Table 條目的索引(而非直接存儲對象地址)。- 對象釋放時,通過 Side Table 找到所有指向它的
__weak
變量,并將其置空。
3.2 關聯對象(Associated Objects)
通過 objc_setAssociatedObject
和 objc_getAssociatedObject
設置的關聯對象,其數據實際存儲在 Side Table 中:
示例:
// 為 NSObject 實例添加關聯對象(鍵為 "com.example.name")
NSString *name = @"張三";
objc_setAssociatedObject(obj, (__bridge const void *)(@"com.example.name"), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);// 從 Side Table 中讀取關聯對象
NSString *storedName = objc_getAssociatedObject(obj, (__bridge const void *)(@"com.example.name"));
底層邏輯:
關聯對象的鍵(如 @"com.example.name"
)和值會被存儲在 Side Table 的 associated_objects
字段中,鍵通過哈希映射快速查找。
3.3 KVO(鍵值觀察)的實現
KVO 的核心是 動態生成子類 并重寫 setter
方法,但觀察者列表(observers
)的存儲依賴 Side Table:
流程:
- 當對對象屬性添加 KVO 觀察時,系統會生成一個該對象的子類(如
NSKVONotifying_Obj
)。 - 子類的
isa
指針指向 Side Table 中的觀察者列表條目。 - 屬性值變化時,通過 Side Table 找到所有觀察者并觸發通知。
3.4 引用計數的優化存儲
ARC 下,對象的引用計數(retainCount
)不再直接存儲在對象內存中,而是通過 Side Table 的 refcount
字段統一管理:
- 引用計數增加(
retain
)時,更新 Side Table 中的refcount
。 - 引用計數減少(
release
)時,檢查refcount
是否為 0,若為 0 則觸發對象釋放。
4??SideTable特點
- 內存布局:緊湊高效
SideTable 的條目(Entry)在內存中是 連續存儲 的,通過哈希表快速查找。每個條目的大小根據存儲內容動態調整(如僅存儲弱引用時,條目較小;存儲關聯對象時,條目較大)。
- 線程安全:鎖保護
Side Table 的讀寫需保證線程安全,蘋果通過 自旋鎖(Spin Lock) 或 信號量(Semaphore) 實現:修改 Side Table(如添加/刪除條目)時加鎖;讀取 Side Table(如獲取弱引用列表)時加鎖或使用無鎖讀取(CAS 操作)。
- 版本演進:從
__objc_sideTable
到objc_sideTable
早期 iOS 版本(如 iOS 9 前)使用 __objc_sideTable
結構體,存儲方式為數組;iOS 10 后優化為哈希表(objc_sideTable
),提升了查找效率。
總結
Side Table 是 iOS 內存管理的“元數據中心”,通過集中存儲對象的擴展信息(引用計數、弱引用、關聯對象等),解決了傳統對象內存布局的局限性。它的存在讓 OC 能夠高效支持 ARC、弱引用、KVO 等高級特性,是蘋果內存管理優化的關鍵技術之一。