程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> Objective-C Autorelease Pool 的實現原理

Objective-C Autorelease Pool 的實現原理

編輯:關於C語言

Objective-C Autorelease Pool 的實現原理


內存管理一直是學習 Objective-C 的重點和難點之一,盡管現在已經是 ARC 時代了,但是了解 Objective-C 的內存管理機制仍然是十分必要的。其中,弄清楚 autorelease 的原理更是重中之重,只有理解了 autorelease 的原理,我們才算是真正了解了 Objective-C 的內存管理機制。注:本文使用的 runtime 源碼是當前的最新版本 objc4-646.tar.gz

autoreleased 對象什麼時候釋放

autorelease 本質上就是延遲調用 release ,那 autoreleased 對象究竟會在什麼時候釋放呢?為了弄清楚這個問題,我們先來做一個小實驗。這個小實驗分 3 種場景進行,請你先自行思考在每種場景下的 console 輸出,以加深理解。注:本實驗的源碼可以在這裡 AutoreleasePool 找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
__weak NSString *string_weak_ = nil;

- (void)viewDidLoad {
    [super viewDidLoad];

    // 場景 1
    NSString *string = [NSString stringWithFormat:@leichunfeng];
    string_weak_ = string;

    // 場景 2
//    @autoreleasepool {
//        NSString *string = [NSString stringWithFormat:@leichunfeng];
//        string_weak_ = string;
//    }

    // 場景 3
//    NSString *string = nil;
//    @autoreleasepool {
//        string = [NSString stringWithFormat:@leichunfeng];
//        string_weak_ = string;
//    }

    NSLog(@string: %@, string_weak_);
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@string: %@, string_weak_);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@string: %@, string_weak_);
}

思考得怎麼樣了?相信在你心中已經有答案了。那麼讓我們一起來看看 console 輸出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 場景 1
2015-05-30 10:32:20.837 AutoreleasePool[33876:1448343] string: leichunfeng
2015-05-30 10:32:20.838 AutoreleasePool[33876:1448343] string: leichunfeng
2015-05-30 10:32:20.845 AutoreleasePool[33876:1448343] string: (null)

// 場景 2
2015-05-30 10:32:50.548 AutoreleasePool[33915:1448912] string: (null)
2015-05-30 10:32:50.549 AutoreleasePool[33915:1448912] string: (null)
2015-05-30 10:32:50.555 AutoreleasePool[33915:1448912] string: (null)

// 場景 3
2015-05-30 10:33:07.075 AutoreleasePool[33984:1449418] string: leichunfeng
2015-05-30 10:33:07.075 AutoreleasePool[33984:1449418] string: (null)
2015-05-30 10:33:07.094 AutoreleasePool[33984:1449418] string: (null)

跟你預想的結果有出入嗎?Any way ,我們一起來分析下為什麼會得到這樣的結果。

分析:3 種場景下,我們都通過 [NSString stringWithFormat:@leichunfeng] 創建了一個 autoreleased 對象,這是我們實驗的前提。並且,為了能夠在 viewWillAppearviewDidAppear中繼續訪問這個對象,我們使用了一個全局的 __weak 變量 string_weak_ 來指向它。因為 __weak 變量有一個特性就是它不會影響所指向對象的生命周期,這裡我們正是利用了這個特性。

場景 1:當使用 [NSString stringWithFormat:@leichunfeng] 創建一個對象時,這個對象的引用計數為 1 ,並且這個對象被系統自動添加到了當前的 autoreleasepool 中。當使用局部變量 string 指向這個對象時,這個對象的引用計數 +1 ,變成了 2 。因為在 ARC 下 NSString *string 本質上就是 __strong NSString *string 。所以在 viewDidLoad 方法返回前,這個對象是一直存在的,且引用計數為 2 。而當 viewDidLoad 方法返回時,局部變量 string 被回收,指向了 nil 。因此,其所指向對象的引用計數 -1 ,變成了 1 。

