iOS開發進階:retain、release、dealloc

一、Tagged Pointer細節探究

蘋果爲了提高執行效率和節省內存,引入了Tagged Pointer的概念,對於64位程序來說可以達到3倍的訪問速度和100多倍的創建銷燬的速度。支持Tagged Pointer的類型以某種方式創建後便是Tagged Pointer指針,這種特殊的指針包括了數據內容和附加信息,訪問的時候可以通過指針地址解碼獲得。

objc源碼中定義了全部的支持Tagged Pointer的類型,常用的類型摘錄如下,如NSStringNSNumberNSIndexPathNSDateUIColorNSIndexSet等:

...// 60-bit payloads
OBJC_TAG_NSString          = 2, 
OBJC_TAG_NSNumber          = 3, 
OBJC_TAG_NSIndexPath       = 4, 
OBJC_TAG_NSManagedObjectID = 5, 
OBJC_TAG_NSDate            = 6,
...// 52-bit payloads
OBJC_TAG_UIColor           = 17,
OBJC_TAG_CGColor           = 18,
OBJC_TAG_NSIndexSet        = 19,
OBJC_TAG_NSMethodSignature = 20,

先來看如下代碼的打印結果:

NSString *str1 = @"abcd";
NSString *str2 = [[NSString alloc] initWithString:str1];
NSString *str3 = [[NSString alloc] initWithFormat:@"%@", str1];
NSString *str4 = [[NSString alloc] initWithFormat:@"%@-%@-%@", str1,str1,str1];
NSLog(@"str1=%@, ptr=%p, class=%@;", str1, str1, [str1 class]);
NSLog(@"str2=%@, ptr=%p, class=%@;", str2, str2, [str2 class]);
NSLog(@"str3=%@, ptr=%p, class=%@;", str3, str3, [str3 class]);
NSLog(@"str4=%@, ptr=%p, class=%@;", str4, str4, [str4 class]);

打印結果:

str1=abcd, ptr=0x1020fc9d0, class=__NSCFConstantString;
str2=abcd, ptr=0x1020fc9d0, class=__NSCFConstantString;
str3=abcd, ptr=0xa1e53d6849de69de, class=NSTaggedPointerString;
str4=abcd-abcd-abcd, ptr=0x283195de0, class=__NSCFString;
  • 打印結果中str3的真實類型爲NSTaggedPointerStringstr1str2的真實類型爲__NSCFConstantStringstr4的真實類型爲__NSCFString。通過打印superclass找到了他們之間的繼承關係,其中NSTaggedPointerStringNSString的子類。
  • 打印結果可以看出,是否支持Tagged Pointer跟創建的方式和初始化的內容長度等也有關係。

先從這裏入口,如何判斷一個對象是不是支持Tagged Pointer?

static inline bool  _objc_isTaggedPointer(const void * _Nullable ptr){
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

//而_OBJC_TAG_MASK的定義與架構平臺相關,真機的_OBJC_TAG_MASK = (1UL<<63) ,也就是高1位是1就是Tagged Pointer指針。

#if __arm64__
#   define OBJC_SPLIT_TAGGED_POINTERS 1  //64位真機
#else
#   define OBJC_SPLIT_TAGGED_POINTERS 0
#endif

#if OBJC_SPLIT_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)   //真機:指針最高位爲1
#elif OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
# endif

爲了避免開發人員直接從指針地址上直接獲取到內容,我們直接通過指針取地址獲取到的都是encode之後的,要想拿到真實的信息需要decode:

static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr){
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return (void *)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;
#endif
    return (void *)value;
}

static inline uintptr_t_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr){
    uintptr_t value = (uintptr_t)ptr;
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return value;
#endif
    return value ^ objc_debug_taggedpointer_obfuscator;
}

encode的時候通過objc_debug_taggedpointer_obfuscator與指針地址按位異或,decodeobjc_debug_taggedpointer_obfuscator 按位異或encode之後的值。而objc_debug_taggedpointer_obfuscator則是在應用啓動時_read_images()->initializeTaggedPointerObfuscator()初始化的。在按位異或中相同爲0,不同爲1,如果objc_debug_taggedpointer_obfuscator的值爲0encode/decode前後的值應該相同。DisableTaggedPointerObfuscation==YES的時候初始化的objc_debug_taggedpointer_obfuscator=0,可以在scheme的環境變量中配置OBJC_DISABLE_TAG_OBFUSCATION==YES之後在調試更方便。
設置環境變量操作如下:


設置之後再次打印如上數據,其中str3的打印結果發生變化:str3=abcd, ptr=0x8000003231b130a2, class=NSTaggedPointerString;接下來按照官方說的方式去還原一下數據:

