iOS 內(nèi)存管理底層分析(一)- 內(nèi)存相關(guān)

相關(guān)文獻:
iOS 內(nèi)存管理底層分析(一)- 內(nèi)存相關(guān)
iOS 內(nèi)存管理底層分析(二)- AutoreleasePool底層

本文掌握知識點:
1.內(nèi)存的五大分區(qū)
2.內(nèi)存管理方案:MRC跋涣、ARC、TaggedPointer鸟悴、nonpointer_isa陈辱、SideTables、自動釋放池
3.weak_table_t 弱引用表底層原理细诸、__weak的底層原理沛贪、弱引用對象的引用計數(shù)問題
4.retain、release震贵、dealloc 的源碼分析

一利赋、內(nèi)存的五大分區(qū)

  • 堆區(qū)
    堆是向高地址擴展的數(shù)據(jù)結(jié)構(gòu);是不連續(xù)的內(nèi)存區(qū)域猩系,類似于鏈表結(jié)構(gòu)(便于增刪媚送,不便于查詢),遵循先進先出(FIFO)原則寇甸;通常以 alloc/new/malloc 方式創(chuàng)建的對象塘偎。內(nèi)存地址以0x6開頭。
    優(yōu)點:靈活方便拿霉,數(shù)據(jù)適應(yīng)面廣泛吟秩。
    缺點:需手動管理,速度慢绽淘、容易產(chǎn)生內(nèi)存碎片涵防。

  • 棧區(qū)
    棧是系統(tǒng)數(shù)據(jù)結(jié)構(gòu),其對應(yīng)的進程或線程是唯一的沪铭;棧是向低地址擴展數(shù)據(jù)結(jié)構(gòu)壮池,是一塊連續(xù)的存儲區(qū)域,遵循先進后出(FILO)的原則杀怠。堆區(qū)的分配一般是在運行時分配火窒;存儲局部變量、方法參數(shù)驮肉、對象的指針等熏矿。內(nèi)存地址以0x7開頭。
    優(yōu)點:棧是由編譯器自動分配并釋放的,不會產(chǎn)生內(nèi)存碎片票编,所以快速高效褪储,便于查詢,不便于增刪慧域。
    缺點:內(nèi)存大小有限制鲤竹,數(shù)據(jù)不靈活主線程棧大小是1MB,子線程棧大小是512KB昔榴。

  • 全局區(qū)(靜態(tài)區(qū))
    全局區(qū)是編譯時分配的內(nèi)存空間辛藻,程序運行過程中,此內(nèi)存中的數(shù)據(jù)一直存在互订,程序結(jié)束后由系統(tǒng)釋放吱肌,主要存放:未初始化的全局變量和靜態(tài)變量,即bss區(qū)(.bss)仰禽;已初始化的全局變量和靜態(tài)變量氮墨,即數(shù)據(jù)區(qū)(.data)。內(nèi)存地址以0x1開頭吐葵。
    其中规揪,全局變量是指變量值可以在運行時被動態(tài)修改,而靜態(tài)變量是static修飾的變量温峭,包含靜態(tài)局部變量和靜態(tài)全局變量猛铅。

  • 常量區(qū)
    常量區(qū)是編譯時分配的內(nèi)存空間,在程序結(jié)束后由系統(tǒng)釋放凤藏,主要存放 已經(jīng)使用了的奕坟,且沒有指向的字符串常量

  • 代碼區(qū)
    代碼區(qū)是編譯時分配主要用于存放程序運行時的代碼,代碼會被編譯成二進制存進內(nèi)存的

內(nèi)存五大區(qū)的驗證:

內(nèi)存布局

介紹了內(nèi)存的五大區(qū)清笨,但其實除了內(nèi)存區(qū),還有內(nèi)核區(qū)保留區(qū)刃跛。
以4GB手機為例抠艾,如下所示,系統(tǒng)將其中的3GB給了五大區(qū)+保留區(qū)桨昙,剩余的1GB給內(nèi)核區(qū)使用:

  • 內(nèi)核區(qū):系統(tǒng)用來進行內(nèi)核處理操作的區(qū)域
  • 五大區(qū):上面已說明
  • 保留區(qū):預(yù)留給系統(tǒng)處理nil等

