內存管理剖析(七)—— weak指針實現原理

iOS引用計數的存儲

我在isa的深入體會一文中介紹過,蘋果從arm64架構開始,對isa進行了優(yōu)化,通過位域計數將更多信息存儲在了isa指針當中,充分利用了isa的內存空間。目前isa的結構如下

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };

其中的extra_rc就是用來存放引用計數的,它使用了isa上面的19個二進制為作為存儲空間。extra_rc這個命名含義是額外的引用計數,也就是除了創(chuàng)建時候的那一次retain操作之外,在其他時刻對象進行過retain操作的次數。因此一個對象實際的引用計數 = extra_rc + 1(創(chuàng)建的那一次)。當然extra_rc能夠表達的數量也是有限的,當對象的引用超過了extra_rc的表示范圍之后,isa內部的has_sidetable_rc,用來指示對象的引用計數無法存儲在isa當中,并且將引用計數的值存放到一個叫SideTable的類的屬性當中。SideTable的定義可以在objc源碼NSObject.mm文件中找到。如下

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
    }

如果isa存不下引用計數的話,那么引用計數就會被存放在SideTablerefcnts中,從類型RefcountMap可以看出,它實際上是一個散列表的結構(類似OC中的字典)。

weak指針實現原理

我們可以給property屬性設置strongweak、unsafe_unretained,轉化到成員變量上分別是__strong、__weak、__unsafe_unretained.下面我們來看一下他們的區(qū)別,我們通過下面幾段代碼案例以及運行結果來逐個說明

*********************mian.m***************************
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"臨時作用域開始");
        {
            CLPerson *person = [[CLPerson alloc] init];
            NSLog(@"person對象:%@", person);
        }
        NSLog(@"臨時作用域結束");
        
        NSLog(@"strongPerson:%@", strongPerson);
            
    }
    return 0;
}


*******************運行結果********************
2019-09-02 19:59:00.835983+0800 Block學習[24021:2941713] 臨時作用域開始
2019-09-02 19:59:00.836482+0800 Block學習[24021:2941713] person對象:<CLPerson: 0x100704a60>
2019-09-02 19:59:00.836541+0800 Block學習[24021:2941713] -[CLPerson dealloc]
2019-09-02 19:59:00.836575+0800 Block學習[24021:2941713] 臨時作用域結束
Program ended with exit code: 0

上面這個案例很清晰的說明,局部變量person在出了臨時作用域之后,就釋放了。


*********************mian.m***************************
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        __strong CLPerson *strongPerson;
        
        NSLog(@"臨時作用域開始");
        {
            CLPerson *person = [[CLPerson alloc] init];
            NSLog(@"person對象:%@", person);
            strongPerson = person;
        }
        NSLog(@"臨時作用域結束");
            
        NSLog(@"strongPerson:%@", strongPerson);
    }
    return 0;
}


*******************運行結果********************
2019-09-02 20:00:07.368972+0800 Block學習[24033:2942509] 臨時作用域開始
2019-09-02 20:00:07.369392+0800 Block學習[24033:2942509] person對象:<CLPerson: 0x1007003c0>
2019-09-02 20:00:07.369430+0800 Block學習[24033:2942509] 臨時作用域結束
2019-09-02 20:00:07.369442+0800 Block學習[24033:2942509] strongPerson:<CLPerson: 0x1007003c0>
2019-09-02 20:00:07.369460+0800 Block學習[24033:2942509] -[CLPerson dealloc]
Program ended with exit code: 0

person被作用域外的__strong指針指向時,可以看到臨時作用域結束之后,person對象并沒有被銷毀,說明__strong指針增加了person的引用計數


*********************mian.m***************************
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        __weak CLPerson *weakPerson;
                
        NSLog(@"臨時作用域開始");
        {
            CLPerson *person = [[CLPerson alloc] init];
            NSLog(@"person對象:%@", person);
            weakPerson = person;
        }
        NSLog(@"臨時作用域結束");
        
        NSLog(@"weakPerson:%@", weakPerson);    
    }
    return 0;
}


*******************運行結果********************
2019-09-02 21:48:58.332332+0800 Block學習[24180:2987983] 臨時作用域開始
2019-09-02 21:48:58.332851+0800 Block學習[24180:2987983] person對象:<CLPerson: 0x100600d20>
2019-09-02 21:48:58.332889+0800 Block學習[24180:2987983] -[CLPerson dealloc]
2019-09-02 21:48:58.332920+0800 Block學習[24180:2987983] 臨時作用域結束
2019-09-02 21:48:58.332938+0800 Block學習[24180:2987983] weakPerson:(null)
Program ended with exit code: 0

person被作用域外的__weak指針指向時,可以看到臨時作用域結束之后,person和第一種情況一樣,直接釋放了,說明__weak指針沒有增加person的引用計數,并且,person釋放時候,__weak指針被置為nil,防止了野指針錯誤


*********************mian.m***************************
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        __unsafe_unretained CLPerson *unsafePerson;
        
        NSLog(@"臨時作用域開始");
        {
            CLPerson *person = [[CLPerson alloc] init];
            NSLog(@"person對象:%@", person);
            unsafePerson = person; 
        }
        NSLog(@"臨時作用域結束");
        
        NSLog(@"unsafePerson:%@", unsafePerson);    
    }
    return 0;
}


*******************運行結果********************

image

person被作用域外的__unsafe_unretained指針指向時,可以看到臨時作用域結束之后,person和第一種情況一樣,直接釋放了,說明__unsafe_unretained指針也沒有增加person的引用計數,但是最后卻出現了EXC_BAD_ACCESS報錯,說明是野指針問題。

這樣就看出了__weak__unsafe_unretained的區(qū)別就是前者會在對象被釋放的時候自動置為nil,而后者卻不行。 】那么蘋果是如何實現對象釋放后,自動將__weak指針清空的呢?下面我們就從源碼來挖掘一下。

因為__weak指針是在對象釋放的時候被清空的,所以我們從對象的dealloc方法入手,我們可以在objc源碼的NSObject.mm文件中找到dealloc的實現如下

- (void)dealloc {
    _objc_rootDealloc(self);
}
**********************************
void _objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}
***********************************
inline void objc_object::rootDealloc()
{
    //??如果是Tagged Pointer,就直接返回
    if (isTaggedPointer()) return;  // fixme necessary?

    /*
    ??如果同時滿足 
    1. 是優(yōu)化過的isa、
    2. 沒有被weak指針引用過、
    3. 沒有關聯對象、
    4. 沒有C++析構函數、
    5. 沒有sideTable,
    就可以直接釋放內存free()
    */
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {//否則的話就需要通過下面的函數處理
        object_dispose((id)this);
    }
}

進入object_dispose函數

id object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

*******************************
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //??如果有C++析構函數,就調用一下
        if (cxx) object_cxxDestruct(obj);
        //??如果有關聯對象,就進行關聯對象移除操作
        if (assoc) _object_remove_assocations(obj);
        //??完了之后調用下面的函數
        obj->clearDeallocating();
    }

    return obj;
}

*********************************
inline void  objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

上看的方法里,如果isa是普通指針,就直接調用sidetable_clearDeallocating函數,如果是個優(yōu)化過isa,那么就走clearDeallocating_slow函數。我們查看一下這兩個函數

image

image

可以看到這兩個函數內部都是通過調用weak_clear_no_lock(&table.weak_table, (id)this);來處理__weak指針的,其中第一個參數就是sideTable的成員weak_table,第二個參數就是需要被釋放的對象。我們看看該函數的內部邏輯
image

這里的核心方法是weak_entry_for_referent再點進去
image

很明顯,上面的方法里面,通過需要釋放的對象referent根據一定的算法得出一個索引index,然后再從weak_table里面利用index拿到對象referent所對應的weak指針,這就說明weak_table內部其實就是一個散列表結構,通過對象作為keyvalue就是指向該對象weak指針組成的數組。

weak實現原理總結

  • 當一個對象objweak指針指向時,這個weak指針會以obj作為key,被存儲到sideTable類的weak_table這個散列表上對應的一個weak指針數組里面。
  • 當一個對象objdealloc方法被調用時,Runtime會以objkey,從sideTableweak_table散列表中,找出對應的weak指針列表,然后將里面的weak指針逐個置為nil。
    以上就是weak指針的實現原理
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容