OC內存管理--引用計數器

博客連接OC內存管理--引用計數器c++

引用計數的存儲策略

  1. 有些對象若是支持使用TaggedPointer,蘋果會直接將其指針值做爲引用計數返回;
  2. 若是當前設備是64位環境而且使用Objective-C 2.0,那麼「一些」對象會使用其isa指針的一部分空間來存儲它的引用計數;
  3. 不然Runtime會使用一張散列表來管理引用計數。

Tagged Pointer

Tagged Pointer用來優化內存,其特色:安全

  1. Tagged Pointer專門用來存儲小的對象,例如NSNumberNSDate等;
  2. Tagged Pointer指針的值再也不是地址了,而是真正的值。因此,實際上它再也不是一個對象了,它只是一個披着對象皮的普通變量而已。因此,它的內存並不存儲在堆中,也不須要mallocfree
  3. 在內存讀取上有着3倍的效率,建立時比之前快106倍。

下面這個實現用來反映在64位系統下Tagged Pointer的應用:bash

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @3;
        NSNumber *number4 = @4;
        
        NSNumber *numberLager = @(MAXFLOAT);
        
        NSLog(@"number1 pointer is %p", number1);
        NSLog(@"number2 pointer is %p", number2);
        NSLog(@"number3 pointer is %p", number3);
        NSLog(@"number4 pointer is %p", number4);
        NSLog(@"numberLager pointer is %p", numberLager);
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

//打印結果:
2018-09-25 15:26:05.788382+0800 NSObjectProject[68029:24580896] number1 pointer is 0x9c344c19d780bc93
2018-09-25 15:26:05.789257+0800 NSObjectProject[68029:24580896] number2 pointer is 0x9c344c19d780bca3
2018-09-25 15:26:05.789383+0800 NSObjectProject[68029:24580896] number3 pointer is 0x9c344c19d780bcb3
2018-09-25 15:26:05.789489+0800 NSObjectProject[68029:24580896] number4 pointer is 0x9c344c19d780bcc3
2018-09-25 15:26:05.789579+0800 NSObjectProject[68029:24580896] numberLager pointer is 0x600001e60d80
複製代碼

咱們知道,全部對象都有其對應的isa指針,那麼引入Tagged Pointer會對isa指針產生影響。多線程

咱們看下對象中的Tagged Pointer的使用併發

inline bool 
objc_object::isTaggedPointer() {
    return _objc_isTaggedPointer(this);
}
複製代碼

那麼如何判斷是不是Tagged Pointer的對象:異步

  1. 看對象。前面說到Tagged Pointer專門用來存儲小的對象,這些對象有NSDateNSNumberNSString
  2. 本身設置。在環境變量中設置OBJC_DISABLE_TAGGED_POINTERSYES表示強制不啓用Tagged Pointer

isa指針

isa的本質——isa_t聯合體

objc_object這個結構體中定義了isa指針:async

struct objc_object {
    isa_t isa;
}

//isa_t的定義
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
    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;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
    };

# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
    };

# else
# error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif


#if SUPPORT_INDEXED_ISA

# if __ARM_ARCH_7K__ >= 2

# define ISA_INDEX_IS_NPI 1
# define ISA_INDEX_MASK 0x0001FFFC
# define ISA_INDEX_SHIFT 2
# define ISA_INDEX_BITS 15
# define ISA_INDEX_COUNT (1 << ISA_INDEX_BITS)
# define ISA_INDEX_MAGIC_MASK 0x001E0001
# define ISA_INDEX_MAGIC_VALUE 0x001C0001
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t indexcls          : 15;
        uintptr_t magic             : 4;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 7;
# define RC_ONE (1ULL<<25)
# define RC_HALF (1ULL<<6)
    };

# else
# error unknown architecture for indexed isa
# endif

// SUPPORT_INDEXED_ISA
#endif

};
複製代碼

這裏定義了不少環境,咱們主要看64位CPU(if __arm64__)的定義:ide

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
    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;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
    };
複製代碼

nonpointer