為什么五大區(qū)的最后內(nèi)存地址是從0x00400000開始的检号?
主要原因是0x00000000表示nil,不能直接用nil表示一個段蛙酪,所以單獨給了一段內(nèi)存用于處理nil等情況齐苛。

內(nèi)存布局面試題
面試題:全局變量和局部變量在內(nèi)存中是否有區(qū)別?如果有桂塞,是什么區(qū)別凹蜂? 答案是有區(qū)別。

  • 全局變量保存在內(nèi)存的全局存儲區(qū)(即bss+data段),占用靜態(tài)的存儲單元玛痊;
  • 局部變量保存在中汰瘫,只有在所在函數(shù)被調(diào)用時才動態(tài)的為變量分配存儲單元。

二擂煞、內(nèi)存管理方案

  • MRC - 手動引用計數(shù)
  • ARC - 自動引用計數(shù)
  • nonpointer_isa - 新版OC對象的isa指針優(yōu)化
  • TaggedPointer - 小對象優(yōu)化
  • SideTables - 散列表 (引用計數(shù)表和弱引用表)
  • autoreleasePool - 自動釋放池

三混弥、MRC 與 ARC (引用計數(shù))

引用計數(shù)是管理對象聲明周期的一種方式。當新建一個對象它的引用計數(shù)為1对省,當這個對象引用計數(shù)為0的時候蝗拿,這個對象就會被銷毀并釋放其所占用的內(nèi)存空間。

MRC

在MRC時代蒿涎,系統(tǒng)是通過對象的引用計數(shù)來判斷一個是否銷毀哀托,有以下規(guī)則:

  • 對象被創(chuàng)建時引用計數(shù)都為1;
  • 當對象被其他指針引用時同仆,需要手動調(diào)用[objc retain]萤捆,使對象的引用計數(shù)+1;
  • 當指針變量不再使用對象時俗批,需要手動調(diào)用[objc release]來釋放對象俗或,使對象的引用計數(shù)-1;
  • 當一個對象的引用計數(shù)為0時岁忘,系統(tǒng)就會銷毀這個對象辛慰。
ARC

ARC模式是在WWDC2011iOS5引入的自動管理機制,即自動引用計數(shù)干像。是編譯器的一種特性帅腌。其規(guī)則與MRC一致,區(qū)別在于無需程序員手動插入內(nèi)存管理相關(guān)代碼麻汰。

結(jié)論:
1.在MRC模式下速客,必須遵守:誰創(chuàng)建,誰釋放五鲫,誰引用溺职,誰管理。
2.ARC模式不需要手動retain位喂、release浪耘、autorelease,編譯器會在適當?shù)奈恢貌迦?code>release和autorelease塑崖。

四七冲、nonpointer_isa - isa指針優(yōu)化

nonpointer_isa:非指針類型的isa,主要是在創(chuàng)建對象時规婆,用來優(yōu)化isa指針的64位地址澜躺,具體內(nèi)容在Objective-C 對象的底層探索蝉稳。

我們知道在創(chuàng)建OC對象的時候,會初始化一個8字節(jié)的isa指針指向該OC對象的類對象苗踪。在舊版本的OC對象的isa指針主要記錄著對象的引用計數(shù)颠区,很顯然僅僅是記錄這就使用8字節(jié)(64位)是非常奢侈的。于是在新版本的OC對象對isa的64位進行了優(yōu)化通铲。

nonPointerIsa

五毕莱、TaggedPointer - 小對象

TaggedPointer:是一個被打上標記的指針,在棧上分配8字節(jié)指針(不再需要堆去分配)颅夺,該指針指向的不再是地址朋截,而是真實值。TaggedPointer專門用來處理小對象吧黄,例如NSNumber部服、NSDate、小NSString等拗慨。(它是在64位iOS系統(tǒng)下提出來的 iPhone5s以后)

NSString為例廓八,運行下面代碼打印:

    NSString *firstString = @"helloworld"; // __NSCFConstantString 常量區(qū)
    NSString *secondString = [NSString stringWithFormat:@"helloworld"]; // __NSCFString 堆區(qū)
    NSString *thirdString = @"hello"; // __NSCFConstantString 常量區(qū)
    NSString *fourthSting = [NSString stringWithFormat:@"hello"]; // NSTaggedPointerString 棧指針

    NSLog(@"%p %@",firstString,[firstString class]); // 0x1058d60c0 __NSCFConstantString
    NSLog(@"%p %@",secondString,[secondString class]); // 0x600000b7b960 __NSCFString
    NSLog(@"%p %@",thirdString,[thirdString class]); // 0x1058d60e0 __NSCFConstantString
    NSLog(@"%p %@",fourthSting,[fourthSting class]); // 0xd9d08f5a3bd7e123 NSTaggedPointerString

