iOS引用計數管理之揭祕計數儲存

weixin_34138377發表於2018-07-10

前言

最近偶爾出去面試瞭解一下現在iOS行情和麵試會問的問題。其中有這樣的一個問題被問到很多次:引用計數原理。回去查資料發現當時回答的很糟糕,於是就在這裡單獨寫一篇文章記錄下來。這篇文章只講一個問題:引用計數的數量存哪裡的,文末提到的其他問題後面會單獨再寫。

預備知識

要說清楚這個問題,我們需要先來了解下面的三個知識點。
除錯環境如下。

macOS:10.13.4;
XCode:9.4;
除錯裝置:My Mac。
Tagged Pointer

這個玩意的詳細解釋在這裡,簡單的說64位系統下,對於值小(多小?後面有講解)的物件指標本身已經存了值的內容了,而不用去指向一個地址再去取這個地址所存物件的值;相信你也知道了,如果是Tagged Pointer的話就少了建立物件的操作。

我們也可以在WWDC2013的《Session 404 Advanced in Objective-C》視訊中,看到蘋果對於Tagged Pointer特點的介紹:
1:Tagged Pointer專門用來儲存小的物件,例如NSNumber和NSDate
2:Tagged Pointer指標的值不再是地址了,而是真正的值。所以,實際上它不再是一個物件了,它只是一個披著物件皮的普通變數而已。所以,它的記憶體並不儲存在堆中,也不需要malloc和free。
3:在記憶體讀取上有著3倍的效率,建立時比以前快106倍。
1760865-9f513e366aa7db23.png
1760865-95557008984b4a25.png

我們來測試一下。

NSLog(@"%p",@(@(1).intValue));//0x127
NSLog(@"%p",@(@(2).intValue));//0x227
由此可知int型別的tag為27,因為去掉27後0x1 = 1,0x2 = 2,正好是值。

NSLog(@"%p",@(@(1).doubleValue));//0x157
NSLog(@"%p",@(@(2).doubleValue));//0x257
由此可知double型別的tag為57,因為去掉27後0x1 = 1,0x2 = 2,正好是值。

明顯0x127、0x257不是一個地址,所以@(1)、@(2)也不是一個物件,只是一個普通變數。

既然是Tagged Pointer那肯定得有一個tag,經過測試發現值型別不一樣所具有的tag也會相對不一樣。

為什麼說相對,因為測試發現unsigned long和long long具有相同的tag值37。
當然其他型別也有一樣的。

什麼時候NSNumber物件Tagged Pointer失效呢?那就是當值和tag加起來佔用的位元組數要超過地址長度(8位元組64位)時會失效:

為什麼說要超過,而不是超過,這個我也比較糾結,具體的看看下面的例子。

這裡針對double型別來舉個例子,其他型別的結果可能稍有不同,因為上面說到tag有不同的值,所佔用二進位制位長度會不一樣。

int 17:10001,5位;
long long 37:100101,6位;
double 57:111001,6位。
...

這樣64減去已佔用的tag位,剩下的位來表示值,所能表示的範圍也不一樣。

double pow(double, double)返回的是double型別的值。

NSLog(@"%p",@(pow(2, 55) - 3));//0x7ffffffffffffc57
57是double型別的tag,0x7ffffffffffffc57去掉tag剩下的是0x7ffffffffffffc = 
pow(2, 55) - 3 = 36028797018963964;二進位制表示為0...0(9個)1...1(53個)00(2個)。
關於這裡為什麼要-3這就是我比較糾結的原因,因為二進位制表示後面還有2個0啊,還可以多表示3啊;
系統這麼做肯定有自己的考慮,也許是我理解錯了,希望你來指正。

NSLog(@"%p",@(pow(2, 55) - 2));//0x6030002c50c0
這個單純就是一個地址了,沒有57這個tag了,裡面並沒有存值的內容,所以Tagged Pointer失效了。

從這個例子可以知道tag佔用8位,64 - 55 = 9,9 - 1 = 8,因為第一位是來做符號位表示正負數的;
上面我們測試出來57佔用6個二進位制位,為什麼這裡值最長佔用56二進位制長度呢,我也不知道。

關於Tagged Pointer是否啟用,你也可以通過下面的語句來列印,這個語句是runtime原始碼中的。
NSNumber *number = @(pow(2, 55) - 3);
NSLog(@"%d",((uintptr_t)number & 1UL) == 1UL);//true
number = @(pow(2, 55) - 2);
NSLog(@"%d",((uintptr_t)number & 1UL) == 1UL);//false