該變量佔用1bit內存空間,能夠有兩個值:01,分別表明不一樣的isa_t的類型:函數

  1. 0表示isa_t沒有開啓指針優化,不使用isa_t中定義的結構體。訪問 objc_objectisa會直接返回isa_t結構中的cls變量,cls變量會指向對象所屬的類的結構;
  2. 1表示isa_t開啓指針優化,不能直接訪問objc_objectisa成員變量 (此時的isa而是一個Tagged Pointer),isa中包含了類信息、對象的引用計數等信息。

has_assoc

該變量與對象的關聯引用有關。優化

has_cxx_dtor

表示該對象是否有析構函數,若是有析構函數,則須要作析構邏輯;若是沒有,則能夠更快的釋放對象。

shiftcls

在開啓指針優化的狀況下,用33bits存儲類指針的值。在initIsa()中有newisa.shiftcls = (uintptr_t)cls >> 3;這樣的代碼,就是將類指針存在isa中。

magic

用於調試器判斷當前對象是真的對象仍是沒有初始化的空間

weakly_referenced

標誌對象是否被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象能夠更快釋放。

deallocating

標誌對象是否正在釋放內存。

extra_rc

extra_rc佔了19位,能夠存儲的最大引用計數應該是2^{19} - 1 + 1= 524288(爲何要這麼寫是由於extra_rc保存的是值-1,而在獲取引用計數的時候會+1),當超過它就須要SideTablesSideTables內包含一個RefcountMap,用來保存引用計數,根據對象地址取出其引用計數,類型是size_t

這裏有個問題,爲何既要使用一個extra_rc又要使用SideTables

多是由於歷史問題,之前cpu是32位的,isa中能存儲的引用計數就只有2^7=128。所以在arm64下,引用計數一般是存儲在isa中的。

更具體的會在retain操做的時候講到。

has_sidetable_rc

當引用計數器過大的時候,那麼引用計數會存儲在一個叫SideTable的類的屬性中。

ISA_MAGIC_MASK

經過掩碼方式獲取magic值。

ISA_MASK

經過掩碼方式獲取isa的類指針值。

RC_ONE 和 RC_HALF

用於引用計數的相關計算。

isa_t聯合體裏面的宏

SUPPORT_PACKED_ISA

表示平臺是否支持在isa指針中插入除Class以外的信息。

  1. 若是支持就會將Class信息放入isa_t定義的struct內,並附上一些其餘信息,例如上面的nonpointer等等;
  2. 若是不支持,那麼不會使用isa_t內定義的struct,這時isa_t只使用cls(Class 指針)。

在iOS以及MacOSX設備上,SUPPORT_PACKED_ISA定義爲1

SUPPORT_INDEXED_ISA

SUPPORT_INDEXED_ISA表示isa_t中存放的Class信息是Class的地址。在initIsa()中有:

#if SUPPORT_INDEXED_ISA
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
複製代碼

iOS設備上SUPPRT_INDEXED_ISA是0

isa類型有關的宏

SUPPORT_NONPOINTER_ISA

用於標記是否支持優化的isa指針,其定義:

#if !SUPPORT_INDEXED_ISA && !SUPPORT_PACKED_ISA
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
複製代碼

那如何判斷是否支持優化的isa指針?

  1. 已知iOS系統的SUPPORT_PACKED_ISA爲1,SUPPORT_INDEXED_ISA爲0,從上面的定義能夠看出,iOS系統的SUPPORT_NONPOINTER_ISA爲1;
  2. 在環境變量中設置OBJC_DISABLE_NONPOINTER_ISA

這裏須要注意的是,即便是64位環境下,優化的isa指針並非就必定會存儲引用計數,畢竟用19bit iOS 系統)保存引用計數不必定夠。另外這19位保存的是引用計數的值減一。

SideTable

在源碼中咱們常常會看到SideTable這個結構體。它的定義:

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

    //省略其餘代碼
};
複製代碼

從上面可知,SideTable中有三個成員變量:

  1. slock用於保證原子操做的自旋鎖;
  2. refcnts用於引用計數的hash表;
  3. weak_table用於weak引用的hash表。