注意:此時打印NSTaggedPointerString類型指針的內(nèi)容0xd9d08f5a3bd7e123其實是混淆過的赵抢。在下文探討 小對象地址分析 時會介紹剧蹂。

NSString的內(nèi)存管理主要分為3種:

  • __NSCFConstantString:字符串常量,是一種編譯時常量烦却,retainCount值很大宠叼,對其操作不會引起引用計數(shù)變化,存儲在字符串常量區(qū)其爵。

  • __NSCFString:是在運行時創(chuàng)建的NSString子類冒冬,創(chuàng)建后引用計數(shù)會加1,存儲在堆上摩渺。

  • NSTaggedPointerString:標簽指針简烤,是蘋果在64位環(huán)境下對NSString、NSNumber等對象做的優(yōu)化摇幻。
    對于NSString對象來說俱诸,當字符串是由數(shù)字登失、英文字母組合且長度 <= 9時定欧,會自動成為NSTaggedPointerString類型实苞,存儲在常量區(qū)歹篓;當有中文 或者 其他特殊符號 或 長度 > 9時吗铐,會直接成為__NSCFString類型溉瓶,存儲在堆區(qū)万矾。

1.小對象地址分析

以NSString為例:

  • 對于一般的NSString對象指針伤疙,都是string值 + 指針地址银酗,兩者是分開的辆影;
  • 對于TaggedPointer指針,是指針 + 值黍特,都能在小對象中體現(xiàn)蛙讥。所以TaggedPointer 既包含指針,也包含值灭衷。

上文中提到過NSLog(@"%p %@",fourthSting,[fourthSting class]);打印出來的取地址的內(nèi)容是混淆過的次慢。

這是因為objc4-838.1源碼類的加載過程中的_objc_init -> map_images -> _read_images -> initializeTaggedPointerObfuscator

小對象進行了混淆處理

我們可以在源碼中通過objc_debug_taggedpointer_obfuscator查找taggedPointer編碼和解碼

#define OBJC_TAG_INDEX_MASK 0x7UL
#define OBJC_TAG_INDEX_SHIFT 0

extern uintptr_t objc_debug_taggedpointer_obfuscator;
extern uint8_t objc_debug_tag60_permutations[8];

uintptr_t objc_obfuscatedTagToBasicTag(uintptr_t tag) {
    for (unsigned i = 0; i < 7; i++)
        if (objc_debug_tag60_permutations[i] == tag)
            return i;
    return 7;
}

uintptr_t
objc_decodeTaggedPointer(id ptr)
{
    uintptr_t value = (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    uintptr_t basicTag = (value >> OBJC_TAG_INDEX_SHIFT) & OBJC_TAG_INDEX_MASK;

    value &= ~(OBJC_TAG_INDEX_MASK << OBJC_TAG_INDEX_SHIFT);
    value |= objc_obfuscatedTagToBasicTag(basicTag) << OBJC_TAG_INDEX_SHIFT;
    return value;
}

static inline uintptr_t objc_basicTagToObfuscatedTag(uintptr_t tag) {
    return objc_debug_tag60_permutations[tag];
}

void *
objc_encodeTaggedPointer(uintptr_t ptr)
{
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);

    uintptr_t basicTag = (value >> OBJC_TAG_INDEX_SHIFT) & OBJC_TAG_INDEX_MASK;
    uintptr_t permutedTag = objc_basicTagToObfuscatedTag(basicTag);
    value &= ~(OBJC_TAG_INDEX_MASK << OBJC_TAG_INDEX_SHIFT);
    value |= permutedTag << OBJC_TAG_INDEX_SHIFT;
    return (void *)value;
}

于是我們就可以通過調(diào)用objc_decodeTaggedPointer來還原真實指針內(nèi)容:

其中最高位是標記是否是TaggedPointer,最低三位是看是什么類型翔曲,這里010的十進制是2 表示NSString迫像,如下圖:

