iOS底層 -- 記憶體管理底層分析

語言: CN / TW / HK

歡迎閱讀iOS底層系列(建議按順序)

iOS底層 - alloc和init探索

iOS底層 - 包羅永珍的isa

iOS底層 - 類的本質分析

iOS底層 - cache_t流程分析

iOS底層 - 方法查詢流程分析

iOS底層 - 訊息轉發流程分析

iOS底層 - dyld是如何載入app的

iOS底層 - 類的載入分析

iOS底層 - 分類的載入分析

iOS探索 - 多執行緒之相關原理

iOS探索 - 多執行緒之GCD應用

iOS探索 - 多執行緒之底層篇

iOS探索 - block原理

本文主要說明iOS的記憶體優化方案,從底層探索系統優化記憶體的方式等。

1. ROM和RAM

ROM只讀儲存器,是內部儲存器的一種。它用來儲存手機系統檔案、圖片、軟體等等,不會隨著掉電而丟失資料,ROM越大儲存的資料就越多。

RAM隨機存取儲存器,是內部儲存器最重要的一種,我們常稱為執行記憶體(實體記憶體地址)。它的執行速度是比較快的,什麼時候需要資料,就從ROM讀取資料加入記憶體,但同時RAM斷電會丟失資料,所以手機斷電了會丟失原來正在執行的資料。RAM記憶體越大,能同時執行的程式就越多,效能一般是越好的。

我們常說的記憶體管理,記憶體優化,指的是RAM

2.記憶體分割槽

16101390-f1ab0d01f640b860.webp

  • 核心區:核心模組使用的區域。一般4GB的裝置,系統會使用1GB留給核心區。

  • 棧區:從高地址向低地址延伸,所以彙編中開闢棧空間使用sub指令,且地址空間是連續的。它用來儲存區域性變數,函式跳轉時的現場保護多餘引數等。它是由系統管理的,在app啟動時就確定了大小,壓棧超過固定大小會報棧溢位錯誤。所以大量的區域性變數,深遞迴可能耗盡棧記憶體而造成程式崩潰。

  • 堆區:從低地址向高地址延伸,系統使用連結串列來管理此空間,所以它的地址空間是不連續的。堆的空間比較大且是動態變化的, 一般由程式設計師管理。

  • 全域性區:初始化的全域性變數和靜態變數在一塊區域, 未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。程式結束後由系統自動釋放。

  • 常量區:存放一些常量字串,程式結束後由系統自動釋放。

  • 程式碼區:存放編譯後的程式碼資料。

  • 保留區:系統保留區域

其中,程式碼區常量區全域性區在APP啟動時地址已固定,因此不會因為這些區的指標為空而產生崩潰性的錯誤。而堆的建立銷燬,棧的壓棧出棧,導致堆疊的空間時刻都在變化,所以當使用一個指標指向這兩個區裡面的記憶體時,一定要注意記憶體是否已經被釋放,否則會產生程式崩潰。

那為什麼棧區的速度比堆區快?

因為當訪問一個常規物件時,堆疊都參與了工作,需要先找到棧區儲存的指向物件地址的指標,根據指標在找到堆區的物件。

而棧區的資料是直接通過暫存器訪問的,所以棧區的速度比堆區快。

3.系統記憶體管理方案

記憶體是所有程式都使用的重要系統資源,系統一定會採取諸多的記憶體管理方案來優化記憶體。比如VM,NONPOINTER_ISA,TaggedPointer,ARC,autoreleasePool等。

3.1 虛擬記憶體(VM)

在早期,程式是被完整的載入到實體記憶體,後來漸漸意識到這種做法的弊端:

  • 記憶體地址是連續的,黑客可以很輕鬆的從一個程序地址獲取到其他程序的地址

  • 每個時刻只會使用到程式的一小部分記憶體,可是卻把全部記憶體預先載入,明視訊記憶體在記憶體浪費

為了解決這些嚴重的問題,就引入了虛擬記憶體的概念:

虛擬記憶體允許作業系統擺脫物理RAM的限制。虛擬記憶體管理器建立一個虛擬地址空間,然後將其劃分為大小統一的記憶體塊,稱為頁數。處理器及其記憶體管理單元(MMU)維護一個頁面表,將程式虛擬地址空間中的頁面對映到計算機RAM中的硬體地址。當程式的程式碼訪問記憶體中的地址時,MMU使用頁表將指定的虛擬地址轉換為實際的硬體記憶體地址。該轉換自動發生,並且對於正在執行的應用程式是透明的。

虛擬記憶體 -> 對映表 -> 實體記憶體

截圖2021-04-19 上午11.38.33.png

對映需要的地址

截圖2021-04-19 上午11.39.36.png

簡單來說,程式被預先載入到虛擬記憶體空間,當程式的虛擬記憶體地址被訪問時,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 時,則需要借⽤該變數儲存進位

其中nonpointerweakly_referencedunusedextra_rchas_sidetable_rc都和記憶體息息相關。

3.3 TaggedPointer

關於TaggedPointer包含永珍的isa中也做過介紹:

早期64位系統時,當我們儲存基礎資料型別 , 底層封裝成NSNumber物件 , 也會佔用8位元組記憶體 , 32位機器佔用4位元組。為了儲存和訪問一個NSNumber物件,需要在堆上分配記憶體,另外還要維護它的引用計數,管理它的生命期 。這些都給程式增加了額外的邏輯,造成執行效率上的損失 。因此如果沒有額外處理 , 會造成很大空間浪費 .因此蘋果引入了TaggedPointer,當物件為指標為TaggedPointer型別時,指標的值不是地址了,而是真正的值,直接優化了儲存,提升了獲取速度。

TaggedPointer的特點:

  • 專門用來儲存小物件,例如NSNumber和部分NSString

  • 指標不再儲存地址,而是直接儲存物件的值(異或後的值)。所以,它不是一個物件,而是一個偽裝成物件的普通變數。記憶體不在堆,而是在棧,由系統管理,不需要mallocfree

  • 不需要處理引用計數,少了retainrelease的流程

  • 在記憶體讀取上有著3倍的效率,建立時比以前快106倍。(少了malloc流程,獲取時直接從地址提取值)

iOS10.14以下的版本,TaggedPointer的地址儲存著真正的值,這對於攻擊者來說和明文沒有區別。因此iOS10.14之後,蘋果加入了TaggedPointerObfuscator(混淆器)的概念,TaggedPointer的指標儲存的值就不是原始的值的了。

3.4 ARC

關於ARC只要牢記這幾個規則:

  1. 自己生成的物件,自己持有
  2. 非自己生成的物件,自己也能持有
  3. 不再需要自己持有的物件時釋放
  4. 非自己持有的物件無法釋放

ARC的其他相關部分大家已經足夠熟悉,不再贅述,後面只會從相關的原始碼進行解讀。

3.5 autoreleasePool

自動釋放池提供了一種機制:可以放棄物件的所有權,但又避免其被提早釋放的可能性

預設情況下,自動釋放池在runloop的一個迭代週期結束時,會自動釋放這個週期所對應的哨兵物件的指標之後的所有物件。

大部分情況下,預設的情況足以保證記憶體的合理分配,但是某些特殊時刻,比如一個週期產生大量的臨時物件,會產生記憶體峰值,這時候就需要手動新增自動釋放池了。

手動新增的自動釋放池,在自動釋放池的作用域結束時,就會銷燬其中產生的物件。這個時間點是快於runloop的一個迭代週期,也就減少了峰值記憶體產生的可能。

4.相關原始碼探索

簡單介紹了幾種記憶體優化機制,下面從原始碼的角度看看它們相關實現。

4.1 TaggedPointer相關

之前在類的載入分析中,分析過libobjc會從dyld那邊接手map_imagesload_imagesunmap_image三件事,其中map_images時會來到_read_images這個重要函式,

``` void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){

...
initializeTaggedPointerObfuscator();
...

} `` 省略處的程式碼分析過,直接來看initializeTaggedPointerObfuscator()`