這裏咱們主要看引用計數的哈希表。RefcountMap的定義:typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

能夠看出SideTable用來保存引用計數具體是用DenseMap這個類(在llvm-DenseMap.h中)實現的。DenseMapDisguisedPtr<objc_object>keysize_tvalueDisguisedPtr類是對objc_object *指針及其一些操做進行的封裝,其內容能夠理解爲對象的內存地址,值的類型爲__darwin_size_t,在 darwin 內核通常等同於 unsigned long。其實這裏保存的值也是等於引用計數減1。

引用計數的獲取

經過retainCount能夠獲取到引用計數器,其定義:

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

inline uintptr_t 
objc_object::rootRetainCount() {
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    //加鎖,用匯編指令ldxr來保證原子性
    isa_t bits = LoadExclusive(&isa.bits);
    //釋放鎖,使用匯編指令clrex
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

//sidetable_retainCount()函數實現
uintptr_t
objc_object::sidetable_retainCount() {
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;
    
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}
複製代碼

從上面的代碼可知,獲取引用計數的時候分爲三種狀況:

  1. Tagged Pointer的話,直接返回isa自己;
  2. Tagged Pointer,且開啓了指針優化,此時引用計數先從extra_rc中去取(這裏將取出來的值進行了+1操做,因此在存的時候須要進行-1操做),接着判斷是否有SideTable,若是有再加上存在SideTable中的計數;
  3. Tagged Pointer,沒有開啓了指針優化,使用sidetable_retainCount()函數返回。

手動操做對引用計數的影響

objc_retain()

#if __OBJC2__
__attribute__((aligned(16)))
id 
objc_retain(id obj) {
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}
#else
id objc_retain(id obj) { return [obj retain]; }
複製代碼

首先判斷是不是Tagged Pointer的對象,是就返回對象自己,不然經過對象的retain()返回。

inline id 
objc_object::retain() {
    assert(!isTaggedPointer());
    // hasCustomRR方法檢查類(包括其父類)中是否含有默認的方法
    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
複製代碼

首先判斷是不是Tagged Pointer,這個函數並不但願處理的對象是Tagged Pointer;接着經過hasCustomRR函數檢查類(包括其父類)中是否含有默認的方法,有則調用自定義的方法;若是沒有,調用rootRetain()函數。

ALWAYS_INLINE id 
objc_object::rootRetain() {
    return rootRetain(false, false);
}

//將源碼精簡後的邏輯
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;
    
    isa_t oldisa;
    isa_t newisa;

    // 加鎖,用匯編指令ldxr來保證原子性
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    
    if (newisa.nonpointer = 0) {
        // newisa.nonpointer = 0說明全部位數都是地址值
        // 釋放鎖,使用匯編指令clrex
        ClearExclusive(&isa.bits);
        
        // 因爲全部位數都是地址值,直接使用SideTable來存儲引用計數
        return sidetable_retain();
    }
    
    // 存儲extra_rc++後的結果
    uintptr_t carry;
    // extra_rc++
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
    
    if (carry == 0) {
        // extra_rc++後溢出,進位到side table
        newisa.extra_rc = RC_HALF;
        newisa.has_sidetable_rc = true;
        sidetable_addExtraRC_nolock(RC_HALF);
    }
        
    // 將newisa寫入isa
    StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)
    return (id)this;
}
複製代碼

從上面的能夠看到:

  • Tagged Pointer直接返回對象自己;
  • newisa.nonpointer == 0沒有開啓指針優化,直接使用SideTable來存儲引用計數;
  • 開啓指針優化,使用isa的extra_rc保存引用計數,當超出的時候,使用SideTable來存儲額外的引用計數。

objc_release()