而在 viewWillAppear 方法中,我們仍然可以打印出這個對象的值,說明這個對象並沒有被釋放。咦,這不科學吧?我讀書少,你表騙我。不是一直都說當函數返回的時候,函數內部產生的對象就會被釋放的嗎?如果你這樣想的話,那我只能說:騷年你太年經了。開個玩笑,我們繼續。前面我們提到了,這個對象是一個 autoreleased 對象,autoreleased 對象是被添加到了當前最近的 autoreleasepool 中的,只有當這個 autoreleasepool 自身 drain 的時候,autoreleasepool 中的 autoreleased 對象才會被 release 。

另外,我們注意到當在 viewDidAppear 中再打印這個對象的時候,對象的值變成了 nil ,說明此時對象已經被釋放了。因此,我們可以大膽地猜測一下,這個對象一定是在 viewWillAppearviewDidAppear 方法之間的某個時候被釋放了,並且是由於它所在的 autoreleasepool 被 drain 的時候釋放的。

你說什麼就是什麼咯?有本事你就證明給我看你媽是你媽。額,這個我真證明不了,不過上面的猜測我還是可以證明的,不信,你看!

在開始前,我先簡單地說明一下原理,我們可以通過使用 lldbwatchpoint 命令來設置觀察點,觀察全局變量 string_weak_ 的值的變化,string_weak_ 變量保存的就是我們創建的 autoreleased 對象的地址。在這裡,我們再次利用了 __weak 變量的另外一個特性,就是當它所指向的對象被釋放時,__weak 變量的值會被置為 nil 。了解了基本原理後,我們開始驗證上面的猜測。

我們先在第 35 行打一個斷點,當程序運行到這個斷點時,我們通過 lldb 命令 watchpoint set v string_weak_ 設置觀察點,觀察 string_weak_ 變量的值的變化。如下圖所示,我們將在 console 中看到類似的輸出,說明我們已經成功地設置了一個觀察點:

設置觀察點

設置好觀察點後,點擊 Continue program execution 按鈕,繼續運行程序,我們將看到如下圖所示的界面:

設置觀察點

我們先看 console 中的輸出,注意到 string_weak_ 變量的值由 0x00007f9b886567d0 變成了 0x0000000000000000 ,也就是 nil 。說明此時它所指向的對象被釋放了。另外,我們也可以注意到一個細節,那就是 console 中打印了兩次對象的值,說明此時 viewWillAppear 也已經被調用了,而 viewDidAppear 還沒有被調用。

接著,我們來看看左側的線程堆棧。我們看到了一個非常敏感的方法調用 -[NSAutoreleasePool release] ,這個方法最終通過調用 AutoreleasePoolPage::pop(void *) 函數來負責對 autoreleasepool 中的 autoreleased 對象執行 release 操作。結合前面的分析,我們知道在 viewDidLoad 中創建的 autoreleased 對象在方法返回後引用計數為 1 ,所以經過這裡的 release 操作後,這個對象的引用計數 -1 ,變成了 0 ,該 autoreleased 對象最終被釋放,猜測得證。

另外,值得一提的是,我們在代碼中並沒有手動添加 autoreleasepool ,那這個 autoreleasepool 究竟是哪裡來的呢?看完後面的章節你就明白了。

場景 2:同理,當通過 [NSString stringWithFormat:@leichunfeng] 創建一個對象時,這個對象的引用計數為 1 。而當使用局部變量 string 指向這個對象時,這個對象的引用計數 +1 ,變成了 2 。而出了當前作用域時,局部變量 string 變成了 nil ,所以其所指向對象的引用計數變成 1 。另外,我們知道當出了 @autoreleasepool {} 的作用域時,當前 autoreleasepool 被 drain ,其中的 autoreleased 對象被 release 。所以這個對象的引用計數變成了 0 ,對象最終被釋放。

場景 3:同理,當出了 @autoreleasepool {} 的作用域時,其中的 autoreleased 對象被 release ,對象的引用計數變成 1 。當出了局部變量 string 的作用域,即 viewDidLoad 方法返回時,string 指向了 nil ,其所指向對象的引用計數變成 0 ,對象最終被釋放。

