博客連接OC內存管理--引用計數器c++
TaggedPointer
,蘋果會直接將其指針值做爲引用計數返回;64
位環境而且使用Objective-C 2.0
,那麼「一些」對象會使用其isa
指針的一部分空間來存儲它的引用計數;Runtime
會使用一張散列表來管理引用計數。Tagged Pointer
用來優化內存,其特色:安全
Tagged Pointer
專門用來存儲小的對象,例如NSNumber
和NSDate
等;Tagged Pointer
指針的值再也不是地址了,而是真正的值。因此,實際上它再也不是一個對象了,它只是一個披着對象皮的普通變量而已。因此,它的內存並不存儲在堆中,也不須要malloc
和free
;下面這個實現用來反映在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
的對象:異步
Tagged Pointer
專門用來存儲小的對象,這些對象有NSDate
、NSNumber
、NSString
;OBJC_DISABLE_TAGGED_POINTERS
爲YES
表示強制不啓用Tagged Pointer
。在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)
};
複製代碼
該變量佔用1bit
內存空間,能夠有兩個值:0
和1
,分別表明不一樣的isa_t
的類型:函數
0
表示isa_t
沒有開啓指針優化,不使用isa_t
中定義的結構體。訪問 objc_object
的isa
會直接返回isa_t
結構中的cls
變量,cls
變量會指向對象所屬的類的結構;1
表示isa_t
開啓指針優化,不能直接訪問objc_object
的isa
成員變量 (此時的isa而是一個Tagged Pointer
),isa
中包含了類信息、對象的引用計數等信息。該變量與對象的關聯引用有關。優化
表示該對象是否有析構函數,若是有析構函數,則須要作析構邏輯;若是沒有,則能夠更快的釋放對象。
在開啓指針優化的狀況下,用33bits存儲類指針的值。在initIsa()
中有newisa.shiftcls = (uintptr_t)cls >> 3;
這樣的代碼,就是將類指針存在isa中。
用於調試器判斷當前對象是真的對象仍是沒有初始化的空間
標誌對象是否被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象能夠更快釋放。
標誌對象是否正在釋放內存。
extra_rc
佔了19位,能夠存儲的最大引用計數應該是(爲何要這麼寫是由於
extra_rc
保存的是值-1,而在獲取引用計數的時候會+1),當超過它就須要SideTables
。SideTables
內包含一個RefcountMap
,用來保存引用計數,根據對象地址取出其引用計數,類型是size_t
。
這裏有個問題,爲何既要使用一個extra_rc
又要使用SideTables
?
多是由於歷史問題,之前cpu是32
位的,isa
中能存儲的引用計數就只有。所以在
arm64
下,引用計數一般是存儲在isa
中的。
更具體的會在retain操做的時候講到。
當引用計數器過大的時候,那麼引用計數會存儲在一個叫SideTable
的類的屬性中。
經過掩碼方式獲取magic
值。
經過掩碼方式獲取isa
的類指針值。
用於引用計數的相關計算。
表示平臺是否支持在isa
指針中插入除Class
以外的信息。
Class
信息放入isa_t
定義的struct內,並附上一些其餘信息,例如上面的nonpointer
等等;isa_t
內定義的struct
,這時isa_t
只使用cls
(Class 指針)。在iOS以及MacOSX設備上,SUPPORT_PACKED_ISA
定義爲1。
SUPPORT_INDEXED_ISA
表示isa_t
中存放的Class
信息是Class
的地址。在initIsa()
中有:
#if SUPPORT_INDEXED_ISA
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
複製代碼
iOS設備上SUPPRT_INDEXED_ISA是0。
用於標記是否支持優化的isa
指針,其定義:
#if !SUPPORT_INDEXED_ISA && !SUPPORT_PACKED_ISA
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
複製代碼
那如何判斷是否支持優化的isa指針?
SUPPORT_PACKED_ISA
爲1,SUPPORT_INDEXED_ISA
爲0,從上面的定義能夠看出,iOS系統的SUPPORT_NONPOINTER_ISA
爲1;OBJC_DISABLE_NONPOINTER_ISA
。這裏須要注意的是,即便是64位環境下,優化的isa
指針並非就必定會存儲引用計數,畢竟用19bit iOS 系統)保存引用計數不必定夠。另外這19位保存的是引用計數的值減一。
在源碼中咱們常常會看到SideTable
這個結構體。它的定義:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
//省略其餘代碼
};
複製代碼
從上面可知,SideTable
中有三個成員變量:
slock
用於保證原子操做的自旋鎖;refcnts
用於引用計數的hash
表;weak_table
用於weak引用的hash
表。這裏咱們主要看引用計數的哈希表。RefcountMap
的定義:typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
能夠看出SideTable
用來保存引用計數具體是用DenseMap
這個類(在llvm-DenseMap.h
中)實現的。DenseMap
以DisguisedPtr<objc_object>
爲key
,size_t
爲value
,DisguisedPtr
類是對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;
}
複製代碼
從上面的代碼可知,獲取引用計數的時候分爲三種狀況:
Tagged Pointer
的話,直接返回isa自己;Tagged Pointer
,且開啓了指針優化,此時引用計數先從extra_rc
中去取(這裏將取出來的值進行了+1操做,因此在存的時候須要進行-1操做),接着判斷是否有SideTable
,若是有再加上存在SideTable
中的計數;Tagged Pointer
,沒有開啓了指針優化,使用sidetable_retainCount()
函數返回。#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
來存儲引用計數;extra_rc
保存引用計數,當超出的時候,使用SideTable
來存儲額外的引用計數。#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;
}
複製代碼
從上面能夠看到:
Tagged Pointer
的對象,是就直接返回;SideTable
存儲的引用計數-1;extra_rc
保存的引用計數-1,當carry==0
表示須要從SideTable
保存的引用計數也用完了或者說引用計數爲0,因此執行最後一步;dealloc
,因此這也回答了以前的《OC內存管理--對象的生成與銷燬》中dealloc
何時被調用這個問題,在rootRelease(bool performDealloc, bool handleUnderflow)
函數中若是判斷出引用計數爲0了,就要調用dealloc
函數了。引用計數存在什麼地方?
Tagged Pointer
不須要引用計數,蘋果會直接將對象的指針值做爲引用計數返回;nonpointer == 1
)的對象其引用計數優先存在isa
的extra_rc
中,大於524288
便存在SideTable
的RefcountMap
或者說是DenseMap
中;SideTable
的RefcountMap
或者說是DenseMap
中。retain/release的實質
Tagged Pointer
不參與retain
/release
;dealloc
函數。isa是什麼
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
複製代碼
ISA()
獲取類指針;Tagged Pointer
的對象沒有isa
指針,有的是isa_t
的結構體;對象的值是什麼
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的緣由:過分釋放。
關鍵知識點:
target
使用strong
進行了修飾,Block是會截獲對象的修飾符的;_target
效果也是同樣,由於默認使用strong
修飾符隱式修飾;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。
解決方法:
DISPATCH_OBJ_BARRIER_BIT
設置阻塞標誌位