Objective-C之Autorelease Pool底層實現原理記錄(雙向鏈表)以及在Runloop中是如何參與進去的

最近需要重新整理知識點備用,把一些重要的原理都搞了一遍

NSDictionary和NSArray底層原理

HTTPS層引出OSI全部模型數據協議流轉全過程

Xcode Command + R全過程以及啓動優化

前言

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([MTFAppDelegate class]));
    }
}

以上就是我們所看到的第一個自動釋放池寫法,按我之前的理解如下

1.自動釋放池內部是由 AutoreleasePoolPage爲節點的雙向鏈表結構形成,AutoreleasePool本身沒有任何形式的結構

2.當對象在autoreleasepool裏面調用隱式執行autorelease的時候,會將對象加入上述以AutoreleasePoolPage爲節點的雙向鏈表中

3.每一個自動釋放池初始化調用objc_autoreleasePoolPush(內部是會有一個哨兵對象作爲標記,我的理解是一個自動釋放池對應一個哨兵token),當objc_autoreleasePoolPop調用會根據傳入的哨兵對象進行地址偏移,然後遍歷出對象挨個執行release操作,知道遇到下一個哨兵或者stop爲止

由於很早之前看到雷純峯德萊文大神的文章,知道原理,但是一直沒有系統記錄下知識點,乘國慶有時間,又閱讀了這兩位大神的文章,特此記錄下知識點,並加上些自己的理解,方便新手看懂和自己溫故知新

 

@autoreleasepool

根據我們看到的第一個main函數的自動釋放池,可以看到整個 iOS 的應用都是包含在一個自動釋放池 block 中的

首先通過clang把OC代碼轉換成c++runtime代碼

$ clang -rewrite-objc main.m

如下 也就是說@autoreleasepool {} 被轉換爲一個 __AtAutoreleasePool 結構體: 

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_cz_5w_ql3y92hzcthzvjv84fcl80000gn_T_main_7919a8_mi_0);
    }
    return 0;
}
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

最終轉換出來實際上我們看到的main函數代碼就是這樣的

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        // do whatever you want
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

 

AutoreleasePoolPage 的結構

上述展開的代碼實際上就是如下PoolPage的操作

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

什麼是AutoreleasePoolPage?

其實AutoreleasePool沒有單獨的內存結構,而是通過AutoreleasePoolPage爲節點的雙向鏈表來實現。

  • 每一個線程的 autoreleasepool 其實就是一個指針的堆棧;
  • 每一個指針代表一個需要 release 的對象或者 POOL_SENTINEL(哨兵對象,代表一個 autoreleasepool 的邊界);
  • 一個 pool token 就是這個 pool 所對應的 POOL_SENTINEL 的內存地址。當這個 pool 被 pop 的時候,所有內存地址在 pool token 之後的對象都會被 release ;
  • 這個堆棧被劃分成了一個以 page 爲結點的雙向鏈表。pages 會在必要的時候動態地增加或刪除;
  • Thread-local storage(線程局部存儲)指向 hot page ,即最新添加的 autoreleased 對象所在的那個 page 

空的poolpage如下

  1. magic 用來校驗 AutoreleasePoolPage 的結構是否完整;
  2. next 指向最新添加的 autoreleased 對象的下一個位置,初始化時指向 begin() ;
  3. thread 指向當前線程;
  4. parent 指向父結點,第一個結點的 parent 值爲 nil ;雙向鏈表上一個節點
  5. child 指向子結點,最後一個結點的 child 值爲 nil ;雙向鏈表下一個節點
  6. depth 代表深度,從 0 開始,往後遞增 1;
  7. hiwat 代表 high water mark 。
  8. 每一個自動釋放池都是由一系列的 AutoreleasePoolPage 組成的,並且每一個 AutoreleasePoolPage 的大小都是 4096 字節(16 進制 0x1000)

另外,當 next == begin() 時,表示 AutoreleasePoolPage 爲空;當 next == end() 時,表示 AutoreleasePoolPage 已滿。

其中有 56 bit 用於存儲 AutoreleasePoolPage 的成員變量,剩下的 0x100816038 ~ 0x100817000 都是用來存儲加入到自動釋放池中的對象

begin() 和 end() 這兩個類的實例方法幫助我們快速獲取 0x100816038 ~ 0x100817000 這一範圍的邊界地址。

next 指向了下一個爲空的內存地址,如果 next 指向的地址加入一個 object,也就是AutoreleasePool當中加入一個對象執行autorelease方法後,它就會如下圖所示移動到下一個爲空的內存地址中

objc_autoreleasePoolPush (Push操作)

一些列的轉換autoreleasePool的push方法轉換爲 AutoreleasePoolPage 的 push 函數,來看下它的作用和執行過程。一個 push 操作其實就是創建一個新的 autoreleasepool ,對應 AutoreleasePoolPage 的具體實現就是往 AutoreleasePoolPage 中的 next 位置插入一個 POOL_SENTINEL ,並且返回插入的 POOL_SENTINEL 的內存地址。這個地址也就是我們前面提到的 pool token ,在執行 pop 操作的時候作爲函數的入參。 

上面的AutoreleasePoolPage在這裏會進入一個比較關鍵的方法 autoreleaseFast,並傳入哨兵對象 POOL_SENTINEL

static inline id *autoreleaseFast(id obj)
{
   AutoreleasePoolPage *page = hotPage();
   if (page && !page->full()) {
       return page->add(obj);
   } else if (page) {
       return autoreleaseFullPage(obj, page);
   } else {
       return autoreleaseNoPage(obj);
   }
}