截圖2021-04-23 下午5.30.19.png

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傳入的tryRetainfalsevariant傳入的是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;

} ```

  1. 如果不是nonpointerisa,引用計數直接存引用計數表

  2. nonpointerisa時,如果isa的指向正在析構,就不需要處理引用計數

  3. 如果不再析構,isa的引用計數的位做加1操作,同時返回是否carry

  4. 如果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_tableweak

因為每次操作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根據不同的架構做位移,雖然各種架構的值不同,但最終都是得到isaextra_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的物件是TaggedPointerretain的物件isa指向是元類不作處理。如果是普通的isa,引用計數只存引用計數表中,如果是NONPOINTER_ISA,如果retain的物件正在析構不作處理,否則引用計數先存isaextra_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的物件是TaggedPointerrelease的物件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時,引用計數的值等於isaextra_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時,引用計數的值等於isaextra_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多了一個操作:

截圖2021-06-04 下午3.44.19.png

果然,新版的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);
}

} ```

  1. TaggedPointer攔截,不作處理
  2. 如果是NONPOINTER_ISA時,且弱引用表,關聯物件表,c++解構函式,引用計數表都沒有使用到這個物件,直接free快速釋放物件
  3. 其中之一存在時,走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;

} ```

  1. 獲取是否存在c++析構標識和新增到關聯物件表標識
  2. 哪個存在,就呼叫c++解構函式或者從關聯物件表中刪除
  3. 呼叫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());

} ```

  1. 是普通的isa,呼叫sidetable_clearDeallocating
  2. NONPOINTER_ISA,且weakly_referencedhas_sidetable_rc存在時,呼叫clearDeallocating_slow
  3. sidetable_clearDeallocatingclearDeallocating_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(); }

  1. SideTables獲取對應的SideTable,然後加鎖
  2. 如果有weak標識,清空weak表中對應的entry陣列,entry陣列中存在所有對該物件的弱引用
  3. 如果有has_sidetable_rc標識,抹去引用計數表,注意是引用計數表,而不是引用計數(表都刪了也不需要處理資料了)

dealloc流程總結:

如果TaggedPointer型別,不作處理。如果物件的弱引用,關聯物件,c++解構函式,引用計數標識都不存在,直接釋放。否則存在c++解構函式標識就呼叫c++解構函式,存在關聯物件標識就從關聯物件表中刪除物件,存在弱引用標識就從弱引用表中刪除對應的陣列,存在引用計數標識就抹去對應的引用計數表。最後釋放物件。

騎槍弓策劍

4.2.5 剩餘面試題解答

根據以上所有的流程分析,還剩下的兩道面試題也就有了答案:

  • 引用計數是如果儲存的?

如果不是NONPOINTER_ISA,引用計數只存在散列表中的引用計數表中;如果是NONPOINTER_ISA,引用計數優先儲存在isaextra_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的卻不行。這是為什麼?

看下關於timerapi的官方文件:

截圖2021-06-07 下午3.15.35.png

譯: 當定時器觸發時將Selector指定的訊息傳送到的物件。計時器保持對該物件的強引用,直到它(計時器)失效。

target會對傳入的引數進行一次強引用。那強引用為什麼不能使用__weak解決呢?

要搞清楚這個問題,先看下相關列印:

截圖2021-06-07 下午3.23.19.png

很明顯,selfweakSelf指向的是同一個地址空間,也就是同一個物件,timer操作weakSelf實際上就是操作self,所以使用weak解決timer的迴圈引用是無效的。

那為什麼block的迴圈引用解決不存在這個這個問題呢?繼續列印:

截圖2021-06-07 下午3.23.14.png

雖然selfweakSelf指向的地址相同,但它們自身的指標地址是不同的。

截圖2021-06-07 下午4.19.50.png

看下block捕獲物件的原始碼:

截圖2021-06-07 下午4.22.16.png

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];

```

截圖2021-06-07 上午10.57.28.png

—————————————————————————————————————————————

  1. 第一次輸出為100,地址為0x10ea9d338

  2. 第二次輸出為10000,地址為0x10ea9d338

  3. 第三次輸出為101,地址為0x10ea9d328

  4. 第四次輸出為10000,地址為0x10ea9d338

  5. 第五次輸出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物件,雖然也是處於多執行緒未加鎖競爭資源的條件下,但因為realaseretain在操作時,第一步就對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的底層分析。

敬請關注。