理解在這 3 種場景下,autoreleased 對象什麼時候釋放對我們理解 Objective-C 的內存管理機制非常有幫助。其中,場景 1 出現得最多,就是不需要我們手動添加 @autoreleasepool {} 的情況,直接使用系統維護的 autoreleasepool ;場景 2 就是需要我們手動添加 @autoreleasepool {} 的情況,手動干預 autoreleased 對象的釋放時機;場景 3 是為了區別場景 2 而引入的,在這種場景下並不能達到出了 @autoreleasepool {} 的作用域時 autoreleased 對象被釋放的目的。

PS:請讀者參考場景 1 的分析過程,使用 lldb 命令 watchpoint 自行驗證下在場景 2 和場景 3 下 autoreleased 對象的釋放時機,you should give it a try yourself 。

AutoreleasePoolPage

細心的讀者應該已經有所察覺,我們在上面已經提到了 -[NSAutoreleasePool release] 方法最終是通過調用 AutoreleasePoolPage::pop(void *) 函數來負責對 autoreleasepool 中的 autoreleased 對象執行 release 操作的。

那這裡的 AutoreleasePoolPage 是什麼東西呢?其實,autoreleasepool 是沒有單獨的內存結構的,它是通過以 AutoreleasePoolPage 為結點的雙向鏈表來實現的。我們打開 runtime 的源碼工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的實現源碼。通過閱讀源碼,我們可以知道:

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

    一個空的 AutoreleasePoolPage 的內存結構如下圖所示:

    AutoreleasePoolPage

    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 。

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

      Autorelease Pool Blocks

      我們使用 clang -rewrite-objc 命令將下面的 Objective-C 代碼重寫成 C++ 代碼:

      1
      2
      3
      
      @autoreleasepool {
      
      }
      

      將會得到以下輸出結果(只保留了相關代碼):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      extern C __declspec(dllimport) void * objc_autoreleasePoolPush(void);
      extern C __declspec(dllimport) void objc_autoreleasePoolPop(void *);
      
      struct __AtAutoreleasePool {
        __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
        ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
        void * atautoreleasepoolobj;
      };
      
      /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
      
      }
      

      不得不說,蘋果對 @autoreleasepool {} 的實現真的是非常巧妙,真正可以稱得上是代碼的藝術。蘋果通過聲明一個 __AtAutoreleasePool 類型的局部變量 __autoreleasepool 來實現 @autoreleasepool {} 。當聲明 __autoreleasepool 變量時,構造函數 __AtAutoreleasePool()被調用,即執行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;當出了當前作用域時,析構函數 ~__AtAutoreleasePool() 被調用,即執行 objc_autoreleasePoolPop(atautoreleasepoolobj); 。也就是說 @autoreleasepool {} 的實現代碼可以進一步簡化如下:

      1
      2
      3
      4
      5
      
      /* @autoreleasepool */ {
          void *atautoreleasepoolobj = objc_autoreleasePoolPush();
          // 用戶代碼,所有接收到 autorelease 消息的對象會被添加到這個 autoreleasepool 中
          objc_autoreleasePoolPop(atautoreleasepoolobj);
      }
      

      因此,單個 autoreleasepool 的運行過程可以簡單地理解為 objc_autoreleasePoolPush()[對象 autorelease]objc_autoreleasePoolPop(void *) 三個過程。

      push 操作

      上面提到的 objc_autoreleasePoolPush() 函數本質上就是調用的 AutoreleasePoolPage 的 push 函數。

      1
      2
      3
      4
      5
      6
      
      void *
      objc_autoreleasePoolPush(void)
      {
          if (UseGC) return nil;
          return AutoreleasePoolPage::push();
      }
      

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

      1
      2
      3
      4
      5
      6
      
      static inline void *push()
      {
          id *dest = autoreleaseFast(POOL_SENTINEL);
          assert(*dest == POOL_SENTINEL);
          return dest;
      }
      

      push 函數通過調用 autoreleaseFast 函數來執行具體的插入操作。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      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);
          }
      }
      

      autoreleaseFast 函數在執行一個具體的插入操作時,分別對三種情況進行了不同的處理:

      1. 當前 page 存在且沒有滿時,直接將對象添加到當前 page 中,即 next 指向的位置;
      2. 當前 page 存在且已滿時,創建一個新的 page ,並將對象添加到新創建的 page 中;
      3. 當前 page 不存在時,即還沒有 page 時,創建第一個 page ,並將對象添加到新創建的 page 中。

        每調用一次 push 操作就會創建一個新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一個 POOL_SENTINEL ,並且返回插入的 POOL_SENTINEL 的內存地址。

        autorelease 操作

        通過 NSObject.mm 源文件,我們可以找到 -autorelease 方法的實現:

        1
        2
        3
        
        - (id)autorelease {
            return ((id)self)->rootAutorelease();
        }
        

        通過查看 ((id)self)->rootAutorelease() 的方法調用,我們發現最終調用的就是 AutoreleasePoolPage 的 autorelease 函數。

        1
        2
        3
        4
        5
        6
        7
        
        __attribute__((noinline,used))
        id
        objc_object::rootAutorelease2()
        {
            assert(!isTaggedPointer());
            return AutoreleasePoolPage::autorelease((id)this);
        }
        

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

        1
        2
        3
        4
        5
        6
        7
        8
        
        static inline id autorelease(id obj)
        {
            assert(obj);
            assert(!obj->isTaggedPointer());
            id *dest __unused = autoreleaseFast(obj);
            assert(!dest  ||  *dest == obj);
            return obj;
        }
        

        pop 操作

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

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        
        void
        objc_autoreleasePoolPop(void *ctxt)
        {
            if (UseGC) return;
        
            // fixme rdar://9167170
            if (!ctxt) return;
        
            AutoreleasePoolPage::pop(ctxt);
        }
        

        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 的內存地址。

        AutoreleasePoolPage

        此時,如果執行 pop(token1) 操作,那麼該 autoreleasepool 堆棧的內存結構將會變成如下圖所示:

        AutoreleasePoolPage

        NSThread、NSRunLoop 和 NSAutoreleasePool

        根據蘋果官方文檔中對 NSRunLoop 的描述,我們可以知道每一個線程,包括主線程,都會擁有一個專屬的 NSRunLoop 對象,並且會在有需要的時候自動創建。

        Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed.

        同樣的,根據蘋果官方文檔中對 NSAutoreleasePool 的描述,我們可知,在主線程的 NSRunLoop 對象(在系統級別的其他線程中應該也是如此,比如通過 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 獲取到的線程)的每個 event loop 開始前,系統會自動創建一個 autoreleasepool ,並在 event loop 結束時 drain 。我們上面提到的場景 1 中創建的 autoreleased 對象就是被系統添加到了這個自動創建的 autoreleasepool 中,並在這個 autoreleasepool 被 drain 時得到釋放。

        The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

        另外,NSAutoreleasePool 中還提到,每一個線程都會維護自己的 autoreleasepool 堆棧。換句話說 autoreleasepool 是與線程緊密相關的,每一個 autoreleasepool 只對應一個線程。

        Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.

        弄清楚 NSThread、NSRunLoop 和 NSAutoreleasePool 三者之間的關系可以幫助我們從整體上了解 Objective-C 的內存管理機制,清楚系統在背後到底為我們做了些什麼,理解整個運行機制等。

        總結

        看到這裡,相信你應該對 Objective-C 的內存管理機制有了更進一步的認識。通常情況下,我們是不需要手動添加 autoreleasepool 的,使用線程自動維護的 autoreleasepool 就好了。根據蘋果官方文檔中對 Using Autorelease Pool Blocks 的描述,我們知道在下面三種情況下是需要我們手動添加 autoreleasepool 的:

        1. 如果你編寫的程序不是基於 UI 框架的,比如說命令行工具;
        2. 如果你編寫的循環中創建了大量的臨時對象;
        3. 如果你創建了一個輔助線程。

          最後,希望本文能對你有所幫助,have fun !

           

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved