天天看點

記憶體管理—weak的實作原理

記憶體管理系列文章

記憶體管理—MRC時代的手動記憶體管理

記憶體管理—weak的實作原理

記憶體管理——autorelease原理分析

記憶體管理——定時器問題

iOS程式的記憶體布局

iOS引用計數的存儲

我在isa的深入體會一文中介紹過,蘋果從arm64架構開始,對isa進行了優化,通過位域計數将更多資訊存儲在了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這個命名含義是額外的引用計數,也就是除了建立時候的那一次

retain

操作之外,在其他時刻對象進行過

retain

操作的次數。是以一個對象實際的引用計數 =

extra_rc

+

1

(建立的那一次)。當然

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

存不下引用計數的話,那麼引用計數就會被存放在

SideTable

refcnts

中,從類型RefcountMap可以看出,它實際上是一個散清單的結構(類似OC中的字典)。

weak指針實作原理

我們可以給property屬性設定

strong

weak

unsafe_unretained

,轉化到成員變量上分别是

__strong

__weak

__unsafe_unretained

.下面我們來看一下他們的差別,我們通過下面幾段代碼案例以及運作結果來逐個說明

*********************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;
}


*******************運作結果********************
           
記憶體管理—weak的實作原理

person

被作用域外的

__unsafe_unretained

指針指向時,可以看到臨時作用域結束之後,

person

和第一種情況一樣,直接釋放了,說明

__unsafe_unretained

指針也沒有增加

person

的引用計數,但是最後卻出現了EXC_BAD_ACCESS報錯,說明是野指針問題。

【這樣就看出了

__weak

__unsafe_unretained

的差別就是前者會在對象被釋放的時候自動置為

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. 是優化過的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

函數,如果是個優化過的

isa

,那麼就走

clearDeallocating_slow

函數。我們檢視一下這兩個函數

記憶體管理—weak的實作原理
記憶體管理—weak的實作原理

可以看到這兩個函數内部都是通過調用

weak_clear_no_lock(&table.weak_table, (id)this);

來處理

__weak

指針的,其中第一個參數就是

sideTable

的成員

weak_table

,第二個參數就是需要被釋放的對象。我們看看該函數的内部邏輯

記憶體管理—weak的實作原理

這裡的核心方法是

weak_entry_for_referent

再點進去

記憶體管理—weak的實作原理

很明顯,上面的方法裡面,通過需要釋放的對象

referent

根據一定的算法得出一個索引

index

,然後再從

weak_table

裡面利用

index

拿到對象

referent

所對應的

weak

指針,這就說明

weak_table

内部其實就是一個散清單結構,通過對象作為

key

value

就是指向該對象的

weak

指針組成的數組。

weak實作原理總結

  • 當一個對象

    obj

    weak

    指針指向時,這個

    weak

    指針會以

    obj

    作為

    key

    ,被存儲到

    sideTable

    類的

    weak_table

    這個散清單上對應的一個weak指針數組裡面。
  • 當一個對象

    obj

    dealloc

    方法被調用時,Runtime會以

    obj

    key

    ,從

    sideTable

    weak_table

    散清單中,找出對應的

    weak

    指針清單,然後将裡面的

    weak

    指針逐個置為

    nil

    以上就是

    weak

    指針的實作原理