#if __OBJC2__
__attribute__((aligned(16)))
void 
objc_release(id obj) {
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
#else
void objc_release(id obj) { [obj release]; }
#endif

//release()源碼
inline void
objc_object::release()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
複製代碼

這邊的邏輯和objc_retain()的邏輯一致,因此直接看rootRelease()函數,與上面同樣,下面的代碼也是通過精簡的。

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow) {
    if (isTaggedPointer()) return false;
    
    isa_t oldisa;
    isa_t newisa;
    
retry:
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    if (newisa.nonpointer == 0) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return sidetable_release(performDealloc);
    }
    
    uintptr_t carry;
    // extra_rc--
    newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
    if (carry == 0) {
        // 須要從SideTable借位,或者引用計數爲0
        goto underflow;
    }
    
    // 存儲引用計數到isa
    StoreReleaseExclusive(&isa.bits,
                          oldisa.bits, newisa.bits)
    return false;
    
underflow:
    // 從SideTable借位
    // 或引用計數爲0,調用delloc
    
    // 此處省略N多代碼
    // 總結一下:修改Side Table與extra_rc,
    
    // 引用計數減爲0時,調用dealloc
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}
複製代碼

從上面能夠看到:

  1. 判斷是不是Tagged Pointer的對象,是就直接返回;
  2. 沒有開啓指針優化,使用SideTable存儲的引用計數-1;
  3. 開啓指針優化,使用isa的extra_rc保存的引用計數-1,當carry==0表示須要從SideTable保存的引用計數也用完了或者說引用計數爲0,因此執行最後一步;
  4. 最後調用dealloc,因此這也回答了以前的《OC內存管理--對象的生成與銷燬》中dealloc何時被調用這個問題,在rootRelease(bool performDealloc, bool handleUnderflow)函數中若是判斷出引用計數爲0了,就要調用dealloc函數了。

總結

  1. 引用計數存在什麼地方?

    • Tagged Pointer不須要引用計數,蘋果會直接將對象的指針值做爲引用計數返回;
    • 開啓了指針優化(nonpointer == 1)的對象其引用計數優先存在isaextra_rc中,大於524288便存在SideTableRefcountMap或者說是DenseMap中;
    • 沒有開啓指針優化的對象直接存在SideTableRefcountMap或者說是DenseMap中。
  2. retain/release的實質

    • Tagged Pointer不參與retain/release
    • 找到引用計數存儲區域,而後+1/-1,並根據是否開啓指針優化,處理進位/借位的狀況;
    • 當引用計數減爲0時,調用dealloc函數。
  3. isa是什麼

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    複製代碼
    • 首先要知道,isa指針已經不必定是類指針了,因此須要用ISA()獲取類指針;
    • Tagged Pointer的對象沒有isa指針,有的是isa_t的結構體;
    • 其餘對象的isa指針仍是類指針。
  4. 對象的值是什麼

    • 若是是Tagged Pointer,對象的值就是指針;
    • 若是非Tagged Pointer, 對象的值是指針指向的內存區域中的值。

補充: 一道多線程安全的題目

如下代碼運行結果

@property (nonatomic, strong) NSString *target;
//....

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
    });
}
複製代碼

答案:Crash。

Crash的緣由:過分釋放。

關鍵知識點:

  1. 全局隊列和自定義並行隊列在異步執行的時候會根據任務系統決定開闢線程個數;
  2. target使用strong進行了修飾,Block是會截獲對象的修飾符的;
  3. 即便使用_target效果也是同樣,由於默認使用strong修飾符隱式修飾;
  4. strong的源代碼以下:
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
複製代碼

假設這個併發隊列建立了兩個線程A和B,因爲是異步的,能夠同時執行。所以會出現這麼一個場景,在線程A中,代碼執行到了objc_retain(obj),可是在線程B中可能執行到了objc_release(prev),此時prev已經被釋放了。那麼當A在執行到objc_release(prev)就會過分釋放,從而致使程序crash。

解決方法:

  1. 加個互斥鎖
  2. 使用串行隊列,使用串行隊列的話,其實內部是靠DISPATCH_OBJ_BARRIER_BIT設置阻塞標誌位
  3. 使用weak
  4. 使用Tagged Pointer
相關文章
相關標籤/搜索