1. 程式人生 > >Objective-C之Autorelease Pool底層實現原理記錄(雙向連結串列)以及在Runloop中是如何參與進去的

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

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

前言

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對應的堆疊資訊就會變成如下

總結:

[email protected]展開來其實就是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優先順序最低,發生在所有回撥函式之後。

相關推薦

Objective-CAutorelease Pool底層實現原理記錄雙向連結串列以及Runloop是如何參與進去

最近需要重新整理知識點備用,把一些重要的原理都搞了一遍 前言 int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, a

c++stl的list雙向連結串列

1.list初始化: (1)list<int>  t;  //沒有任何元素 (2)list<int>  t(10);  //建立有10個元素的連結串列 (3)lis

Objective-C Autorelease Pool實現原理

記憶體管理一直是學習 Objective-C 的重點和難點之一,儘管現在已經是 ARC 時代了,但是瞭解 Objective-C 的記憶體管理機制仍然是十分必要的。其中,弄清楚 autorelease 的原理更是重中之重,只有理解了 autorelease 的原理,我們才算是真正瞭解了 Obje

C++ 類模板小結雙向連結串列的類模板實現

一、類模板定義 定義一個類模板:template<class 模板引數表> class 類名{ // 類定義...... };其中,template 是宣告類模板的關鍵字,表示宣告一個模板,模板引數可以是一個,也可以是多個,可以是型別引數,也可以是非型別引數。型

華為面試題——約瑟夫問題的C++簡單實現迴圈連結串列

/*     author:jiangxin     Blog:http://blog.csdn.net/jiangxinnju     Function:method of Josephus question */ #include <iostream> us

資料結構與演算法JavaScript描述讀書筆記js實現連結串列-雙向連結串列

雙向連結串列 雙向連結串列的 remove() 方法比單向連結串列的效率更高,因為不需要再查詢前驅節點了 //建立建構函式建立節點 function Node(element){ this.element = element; this.next = null; th

LeetCode 刪除連結串列重複的元素簡單 連結串列

問題描述 給定一個排序連結串列,刪除所有重複的元素,使得每個元素只出現一次。 示例 1: 輸入: 1->1->2 輸出: 1->2 示例 2: 輸入: 1->1->2->3->3 輸出: 1->2->3

模板容器類及迭代器的實現基於連結串列

節點類標頭檔案node.h: #ifndef MAIN_WTF_NODE1_H #define MAIN_WTF_NODE1_H #include <cstdlib> namespace main_wtf_6B {template<class Item>class

Lava連結串列雙向連結串列---介面實現

在Java中連標配的結點需要用類來封裝,下面的簡單的雙向連結串列實現: class Node { private Object data; private Node next; public Node(Object data) {

LeetCode兩數相加中等 連結串列

問題描述: 給定兩個非空連結串列來表示兩個非負整數。位數按照逆序方式儲存,它們的每個節點只儲存單個數字。將兩數相加返回一個新的連結串列。 你可以假設除了數字 0 之外,這兩個數字都不會以零開頭。 示例: 輸入:(2 -> 4 -> 3) + (5 ->

c++ 模版程式設計,構造迭代器和雙向連結串列

#pragma once #include <iostream> #include <stdexcept> #include "func.h" // 連結串列 template <typename T>class li

演算法精解C語言描述迴圈連結串列

迴圈連結串列      顧名思義,首尾相連的連結串列即是迴圈連結串列。可以是單鏈表,也可以是雙鏈表。     迴圈連結串列是另一種形式的連結串列,它提供了更為靈活的遍歷連結串列元素的能力。迴圈連結串列可以是單向的或雙向的,但區分一個連結串列是

osgEarth的Rex引擎原理分析三十一osgEarth::TerrainLayer的_memCacheosgEarth::MemCache詳解

目標:(二十一)中的問題66 繼承關係: osgEarth::Cache     osgEarth::MemCache cache中的資料是通過cachebin來實現存放的,cachebin有兩種,繼承關係為: osgEarth::CacheBin

C語言資料結構----連結串列靜態連結串列

看了老唐的靜態連結串列,越發的覺得自己是菜鳥了,因為看的過程實在是太糾結了。下面就把自己看老唐靜態連結串列的內容寫下來。 一、靜態連結串列的基礎知識 1.單鏈表的缺陷:單鏈表的實現嚴重依賴指標,每一個數據元素都要有額外的指標域。 2.在靜態表中我們把資料元素放在一個數組裡,

LeetCode 206. Reverse Linked List反轉連結串列兩種方法實現

本文用兩種方法實現連結串列的反轉(LeetCode 206. Reverse Linked List):①利用next指標穿針引線;②遞迴演算法。 題目: Reverse a singly linked list. Example: Input: 1->2-&g

c++ stl list環狀雙向連結串列

1.list   相較於vector的連續線性空間,list就顯得複雜很多,它的好處是每次插入或刪除一個元素,就配置或釋放一個元素的空間。因此,list對空間的運用有絕對的精確,一點也不浪費。而且,對於任何位置的元素插入和元素移除,list永遠是常數時間。   list的節

LeetCode Sliding Window Maximum 滑動視窗雙向連結串列實現佇列效果

思路: 使用雙向連結串列(LinkedList,LinkedList類是雙向列表,列表中的每個節點都包含了對前一個和後一個元素的引用)。 雙向連結串列的大小就是視窗的個數,每次向視窗中增加一個元素時,如果比視窗中最後一個大,就刪除視窗中最後一個,以此類推,來

一步一步寫演算法雙向連結串列

【 宣告:版權所有,歡迎轉載,請勿用於商業用途。  聯絡信箱:feixiaoxing @163.com】     前面的部落格我們介紹了單向連結串列。那麼我們今天介紹的雙向連結串列,顧名思義,就是資料本身具備了左邊和右邊的雙向指標。雙向連結串列相比較單向連結串列,主要有下

線性錶鏈式儲存靜態連結串列及其12種操作的實現

該表格中的所有複雜度均指的是當前程式中演算法的複雜度,同一個操作演算法不同複雜度不同。 對於空間複雜度:沒有程式的空間複雜度為0,任何程式的執行必須要空間。當所需的空間是常數的,就是O(1);是線性的,就是O(n),以此類推。 操作 時間複雜度(T(n)) 空間複

資料結構線性表順序表,單鏈表,迴圈連結串列雙向連結串列-- 圖書管理系統

順序表 #include <iostream> #include <cstring> #include <cstdlib>///exit()標頭檔案exit(0):正常執行程式並退出程式。exit(1):非正常執行導致退出程式 #incl