目前我所知的系統中可以啟用Tagged Pointer的類物件有:NSDate、NSNumber、NSString,上面我們只舉例了NSNumber,你可以自己下來試試另外的。

當然了你可以在環境變數中設定OBJC_DISABLE_TAGGED_POINTERS=YES強制不啟用Tagged Pointer,環境變數我們可以新增很多東西的,具體的你可以看看runtime原始碼的objc-env.h檔案。
1760865-3de146c54f163fed.png
不啟用Tagged Pointer
這樣runtime就會做相應的處理了。

不啟用後上面的例子就會得到這樣的結果,也就表示關閉成功了。

NSLog(@"%p",@(pow(2, 55) - 3));//0x6030002ccbc0
NSLog(@"%p",@(pow(2, 55) - 2));//0x6030002ccc50

NSNumber *number = @(pow(2, 55) - 3);
NSLog(@"%d",((uintptr_t)number & 1UL) == 1UL);//false
number = @(pow(2, 55) - 2);
NSLog(@"%d",((uintptr_t)number & 1UL) == 1UL);//false

Non-pointer isa

我們一直認為例項物件的isa都指向類物件,甚至還看到這樣的原始碼。

typedef struct objc_object *id
struct objc_object {
    Class _Nonnull isa;
}

其實這是之前版本的程式碼了,現在版本的程式碼早就變了。

struct objc_object {
private:
    isa_t isa;
  ...
}

所以例項物件的isa都指向類物件這樣的說法不對。
現在例項物件的isa是一個isa_t聯合體,裡面存了很多其他的東西,相信你也猜到了引用計數也在其中;如果該例項物件啟用了Non-pointer,那麼會對isa的其他成員賦值,否則只會對cls賦值。

union isa_t {
  Class cls;
  ...
  (還有很多其他的成員,包括引用計數數量)
}

物件是否不啟用Non-pointer目前有這麼幾個判斷條件,這些都可以在runtime原始碼objc-runtime-new.m中找到邏輯。

1:包含swift程式碼;
2:sdk版本低於10.11;
3:runtime讀取image時發現這個image包含__objc_rawisa段;
4:開發者自己新增了OBJC_DISABLE_NONPOINTER_ISA=YES到環境變數中;
5:某些不能使用Non-pointer的類,GCD等;
6:父類關閉。

我們自己新建一個Person類,通過OBJC_DISABLE_NONPOINTER_ISA=YES/NO來看看isa結構體的具體內容,設定方法上面有截圖。

設定了OBJC_DISABLE_NONPOINTER_ISA不一定就使用Non-pointer了,因為上面說到了還有其他條件會關閉Non-pointer。
所以我們自己建立一個Person類繼承自NSObject,這樣只通過在環境變數中設定OBJC_DISABLE_NONPOINTER_ISA
就可以控制Person物件是否啟用Non-pointer,因為我們排除了除4之外的其他條件。
不使用Non-pointer的isa
isa_t isa = {
    Class class = Person;
    uintptr_t bits = 4294976320;
    struct {
        uintptr_t nonpointer = 0;
        uintptr_t has_assoc  = 0;
        uintptr_t has_cxx_dtor = 0;
        uintptr_t shiftcls = 536872040; 
        uintptr_t magic = 0;
        uintptr_t weakly_referenced = 0;
        uintptr_t deallocating = 0;
        uintptr_t has_sidetable_rc = 0;
        uintptr_t extra_rc = 0;
    }
}
其實可以簡化為
isa_t isa = {
    Class class = Person;
}
因為原始碼中顯示不使用Non-pointer則只對isa的class賦值了,其他的都是預設值,而且除了class其他成員也不會在原始碼中被使用到。
使用Non-pointer的isa
isa_t isa = {
    Class class = Person;
    uintptr_t bits = 8303516107940673;
    struct {
        uintptr_t nonpointer = 1;
        uintptr_t has_assoc  = 0;
        uintptr_t has_cxx_dtor = 0;
        uintptr_t shiftcls = 536872040; 
        uintptr_t magic = 59;
        uintptr_t weakly_referenced = 0;
        uintptr_t deallocating = 0;
        uintptr_t has_sidetable_rc = 0;
        uintptr_t extra_rc = 0;
    }
}
extra_rc就是存的引用計數,nonpointer = 1表示啟用Non-pointer。

