iOS底層 -- 記憶體管理底層分析
歡迎閱讀iOS底層系列(建議按順序)
本文主要說明iOS
的記憶體優化方案,從底層探索系統優化記憶體的方式等。
1. ROM和RAM
ROM
是只讀儲存器
,是內部儲存器的一種。它用來儲存手機系統檔案、圖片、軟體等等,不會隨著掉電而丟失資料,ROM
越大儲存的資料就越多。
RAM
是隨機存取儲存器
,是內部儲存器最重要的一種,我們常稱為執行記憶體
(實體記憶體地址)。它的執行速度是比較快的,什麼時候需要資料,就從ROM
讀取資料加入記憶體,但同時RAM
斷電會丟失資料,所以手機斷電了會丟失原來正在執行的資料。RAM
記憶體越大,能同時執行的程式就越多,效能一般是越好的。
我們常說的記憶體管理,記憶體優化,指的是RAM
。
2.記憶體分割槽
-
核心區
:核心模組使用的區域。一般4GB的裝置,系統會使用1GB留給核心區。 -
棧區
:從高地址向低地址延伸,所以彙編中開闢棧空間使用sub
指令,且地址空間是連續的。它用來儲存區域性變數
,函式跳轉時的現場保護
,多餘引數
等。它是由系統管理的,在app
啟動時就確定了大小,壓棧超過固定大小會報棧溢位錯誤。所以大量的區域性變數,深遞迴可能耗盡棧記憶體而造成程式崩潰。 -
堆區
:從低地址向高地址延伸,系統使用連結串列來管理此空間,所以它的地址空間是不連續的。堆的空間比較大且是動態變化的, 一般由程式設計師管理。 -
全域性區
:初始化的全域性變數和靜態變數在一塊區域, 未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。程式結束後由系統自動釋放。 -
常量區
:存放一些常量字串,程式結束後由系統自動釋放。 -
程式碼區
:存放編譯後的程式碼資料。 -
保留區
:系統保留區域
其中,程式碼區
,常量區
,全域性區
在APP啟動時地址已固定,因此不會因為這些區的指標為空而產生崩潰性的錯誤。而堆的建立銷燬,棧的壓棧出棧,導致堆疊的空間時刻都在變化,所以當使用一個指標指向這兩個區裡面的記憶體時,一定要注意記憶體是否已經被釋放,否則會產生程式崩潰。
那為什麼棧區的速度比堆區快?
因為當訪問一個常規物件時,堆疊都參與了工作,需要先找到棧區儲存的指向物件地址的指標,根據指標在找到堆區的物件。
而棧區的資料是直接通過暫存器訪問的,所以棧區的速度比堆區快。
3.系統記憶體管理方案
記憶體是所有程式都使用的重要系統資源,系統一定會採取諸多的記憶體管理方案來優化記憶體。比如VM
,NONPOINTER_ISA
,TaggedPointer
,ARC
,autoreleasePool
等。
3.1 虛擬記憶體(VM)
在早期,程式是被完整的載入到實體記憶體,後來漸漸意識到這種做法的弊端:
-
記憶體地址是連續的,黑客可以很輕鬆的從一個程序地址獲取到其他程序的地址
-
每個時刻只會使用到程式的一小部分記憶體,可是卻把全部記憶體預先載入,明視訊記憶體在記憶體浪費
為了解決這些嚴重的問題,就引入了虛擬記憶體的概念:
虛擬記憶體允許作業系統擺脫物理
RAM
的限制。虛擬記憶體管理器建立一個虛擬地址空間,然後將其劃分為大小統一的記憶體塊,稱為頁數。處理器及其記憶體管理單元(MMU)維護一個頁面表,將程式虛擬地址空間中的頁面對映到計算機RAM
中的硬體地址。當程式的程式碼訪問記憶體中的地址時,MMU
使用頁表將指定的虛擬地址轉換為實際的硬體記憶體地址。該轉換自動發生,並且對於正在執行的應用程式是透明的。
虛擬記憶體 -> 對映表 -> 實體記憶體
對映需要的地址
簡單來說,程式被預先載入到虛擬記憶體空間,當程式的虛擬記憶體地址被訪問時,MMU
使用頁表將這個虛擬地址對映到實體地址,保證程式的正常執行。
通過引入虛擬記憶體,被使用的資料才會被對映到實體記憶體,不僅解決了記憶體浪費的問題,也解決了實體地址連續易破解的問題。
但在虛擬記憶體中地址依然是連續的,可以檢視編譯後在macho
中的虛擬記憶體地址是確定且連續的。
連續的地址始終是不安全的,蘋果為了解決這個問題,又引入了ASLR
的概念。
- ASLR(Address space layout randomization):地址空間佈局隨機化,虛擬地址在記憶體中會發生的偏移量。增加攻擊者預測目的地址的難度,防止攻擊者直接定位攻擊程式碼位置,達到阻止溢位攻擊的目的。
在app
啟動後,蘋果在原先虛擬記憶體的基礎上,又加上了一個隨機的地址值,且每次啟動不一致,保證了記憶體地址無法被直接定位。
這裡存在兩個虛擬地址的概念:
1.編譯後的虛擬地址:就是
macho
檔案中固定的地址2.執行後的虛擬地址:
macho
檔案中固定的地址加上ASLR
,就是執行後除錯輸出的地址
3.2 NONPOINTER_ISA
關於NONPOINTER_ISAd
的優化在包含永珍的isa中已經說明,這裡直接給出結論:
isa
是串聯物件,類,元類和根元類的重要線索,採用聯合體加位域
的資料結構使有限的空間充分利用,儲存了豐富的資訊
isa
的底層結構:
```
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
endif
};
``
因為
聯合體內部的的元素在記憶體中是互相覆蓋的,
isa採用聯合體的特點優化了記憶體;
而且類中有大量的資訊需要記錄,如果都額外宣告屬性儲存,需要不少記憶體,
NONPOINTER_ISA採用了
位域`的形式,使用每個二進位制位分別定義儲存的內容。
isa
做為物件和類的固有屬性,在記憶體中是大量存在的,任何一點優化帶來的價值是很可觀的。
關於isa
儲存的內容:
nonpointer
:表示是否對 isa 指標開啟指標優化(0:純isa指標,1:不⽌是類物件地址,isa 中包含了類資訊、物件的引⽤計數等)
has_assoc
:關聯物件標誌位(0沒有,1存在)
has_cxx_dtor
:該物件是否有 C++ 或者 Objc 的析構器,如果有解構函式,則需要做析構邏輯,如果沒有,則可以更快的釋放物件
shiftcls
:儲存類指標的值。開啟指標優化的情況下,在arm64架構下有33位用來儲存類指標
magic
:用於偵錯程式判斷當前物件是真的物件還是沒有初始化的空間
weakly_referenced
:物件是否被指向或者曾經指向⼀個 ARC 的弱變數,沒有弱引⽤的物件可以更快釋放。
unused(之前是deallocating)
:標誌物件是否正在釋放記憶體
extra_rc
:當表示該物件的引⽤計數值,實際上是引⽤計數值減 1,例如,如果物件的引⽤計數為 10,那麼 extra_rc 為 9。如果引⽤計數⼤於 10,則需要使⽤到下⾯的 has_sidetable_rc。
has_sidetable_rc
:當物件引⽤技術⼤於 10 時,則需要借⽤該變數儲存進位
其中nonpointer
,weakly_referenced
,unused
,extra_rc
,has_sidetable_rc
都和記憶體息息相關。
3.3 TaggedPointer
關於TaggedPointer
在包含永珍的isa中也做過介紹:
早期64位系統時,當我們儲存基礎資料型別 , 底層封裝成
NSNumber
物件 , 也會佔用8位元組記憶體 , 32位機器佔用4位元組。為了儲存和訪問一個NSNumber
物件,需要在堆上分配記憶體,另外還要維護它的引用計數,管理它的生命期 。這些都給程式增加了額外的邏輯,造成執行效率上的損失 。因此如果沒有額外處理 , 會造成很大空間浪費 .因此蘋果引入了TaggedPointer
,當物件為指標為TaggedPointer
型別時,指標的值不是地址了,而是真正的值,直接優化了儲存,提升了獲取速度。
TaggedPointer
的特點:
-
專門用來儲存小物件,例如
NSNumber
和部分NSString
-
指標不再儲存地址,而是直接儲存物件的值(異或後的值)。所以,它不是一個物件,而是一個偽裝成物件的普通變數。記憶體不在堆,而是在棧,由系統管理,不需要
malloc
和free
-
不需要處理引用計數,少了
retain
和release
的流程 -
在記憶體讀取上有著3倍的效率,建立時比以前快106倍。(少了
malloc
流程,獲取時直接從地址提取值)
在
iOS10.14
以下的版本,TaggedPointer
的地址儲存著真正的值,這對於攻擊者來說和明文沒有區別。因此iOS10.14
之後,蘋果加入了TaggedPointerObfuscator
(混淆器)的概念,TaggedPointer
的指標儲存的值就不是原始的值的了。
3.4 ARC
關於ARC
只要牢記這幾個規則:
- 自己生成的物件,自己持有
- 非自己生成的物件,自己也能持有
- 不再需要自己持有的物件時釋放
- 非自己持有的物件無法釋放
ARC
的其他相關部分大家已經足夠熟悉,不再贅述,後面只會從相關的原始碼進行解讀。
3.5 autoreleasePool
自動釋放池
提供了一種機制:可以放棄物件的所有權,但又避免其被提早釋放的可能性。
預設情況下,自動釋放池在runloop
的一個迭代週期結束時,會自動釋放這個週期所對應的哨兵物件的指標之後的所有物件。
大部分情況下,預設的情況足以保證記憶體的合理分配,但是某些特殊時刻,比如一個週期產生大量的臨時物件,會產生記憶體峰值,這時候就需要手動新增自動釋放池
了。
手動新增的自動釋放池,在自動釋放池的作用域結束時,就會銷燬其中產生的物件。這個時間點是快於runloop
的一個迭代週期,也就減少了峰值記憶體產生的可能。
4.相關原始碼探索
簡單介紹了幾種記憶體優化機制,下面從原始碼的角度看看它們相關實現。
4.1 TaggedPointer相關
之前在類的載入分析中,分析過libobjc
會從dyld
那邊接手map_images
,load_images
,unmap_image
三件事,其中map_images
時會來到_read_images
這個重要函式,
``` void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){
...
initializeTaggedPointerObfuscator();
...
}
``
省略處的程式碼分析過,直接來看
initializeTaggedPointerObfuscator()`
iOS10.14
之前,objc_debug_taggedpointer_obfuscator
預設為0
,之後等於一個隨機數。
然後系統對TaggedPointer
的賦值和獲取採用了較簡單且快速的方式:異或同一個數。
``` static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr) { return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr); }
static inline uintptr_t _objc_decodeTaggedPointer(const void * _Nullable ptr) { return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; } ``` 賦值時異或生成的隨機數,獲取時在異或這個隨機數。兩次異或後就會得到原來的值。
4.2 ARC
引用計數是如果儲存的?
為什麼優先存
isa
?
retain
如何處理引用計數的?
release
如何處理引用計數的?
alloc
的物件引用計數是多少?什麼時候呼叫
dealloc
,流程是怎樣的?
以上常見的面試題可以從原始碼的角度一一找到答案。本文使用的原始碼版本是objc4-818.2
4.2.1 retain
跟一下retain
的呼叫,(跟蹤流程比較簡單就跳過了,可以直接搜下面關鍵字)
retain -> _objc_rootRetain -> rootRetain
,
最後會來到rootRetain
:
objc_object::rootRetain()
{
return rootRetain(false, RRVariant::Fast);
}
傳入的引數會影響處理流程,需要注意下。
其中,rootRetain
傳入的tryRetain
是false
,variant
傳入的是Fast
``` ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant) { //TaggedPointer攔截 if (slowpath(isTaggedPointer())) return (id)this;
//散列表是否加鎖
bool sideTableLocked = false;
//是否轉錄到散列表
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
//獲取isa
oldisa = LoadExclusive(&isa.bits);
//variant不等於FastOrMsgSend所以不會走
if (variant == RRVariant::FastOrMsgSend) {
///它們在這裡是為了避免重新載入isa
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
return swiftRetain.load(memory_order_relaxed)((id)this);
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
}
//不是nonpointer的isa時
if (slowpath(!oldisa.nonpointer)) {
//isa是指向元類時
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return (id)this;
}
}
//dowhile處理引用計數
do {
transcribeToSideTable = false;
newisa = oldisa;
//不是nonpointer的isa時
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
//tryRetain是false,所以走sidetable_retain直接存散列表中的引用計數表
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain(sideTableLocked);
}
//是否正在析構,都在析構了,還想retain我?
if (slowpath(newisa.isDeallocating())) {
ClearExclusive(&isa.bits);
if (sideTableLocked) {
ASSERT(variant == RRVariant::Full);
sidetable_unlock();
}
if (slowpath(tryRetain)) {
return nil;
} else {
return (id)this;
}
}
/*
到這裡說明是NONPOINTER_ISA了
*/
//carry 進位標識,表示是否溢位
uintptr_t carry;
//isa對應的表示引用計數的位extra_rc做加1操作,同時返回是否carry
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
//如果carry,說明isa對應的表示引用計數的位裝滿了,溢位了
if (slowpath(carry)) {
//傳入的RRVariant是Fast,所以走rootRetain_overflow
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
//rootRetain_overflow傳入是RRVariant是Full,就間接來到這裡
if (!tryRetain && !sideTableLocked) sidetable_lock();
//散列表需要加鎖,需要轉錄到散列表,
sideTableLocked = true;
transcribeToSideTable = true;
//isa的extra_rc位儲存的值拿走一半,isa的has_sidetable_rc位開啟
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
//當發生進位溢位時,走sidetable_addExtraRC_nolock,isa的extra_rc位儲存的值存進去一半
if (variant == RRVariant::Full) {
if (slowpath(transcribeToSideTable)) {
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!transcribeToSideTable);
ASSERT(!sideTableLocked);
}
//處理完引用計數返回自己
return (id)this;
} ```
-
如果不是
nonpointer
的isa
,引用計數直接存引用計數表 -
是
nonpointer
的isa
時,如果isa
的指向正在析構,就不需要處理引用計數 -
如果不再析構,
isa
的引用計數的位做加1操作,同時返回是否carry
-
如果
carry
,呼叫rootRetain_overflow
把一半儲存到引用計數表
分析其中幾個比較重要的地方:
- sidetable_retain
``` id objc_object::sidetable_retain(bool locked) {
if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
endif
//根據當前物件在SideTables取出對應的SideTable
SideTable& table = SideTables()[this];
//加鎖
if (!locked) table.lock();
//得到已經儲存的引用計數
size_t& refcntStorage = table.refcnts[this];
//如果引用計數沒有越界,還夠儲存
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
//對應的引用計數的的位做加1操作
refcntStorage += SIDE_TABLE_RC_ONE;
}
//解鎖
table.unlock();
return (id)this;
} ```
總的來看,sidetable_retain
是對散列表中的引用計數表做加1
操作
先根據當前物件從SideTables
取出SideTable
,那為什麼不把所有物件都存在一張全域性的SideTable
,而是需要SideTables
來獲取對應的SideTable
呢?看下SideTable
的結構:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
SideTable
有三個屬性:
slock
,鎖refcnts
,引用計數表weak_table
,weak
表
因為每次操作SideTable
都需要加鎖解鎖,如果所有的物件都存在一張全域性的SideTable
,每次增刪改查時都需要操作這一整張大表,後續的操作都要等待前一次操作的完成,非常影響效能。
所以蘋果使用一張SideTables
儲存64張SideTable
的形式,每個物件通過hash找到自己的SideTable
進行操作,以空間換時間的方式提高了效能。
- RC_ONE
``` # define ISA_BITFIELD \ 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 unused : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19 //1+1+1+33+6+1+1+1 = 45
define RC_ONE (1ULL<<45)
``
RC_ONE根據不同的架構做位移,雖然各種架構的值不同,但最終都是得到
isa的
extra_rc`所在的位
- rootRetain_overflow
objc_object::rootRetain_overflow(bool tryRetain)
{
//再次呼叫rootRetain,標識為Full
return rootRetain(tryRetain, RRVariant::Full);
}
rootRetain_overflow
再次呼叫rootRetain
,只是改變了RRVariant
的標識為Full
,保證內部來到sidetable_addExtraRC_nolock
做溢位時存值到引用計數表的操作。
retain
如何處理引用計數 總結:
如果
retain
的物件是TaggedPointer
,retain
的物件isa
指向是元類不作處理。如果是普通的isa
,引用計數只存引用計數表中,如果是NONPOINTER_ISA
,如果retain
的物件正在析構不作處理,否則引用計數先存isa
的extra_rc
中,如果extra_rc
存滿了,拿一半存引用計數表中,依次反覆。
4.2.2 release
明白了retain
的流程,release
也就好理解了。
跟一下release
的呼叫,
release -> _objc_rootRelease -> rootRelease
,
最後會來到rootRelease
:
``` ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant) { //TaggedPointer攔截 if (slowpath(isTaggedPointer())) return false;
//散列表是否加鎖標識
bool sideTableLocked = false;
isa_t newisa, oldisa;
//獲取isa
oldisa = LoadExclusive(&isa.bits);
//RRVariant等於FastOrMsgSend時才走,傳的是Fast所以不走
if (variant == RRVariant::FastOrMsgSend) {
if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR()){
ClearExclusive(&isa.bits);
if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
swiftRelease.load(memory_order_relaxed)((id)this);
return true;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
return true;
}
}
//不是NONPOINTER_ISA時
if (slowpath(!oldisa.nonpointer)) {
//判斷是不是元類,元類就不處理了
if (oldisa.getDecodedClass(false)->isMetaClass()) {
ClearExclusive(&isa.bits);
return false;
}
}
retry: //dowhile處理引用計數 do { newisa = oldisa; //不是NONPOINTER_ISA時,說明isa沒有儲存引用計數 if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits); //直接呼叫sidetable_release減少sidetable中引用計數表的引用計數 return sidetable_release(sideTableLocked, performDealloc); } //如果正在析構,release沒有意義,不處理 if (slowpath(newisa.isDeallocating())) { ClearExclusive(&isa.bits); //如果表被鎖了,就解鎖 if (sideTableLocked) { ASSERT(variant == RRVariant::Full); sidetable_unlock(); } return false; }
//到這裡說明是NONPOINTER_ISA了
//carry 進位標識,表示是否溢位
uintptr_t carry;
//isa對應的表示引用計數的位extra_rc做減1操作,同時返回是否carry,這裡的進位是指isa中的引用計數減完時
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
if (slowpath(carry)) {
// don't ClearExclusive()
//發生溢位時,跳轉underflow分支
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
//isa存在析構標識時,跳轉deallocate分支
if (slowpath(newisa.isDeallocating()))
goto deallocate;
if (variant == RRVariant::Full) {
if (slowpath(sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!sideTableLocked);
}
return false;
//underflow分支
underflow:
newisa = oldisa;
//判斷isa的has_sidetable_rc位是否使用,使用說明引用計數表裡有引用計數
if (slowpath(newisa.has_sidetable_rc)) {
//RRVariant不是Full時
if (variant != RRVariant::Full) {
ClearExclusive(&isa.bits);
//呼叫rootRelease_underflow,再走一次rootRelease流程,RRVariant標識傳Full
return rootRelease_underflow(performDealloc);
}
//再走一次的rootRelease流程時來到這裡,下面的操作就是isa中的引用計數減完時,把引用計數表中的引用計數轉移到isa中
//如果sideTable沒加鎖,需要對sideTable加鎖,重新開始獲取isa以避免指標轉換,然後再去retry分支
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
// 嘗試從引用計數表中借一些引用計數
auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
//如果借完剩下是0,標識清除引用計數表
bool emptySideTable = borrow.remaining == 0;
//如果借出來的引用計數大於0,
if (borrow.borrowed > 0) {
//是否過渡釋放的標識
bool didTransitionToDeallocating = false;
//isa中存借出來的引用計數-1
//為什麼需要減1,看下上面給的extra_rc位的定義就知道了
newisa.extra_rc = borrow.borrowed - 1;
//has_sidetable_rc位是否開啟取決於引用計數表中是否還有引用計數
newisa.has_sidetable_rc = !emptySideTable;
//StoreReleaseExclusive內部呼叫__c11_atomic_compare_exchange_weak
//當前值與期望值相等時,修改當前值為設定值,返回true
//當前值與期望值不等時,將期望值修改為當前值,返回false
//isa更新是否成功標識
bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
//更新失敗且是NONPOINTER_ISA時,往isa中回填引用計數
if (!stored && oldisa.nonpointer) {
uintptr_t overflow;
newisa.bits =
addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
newisa.has_sidetable_rc = !emptySideTable;
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
if (stored) {
didTransitionToDeallocating = newisa.isDeallocating();
}
}
}
//更新失敗且不是NONPOINTER_ISA時,把引用計數放回引用計數表,再走retry分支
if (!stored) {
ClearExclusive(&isa.bits);
sidetable_addExtraRC_nolock(borrow.borrowed);
oldisa = LoadExclusive(&isa.bits);
goto retry;
}
//走到這裡,就是從引用計數表借值後減量成功
//如果emptySideTable為空,就清除引用計數表
if (emptySideTable)
sidetable_clearExtraRC_nolock();
if (!didTransitionToDeallocating) {
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
}
}
else {
}
}
//deallocate分支
deallocate:
ASSERT(newisa.isDeallocating());
ASSERT(isa.isDeallocating());
if (slowpath(sideTableLocked)) sidetable_unlock();
//在同一個執行緒(函式)中執行的執行緒和訊號處理程式之間的柵欄,dealloc要一個個來
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
//來到deallocate分支時,傳送dealloc訊息
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
} ```
其中幾個關鍵的地方說明下:
- sidetable_release
``` uintptr_t objc_object::sidetable_release(bool locked, bool performDealloc) {
if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
endif
//依然是在SideTables中找到對應SideTable
SideTable& table = SideTables()[this];
//是否要做dealloc操作的識別符號
bool do_dealloc = false;
//加鎖
if (!locked) table.lock();
//判斷該引用計數是否為引用計數表中的最後一個,如果是則表示release後需要執行dealloc清除物件所佔用的記憶體
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
auto &refcnt = it.first->second;
if (it.second) {
do_dealloc = true;
} else if (refcnt < SIDE_TABLE_DEALLOCATING) {
do_dealloc = true;
refcnt |= SIDE_TABLE_DEALLOCATING;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
refcnt -= SIDE_TABLE_RC_ONE;
}
table.unlock();
//傳送dealloc訊息
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
} ```
- rootRelease_underflow
objc_object::rootRelease_underflow(bool performDealloc)
{
//再次呼叫rootRelease,標識為Full
return rootRelease(performDealloc, RRVariant::Full);
}
release
如何處理引用計數總結:
如果
release
的物件是TaggedPointer
,release
的物件isa
指向是元類不作處理。如果是普通的isa
,從引用計數表中刪除引用計數,刪完了傳送dealloc
訊息;如果是NONPOINTER_ISA
,如果release
的物件正在析構不作處理,否則先從isa
中刪除引用計數,不夠刪時,從引用計數表取出來到isa
中刪除,兩者都刪完時傳送dealloc
訊息。
4.2.3 retainCount
alloc
的物件引用計數是多少?
現在來分析下引用計數是如何計算的。
跟一下retainCount
的呼叫,
retainCount -> _objc_rootRetainCount -> rootRetainCount
最後會來到rootRetainCount
:
需要注意的是,
objc4-818.2
的原始碼和objc4-7
系列rootRetainCount
實現有比較大的區別,下面給出兩個版本的原始碼做對比避免入坑
4.2.3.1 objc4-818.2
``` inline uintptr_t objc_object::rootRetainCount() { //TaggedPointer攔截 if (isTaggedPointer()) return (uintptr_t)this;
//加鎖
sidetable_lock();
//獲取isa
isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
//如果是NONPOINTER_ISA
if (bits.nonpointer) {
//引用計數等於extra_rc存的值
uintptr_t rc = bits.extra_rc;
//如果has_sidetable_rc位有值,說明引用計數表有儲存
if (bits.has_sidetable_rc) {
//引用計數加上引用計數表中存的值
rc += sidetable_getExtraRC_nolock();
}
//解鎖
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
``
當是
NONPOINTER_ISA時,引用計數的值等於
isa中
extra_rc的值加上引用計數表中存的值,不是
NONPOINTER_ISA時,來到
sidetable_retainCount`
``` uintptr_t objc_object::sidetable_retainCount() { //依然是SideTables找到對應的SideTable SideTable& table = SideTables()[this];
//至少為1
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;
} ```
如果是普通的isa
時,引用計數只等於引用計數表中的值。
4.2.3.2 objc4-781
``` inline uintptr_t objc_object::rootRetainCount() { //TaggedPointer攔截 if (isTaggedPointer()) return (uintptr_t)this;
//加鎖
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//如果是NONPOINTER_ISA
if (bits.nonpointer) {
//引用計數為extra_rc的值 + 1
uintptr_t rc = 1 + bits.extra_rc;
////如果has_sidetable_rc位有值,說明引用計數表有儲存
if (bits.has_sidetable_rc) {
//引用計數加上引用計數表中存的值
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
//原始碼一樣
return sidetable_retainCount();
} ```
當是NONPOINTER_ISA
時,引用計數的值等於isa
中extra_rc
的值加---1---
在加上引用計數表中存的值,不是NONPOINTER_ISA
時,來到sidetable_retainCount
4.2.3.3 讀二者區別引發的思考
有不少面試題會問 alloc的物件引用計數是多少。
這題乍看簡單,但實際是有坑的。
先分析二者最本質的區別:
在於
NONPOINTER_ISA
時,是否有手動加1
的操作。
因為這個操作會直接決定了 alloc的物件引用計數是多少 的答案!
如果是老版本的原始碼,alloc的物件引用計數是
0
如果是新版本的原始碼,alloc的物件引用計數是
1
關於alloc
的物件引用計數是0
,相信不少同學也看過相關文章裡面有這個結論:
alloc
後當獲取引用計數時,引用計數列印1,這只是因為呼叫retainCount
時,內部手動做了個+1
的操作,extra_rc
中實際儲存的是0
,所以物件實際的引用計數是0
。
所以早期的文章說alloc的物件引用計數是0是正確的。
那新版的retainCount
為什麼不需要加1呢?既然這裡不加1
,那可能是alloc
時就做了操作。於是我回頭看了新版的alloc
流程,發現在initIsa
多了一個操作:
果然,新版的alloc
在開闢空間時就把extra_rc
賦值為1
。感興趣的同學可以看下舊版的alloc
流程確認下有沒有這個賦值。
所以現在alloc的物件引用計數是1才是正確答案!
4.2.4 dealloc
什麼時候呼叫dealloc,流程是怎樣的?
現在來分析下這個面試常見題。
根據release
的流程已經得出,無論是哪種isa
,引用計數為0
時都會發送dealloc
訊息。
至於流程,直接來到dealloc
的原始碼:
dealloc -> _objc_rootDealloc -> rootDealloc
```
inline void objc_object::rootDealloc()
{
//TaggedPointer攔截
if (isTaggedPointer()) return;
//是NONPOINTER_ISA時
//weakly_referenced,has_assoc,has_cxx_dtor,has_sidetable_rc都不存在直接free
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
else
!isa.getClass(false)->hasCxxDtor() &&
endif
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
//其中之一存在,就走object_dispose
object_dispose((id)this);
}
} ```
TaggedPointer
攔截,不作處理- 如果是
NONPOINTER_ISA
時,且弱引用表,關聯物件表,c++解構函式,引用計數表都沒有使用到這個物件,直接free
快速釋放物件 - 其中之一存在時,走
object_dispose
慢速釋放
``` id object_dispose(id obj) { if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
``
呼叫
objc_destructInstance後,
free`釋放物件
``` void *objc_destructInstance(id obj) { if (obj) { //是否存在c++析構標識 bool cxx = obj->hasCxxDtor(); //是否有新增到關聯物件表標識 bool assoc = obj->hasAssociatedObjects();
//存在,呼叫c++解構函式
if (cxx) object_cxxDestruct(obj);
//存在,從關聯物件表中刪除
if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
} ```
- 獲取是否存在
c++
析構標識和新增到關聯物件表標識 - 哪個存在,就呼叫
c++
解構函式或者從關聯物件表中刪除 - 呼叫
clearDeallocating
``` inline void objc_object::clearDeallocating() { //是普通的isa,呼叫sidetable_clearDeallocating if (slowpath(!isa.nonpointer)) { sidetable_clearDeallocating(); } //是NONPOINTER_ISA,且weakly_referenced或has_sidetable_rc存在時,呼叫clearDeallocating_slow else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) { clearDeallocating_slow(); }
assert(!sidetable_present());
} ```
- 是普通的
isa
,呼叫sidetable_clearDeallocating
- 是
NONPOINTER_ISA
,且weakly_referenced
或has_sidetable_rc
存在時,呼叫clearDeallocating_slow
sidetable_clearDeallocating
和clearDeallocating_slow
的處理是一樣的,只是因為isa
型別不同,獲取標誌位的方式也不同,所以寫成了兩個函式表示,所以只看多數情況下的isa
處理
NEVER_INLINE void objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
//依然是從SideTables獲取對應的SideTable
SideTable& table = SideTables()[this];
//加鎖
table.lock();
//如果有weak標識
if (isa.weakly_referenced) {
//清空weak表中對應的entry陣列
weak_clear_no_lock(&table.weak_table, (id)this);
}
//如果有has_sidetable_rc標識
if (isa.has_sidetable_rc) {
//清空引用計數表
table.refcnts.erase(this);
}
table.unlock();
}
- 從
SideTables
獲取對應的SideTable
,然後加鎖 - 如果有
weak
標識,清空weak
表中對應的entry
陣列,entry
陣列中存在所有對該物件的弱引用 - 如果有
has_sidetable_rc
標識,抹去引用計數表,注意是引用計數表,而不是引用計數(表都刪了也不需要處理資料了)
dealloc流程總結:
如果
TaggedPointer
型別,不作處理。如果物件的弱引用,關聯物件,c++
解構函式,引用計數標識都不存在,直接釋放。否則存在c++
解構函式標識就呼叫c++
解構函式,存在關聯物件標識就從關聯物件表中刪除物件,存在弱引用標識就從弱引用表中刪除對應的陣列,存在引用計數標識就抹去對應的引用計數表。最後釋放物件。
騎槍弓策劍
4.2.5 剩餘面試題解答
根據以上所有的流程分析,還剩下的兩道面試題也就有了答案:
- 引用計數是如果儲存的?
如果不是
NONPOINTER_ISA
,引用計數只存在散列表中的引用計數表中;如果是NONPOINTER_ISA
,引用計數優先儲存在isa
的extra_rc
位中,存滿後,在使用引用計數表儲存。
- 為什麼優先存
isa
中?
引用計數存入引用計數表時,需要加鎖解鎖,
hash
查詢一系列過程,而存入isa
時,只需要位操作改變對應的值,所以優先存isa
中效率更高。
5.記憶體管理之迴圈引用和強引用
以下程式碼是否會引起迴圈引用?
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
很明顯,答案是會。那繼續追問:
以下方式可以解決迴圈引用嘛?
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
大部分同學看到__weak
關鍵字第一反應就會覺得__weak
可以解決迴圈引用。
其實,__weak
可以用來解決block
帶來的迴圈引用,但是timer
的卻不行。這是為什麼?
看下關於timer
的api
的官方文件:
譯: 當定時器觸發時將Selector指定的訊息傳送到的物件。計時器保持對該物件的強引用,直到它(計時器)失效。
target
會對傳入的引數進行一次強引用。那強引用為什麼不能使用__weak
解決呢?
要搞清楚這個問題,先看下相關列印:
很明顯,self
和weakSelf
指向的是同一個地址空間,也就是同一個物件,timer
操作weakSelf
實際上就是操作self
,所以使用weak
解決timer
的迴圈引用是無效的。
那為什麼block
的迴圈引用解決不存在這個這個問題呢?繼續列印:
雖然self
和weakSelf
指向的地址相同,但它們自身的指標地址是不同的。
看下block
捕獲物件的原始碼:
block
捕獲物件的不同之處在於:
block
捕獲的是**
型別,也就是捕獲指向傳入物件destArg
的指標地址。
所以,block
持有的是weakself
的指標地址,不是指向的物件本身,而timer
持有的是物件本身,這造成二者迴圈引用有本質的區別的。
至於timer
的解決方案也比較多,不是本文重點,這裡只推薦使用NSProxy
的方案,有興趣的同學可以自行研究下。
6.記憶體相關的程式碼面試題
1.記憶體分割槽之全域性靜態變數面試題
關於全域性靜態變數
有個相關的面試題:
定義一個值為100
的全域性靜態變數,如下操作後輸出:
``` //直接列印主類的personNum,對應第一次列印 NSLog(@"ViewController內部,&personNum = %p--personNum = %d\n",&personNum,personNum);
personNum = 10000;
//修改personNum等於10000後列印,對應第二次列印
NSLog(@"ViewController內部,&personNum = %p--personNum = %d\n",&personNum,personNum);
//內部對personNum做加1操作,內部也列印personNum,對應第三次列印
[[CJPerson new] add];
//列印personNum,對應第四次列印
NSLog(@"ViewController內部,&personNum = %p--personNum = %d\n",&personNum,personNum);
//內部無特殊操作,直接列印personNum,對應第五次列印
[[CJPerson alloc] cate_method];
```
—————————————————————————————————————————————
-
第一次輸出為
100
,地址為0x10ea9d338
-
第二次輸出為
10000
,地址為0x10ea9d338
-
第三次輸出為
101
,地址為0x10ea9d328
-
第四次輸出為
10000
,地址為0x10ea9d338
-
第五次輸出
100
,地址為0x10ea9d33c
—————————————————————————————————————————————
由①到②,可得:全域性靜態變數是可以被修改的。
由②到③,可得:生成新的全域性靜態變數地址,修改只針對自身檔案的全域性靜態變數有效。
由③到④,可是:其他檔案的全域性靜態變數修改不對自身檔案的產生影響。
由④到⑤,可得:分類也是新檔案,也會生成新的全域性靜態變數地址。
由以上可得總結:
全域性靜態變數的值是可以被
修改
的,全域性靜態變數的值只針對檔案
有效,每個檔案都生成自己的變數。
2.TaggedPointer面試題
以下兩段程式碼執行後分別有什麼結果?
```
self.queue = dispatch_queue_create("com.juejin.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"juejin"];
NSLog(@"%@",self.nameStr);
});
}
```
``` self.queue = dispatch_queue_create("com.juejin.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"juejin_iOS底層--記憶體管理分析"];
NSLog(@"%@",self.nameStr);
});
}
```
第一段正常執行,第二段執行的過程中崩潰。
為什麼程式碼看似簡單,內容也基本一樣,結果卻不一樣?
因為這題其實還涉及了多執行緒下setter
的使用。
先說下第二種崩潰的原因如下:
setter
方法主要做的事是對newvalue
進行retian
,對oldvalue
進行realase
,在多執行緒的環境中,對寫操作沒有加鎖,那麼一定會有多條執行緒競爭使用資源。某一時刻,當某條執行緒已經realase
了舊值,另一條執行緒這時候也開始setter
,對已經釋放的值進行retain
,這也就導致了程式的崩潰。
而第一種情況的特殊之處在於:
因為nameStr
的長度較短,系統把它設為TaggedPointer
物件,雖然也是處於多執行緒未加鎖競爭資源的條件下,但因為realase
和retain
在操作時,第一步就對TaggedPointer
進行了攔截,自然也就不會奔潰了。
``` ALWAYS_INLINE id objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant) { if (slowpath(isTaggedPointer())) return false; ... }
ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant) { if (slowpath(isTaggedPointer())) return (id)this; ... }
```
7.寫在後面
以上是對iOS
記憶體相關的一些分析,而記憶體分析遠不止於此,比如autoreleasePool
也是記憶體優化的重要組成部分。但因為擔心行文過長影響閱讀,就到此為止吧。
所以下一章是autoreleasePool
的底層分析。
敬請關注。