從iOS中的引用計數提及

本文首發於我的博客html

前言

維基百科中這麼定義引用計數c++

引用計數是計算機編程語言中的一種內存管理技術,是指將資源(能夠是對象、內存或磁盤空間等等)的被引用次數保存起來,當被引用次數變爲零時就將其釋放的過程。使用引用計數技術能夠實現自動資源管理的目的。同時引用計數還能夠指使用引用計數技術回收未使用資源的垃圾回收算法。git

當建立一個對象的實例並在堆上申請內存時,對象的引用計數就爲1,在其餘對象中須要持有這個對象時,就須要把該對象的引用計數加1,須要釋放一個對象時,就將該對象的引用計數減1,直至對象的引用計數爲0,對象的內存會被馬上釋放。github

在iOS中,使用引用計數來管理OC對象的內存

  • 一個新建立的OC對象引用計數默認是1,當引用計數減爲0,OC對象就會銷燬,釋放其佔用的內存空間算法

  • 調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1編程

  • 內存管理的經驗總結bash

    • 當調用alloc、new、copy、mutableCopy方法返回了一個對象,在不須要這個對象時,要調用release或者autorelease來釋放它
    • 想擁有某個對象,就讓它的引用計數+1;不想再擁有某個對象,就讓它的引用計數-1
  • 能夠經過如下私有函數來查看自動釋放池的狀況架構

    • extern void _objc_autoreleasePoolPrint(void);

isa

詳解iOS中的Runtime一文中,對isa進行了詳解。app

這裏進行簡單概述編程語言

從arm64架構開始,蘋果對isa進行了優化,變成了一個共用體(union)結構,還使用位域來存儲更多的信息。以下

define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;   //指針是否優化過                                   \
      uintptr_t has_assoc         : 1;   //是否有設置過關聯對象,若是沒有,釋放時會更快                                   \
      uintptr_t has_cxx_dtor      : 1; 	 //是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快                                     \
      uintptr_t shiftcls          : 33; //存儲着Class、Meta-Class對象的內存地址信息 \
      uintptr_t magic             : 6;  //用於在調試時分辨對象是否未完成初始化                                     \
      uintptr_t weakly_referenced : 1;  //是否有被弱引用指向過,若是沒有,釋放時會更快                                     \
      uintptr_t deallocating      : 1;  //對象是否正在釋放                                     \
      uintptr_t has_sidetable_rc  : 1;  //引用計數器是否過大沒法存儲在isa中                                     \
      uintptr_t extra_rc          : 19 //裏面存儲的值是引用計數器減1
複製代碼

isa中不一樣的位域表明不一樣的含義。

  • nonpointer

    • 0,表明普通的指針,存儲着Class、Meta-Class對象的內存地址
    • 1,表明優化過,使用位域存儲更多的信息
  • has_assoc

    • 是否有設置過關聯對象,若是沒有,釋放時會更快
  • has_cxx_dtor

    • 是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快
  • shiftcls

    • 存儲着Class、Meta-Class對象的內存地址信息
  • magic

    • 用於在調試時分辨對象是否未完成初始化
  • weakly_referenced

    • 是否有被弱引用指向過,若是沒有,釋放時會更快
  • deallocating

    • 對象是否正在釋放
  • extra_rc

    • 裏面存儲的值是引用計數器減1
  • has_sidetable_rc

    • 引用計數器是否過大沒法存儲在isa中
    • 若是爲1,那麼引用計數會存儲在一個叫SideTable的類的屬性中

Tagged Pointer

背景

再開始以前,先看這個代碼

NSNumber *num = @(20);

咱們只有一個須要存儲20這個數據,按照正常的技術方案,在64位CPU下,應該先去建立NSNumber對象,其值是20,而後再有個指向該地址的指針num。這樣作存在什麼問題呢?

  • 內存浪費

    • 因爲OC中的內存對齊,在64位下,建立一個對象至少16字節,再加上一個指針8個字節,總共24字節,也就是說,爲了存儲這個20而須要24字節,對內存方面是極大的浪費。
  • 性能浪費

    • 爲了存儲和訪問一個 NSNumber 對象,咱們須要在堆上爲其分配內存,另外還要維護它的引用計數,管理它的生命期。這些都給程序增長了額外的邏輯,形成運行效率上的損失

Tagged Pointer技術