isa的賦值是在alloc方法呼叫時,內部會進入initIsa()方法,你可以進去看一看有啥不同之處。

objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { 
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        isa_t newisa(0);
        ......
        (成員賦值)
        ......
        isa = newisa;
    }
}

SideTable

雜湊表,這是一個比較重要的資料結構,相信你也猜到了這個和物件引用計數有關;如果該物件不是Tagged Pointer且關閉了Non-pointer,那該物件的引用計數就使用SideTable來存。我們先來看一下SideTable結構體定義,至於怎麼被使用的且聽我慢慢道來。

struct SideTable {
    //鎖
    spinlock_t slock;
    //強引用相關
    RefcountMap refcnts;
    //弱引用相關
    weak_table_t weak_table;
      ...
}

啟動應用後,我們第一次看到SideTable其實是在runtime讀取image的時候。

void map_images_nolock(unsigned mhCount, const char* const mhPaths[],
                  const struct mach_header *const mhdrs[]) {
    ...
    static bool firstTime = YES;
    if (firstTime) {
        AutoreleasePoolPage::init();
        SideTableInit();
    }
    ...  
}
static void SideTableInit() {
    new (SideTableBuf)StripedMap<SideTable>();
}

map_images_nolock會多次呼叫,因為ImageLoader一批載入很多個image到記憶體,然後通知runtime去讀取這一批image,沒錯這時候runtime開始從image中處理類了;SideTableInit()方法只會執行一次。SideTableInit內部用到了SideTableBuf,SideTableBuf的定義如下。

alignas(StripedMap<SideTable>) static uint8_t
    SideTableBuf[sizeof(StripedMap<SideTable>)];
sizeof(StripedMap<SideTable>) = 4096;
alignas(StripedMap<SideTable>)是位元組對齊的意思,表示讓陣列中每一個元素的起始位置對齊到4096的倍數,也把陣列中每一個元素都變成了4096大小,能理解吧。
所以這句話就簡化為static uint8_t SideTableBuf[4096],也就是定義了一個4096大小型別為uint8_t的陣列,每一個元素大小為4096,名字為SideTableBuf;

現在來理解SideTableInit()中的new (SideTableBuf)StripedMap<SideTable>()。你會發現這句話沒有
任何意思,你註釋後一樣可以正常執行。因為上面那句話已經初始化SideTableBuf了,怎麼說?看下面。

在SideTableBuf定義上方有這樣的一段註釋。
We cannot use a C++ static initializer to initialize SideTables because
libc calls us before our C++ initializers run. We also don't want a global 
pointer to this struct because of the extra indirection.
Do it the hard way.
我來翻譯一下:我們不能用C++靜態初始化方法去初始化SideTables,
因為C++初始化方法執行之前libc就會呼叫我們;我們同樣不想用一個全域性的指標去指向SideTables,
因為需要額外的代價。但是沒辦法我們只能這樣。
看不懂沒關係,下面就是答案。

什麼是C++ static initializer呢,我們依然可以在runtime原始碼中找到答案。在objc-os.mm中有這樣的程式碼。
size_t count;
Initializer *inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
    inits[i]();
}
是的,這個就是在呼叫C++的initializer了,這個操作在map_images_nolock之前執行,也就是這時候還沒有執行SideTableInit()。