常用類型總結:

變量 地址 二進制地址 說明
[NSString stringWithFormat:@"abcd"] 0x8000003231b130a2 0B1-0000000000000000000000000110
0100011000110110001001100001-0100-010
最高位爲1,最低3位2代表NSString類型,4-7位爲4表示字符串長度
[NSString stringWithFormat:@"abcdefg"] 0xb3b332b231b130ba 0B1-0110011101100110011001010110
0100011000110110001001100001-0111-010
最高位爲1,最低3位2代表NSString類型,4-7位爲7表示字符串長度
[NSNumber numberWithChar:1] 0x8000000000000083 0B1-0000000000000000000000000000
0000000000000000000000000001-0000-011
最高位爲1,最低3位3代表NSNumber類型,4-7位爲0表示char型
[NSNumber numberWithShort:3]] 0x800000000000018b 0B1-0000000000000000000000000000
0000000000000000000000000011-0001-011
最高位爲1,最低3位3代表NSNumber類型,4-7位爲1表示short型
[NSNumber numberWithInt:7] 0x8000000000000393 0B1-0000000000000000000000000000
0000000000000000000000000111-0010-011
最高位爲1,最低3位3代表NSNumber類型,4-7位爲2表示int型
[NSNumber numberWithLong:52] 0x8000000000001a1b 0B1-0000000000000000000000000000
0000000000000000000000110100-0011-011
最高位爲1,最低3位3代表NSNumber類型,4-7位爲3表示long型
[NSIndexPath indexPathWithIndex:5] 0x8000000000002874 0B1-0000000000000000000000000000
0000000000000000000001010000-1110-100
最高位爲1,最低3位4代表NSIndexPath類型
[NSDate date] 0x969db1df206a00a6 0B1-0010110100111011011000111011
1110010000001101010000000001-0100-110
最高位爲1,最低3位6代表NSDate類型

採用Tagged Pointer存儲的小對象,需要在類型、創建方式、內容長度等方面滿足要求,簡單老說就是數據內容、標識位和擴展信息需要在2^64位中能存儲完整完整。我們在實際開發中不應該依賴這些細節,這些內容不同平臺不一樣,而且可能會經常改變。

二、retain/release的流程梳理

先看retain:


retain核心流程梳理如下:

  • 1.首次進入rootRetain(tryRetain, variant):參數tryRetain=falsevariant=FastOrMsgSend
  • 2.如果是isTaggedPointer,則直接return this;反之繼續3.
  • 3.variant=FastOrMsgSend,執行objc_msgSend(this, @selector(retain)),繼續4。
  • 4.二次進入rootRetain(tryRetain, variant):參數tryRetain=falsevariant=Fast
  • 5.如果isa.nonpointer==0,執行sidetable_retain():引用計數全部全部存儲在sidetable中;直接根據當前對象找到存儲該對象的table,然後找到原有的refcntStorage+=SIDE_TABLE_RC_ONE即可。
  • 6.如果isa.nonpointer==1,應用計數存儲在isa.extra_rcsidetable中。引用計數+1會優先添加到isa.extra_rc上。如果存滿了,則先保存一半RC_HALFextra_rc中,並標記has_sidetable_rc=true已使用引用計數表,處理完isa後更新isa的數據。再將另一半RC_HALF追加到sidetable中,保存到side_table的流程與2同。
  • 7.retain的最後返回this指針。

源碼中release的核心流程objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)中,源碼較多,這裏梳理核心流程如下:
release核心流程梳理如下:

  • 1.首次進入rootRelease(performDealloc, variant):參數performDealloc=true, variant= FastOrMsgSend
  • 2.如果是isTaggedPointer,則直接return false;反之繼續3
  • 3.variant=FastOrMsgSend,執行objc_msgSend)(this, @selector(release)),繼續4。
  • 4.二次進入rootRelease(performDealloc, variant):參數performDealloc=true, variant= Fast
  • 5.如果isa.nonpointer==0,執行sidetable_release():引用計數全部存在sidetable中;根據當前的對象找到存儲該對象引用計數的table,然後找到原有的refcnt -= SIDE_TABLE_RC_ONE;即可。滿足dealloc條件的,繼續執行dealloc流程。
  • 6.如果isa.nonpointer==1,應用計數存儲在isa.extra_rcsidetable中。引用繼續-1會先從isa.extra_rc上減。如果不夠減了,會進入underflow流程7.
  • 7.如果該對象有has_sidetable_rc,執行rootRelease_underflow流程,三次進入rootRelease(performDealloc, variant):參數performDealloc=true, variant= Full
  • 8.執行auto borrow = sidetable_subExtraRC_nolock(RC_HALF);也就是問sidetableRC_HALF,返回借到的數量和剩餘的數量。
  • 9.如果借到了則將借到的數量-1保存到isa.extrac_rc中。如果sidetable中剩餘爲0則標記isa.has_sidetable_rc=0,再存儲新的isa.bits的數據。處理存儲失敗的情況。
  • 10.如果沒有借到或者根本就沒有再sidetable中存儲則執行dealloc相關流程。