上述方法分三種情況選擇不同的代碼執行:

  • 有 hotPage 並且當前 page 不滿
    • 調用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中
  • 有 hotPage 並且當前 page 已滿
    • 調用 autoreleaseFullPage 初始化一個新的頁
    • 調用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中
  • 無 hotPage
    • 調用 autoreleaseNoPage 創建一個 hotPage
    • 調用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中

最後的都會調用 page->add(obj) 將對象添加到自動釋放池中。

hotPage 可以理解爲當前正在使用的 AutoreleasePoolPage

 

POOL_SENTINEL(哨兵對象)

這裏很有必要先介紹下這個東西,正常情況下他就是nil的別名

#define POOL_SENTINEL nil

在每個自動釋放池初始化調用 objc_autoreleasePoolPush 的時候,都會把一個 POOL_SENTINEL push 到自動釋放池的棧頂,並且返回這個 POOL_SENTINEL 哨兵對象。

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        // do whatever you want
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

上面的 atautoreleasepoolobj 就是一個 POOL_SENTINEL

理解:

當我們看到一個@autoreloeasepool{}的代碼的時候,轉換之後如上代碼,可以理解爲在雙向鏈表結構的基礎上,每個node節點就是poolpage對象,該對象有固定大小4096,前幾個字節用於存儲屬性字段,後面從begin地址開始到end地址結束用來存儲自動釋放池裏面的對象,就會在屬性字段挨着的地址上出現一個哨兵標誌POOL_SENTINEL,也就是nil標誌自動釋放池的出現,返回值地址用來標誌對應的池子,後續pop的時候根據池子遍歷對象挨個執行release操作

 

 autorelease 操作

AutoreleasePoolPage 的 autorelease 函數的實現對我們來說就比較容量理解了,它跟 push 操作的實現非常相似。只不過 push 操作插入的是一個 POOL_SENTINEL ,而 autorelease 操作插入的是一個具體的 autoreleased 對象。


static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  *dest == obj);
    return obj;
}

 

objc_autoreleasePoolPop 方法

同理,前面提到的 objc_autoreleasePoolPop 函數本質上也是調用的 AutoreleasePoolPage 的 pop 函數。

pop 函數的入參就是 push 函數的返回值,也就是 POOL_SENTINEL 的內存地址,即 pool token 。當執行 pop 操作時,內存地址在 pool token 之後的所有 autoreleased 對象都會被 release 。直到 pool token 所在 page 的 next 指向 pool token 爲止。

下面是某個線程的 autoreleasepool 堆棧的內存結構圖,在這個 autoreleasepool 堆棧中總共有兩個 POOL_SENTINEL ,即有兩個 autoreleasepool 。該堆棧由三個 AutoreleasePoolPage 結點組成,第一個 AutoreleasePoolPage 結點爲 coldPage() ,最後一個 AutoreleasePoolPage 結點爲 hotPage() 。其中,前兩個結點已經滿了,最後一個結點中保存了最新添加的 autoreleased 對象 objr3 的內存地址。

如果執行pop(token),autoreleasepool對應的堆棧信息就會變成如下

 

總結:

1.@autorelease展開來其實就是objc_autoreleasePoolPush和objc_autoreleasePoolPop,但是這兩個函數也是封裝的一個底層對象AutoreleasePoolPage,實際對應的是AutoreleasePoolPage::push和AutoreleasePoolPage::pop

2.autoreleasepool本身並沒有內部結構,而是一種通過AutoreleasePoolPage爲節點的雙向鏈表結構

3.根據AutoreleasePoolPage雙向鏈表的結構,可以看到當調用objc_autoreleasePoolPush的時候實際上除了初始化poolpage對象屬性之外,還會插入一個POOL_SENTINEL哨兵,用來區分不同autoreleasepool之間包裹的對象。

4.當對象調用 autorelease 方法時,會將實際對象插入 AutoreleasePoolPage 的棧中,通過next指針移動。

5.autoreleasePoolPage的結構字段上面有介紹,其中每個雙向鏈表的node節點也就是poolpage對象內存大小爲4096,除了基礎屬性之外,外插一個POOL_SENTINEL,每出現一個@autorelease就會有一個哨兵,剩下的通過begin和end來標識是否存儲滿,滿了就會重新創建一個poolpage來鏈接鏈表,按照這個套路,出現一個PoolPush就創建一個哨兵,出現一個對象的autorelease,就增加一個實際的對象,滿了就創建新的鏈表節點這樣衍生下去

6.AutoreleasePoolPage::pop那麼當調用pop的時候,會傳入需要drain的哨兵節點,遍歷該內存地址上方所有對象,直到遇到對應的哨兵,然後釋放棧中遍歷到的對象,每刪除一頁就修正雙向鏈表的指針,最後兩張圖很容易理解

7.ARC下,直接調用上面的方法,整個線程都被自動釋放池雙向鏈表管理,Push創建的時候插入哨兵對象,當我們在內部寫代碼的時候,會自動添加Autorelease,對象會加入到在哨兵節點之間,加入到next指針上,一個個往後移,滿了4096就換下一個poolPage對象節點來存儲,出了釋放池,會調用pop,傳入自動釋放池的哨兵給pop,然後遍歷哨兵內存地址之後的所有對象執行release,最後吧next指針移到目標哨兵

8.Runloop這裏就不介紹了,可以翻看另外寫的博客,App啓動的時候會在主Runloop裏面註冊兩個觀察者和一個回調函數,

第一個Observe觀察到entry即將進入loop的時候,會調用_objc_autoreleasePoolPush()創建自動釋放池,優先級最高,保證在所有回調方法之前。

第二個Observe觀察到即將進入休眠或者退出的時候,當監聽到Beforewaiting的時候,調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的創建新的,當監聽到Exit的時候調用_objc_autoreleasePoolPop釋放pool,這裏的Observe優先級最低,發生在所有回調函數之後。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章