我們列印出其中一個方法名。
......
libobjc.A.dylib`defineLockOrder() at objc-os.mm:674
......
然後我們去defineLockOrder()方法中打個斷點,跟蹤一波。
__attribute__((constructor)) static void defineLockOrder() {
    ......
    SideTableLocksPrecedeLock(&crashlog_lock);
    ......
}
void SideTableLocksPrecedeLock(const void *newlock) {
    SideTables().precedeLock(newlock);
}
然後會進入這個方法。
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
你會發現這裡已經在使用SideTableBuf了,說明SideTableBuf肯定提前被賦值了。而我們剛才說了SideTableInit()方法呼叫是C++的initializer呼叫之後,這也就是註釋說的內容。
白話文翻譯一下:通過SideTableInit()來初始化SideTable是不對的,因為在SideTableInit()之前會先執行C++的initializer,而在那個時候就已經用到SideTable了,所以我們才用靜態全域性變數來初始化SideTable,檔案被載入就會初始化。

你會在runtime原始碼中經常看到這樣的程式碼,其實剛才說到:C++的initializer呼叫階段也用到了。

SideTable &table = SideTables()[this];
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

所以我們很有必要理解這句話什麼意思。StripedMap是一個模版類,熟悉C++的應該非常熟悉這個,來看看StripedMap<SideTable>會生成怎樣的一個類。

//簡化版本,巨集啥的都替換了
class StripedMap {
    //存SideTable的結構體
    struct PaddedT {
        SideTable value;
    };
    PaddedT array[64];
    //取得p的雜湊值,p就是例項物件的地址
    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % 64;
    }
public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap< SideTable >>(this)[p]; 
    }
    ...
}

這樣一來就很清晰了,StripedMap裡面有一個PaddedT陣列,StripedMap過載了[]符號,根據引數的雜湊值取PaddedT陣列的內容,陣列裡存的就是SideTable。
現在來理解reinterpret_cast什麼意思。

reinterpret_cast:轉換一個指標為其它型別的指標等,我們沒必要去深究,這樣理解就夠了。

所以

SideTable &table = SideTables()[this];
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
意思就是:將SideTableBuf轉換為StripedMap<SideTable>*型別並返回,也就
是把SideTableBuf當成StripedMap使用,這也是為什麼要寫
alignas(StripedMap<SideTable>)的原因,這樣SideTableBuf陣列每一個元素都正好
對應一個StripedMap<SideTable>物件。

這裡要特別注意了,會有雜湊衝突嗎?

我們建立兩個不同的類Person和Car,列印一下通過indexForPointer得到的雜湊值。
雜湊值計算公式:((addr >> 4) ^ (addr >> 9)) % 64;addr就是例項物件的地址。這個公式歲隨便寫的吧,看不出啥端倪。

Person *one = [[Person alloc] init];
NSLog(@"%p",one);//0x60200000bf30 105690555268912
indexForPointer(105690555268912) = 44;

Car *two = [[Car alloc] init];
NSLog(@"%p",two);//0x6030002c9710 105759277618960
indexForPointer(105759277618960) = 58;

計算出來的雜湊值確實是不一樣的,我們可以手動更改雜湊演算法把雜湊值都設定為1,看看程式是否能正常執行。
也就是更改這個方法。
static unsigned int indexForPointer(const void *p) {
      return 1;
}

然後我們列印one和two的retainCount看是否正確。
[one retain];
NSLog(@"%d",[one retainCount]);//2 
[two retain];
NSLog(@"%d",[two retainCount]);//2
看來都沒問題,那麼系統是怎麼解決雜湊衝突併成功的進行存取值的呢?我們下面講。

進入正題

下面我們就開始看看物件的引用計數到底存哪裡了。我先把判斷優先順序寫一下。

1:物件是否是Tagged Pointer物件;
2:物件是否啟用了Non-pointer;
3:物件未啟用Non-pointer。

滿足1則不判斷2,依次類推。

Tagged Pointer物件

retain時。

id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
    if (isTaggedPointer()) return (id)this;
    ...
}

release時。

bool  objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
    if (isTaggedPointer()) return false;
    ...
}

retainCount時。

uintptr_t objc_object::rootRetainCount() {
    if (isTaggedPointer()) return (uintptr_t)this;
    ...
}

由此可見對於Tagged Pointer物件,並沒有任何的引用計數操作,引用計數數量也只是單純的返回自己地址罷了。

開啟了Non-pointer

retain時。

id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
    ...
    //其實就是對isa的extra_rc變數進行+1,前面說到isa會存很多東西
    addc(newisa.bits, 1, 0, &carry);
    ...
}

release時。

bool  objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
    ...
    //其實就是對isa的extra_rc變數進行-1
    subc(newisa.bits, 1, 0, &carry);
    ...
}

retainCount時。

uintptr_t objc_object::rootRetainCount() {
    ...
    //其實就是獲取isa的extra_rc值再+1,alloc新建一個物件時bits.extra_rc為0並不是1,這個要注意。
    uintptr_t rc = 1 + bits.extra_rc;
    ...
}

如果物件開啟了Non-pointer,那麼引用計數是存在isa中的,引用計數超過255將附加SideTable輔助儲存。
更新:看網路上該系列文章時發現自己漏了一個細節,那就是extra_rc是有儲存限制,經過測試為255,如果超過255將會附加SideTable輔助儲存。詳細解釋看這裡

未開啟Non-pointer isa

這個是最麻煩的,因為要用到SideTable,裡面一大堆邏輯;我們拿上面的Person舉例,請記住物件的地址。
retain時。

id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
    ...
   sidetable_retain();
    ...
}
id objc_object::sidetable_retain() {
    SideTable& table = SideTables()[this];
}

在這裡不得不講清楚SideTable的內部實現了,如果不講清楚則沒辦法繼續看程式碼。

SideTable中有三個結構體。
spinlock_t:鎖,這個就不用說了,一個支援多執行緒環境執行的庫肯定得考慮這個;
weak_table_t:weak表就是這個,用來處理弱引用的,不過本文不講;
RefcountMap:引用表,引用計數就是這個存的,這個要好好的說明白。

RefcountMap的定義:
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
又臭又長,本來想把類展開出來的,但是發現類會非常的大,而且很難懂;所以我這裡講一下邏輯就可以了,你感興趣可以深入看看。
當我們第一次通過SideTables()[this]取得table時,這個table中refcnts內容是空的。
(我們省略spinlock_t和weak_table_t):
SideTable table = {
    ...
    RefcountMap refcnts = {  
        BucketT *Buckets = NULL;
        unsigned NumEntries = 0;
        unsigned NumTombstones = 0;
        unsigned NumBuckets = 0;
    }
    ...
}
接下來程式會執行size_t &refcntStorage = table.refcnts[this];這句話是在幹嘛呢?
RefcountMap繼承自DenseMapBase,DenseMapBase中過載了[]操作符,所以會進入[],下面的程式碼是我展開後的部分程式碼,方便理解。
class DenseMapBase {
    ...
    //目的很明確,就是取得一個pair<DisguisedPtr<objc_object>, size_t>
    size_t &operator[](DisguisedPtr<objc_object> &&Key) {
        pair<DisguisedPtr<objc_object>, size_t> &reslut = FindAndConstruct(Key);
        return reslut.second;
    }
    ...
}
這裡要注意一個細節,因為我們傳進來的是this,而這裡是用DisguisedPtr<objc_object>來接收的。
所以會觸發DisguisedPtr的初始化方法,所以this被轉成了下面的物件。
class DisguisedPtr Key =  {
    uintptr_t  value = 18446638383154282704;
}
下面來看FindAndConstruct()做了什麼。
pair<DisguisedPtr<objc_object>, size_t>& FindAndConstruct(const DisguisedPtr<objc_object> &Key) {
    //先定義一個用於接收結果的pair物件,關於pair我們肯定很熟悉了,相當於字典的一個key-value對,
    //pair.first就是例項物件地址轉換成的DisguisedPtr< objc_object >類,pair.second就是這個物件的引用計數數量。
    pair<DisguisedPtr<objc_object>, size_t> *TheBucket = nil;
    //如果找到了直接返回,因為TheBucket會在LookupBucketFor中被賦值
    if (LookupBucketFor(Key, TheBucket))
        return *TheBucket;
    //沒有找到,就插入一個
    return *InsertIntoBucket(Key, ValueT(), TheBucket);
}
//看看能不能找到key對於的pair<DisguisedPtr<objc_object>, size_t>
bool LookupBucketFor(const LookupKeyT &Val,
                             const BucketT *&FoundBucket) const {
    const  pair<DisguisedPtr<objc_object>, size_t> *BucketsPtr = getBuckets();
    const unsigned NumBuckets = getNumBuckets();
    ......  
    (程式碼還是不貼出來了,我相信你也不想看,總結起來就是從buckets()中找到該例項物件對應的pair<DisguisedPtr<objc_object>, size_t>)
}
總結:
1:取table時我們知道了物件的雜湊值是可能一樣的,如果雜湊值一樣那麼會得到相同的table;
2:相同的table又會根據物件轉換成的DisguisedPtr<objc_object>物件在buckets中去取pair<DisguisedPtr<objc_object>, size_t>對;如果沒有就會插入一條;
插入到buckets哪個位置呢?雜湊演算法如下。
static inline uint32_t ptr_hash(uint64_t key) {
    key ^= key >> 4;
    key *= 0x8a970be7488fda55;//這個是隨意寫的吧,沒發現啥特別的
    key ^= __builtin_bswap64(key);
    return (uint32_t)key;
}
為此我依然做了一個測試,我把ptr_hash的值都返回1,模擬雜湊衝突。
static inline uint32_t ptr_hash(uint64_t key) {
    return (uint32_t)1;
}
然後我們列印one和two的retainCount看是否正確。
[one retain];
NSLog(@"%d",[one retainCount]);//2 
[two retain];
NSLog(@"%d",[two retainCount]);//2
經過測試依然是正確的,說明內部會解決雜湊衝突,也說明了這個雜湊演算法並不能產生唯一的值。這也就解決了
上面留下的問題,獲取table的時候沒有解決雜湊衝突,而是在查詢pair對的時候有解決雜湊衝突,方法就是
找到下一個可用的位置,這也是很常見的雜湊衝突解決方法;另外一個方法是用連結串列存所有雜湊值一樣的value,不過系統在這裡並沒有用這種方法。
3:這個buckets的大小是會動態改變的,這也是
RefcountMap refcnts = {  
    BucketT *Buckets = NULL;
    unsigned NumEntries = 0;
    unsigned NumTombstones = 0;
    unsigned NumBuckets = 0;
}中後三個變數的作用;裝逼點的說法就是讓陣列具有伸縮性,提前處理一些臨界值情況。

所以我們可以把refcnts中的Buckets看成一個陣列,根據物件地址產生的雜湊值和雜湊衝突演算法肯定能在Buckets中找到其對應的pair;我們接著往下走。

id objc_object::sidetable_retain() {
    ...
    //取得該例項物件在該table中對應pair<DisguisedPtr<objc_object>, size_t>對中size_t的引用,預設值為0。
    size_t &refcntStorage = table.refcnts[this];
    //SIDE_TABLE_RC_PINNED的值在64位系統為1<<63,也就是pow(2,63)
    if ((refcntStorage & SIDE_TABLE_RC_PINNED) == false) {
        //SIDE_TABLE_RC_ONE的值為4,為什麼要以4為單位,我不知道,估計是控制最大引用計數的值吧。
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    return (id)this;
}

我們知道了refcntStorage值最大為pow(2,63)-4,因為再加refcntStorage & SIDE_TABLE_RC_PINNED就為false了。
而引用計數最大為(pow(2,63) - 4) >> 2  + 1 = pow(2,61) = 2305843009213693952,為什麼要+1?後面會說。

還是需要資料說話,不然有人不相信。
unsigned long number = SIDE_TABLE_RC_PINNED;//pow(2,63)
unsigned long first = (unsigned long)pow(2, 63) - 4;
unsigned long second = (unsigned long)pow(2, 63);
unsigned long max = first >> 2;
unsigned long max111 = (unsigned long)pow(2, 61);
(lldb) po second & number
9223372036854775808
(lldb) po first & number
0
(lldb) po max
2305843009213693951
(lldb) po max111
2305843009213693952

retain我們就說完了,其實release也是這樣的邏輯。
release時。

前面的邏輯一樣,拿到例項物件對應的pair。
uintptr_t objc_object::sidetable_release(bool performDealloc) {
    ...
    //迭代器,it指向的就是pair<DisguisedPtr<objc_object>, size_t>
    RefcountMap::iterator it = table.refcnts.find(this);
    ...
    //SIDE_TABLE_RC_ONE = 4
    it->second -= SIDE_TABLE_RC_ONE;
    ...
}

retainCount時。

uintptr_t objc_object::sidetable_retainCount() {
    SideTable& table = SideTables()[this];
    //這就是上面為什麼說要+1的原因
    size_t refcnt_result = 1;
    //迭代器,it指向的就是pair<DisguisedPtr<objc_object>, size_t>
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        //SIDE_TABLE_RC_SHIFT = 2
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    ...
}

總結

這篇文章主要是講了引用計數存在哪裡,這只是龐大的引用計數管理中一個小的細節;引用計數管理還有其他的問題,比如。

1:系統怎麼存weak變數,什麼時候把weak變數置為nil的?
2:autorelease、autoreleasepool以及RunLoop怎麼合作的?
3:ARC相對於MRC多出了什麼內容?
......

後面有時間我會寫成文章的。

後記

這篇文章我用了兩天的時間完成,自己仔細讀了三次,本人是一枚菜鳥,如果你在閱讀過程中發現不嚴謹或者錯誤的地方還請指出來;感激不盡。

相關文章