小結:如果是Tagged Pointer小對象,沒有佔用堆空間分配內存,無需引用計數的管理,小對象的釋放隨着棧空間的回收而釋放。常規對象,先判斷有沒開啓isa優化(isa.nonpointer==0),沒有開啓則對象的引用計數都存儲在sidetable中,無論retain還是release都操作的是sidetable中的計數+1-1,如果是release,則當計數爲0的時候執行dealloc操作。如果開啓了isa優化(isa.nonpointer==1),則對象的引用計數存儲在isa.extra_rc和sidetable中,優先操作isa.extra_rcretain操作isa.extra_rc++,當isa.extra_rc中存滿255個後,就會分一半(1<<7)到sidetable中,並在isa中標記isa.has_sidetable_rc=1。而release操作isa.extra_rc--,當isa.extra_rc中不夠減了,則會從sidetable中嘗試借1<<7個,如果sidetable中被借了之後沒有了會設置isa.has_sidetable_rc=0,借到的數據會加到isa.extra_rc中,方便後續使用。如果沒有借到則執行dealloc操作。

三、dealloc流程梳理

先看看底層objc_object::rootDealloc函數:

inline void objc_object::rootDealloc(){
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer                     &&
                 !isa.weakly_referenced             &&
                 !isa.has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &&
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc)){
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

從這裏可以看出如果一個對象是isTaggedPointer,那麼這裏什麼事情都不做。然後再判斷是否沒有開啓isa優化、是否沒有被弱引用、是否沒有關聯對象、是否沒有C++構造/析構函數、是否在sidetable中沒有引用計數了,如果這些都是是,則直接調用底層free(this)回收內存;反之則調用object_dispose(this)

接着進入object_dispose函數:

id object_dispose(id obj){
    if (!obj) return nil;
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}

先執行objc_destructInstance(obj),再執行free(obj)。結合上下文推測objc_destructInstance()應該是處理弱引用表、關聯對象表、引用計數表相關的問題的。

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.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }
    return obj;
}

objc_destructInstance函數中,做了三個事情:

  • 1、如果有c++析構方法,則調用object_cxxDestruct(obj),內部繼續調用object_cxxDestructFromClass()方法,最終會從子類到父類依次調用析構函數(如果存在的話),具體調用析構函數做什麼,還需要進一步探究。
  • 2.如果有關聯對象,則調用_object_remove_assocations(obj, true),將該對象相關的關聯記錄擦除,同時如果關聯對象存在且引用計數策略是OBJC_ASSOCIATION_SETTER_RETAIN,則像改對象發送objc_release(_value)消息。
  • 3.調用obj->clearDeallocating()函數,進行弱引用對象、引用計數相關的處理:如果沒有開啓isa.nonpointer,則調用sidetable_clearDeallocating()。如果該對象被弱引用或者再sidetable中存儲了引用計數則調用clearDeallocating_slow()
sidetable_clearDeallocating()和clearDeallocating_slow()做的事情是一致的,核心代碼如下:
//3.1
void objc_object::sidetable_clearDeallocating(){
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
}
//3.2
NEVER_INLINE void objc_object::clearDeallocating_slow(){
    SideTable& table = SideTables()[this];
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
}
  • 4.weak_clear_no_lock進行弱引用關係的處理:相關細節標記在如下代碼中
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) {
    objc_object *referent = (objc_object *)referent_id;  //當前對象referent
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);//從weak_table取出entry
    if (entry == nil) {
        return;//如果weak_table中沒有,則直接return
    }

   
    weak_referrer_t *referrers;//weak指針列表,List結構
    size_t count;//weak指針的數量
    
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    //一次遍歷weak指針
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                //如果weak指針指向的是自己,則將weak指針置空,這也是爲啥weak指針在對象釋放後被自動置空的真正原因。
                *referrer = nil;
            }
            else if (*referrer) {
                //weak指針存在entry. referrers中,但是weak指針並不指向自己:異常情況!!!處理
                objc_weak_error();
            }
        }
    }
    weak_entry_remove(weak_table, entry);//從weak_table表中移除該entry。
}
  • 5.table.refcnts.erase()進行引用計數的處理。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章