可以通過objc4源碼中的_objc_makeTaggedPointer方法的參數(shù)tag類型objc_tag_index_t進入其枚舉,其中 2表示NSString瞳遍,3表示NSNumber

舉例NSNumber

2.taggedpointer面試題
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    self.queue = dispatch_queue_create("com.wj.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"WJ"]; // 棧(長度<=9)
            NSLog(@"%@",self.nameStr);
        });
    }
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"安安安安"]; // 堆(中文字符)
            NSLog(@"%@",self.nameStr);
        });
    }
}

結(jié)果:引發(fā)崩潰闻妓,崩潰在第二個for循環(huán)里。
原因:在ARC環(huán)境下掠械,這里使用多線程有可能引發(fā)對同一塊堆內(nèi)存多次調(diào)用了release由缆。

六、SideTables - 散列表

SideTables:散列表猾蒂,在散列表中主要有兩個表均唉,分別是引用計數(shù)表弱引用表。(每次在訪問SideTables時的加鎖解鎖操作婚夫,會降低效率的)

SideTable

但是散列表不是一張表浸卦,而是多張表。
打開objc4-838.1源碼案糙,找到objc_retain -> retain -> rootRetain 這里在本章節(jié)的 七限嫌、retain源碼分析 部分會著重介紹,有興趣可以滑下去看时捌。主要內(nèi)容就是引用計數(shù)的存儲方案怒医。找到 sidetable_addExtraRC_nolock

sidetable_addExtraRC_nolock

它是從多張表SideTables里獲取該對象的SideTable
SideTables其實是StripedMap類型:

SideTables()
StripedMap

多張表并不是無限數(shù)量的奢讨,蘋果設(shè)計出: SideTables在真機下8張表/模擬器下64張表 的方案稚叹,以達到運行效率和節(jié)省內(nèi)存平衡目的

提問一:那我可以設(shè)計只有一張全局的表來存儲所有對象的引用計數(shù)和弱引用信息嗎拿诸?
答案:可以扒袖。但是每次讀寫對象的時候都得操作這張全局表,而這張全局每次讀寫都需要頻繁地加速/解鎖操作亩码,這樣會導(dǎo)致系統(tǒng)非常地慢季率。

提問二:那我可以設(shè)計每一個對象都有它單獨的表來存儲自己的引用計數(shù)和弱引用信息嗎?
答案:可以描沟。但是每創(chuàng)建一個對象會就會產(chǎn)生一張表飒泻,則會產(chǎn)生好多的表鞭光,引發(fā)大量消耗內(nèi)存的問題。

1.RefcountMap - 引用計數(shù)表

RefcountMap是用來存儲引用計數(shù)的泞遗。(RefcountMap在一般情況下是用不到的)惰许。
這里的內(nèi)容在本文章節(jié) 七、retain源碼分析 里有分析史辙,這里只給總結(jié):

  • a.當isa指針不是nonpointer_isa類型的時候汹买,該對象的引用計數(shù)就存儲在SideTable里的RefcountMap;
  • b.當isa指針nonpointer_isa類型,并且nonpointer_isa里的extra_rc存滿了髓霞,會把另一半的引用計數(shù)存儲到SideTable里的RefcountMap里卦睹,而extra_rc只有原來的一半。
2.weak_table_t - 弱引用表(weak底層原理)
UIViewController *oldVC = [UIViewController new];
__weak typeof(oldVC) weakVC = oldVC; // 舊的
UIViewController *newVC = [UIViewController new];
weakVC = newVC; // 新的

接下來看看weak底層邏輯吧方库。

使用__weak修飾的指針指向一個對象時结序,會走源碼中的objc_initWeak
打開objc4-838.1源碼纵潦,搜索objc_initWeak函數(shù)

objc_initWeak

storeWeak函數(shù)就是處理__weak修飾的弱對象指向一個對象的處理:

// Update a weak variable.
// If HaveOld is true, the variable has an existing value 
//   that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be 
//   assigned into the variable. This value might be nil.
// If CrashIfDeallocating is true, the process is halted if newObj is 
//   deallocating or newObj's class does not support weak references. 
//   If CrashIfDeallocating is false, nil is stored instead.
enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
// HaveOld: weak指針是否之前就指向了對象 即weakVC是否指向過oldVC
// HaveNew: weak指針是否將指向的新的對象 即weakVC是否將要指向newVC
// CrashIfDeallocating: 被弱引用的對象是否正在析構(gòu)徐鹤,如果析構(gòu)則Crash
template <HaveOld haveOld, HaveNew haveNew,
          enum CrashIfDeallocating crashIfDeallocating>
