OC記憶體管理--引用計數器
- 有些物件如果支援使用
TaggedPointer
,蘋果會直接將其指標值作為引用計數返回; - 如果當前裝置是
64
位環境並且使用Objective-C 2.0
,那麼“一些”物件會使用其isa
指標的一部分空間來儲存它的引用計數; - 否則
Runtime
會使用一張散列表來管理引用計數。
Tagged Pointer
Tagged Pointer
用來優化記憶體,其特點:
-
Tagged Pointer
專門用來儲存小的物件,例如NSNumber
和NSDate
等; -
Tagged Pointer
指標的值不再是地址了,而是真正的值。所以,實際上它不再是一個物件了,它只是一個披著物件皮的普通變數而已。所以,它的記憶體並不儲存在堆中,也不需要malloc
和free
; - 在記憶體讀取上有著3倍的效率,建立時比以前快106倍。
下面這個實現用來反映在64位系統下 Tagged Pointer
的應用:
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
。
isa指標
isa的本質——isa_t聯合體
在 objc_object
這個結構體中定義了 isa
指標:
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_MASK0x0000000ffffffff8ULL #define ISA_MAGIC_MASK0x000003f000000001ULL #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_MASK0x00007ffffffffff8ULL #define ISA_MAGIC_MASK0x001f800000000001ULL #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_NPI1 #define ISA_INDEX_MASK0x0001FFFC #define ISA_INDEX_SHIFT2 #define ISA_INDEX_BITS15 #define ISA_INDEX_COUNT(1 << ISA_INDEX_BITS) #define ISA_INDEX_MAGIC_MASK0x001E0001 #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__
)的定義:
# if __arm64__ #define ISA_MASK0x0000000ffffffff8ULL #define ISA_MAGIC_MASK0x000003f000000001ULL #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
記憶體空間,可以有兩個值: 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
中包含了類資訊、物件的引用計數等資訊。
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位,可以儲存的最大引用計數應該是(為什麼要這麼寫是因為 extra_rc
儲存的是值-1,而在獲取引用計數的時候會+1),當超過它就需要 SideTables
。 SideTables
內包含一個 RefcountMap
,用來儲存引用計數,根據物件地址取出其引用計數,型別是 size_t
。
這裡有個問題,為什麼既要使用一個 extra_rc
又要使用 SideTables
?
可能是因為歷史問題,以前cpu是 32
位的, isa
中能儲存的引用計數就只有。因此在 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
之外的資訊。
- 如果支援就會將
Class
資訊放入isa_t
定義的struct內,並附上一些其他資訊,例如上面的nonpointer
等等; - 如果不支援,那麼不會使用
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指標?
- 已知iOS系統的
SUPPORT_PACKED_ISA
為1,SUPPORT_INDEXED_ISA
為0,從上面的定義可以看出,iOS系統的SUPPORT_NONPOINTER_ISA
為1; - 在環境變數中設定
OBJC_DISABLE_NONPOINTER_ISA
。
這裡需要注意的是,即使是64位環境下,優化的 isa
指標並不是就一定會儲存引用計數,畢竟用19bit iOS 系統)儲存引用計數不一定夠。另外這19位儲存的是引用計數的值減一。
SideTable
在原始碼中我們經常會看到 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()
函式返回。
手動操作對引用計數的影響
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; } 複製程式碼
從上面可以看到:
- 判斷是否是
Tagged Pointer
的物件,是就直接返回; - 沒有開啟指標優化,使用
SideTable
儲存的引用計數-1; - 開啟指標優化,使用isa的
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
; - 找到引用計數儲存區域,然後+1/-1,並根據是否開啟指標優化,處理進位/借位的情況;
- 當引用計數減為0時,呼叫
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指標已經不一定是類指標了,所以需要用
ISA()
獲取類指標; -
Tagged Pointer
的物件沒有isa
指標,有的是isa_t
的結構體; - 其他物件的isa指標還是類指標。
- 首先要知道,isa指標已經不一定是類指標了,所以需要用
-
物件的值是什麼
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