原文鏈接OC內存管理--引用計數器
更新于2020-02-14 更新Tagged Pointer的知識點
引用計數的存儲策略
- 有些對象如果支持使用
Tagged Pointer
,蘋果會直接將其指針值作為引用計數返回; - 如果當前設備是
64
位環境并且使用Objective-C 2.0
,那么“一些”對象會使用其isa
指針的一部分空間來存儲它的引用計數; - 否則
Runtime
會使用一張散列表來管理引用計數。
Tagged Pointer
從64bit開始,iOS引入了Tagged Pointer
技術,用來優化NSNumber
、NSDate
、NSString
等小對象的內存。
我們知道在沒有使用Tagged Pointer
之前,NSNumber
、NSDate
、NSString
等對象需要動態分配內存,維護引用計數等。對象的指針存的是一個指向堆空間的地址值。以NSNumber
為例,我們在創建NSObject對象的時候,至少需要16個字節的內存大?。ㄟ@個與OC內存對齊所使用的系數有關),另外還要用其他的內存空間來存NSNumber
所對應的值,看上去這無疑在增加內存的開銷。
在使用了Tagged Pointer
之后,它的指針不再是地址了,而是真正的值,準確的說是Tag+Data。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。所以,它的內存并不存儲在堆中,也不需要malloc
和free
。
在內存的讀取上使用了Tagged Pointer
技術的對象也會比之前快很多。
下面的代碼用來反映在64位系統下Tagged Pointer
的應用:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *number4 = @4;
NSNumber *numberLarger = @(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(@"numberLarger pointer is %p", numberLarger);
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] numberLarger pointer is 0x600001e60d80
上面的打印結果中,除了numberLarger
這個變量之外,其他變量都使用Tagged Pointer
技術。那如何知道對象是否是Tagged Pointer
的對象呢?接著看一下源碼中的定義:
inline bool objc_object::isTaggedPointer() {
return _objc_isTaggedPointer(this);
}
static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
_OBJC_TAG_MASK
是一個依賴于環境的宏,其相關定義如下:
#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
如果是mac OS 64bit的環境下,_OBJC_TAG_MASK
是1UL
,如果是iOS 64bit的環境下,_OBJC_TAG_MASK
是1UL<<63
(1UL
就是1)。
所以判斷是否Tagged Pointer
的對象需要分環境考慮。如果是mac 64bit的環境下,則查看其指針最低有效位是否為1,如果是iOS 64bit的環境下,則使用1UL<<63
,即1往左偏移63即0x1000000000000000
,即查看最高有效位是否為1。
上述實驗是在iphone手機下運行的結果。我們將上面地址(16進制)的第一位轉成二進制,分別是1001
、1001
、1001
、1001
、0110
,做$
運算后,除了numberLarger
對應的變量以外,其他的都是0x1000000000000000
。另外我們在創建對象的時候,只要是從堆空間里創建的對象,它的最低有效位一定是0
。這還是因為OC對象的內存對齊使用的系數為16,所以內存地址一定是16的倍數。但是使用了Tagged Pointer
技術的對象,它的最低有效位就不一定是0
了。從這里也證明了使用Tagged Pointer
的對象的內存并不存儲在堆中,也不需要malloc
和free
。在后續的源碼中,也會看到但凡是Tagged Pointer
的對象,很多都會直接return
。
到這里我們得出幾個結論:
- 從64bit開始,iOS對NSNumber、NSDate、NSString等小對象會優先使用Tagged Pointer技術進行存儲,即指針存儲。當指針存儲不下內容的時候,才會使用堆空間即動態分配內存;
- 在mac OS 64bit下,通過看最低有效位是否為1判斷是否Tagged Pointer的對象,在iOS 64bit下,通過查看最高有效位是否為1來判斷;
我們知道,所有對象都有其對應的isa
指針,那么引入Tagged Pointer
會對isa
指針產生什么影響。
isa指針
isa的本質——isa_t聯合體
在Objective-C語言中,類也是對象,且每個對象都包含一個isa
指針,isa
指針指向該對象所屬的類。
在arm64架構之前,isa
就是一個普通的指針,存儲著Class或者Meta-Class對象的內存地址。從arm64開始,Runtime對isa
進行了優化,變成了一個union
(共用體或者聯合體)結構,還使用位域來存儲更多的信息。
在C語言中,結構體可以包含多個類型不同的成員,各個成員會占用不同的內存,互相之間沒有影響。結構體占用的內存大于等于所有成員占用的內存的總和(成員之間可能會存在縫隙)。而聯合體,是一種與結構體非常接近的數據結構,但是有所區別。共用體占用的內存等于最長的成員占用的內存,這就是說共用體的所有成員占用同一段內存,修改一個成員會影響其余所有成員。
在objc_object
這個結構體中定義了isa
指針,這里我們只看arm64
下的相關定義:
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
# 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)
};
# endif
// SUPPORT_PACKED_ISA
#endif
};
nonpointer 該變量占用1 bit內存空間,可以有兩個值:0和1,分別代表不同的
isa_t
的類型:has_assoc 該變量與對象的關聯引用有關。
has_cxx_dtor 表示該對象是否有析構函數,如果有析構函數,則需要做析構邏輯;如果沒有,則可以更快的釋放對象。
shiftcls 在開啟指針優化的情況下,用33bits存儲類指針的值。在
initIsa()
中有newisa.shiftcls = (uintptr_t)cls >> 3;
這樣的代碼,就是將類指針存在isa中。magic 用于調試器判斷當前對象是真的對象還是沒有初始化的空間
weakly_referenced 標志對象是否被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象可以更快釋放。
deallocating 標志對象是否正在釋放內存。
extra_rc 存儲的是引用計數
has_sidetable_rc 當引用計數器過大的時候,那么引用計數會存儲在一個叫
SideTable
的類的屬性中。ISA_MAGIC_MASK 通過掩碼方式獲取
magic
值。SA_MASK 通過掩碼方式獲取
isa
的類指針值。RC_ONE和RC_HALF 用于引用計數的相關計算。
nonpointer
的0表示開啟指針優化即普通的指針,訪問objc_object的isa會直接返回cls變量,cls變量會指向對象所屬的類的結構;1表示開啟指針優化,不能直接訪問objc_object的isa成員變量(此時的isa而是一個Tagged Pointer
),isa中包含了類信息、對象的引用計數等信息。
extra_rc
占了19位,可以存儲的最大引用計數應該是(為什么要這么寫是因為
extra_rc
保存的是值-1,而在獲取引用計數的時候會+1),當超過它就需要SideTables
。SideTables
內包含一個RefcountMap
,用來保存引用計數,根據對象地址取出其引用計數,類型是size_t
。
這里有個問題,為什么既要使用一個extra_rc
又要使用SideTables
?
可能是因為歷史問題,以前cpu是32
位的,isa
中能存儲的引用計數就只有。因此在
arm64
下,引用計數通常是存儲在isa
中的。
更具體的會在retain操作的時候講到。
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
設置阻塞標志位 - 使用weak
- Tagged Pointer,如果說上面的
self.target
指向的是一個Tagged Pointer
技術的NSString
,那程序就沒有問題。