// location: 弱引用指針的地址 即weakVC的地址
// newObj: 將被弱引用指針指向的對象 即newVC對象
static id 
storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj; // 獲取 weakVC之前指向的對象oldVC,若沒有指向過邀层,則為nil
    SideTable *oldTable; // 獲取 weakVC之前指向的對象oldVC的散列表返敬,若沒有指向過,則為nil
    SideTable *newTable; // 獲取 weakVC將被弱引用的對象newVC的散列表

    // 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) { // 如果weakVC曾經(jīng)指向過oldVC寥院,獲取 oldVC對象 和 oldVC散列表
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) { // 如果weakVC將要指向newVC劲赠,獲取 newVC散列表
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
     
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable); // 加鎖
    
    // 排除異常情況(不用管):如果weakVC曾經(jīng)指向過oldVC,并且weakVC指向的還不是oldVC
    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.
    //  排除異常情況(不用管):newVC的類對象還沒有被初始化秸谢,就去初始化
    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) { // 如果weakVC曾經(jīng)指向過oldVC凛澎,則把之前的弱引用注銷掉
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    // weakVC去注冊新的弱引用,并指向newVC
    if (haveNew) {
        newObj = (objc_object *)
            // 怎么注冊估蹄?將weakVC的地址存儲到SideTable里weak_table_t里去
            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.
        // 如果不是TaggedPointer或nil塑煎,將isa里的是否被弱引用weakly_referenced 置為true
        if (!_objc_isTaggedPointerOrNil(newObj)) {
            newObj->setWeaklyReferenced_nolock(); // 將isa的是否被弱引用置為true
        }

        // Do not set *location anywhere else. That would introduce a race.
       // 將weakVC的地址保存newVC
        *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底層調(diào)用:objc_initWeak -> storeWeak

storeWeak的底層邏輯:

  • 1.如果weak指針已經(jīng)指向了一個A對象,則會把weak指針地址SideTable散列表里的弱引用表weak_table_t中注銷weak_unregister_no_lock臭蚁;
  • 2.如果weak指針要 改變指向/指向新的 B對象最铁,則會把weak指針地址注冊進SideTable散列表里的弱引用表weak_table_tweak_register_no_lock,并且將 B對象的類對象的isa里的weakly_referenced置為true垮兑,還會將weak指針指向B對象冷尉。

下面就來研究如何注冊和注銷的。

3.了解weak指針在弱引用表weak_table_t是如何注銷和注冊的(探索weak_unregister_no_lock系枪、weak_register_no_lock)雀哨。
3.1了解weak_table_t的數(shù)據(jù)結(jié)構(gòu)
struct weak_table_t {
    // weak_entry_t是一個hash表,key:當前對象的地址嗤无,value: 存儲弱引用指針的地址的數(shù)組
    weak_entry_t *weak_entries; // (因為一個對象可以被多個弱引用去指向)
    size_t    num_entries; // 個數(shù)
    uintptr_t mask; // 數(shù)組的長度-1(擴容相關(guān))
    uintptr_t max_hash_displacement; // 解決hash沖突
};

// hash數(shù)組
struct weak_entry_t {
    // referent是一個動態(tài)的hash數(shù)組震束,存儲弱引用指針的地址的數(shù)組
    DisguisedPtr<objc_object> referent;
    union {
        // 1.當前對象被弱引用個數(shù)>4時,就會用這個struct來存儲弱引用指針的地址
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        // 2.當前對象被弱引用個數(shù)<=4時当犯,就會用weak_referrer_t來存儲弱引用指針的地址
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    // out_of_line用來判斷垢村,用哪種1/2方式來存儲弱引用指針的地址
    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的構(gòu)造函數(shù)
    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;
        }
    }
};

注意:SideTable在真機下有8張表,所以weak_table_t也有8張表嚎卫。

weak_table_t是一個hash表嘉栓,key:當前對象的地址;value:存儲弱引用指針的地址的數(shù)組拓诸。為什么是數(shù)組侵佃?因為一個對象可以同時被多個弱引用去指向。

weak_table_t 數(shù)據(jù)結(jié)構(gòu)類似于:

 // [key: value]
{
  A對象的地址: [weak1地址, weak2地址],
  B對象的地址: [weak3地址, weak4地址],
  ...
}
3.2 weak_register_no_lock將弱引用注冊到對象的弱引用表weak_table_t
weak_register_no_lock

weak_register_no_lock的底層邏輯:

  • a.對TaggedPointer和nil不處理奠支;
  • b.通過被引用對象的地址去取出weak_entry_t哈希數(shù)組馋辈,如果有哈希數(shù)組,則直接將弱引用指針地址存入這個哈希數(shù)組倍谜;
  • c.如果沒有哈希數(shù)組迈螟,則創(chuàng)建一個,再將弱引用指針地址插入到哈希數(shù)組尔崔。
3.3 weak_unregister_no_lock將弱引用從對象的弱引用表weak_table_t中注冊
weak_unregister_no_lock

weak_unregister_no_lock的底層邏輯:

  • a.通過被引用對象的地址去取出weak_entry_t哈希數(shù)組答毫,如果有哈希數(shù)組,則將弱引用指針地址從哈希數(shù)組中移除季春。倘若哈希數(shù)組空了洗搂,則需要去清空這個對象的弱引用表weak_table_t
weak面試題:(弱引用對象的引用計數(shù)問題)

為什么weakVC打印引用計數(shù)是2呢载弄?

UIViewController *oldVC = [UIViewController new];
__weak typeof(oldVC) weakVC = oldVC; // 舊的
UIViewController *newVC = [UIViewController new];
weakVC = newVC; // 新的
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(newVC)));//1 斷點這
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(weakVC)));//2 斷點這

打開匯編調(diào)試Debug->Debug Workflow->Always Show Disassembly

當打印強引用newVC的引用計數(shù)時耘拇,可以看到匯編會調(diào)用CFGetRetainCount,而打印弱引用weakVC的引用計數(shù)時候侦锯,則會調(diào)用objc_loadWeakRetained

打開objc4-838.1源碼驼鞭,找到objc_loadWeakRetained

objc_loadWeakRetained

注意:在打印弱引用weakVC的引用計數(shù)時候,會對obj進行引用計數(shù)+1的操作尺碰,但是由于obj是一個局部變量挣棕,出了函數(shù)域則會引用計數(shù)-1。
每次打印引用weakVC的引用計數(shù)都是2亲桥,其實是一個假象而已洛心。

七、retain源碼分析

打開objc4-838.1源碼题篷,搜索objc_retain函數(shù)

objc_retain
retain()

rootRetain函數(shù)就是處理對象的引用計數(shù)的邏輯:

ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    // 先去獲取對象的isa指針词身,因為引用計數(shù)信息存儲在isa里
    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_retain()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                return swiftRetain.load(memory_order_relaxed)((id)this);
            }
            return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return (id)this;
        }
    }

    // 核心邏輯在這個do...while!7丁法严!
    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        // 1.不是nonpointer_isa的情況损敷,sidetable存儲引用計數(shù)
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        // 下面代碼邏輯 是nonpointer_isa的情況
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        // 2.是nonpointer_isa的情況,引用技術(shù)位extra_rc存得下深啤,直接+1
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        // 3.是nonpointer_isa的情況拗馒,引用技術(shù)位extra_rc存不下,extra_rc保留一半的引用計數(shù)溯街,并準備將另一半copy到sidetable
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            // 判斷extra_rc是否超出
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            // extra_rc存不下,保留一半的引用計數(shù)诱桂,并準備將另一半copy到sidetable
            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 (variant == RRVariant::Full) {
        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();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }

    return (id)this;
}

總結(jié):
retain源碼調(diào)用流程:objc_retain --> objc_object::retain() --> objc_object::rootRetain

rootRetain操作邏輯:

  • 1.判斷對象是否為taggedPointer類型,如果是 則 return呈昔;
  • 2.獲取對象的isa指針里取出對象的引用計數(shù)信息挥等;
  • 3.判斷isa是否是nonpointer_isa(通常是nonpointer_isa)
    3.1 如果不是nonpointer_isa,將sidetable散列表里的引用計數(shù)+1并return堤尾。
    3.2 如果是nonpointer_isa肝劲,并且對象正在被釋放,直接return郭宝。
    3.3 如果是nonpointer_isa涡相,對象不是正在被釋放,進入下一步4剩蟀;
  • 4.先讓isa里的extra_rc引用計數(shù)+1催蝗,判斷是否能夠存得下:
    4.1 如果extra_rc位能存得下,就存著育特。
    4.2 如果extra_rc位能存不下(少數(shù)情況才會出現(xiàn))丙号,將has_sidetable_rc標志位為1,extra_rc保留一半的引用計數(shù)缰冤,將另一半的引用計數(shù)存儲到sidetable

提問:為什么要設(shè)計使用extra_rc來存儲引用計數(shù)犬缨,extra_rc存滿了才將另一半的引用計數(shù)存儲到sidetable呢?
因為sidetable每次讀寫都需要加鎖解鎖的操作棉浸,系統(tǒng)就沒有那么快怀薛。這樣設(shè)計的目的是提高性能

八迷郑、release源碼分析

打開objc4-838.1源碼枝恋,搜索objc_release函數(shù)

objc_release
release()

rootRelease函數(shù)就是處理對象的引用計數(shù)的邏輯:

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;

    bool sideTableLocked = false;
    // 獲取對象的isa,因為引用計數(shù)信息存儲在isa里
    isa_t newisa, oldisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_release()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return false;
        }
    }

retry:
    // 引用計數(shù)真正操作邏輯在do...while里
    do {
        newisa = oldisa;
        // 1.如果不是nonpointer_isa嗡害,操作sidetable里的引用計數(shù)-1
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        // 判斷對象是否正在釋放焚碌,如果是,則直接return
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }

        // don't check newisa.fast_rr; we already called any RR overrides
        // 將nonpointer_isa里的引用計數(shù)位extra_rc進行-1操作
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow; // 如果extra_rc的值為0的情況霸妹,走underflow代碼塊
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (slowpath(newisa.isDeallocating()))
        goto deallocate; // 釋放對象內(nèi)存

    if (variant == RRVariant::Full) {
        if (slowpath(sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!sideTableLocked);
    }
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;
    // 如果nonpointer_isa的sidetable標志位 has_sidetable_rc == 1
    if (slowpath(newisa.has_sidetable_rc)) {
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.
        // 將引用計數(shù)從sidetable轉(zhuǎn)移回extra_rc
        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            oldisa = LoadExclusive(&isa.bits);
            goto retry;
        }

        // Try to remove some retain counts from the side table.
        // 把sidetable里的引用計數(shù)移除
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

        // 如果sidetable上沒有多余的東西十电,我們就把sidetable清理干凈
        bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there

        if (borrow.borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            // sidetable保留引用計數(shù)-1。嘗試將它們添加到內(nèi)聯(lián)計數(shù)中。
            bool didTransitionToDeallocating = false;
            newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement too
            newisa.has_sidetable_rc = !emptySideTable;

            bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);

            if (!stored && oldisa.nonpointer) {
                // 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.
                uintptr_t overflow;
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                if (!overflow) {
                    stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
                    if (stored) {
                        didTransitionToDeallocating = newisa.isDeallocating();
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                ClearExclusive(&isa.bits);
                sidetable_addExtraRC_nolock(borrow.borrowed);
                oldisa = LoadExclusive(&isa.bits);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // 減量成功后鹃骂,從sidetable拿回引用計數(shù)給extra_rc台盯。
            if (emptySideTable)
                sidetable_clearExtraRC_nolock();

            if (!didTransitionToDeallocating) {
                if (slowpath(sideTableLocked)) sidetable_unlock();
                return false;
            }
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

deallocate:
    // Really deallocate.

    ASSERT(newisa.isDeallocating());
    ASSERT(isa.isDeallocating());

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

總結(jié):
release源碼調(diào)用流程:objc_release --> objc_object::release() --> objc_object::rootRelease

rootRelease操作邏輯:

  • 1.判斷對象是否為taggedPointer類型,如果是則return畏线;
  • 2.獲取對象的isa指針里取出對象的引用計數(shù)信息爷恳;
  • 3.判斷isa是否是nonpointer_isa(通常是nonpointer_isa)
    3.1 如果不是nonpointer_isa,就去操作sidetable里的引用計數(shù)-1象踊,并return。
    3.2 如果是nonpointer_isa棚壁,并且對象正在被釋放杯矩,直接return。
    3.3 如果是nonpointer_isa袖外,對象不是正在被釋放史隆,進入下一步4;
  • 4.先讓isa里的extra_rc引用計數(shù)-1曼验,判斷extra_rc等于0
    4.1 如果extra_rc!=0時泌射,直接return。
    4.2 如果extra_rc==0時鬓照,判斷has_sidetable_rc等于1
    4.2.1 如果has_sidetable_rc==0熔酷,說明該對象引用計數(shù)全部清零,需要被回收內(nèi)存dealloc豺裆。
    4.2.2 如果has_sidetable_rc==1(少數(shù)情況才會出現(xiàn))拒秘,說明該對象借助sidetable存儲引用計數(shù),將sidetable的引用計數(shù)賦值給extra_rc臭猜,將sidetable的引用計數(shù)清空躺酒,has_sidetable_rc賦值為0。

九蔑歌、dealloc源碼分析

打開objc4-838.1源碼羹应,搜索- (void)dealloc方法

dealloc
_objc_rootDealloc
rootDealloc

rootDealloc去判斷:
1.如果isanonpointer_isa、沒有弱引用次屠、沒有關(guān)聯(lián)對象园匹、沒有析構(gòu)函數(shù)、沒有向sidetable借位劫灶,則去直接釋放free偎肃;
2.否則調(diào)用object_dispose

object_dispose
objc_destructInstance
clearDeallocating

object_dispose的邏輯:
1.如果有c++析構(gòu)函數(shù),去調(diào)用析構(gòu)函數(shù)釋放該對象的實例成員變量浑此;
2.如果有關(guān)聯(lián)對象累颂,去移除關(guān)聯(lián)對象;
3.清除弱引用表散列表里引用計數(shù)信息
4.釋放對象 free

dealloc源碼調(diào)用流程:dealloc --> _objc_rootDealloc --> objc_object::rootDealloc() --> 兩個分支
1. -> free
2. -> object_dispose -> free

dealloc總結(jié):

  • 1.判斷對象是否為taggedPointer類型,如果是則 return紊馏;
  • 2.判斷如果isa是nonpointer_isa料饥、沒有弱引用沒有關(guān)聯(lián)對象朱监、沒有析構(gòu)函數(shù)岸啡、沒有向sidetable借位,則直接釋放對象free赫编。否則進入3巡蘸;
  • 3.如果有c++析構(gòu)函數(shù),去調(diào)用析構(gòu)函數(shù)釋放該對象的實例成員變量擂送;
  • 4.如果有關(guān)聯(lián)對象悦荒,去移除關(guān)聯(lián)對象;
  • 5.清除弱引用表散列表里引用計數(shù)信息嘹吨;
  • 6.釋放對象free搬味。

說到這里本章節(jié)就結(jié)束啦,喜歡的朋友點亮??蟀拷!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碰纬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子问芬,更是在濱河造成了極大的恐慌悦析,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件此衅,死亡現(xiàn)場離奇詭異她按,居然都是意外死亡,警方通過查閱死者的電腦和手機炕柔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門酌泰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人匕累,你說我怎么就攤上這事陵刹。” “怎么了欢嘿?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵衰琐,是天一觀的道長。 經(jīng)常有香客問我炼蹦,道長羡宙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任掐隐,我火速辦了婚禮狗热,結(jié)果婚禮上钞馁,老公的妹妹穿的比我還像新娘。我一直安慰自己匿刮,他們只是感情好僧凰,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著熟丸,像睡著了一般训措。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上光羞,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天绩鸣,我揣著相機與錄音,去河邊找鬼纱兑。 笑死呀闻,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的萍启。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼屏鳍,長吁一口氣:“原來是場噩夢啊……” “哼勘纯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起钓瞭,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤驳遵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后山涡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體堤结,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年鸭丛,在試婚紗的時候發(fā)現(xiàn)自己被綠了竞穷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡鳞溉,死狀恐怖瘾带,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情熟菲,我是刑警寧澤看政,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站抄罕,受9級特大地震影響允蚣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜呆贿,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一嚷兔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦谴垫、人聲如沸章母。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乳怎。三九已至,卻和暖如春前弯,著一層夾襖步出監(jiān)牢的瞬間蚪缀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工恕出, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留询枚,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓浙巫,卻偏偏與公主長得像金蜀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子的畴,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容