爲了解決這個問題,蘋果提出了Tagged Pointer的概念。對於 64 位程序,引入 Tagged Pointer 後,相關邏輯能減小一半的內存佔用,以及 3 倍的訪問速度提高,100 倍的建立、銷燬速度提高。

  • 從64bit開始,iOS引入了Tagged Pointer技術,用於優化NSNumber、NSDate、NSString等小對象的存儲

  • 在沒有使用Tagged Pointer以前, NSNumber等對象須要動態分配內存、維護引用計數等,NSNumber指針存儲的是堆中NSNumber對象的地址值

  • 使用Tagged Pointer以後,NSNumber指針裏面存儲的數據變成了:Tag + Data,也就是將數據直接存儲在了指針中

  • 當指針不夠存儲數據時,纔會使用動態分配內存的方式來存儲數據

  • objc_msgSend能識別Tagged Pointer,好比NSNumber的intValue方法,直接從指針提取數據,節省了之前的調用開銷

  • 如何判斷一個指針是否爲Tagged Pointer?

    • 最低有效位是1 (objc4-750以後)
    • 以前的版本(objc4-723之前)(iOS平臺,最高有效位是1(第64bit),Mac平臺,最低有效位是1)

關於Tagged Pointer,想深刻了解的,能夠參照深刻理解 Tagged Pointer,就不在這贅述了。須要注意的是,以前的版本,變量的值直接存儲在指針中,很容易的能夠讀取出來,例如0xb000000000000012 然而如今的版本中,蘋果對這個指針作了一些編碼處理,不能直接看出來是Tagged Pointer,例如0x30a972fb5e339e15然而它依然是Tagged Pointer,由於能夠根據源碼可知,是根據把它轉爲二進制以後最後一位是否爲1來肯定是否爲Tagged Pointer。

#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
    // 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
    // Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif


#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif


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

引用計數的存儲

在64bit中,引用計數能夠直接存儲在優化過的isa指針中,也可能存儲在SideTable類中,那SideTable中有什麼呢?

SideTable的結構以下

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;//refcnts是一個存放着對象引用計數的散列表
    weak_table_t weak_table;

   	...還有不少代碼
};
複製代碼

其中 RefcountMap refcnts中存放着對象引用計數的散列表

獲取引用計數

// 引用計數
- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}
複製代碼
  • rootRetainCount
inline uintptr_t 
objc_object::rootRetainCount()
{
    //TaggedPointer不是一個普通的對象,不須要作引用計數的一些操做
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) { //優化過的isa
        uintptr_t rc = 1 + bits.extra_rc; // 這裏進行了+1操做
        if (bits.has_sidetable_rc) {
            //能來到這裏,說明引用計數不是存儲在isa中,而是存儲在sidetable中
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
複製代碼
  • sidetable_getExtraRC_nolock
size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this]; // this 就是key  根據這個key取出value
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT; // 取出的值 通過位運算以後返回
}
複製代碼

sidetable_retainCount()方法的邏輯就是先從 SideTable 的靜態方法獲取當前實例對應的 SideTable 對象,其 refcnts 屬性就是以前說的存儲引用計數的散列表,而後在引用計數表中用迭代器查找當前實例對應的鍵值對,獲取引用計數值,並在此基礎上 +1 並將結果返回。這也就是爲何以前中說引用計數表存儲的值爲實際引用計數減一。

須要注意的是爲何這裏把鍵值對的值作了向右移位操做(it->second >> SIDE_TABLE_RC_SHIFT)

引用計數的增刪

在MRC 環境下可使用 retain 和 release 方法對引用計數進行加一減一操做,它們分別調用了_objc_rootRetain(id obj)_objc_rootRelease(id obj) 函數,不事後二者在 ARC 環境下也可以使用。最後這兩個函數又會調用 objc_object 的下面兩個方法:

inline id 
objc_object::rootRetain()
{
    assert(!UseGC);

    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

inline bool 
objc_object::rootRelease()
{
    assert(!UseGC);

    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}
複製代碼

就是先看釋放支持isTaggedPointer,而後再操做 SideTable 中的 refcnts 屬性,這與獲取引用計數策略相似。sidetable_retain() 將 引用計數加一後返回對象,sidetable_release() 返回是否要執行 dealloc 方法:

引用計數的增長

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

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides if (slowpath(tryRetain && newisa.deallocating)) { ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); return nil; } uintptr_t carry; newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ if (slowpath(carry)) { // newisa.extra_rc++ overflowed if (!handleOverflow) { 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. 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 (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(); return (id)this; } 複製代碼
  • sidetable_retain
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

複製代碼

引用計數的減小

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

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);//引用計數減小
        }
        // don't check newisa.fast_rr; we already called any RR overrides uintptr_t carry; newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc-- if (slowpath(carry)) { // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

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

    // abandon newisa to undo the decrement
    newisa = oldisa;
    ...還有不少代碼
複製代碼
  • 函數sidetable_release
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) {
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it. do_dealloc = true; it->second |= SIDE_TABLE_DEALLOCATING; } else if (! (it->second & SIDE_TABLE_RC_PINNED)) { it->second -= SIDE_TABLE_RC_ONE; } table.unlock(); if (do_dealloc && performDealloc) {// 來到這裏,說明引用計數爲0,調用dealloc釋放 ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return do_dealloc; } 複製代碼

參考資料

深刻理解 Tagged Pointer

詳解iOS中的Runtime

Runtime源碼

iOS底層原理

更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。

相關文章
相關標籤/搜索