從runtime原始碼解讀oc物件的引用計數原理
現在我們使用oc程式設計不用進行手動記憶體管理得益於ARC機制。ARC幫我們免去了大部分對物件的記憶體管理操作,其實ARC只是幫我們在合適的地方或者時間對物件進行 -retain
或 -release
,並不是不用進行記憶體管理。
引用計數的儲存
通過我之前分析的oc物件記憶體結構可以知道,其實物件的引用計數是存放在物件的 isa
指標中, isa
在 OBJC2
中是一個經過優化的指標不單存放著類物件的地址還存放著其他有用的資訊,其中就包括引用計數資訊的儲存。 isa_t
的結構位域中有兩個成員與引用計數有關分別是
uintptr_t has_sidetable_rc: 1;//isa_t指標第56位開始佔1位 uintptr_t extra_rc: 8//isa_t指標第57位開始佔8位 複製程式碼
extra_rc
存放的是可能是物件部分或全部引用計數值減1。
has_sidetable_rc
為一個標誌位,值為1時代表 extra_rc
的8位記憶體已經不能存放下物件的 retainCount
, 需要把一部分 retainCount
存放地另外的地方。
retain原始碼分析
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) { //isTaggedPointer直接返回指標 if (isTaggedPointer()) return (id)this; bool sideTableLocked = false; bool transcribeToSideTable = false; isa_t oldisa; isa_t newisa; do { //標識是否需要去查詢對應的SideTable transcribeToSideTable = false; oldisa = LoadExclusive(&isa.bits); newisa = oldisa; //這裡是isa沒有經過指標位域優化的情況,直接進入全域性變數中找出對應的SideTable型別值操作retainCount if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); if (tryRetain) return sidetable_tryRetain() ? (id)this : nil; else return sidetable_retain(); } //是否溢位的標識 , 如果呼叫addc函式後 isa的extra_rc++後溢位的話carry會變成非零值 uintptr_t carry; newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);// extra_rc++ if (slowpath(carry)) { // newisa.extra_rc++ overflowedextra_rc 溢位了 if (!handleOverflow) { ClearExclusive(&isa.bits); //這裡是重新呼叫rootRetain引數handleOverflow = true return rootRetain_overflow(tryRetain); } //執行到這裡代表extra_rc已經移除了,需要把 extra_rc 減半 ,把那一半存放到對應的SideTable型別值中 // Leave half of the retain counts inline and // prepare to copy the other half to the side table. if (!tryRetain && !sideTableLocked) sidetable_lock(); sideTableLocked = true; transcribeToSideTable = true; newisa.extra_rc = RC_HALF; newisa.has_sidetable_rc = true; } } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(transcribeToSideTable)) { // Copy the other half of the retain counts to the side table. sidetable_addExtraRC_nolock(RC_HALF); } if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); return (id)this; } 複製程式碼
rootRetain
主要是處理 isa
中 extra_rc
中加法操作: 在 extra_rc ++
沒有溢位的情況下不用特殊處理,如果溢位的話把 extra_rc
一半的值減掉,把減掉的值存到一個 SideTable
型別的變數中。
關於SideTable
struct SideTable { spinlock_t slock; //操作內部資料的鎖,保證執行緒安全 RefcountMap refcnts;//雜湊表[偽裝的物件指標 : 64位的retainCoint資訊值] weak_table_t weak_table;//存放物件弱引用指標的結構體 } 複製程式碼
SideTabel
其實是一個包裝了3個成員變數的結構體上面已註釋各成員的作用,而 RefcountMap refcnts
這個成員就是我們稍後重點要分析的存放物件額外 retainCount
的成員變數。
存放SideTable的StripedMap型別全域性變數
獲取 objc_object
對應的 SideTable
型別變數
alignas(StripedMap<SideTable>) static uint8_t SideTableBuf[sizeof(StripedMap<SideTable>)]; SideTable& table = SideTables()[this]; //函式SideTables() 實現 static StripedMap<SideTable>& SideTables() { return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf); } 複製程式碼
可以看出所有物件對應的 SideTable
。都儲存在一個全域性變數 SideTableBuf
中,把 SideTableBuf
定義成字元陣列其目的是為了方便計算 StripedMap<SideTable>
的記憶體大小,從而開闢一塊與 StripedMap<SideTable>
大小相同的記憶體。其實可以把 SideTableBuf
看成一個全域性的 StripedMap<SideTable>
型別的變數,因為 SideTables()
方法已經把返回值 SideTableBuf
強轉成 StripedMap<SideTable>
型別的變數。下面分析下 StripedMap
這個類
template<typename T> class StripedMap { #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR enum { StripeCount = 8 }; #else enum { StripeCount = 64 }; #endif struct PaddedT { T value alignas(CacheLineSize); }; public: T& operator[] (const void *p) { return array[indexForPointer(p)].value; } } 複製程式碼
從上面定義可以看出 StripedMap<SideTable>
類其實是包裝了一個結構體的成員變數 array
的雜湊表,該成員變數是一個裝著 PaddedT
型別的陣列, PaddedT
這個結構構體實際上就是我們模板類傳進的 SideTable
。因此這裡可以把 array
看成是一個裝著 SideTable
的容器,容量為8或64(執行的平臺不同而不同)。
當系統呼叫 SideTables()[物件指標]
時, StripedMap<SideTable>
這個雜湊表就會在 array
中找出對應陣列指標的 SideTable
類返回,這裡可以看出其中的一個 SideTable
類變數可能對應多個不同的物件指標。
extra_rc++ 溢位處理
if (slowpath(transcribeToSideTable)) { sidetable_addExtraRC_nolock(RC_HALF); } 複製程式碼
執行到下面的if語句裡面的 sidetable_addExtraRC_nolock(RC_HALF);
代表經過 do
語句的執行邏輯得出 extra_rc
已經溢位了,接下來看下溢位處理的實現
// Move some retain counts to the side table from the isa field. // Returns true if the object is now pinned. bool objc_object::sidetable_addExtraRC_nolock(size_tdelta_rc) { assert(isa.nonpointer); SideTable& table = SideTables()[this]; size_t& refcntStorage = table.refcnts[this]; size_t oldRefcnt = refcntStorage; // isa-side bits should not be set here assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0); assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0); //已經溢位了 直接返回true if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true; //把 delta_rc 左已兩位後與 oldRefcnt 相加 判斷是否有溢位 uintptr_t carry; size_t newRefcnt = addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry); if (carry) { // 溢位處理 // SIDE_TABLE_FLAG_MASK = 0b11 = SIDE_TABLE_DEALLOCATING + SIDE_TABLE_WEAKLY_REFERENCED // SIDE_TABLE_RC_PINNED 溢位標誌位 refcntStorage = SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK); return true; } else { //沒有溢位 refcntStorage = newRefcnt; return false; } } 複製程式碼
可以看出 extra_c
溢位的時候是把一半值減掉後存進對應物件指標的 SideTable
的成員變數 RefcountMap refcnts
中。在弄清楚上面程式碼邏輯前,先看下幾個重要的巨集定義
// The order of these bits is important. #define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0) #define SIDE_TABLE_DEALLOCATING(1UL<<1)// MSB-ward of weak bit #define SIDE_TABLE_RC_ONE(1UL<<2)// MSB-ward of deallocating bit #define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1)) 複製程式碼
通過巨集定義及 RefcountMap
的實現(下面會分析)可以發現 refcntStorage
其實是一個8位元組(64位)大小的記憶體其記憶體結構及對應的標識位如下圖

根據上面的程式碼用 this
指標獲取存放在 SideTable
內部引用計數 refcntStorage
後,會分別判斷這3個標識位都為0時才執行計數增加的操作,在呼叫 addc
是也會執行 delta_rc << SIDE_TABLE_RC_SHIFT
左移的操作來避開相應的標識位後在相應的記憶體位上。如果相加後溢位了,會把最高的移除標識位置為1。
經過 sidetable_addExtraRC_nolock
處理後isa指標中的 extrc_rc
在溢位的情況下成功吧一半的數值移寸到了對應 SideTable
的 refcntStorage
雜湊表中,從而釋放了記憶體繼續記錄 retainCount
。
關於DenseMap
我們先看下存放 extra_rc
溢位部分的 RefcountMap
定義:
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap; 複製程式碼
可以看出 RefcountMap
其實是 DenseMap
的模板類的別名, DenseMap
這是繼承自 DenseMapBase
的類,其內部實現可以看出 DenseMap
其實是一個典型的雜湊表(類似oc的 NSDictionary
),通過分析可以發現關於 DenseMap
的幾點
- 模板的
KeyT
用DisguisedPtr<objc_object>
包裝物件指標,此類是對物件指標值(obje_object *)的封裝或說是偽裝,使其不收記憶體洩露測試工具的影響。 - 模板的
ValueT
用size_t
代替,size_t
是一個64位記憶體的unsigned int
- 模板的
KeyInfoT
用DenseMapInfo<KeyT>
代替,在此處就相當於DenseMapInfo<DisguisedPtr<objc_object>
,DenseMapInfo
封裝了比較重要的方法雜湊值的獲取用於查詢對應Key的內容。
DenseMapInfo 實現細節
主要為雜湊表提供了KeyT的判等isEqual,以及KeyT型別值的hashValue的獲取下面是程式碼實現
//Key判等實現,直接用 == 完成判等 static bool isEqual(const T *LHS, const T *RHS) { return LHS == RHS; } //根據Key獲取對應的hash值 static unsigned getHashValue(const T *PtrVal) { //ptr_hash呼叫到下面的行內函數 return ptr_hash((uintptr_t)PtrVal); } #if __LP64__ static inline uint32_t ptr_hash(uint64_t key) { key ^= key >> 4; key *= 0x8a970be7488fda55; key ^= __builtin_bswap64(key); return (uint32_t)key; } #else static inline uint32_t ptr_hash(uint32_t key) { key ^= key >> 4; key *= 0x5052acdb; key ^= __builtin_bswap32(key); return key; } #endif 複製程式碼
DenseMap 根據 Key 查詢 Value 實現
簡化了原始碼看主要查詢實現
//重寫操作符[] ValueT &operator[](const KeyT &Key) { return FindAndConstruct(Key).second; } value_type& FindAndConstruct(const KeyT &Key) { BucketT *TheBucket; if (LookupBucketFor(Key, TheBucket)) return *TheBucket; return *InsertIntoBucket(Key, ValueT(), TheBucket); } //查詢實現 template<typename LookupKeyT> bool LookupBucketFor(const LookupKeyT &Val, const BucketT *&FoundBucket) const { //存放所有內容的bucket陣列 const BucketT *BucketsPtr = getBuckets(); //bucket個數 const unsigned NumBuckets = getNumBuckets(); //沒有內容直接返回 if (NumBuckets == 0) { FoundBucket = 0; return false; } //根據Val的雜湊值算出的bucket的索引 getHashValue呼叫的是KeyInfo的實現 unsigned BucketNo = getHashValue(Val) & (NumBuckets-1); unsigned ProbeAmt = 1; while (1) { //從buckets陣列拿出對應索引的值 const BucketT *ThisBucket = BucketsPtr + BucketNo; if (KeyInfoT::isEqual(Val, ThisBucket->first)) { //符合 key == indexOfKey //賦值外面傳進來的引數 FoundBucket = ThisBucket; return true; } BucketNo += ProbeAmt++; BucketNo&= (NumBuckets-1); } } 複製程式碼
release 原始碼分析
首先我們看下主要處理 release
邏輯的方法實現
ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) 複製程式碼
方法主要分為幾大邏輯模組
extra_rc -- ectra_rc --
首先分析執行 extra_rc--
後正常未下溢位的情況,此情況主要是通過 subc
函式讓 newisa.bit
與 RC_ONE(1ULL<<56)
相加,最後更新 isa
的值。
do { oldisa = LoadExclusive(&isa.bits); newisa = oldisa; //計算溢位的標識位 uintptr_t carry; // extra_rc--RC_ONE -> (1<<56)在isa中剛好是extra_rc的開始位 newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); if (slowpath(carry)) { goto underflow;//下溢位了, 直接跳轉下溢位的處理邏輯 } } while (slowpath(!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits))); // 把newisa.bits 賦值給isa.bits ,並退出 while 迴圈 if (slowpath(sideTableLocked)) sidetable_unlock(); return false; 複製程式碼
加入經過 subc
函式的運算 newisa.bits
發生了下溢位的話,直接跳轉到 underflow
的處理邏輯中。下面分析下 underflow
的主要邏輯
underflow: newisa = oldisa; if (slowpath(newisa.has_sidetable_rc)) { //用SideTabel的refcnts //為對應的SideTable加鎖後在操作器記憶體資料 if (!sideTableLocked) { sidetable_lock(); sideTableLocked = true; //修改下 sideTableLocked = true; 重新呼叫retry goto retry; } // 把一部分的refCount出來賦值給 borrowed size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); if (borrowed > 0) { //把引用計數 - 1 後賦值給 extra_rc newisa.extra_rc = borrowed - 1; //更新isa的extra_rc bool stored = StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits); //下面是處理更新isa值失敗的重試操作 if (!stored) { // Inline update failed. // Try it again right now. This prevents livelock on LL/SC // architectures where the side table access itself may have // dropped the reservation. isa_t oldisa2 = LoadExclusive(&isa.bits); isa_t newisa2 = oldisa2; if (newisa2.nonpointer) { uintptr_t overflow; newisa2.bits = addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow); if (!overflow) { stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, newisa2.bits); } } } //重試更新isa值還是失敗的話,把borrowed再次存進物件的SideTable中。再週一遍retry的程式碼邏輯(開始的do while位置) if (!stored) { // Inline update failed. // Put the retains back in the side table. sidetable_addExtraRC_nolock(borrowed); goto retry; } //執行到這裡代表成功把對應SideTable的值轉移了部分值到isa.ectra_rc中,併為對應SideTable型別值加鎖 sidetable_unlock(); return false; } else { //來到else語句的話代表對應SideTable已經沒有儲存額外的retainCount。接下來要執行物件記憶體釋放的邏輯了。 } } 複製程式碼
通過上面下溢位處理的程式碼分析可以知道, extra_rc--
後發生下溢位的話,系統會優先去查詢物件對應 SideTable
值中儲存的雜湊表 refcnts
變數,在通過 refcnts
查詢到對應物件儲存的8位元組記憶體的 count
去一部分出來(大小為 isa.extra_rc
剛好溢位的一半大小),存放到 isa.extra_rc
中。如果此時 refcnts
取出的值也為0了就代表物件可以釋放掉記憶體了。
物件記憶體釋放的呼叫,主要是把 isa.deallocating
的標識位置為1,然後執行 SEL_dealloc
釋放物件記憶體。
// 上面如果 borrowed == 0 來到這裡代表retainCount等於0 物件可以釋放了 if (slowpath(newisa.deallocating)) { ClearExclusive(&isa.bits); if (sideTableLocked) sidetable_unlock(); return overrelease_error(); // does not actually return } newisa.deallocating = true; if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; if (slowpath(sideTableLocked)) sidetable_unlock(); __sync_synchronize(); if (performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); //代表引用計數已經等於0呼叫dealloc釋放記憶體 } return true; 複製程式碼
總結
物件通過 retain
與 release
巧妙地使內部的 isa.extra_rc
與外部儲存在對應其本身的 SideTable
類中儲存的引用計數值增減有條不紊地進行著加減法。並通過判斷當兩個值都滿足一定條件時就執行物件的 SEL_dealloc
訊息,釋放記憶體