OC高級-autoreleasepool的實現(xiàn)原理

目錄

  • autorelease的本質(zhì)
  • autorelease對象什么時候釋放?
  • autoreleasepool的工作原理
  • autoreleasepool的內(nèi)部結(jié)構(gòu)
  • autoreleasepool的嵌套
  • autoreleasePoolPage
  • NSThread、NSRunLoop 和 NSAutoreleasePool三者之間的關(guān)系
  • 其他autorelease相關(guān)知識點
  • 面試題
  • 參考文章

autorelease的本質(zhì)

  • autorelease本質(zhì)上就是延遲調(diào)用release方法
  • MRC環(huán)境,通過調(diào)用[obj autorelease]延遲內(nèi)存的釋放
  • ARC環(huán)境,甚至可以完全不知道autorelease也能管理好內(nèi)存

autorelease對象什么時候釋放?

實驗環(huán)境

  • ARC
  • 測試機型:iPhone 4S模擬器
  • 注意:在蘋果一些新的硬件設(shè)備上,本實驗的結(jié)果已經(jīng)不再成立
__weak NSString *_weakStr = nil;
- (void)viewDidLoad
{
    [super viewDidLoad];

    // 場景 1
    NSString *string = [NSString stringWithFormat:@"yanhoo"];
    _weakStr = string;

    // 場景 2
//    @autoreleasepool {
//        NSString *string = [NSString stringWithFormat:@"yanhoo"];
//        _weakStr = string;
//    }

    // 場景 3
//    NSString *string = nil;
//    @autoreleasepool {
//        string = [NSString stringWithFormat:@"yanhoo"];
//        _weakStr = string;
//    }

    NSLog(@"string1: %@", _weakStr);
}

- (void)viewWillAppear:(BOOL)animated 
{
    [super viewWillAppear:animated];

    NSLog(@"string2: %@", _weakStr);
}

- (void)viewDidAppear:(BOOL)animated 
{
    [super viewDidAppear:animated];

    NSLog(@"string3: %@", _weakStr);
}

測試結(jié)果

場景1
2016-08-19 01:30:01.686 test[8866:554553] string1: yanhoo
2016-08-19 01:30:01.687 test[8866:554553] string2: yanhoo
2016-08-19 01:30:01.695 test[8866:554553] string3: (null)

場景2
2016-08-19 01:32:07.020 test[8886:556042] string1: (null)
2016-08-19 01:32:07.021 test[8886:556042] string2: (null)
2016-08-19 01:32:07.032 test[8886:556042] string3: (null)

場景3
2016-08-19 01:32:57.038 test[8900:557349] string1: yanhoo
2016-08-19 01:32:57.038 test[8900:557349] string2: (null)
2016-08-19 01:32:57.048 test[8900:557349] string3: (null)

分析

  • 首先來了解下__weak修飾符

    • 不會影響所指向?qū)ο蟮纳芷冢矗菏褂?code>__weak修飾的變量不會導(dǎo)致所引用的對象的引用計數(shù)+1
    • __weak修飾的變量所指向的對象被釋放時,__weak修飾的變量的值會被置為nil不存在野指針問題
  • 場景1分析

    • 當使用[NSString stringWithFormat:@"yanhoo"]創(chuàng)建一個autorelease對象時,這個對象的引用計數(shù)為1,并且這個對象被系統(tǒng)自動添加到了最近autoreleasepool
    • 當使用局部變量string(在 ARC 下不指定變量所有權(quán)修飾符的情況下,默認為__strong)指向這個對象時,這個對象的引用計數(shù) +1 ,變成了 2
    • 所以在viewDidLoad方法返回之前,這個對象是一直存在的,且引用計數(shù)為2
    • viewDidLoad方法返回之后,局部變量 string被回收,其所指向?qū)ο蟮囊糜嫈?shù) -1 ,變成了1
    • viewWillAppear方法中,我們?nèi)匀豢梢源蛴〕鲞@個對象的值,說明這個對象并沒有被釋放,說明到此autoreleasepool還未釋放,從而導(dǎo)致autorelease對象未釋放,因為只有當這個autoreleasepool自身被drain的時候,autoreleasepool中的 autoreleased 對象才會被 release
    • viewDidAppear中再打印這個對象的時候,對象的值變成了 nil,說明此時autoreleased對象已經(jīng)被釋放了,可以大膽猜測autoreleasepool一定在viewWillAppearviewDidAppear方法之間的某個時候被drain
  • 場景2和場景3請讀者自行分析,這里就不再啰嗦了

  • 可以通過lldbwatchpoint命令來觀察,具體參考這篇文章

  • 總結(jié)

    • 場景1出現(xiàn)得最多,就是不需要我們手動添加@autoreleasepool {}的情況,直接使用系統(tǒng)維護autoreleasepool
    • 場景2就是需要我們手動添加@autoreleasepool {}的情況,手動干預(yù) autoreleased對象的釋放時機,在一些很耗內(nèi)存的循環(huán)調(diào)用的場景下有時需要手動干預(yù)autoreleased 對象的釋放時機,不然會導(dǎo)致內(nèi)存暴增,最終導(dǎo)致程序奔潰;
    • 場景3是為了區(qū)別于場景2而引入的,在這種場景下并不能達到出了@autoreleasepool {}的作用域時autoreleased 對象被釋放的目的

autoreleasepool的工作原理

  • ARC環(huán)境下,@autoreleasepool{ }被編譯器編譯后,生成如下代碼(以下代碼是簡化版
// push
void *poolToken = objc_autoreleasePoolPush();

// 這中間為寫在{...}中的代碼

// pop
objc_autoreleasePoolPop(poolToken);
  • 在運行循環(huán)開始前,系統(tǒng)會自動創(chuàng)建一個autoreleasepool(一個autoreleasepool會存在多個AutoreleasePoolPage),此時會調(diào)用一次objc_autoreleasePoolPush函數(shù),runtime會向當前的AutoreleasePoolPage中add進一個POOL_SENTINEL哨兵對象,值為0,也就是個nil,代表autoreleasepool的起始邊界),并返回此哨兵對象的內(nèi)存地址poolToken
  • 在運行循環(huán)結(jié)束時autoreleasepool會被drain掉,此時會調(diào)用objc_autoreleasePoolPop(poolToken)函數(shù),入?yún)⑹侵爱a(chǎn)生的POOL_SENTINEL的內(nèi)存地址poolToken,對在POOL_SENTINEL之后添加的所有autoreleased對象調(diào)用一次release,可以向前跨越若干個page,直到哨兵對象所在的page,并向回移動next指針哨兵對象所在位置
  • 中間{...}所產(chǎn)生的autoreleased對象都會被插入到最近的autoreleasepool中(因為autoreleasepool存在嵌套的情況)
  • 單個autoreleasepool的運行過程可以簡單地理解為以下三個過程
    • objc_autoreleasePoolPush()
      • objc_autoreleasePoolPush()本質(zhì)上就是調(diào)用的 AutoreleasePoolPage的push函數(shù),如下所示
      void *
      objc_autoreleasePoolPush(void)
      {
          if (UseGC) return nil;
          return AutoreleasePoolPage::push();
      }
      
      • 每執(zhí)行一次push操作就會新建一個autoreleasepool,對應(yīng)的具體實現(xiàn)就是往AutoreleasePoolPage中的next位置插入一個POOL_SENTINEL,并且返回插入的POOL_SENTINEL的內(nèi)存地址poolToken,這個地址在執(zhí)行pop操作的時候作為函數(shù)的入?yún)ⅲ旅媸茿utoreleasePoolPage的push函數(shù)代碼
      static inline void *push()
      {
          id *dest = autoreleaseFast(POOL_SENTINEL);
          assert(*dest == POOL_SENTINEL);
          return dest;
      }
      
      • push 函數(shù)通過調(diào)autoreleaseFast函數(shù)來執(zhí)行具體的插入操作
      static inline id *autoreleaseFast(id obj)
      {
          AutoreleasePoolPage *page = hotPage();
          if (page && !page->full()) {// 當前 page 存在且沒有滿時,直接將對象添加到當前 page 中,即 next 指向的位置
              return page->add(obj);
          } else if (page) {// 當前 page 存在且已滿時,創(chuàng)建一個新的 page ,并將對象添加到新創(chuàng)建的 page 中
              return autoreleaseFullPage(obj, page);
          } else {// 當前 page 不存在時,即還沒有 page 時,創(chuàng)建第一個 page ,并將對象添加到新創(chuàng)建的 page 中
              return autoreleaseNoPage(obj);
          }
      }
      
    • [對象 autorelease]
      • 本質(zhì)上就是調(diào)用AutoreleasePoolPage的autorelease函數(shù)
      __attribute__((noinline,used))
      id
      objc_object::rootAutorelease2()
      {
          assert(!isTaggedPointer());
          return AutoreleasePoolPage::autorelease((id)this);
      }
      
      • AutoreleasePoolPage 的 autorelease 函數(shù)的實現(xiàn)對我們來說就比較好理解了,它跟 push 操作的實現(xiàn)非常相似。只不過 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(poolToken)
      • objc_autoreleasePoolPop(poolToken) 函數(shù)本質(zhì)上也是調(diào)用的AutoreleasePoolPage的 pop 函數(shù)
      void
      objc_autoreleasePoolPop(void *ctxt)
      {
          if (UseGC) return;
      
          // fixme rdar://9167170
          if (!ctxt) return;
      
          AutoreleasePoolPage::pop(ctxt);
      }
      
      • pop 函數(shù)的入?yún)⒕褪?push 函數(shù)的返回值,也就是 POOL_SENTINEL 的內(nèi)存地址poolToken。當執(zhí)行pop操作時,在POOL_SENTINEL內(nèi)存地址在之后添加的所有autoreleased 對象都會被release,可以向前跨越若干個page,直到哨兵對象所在的page,并向回移動next指針哨兵對象所在位置

autoreleasepool的內(nèi)部結(jié)構(gòu)

  • autoreleasepool本質(zhì)上就是一個指針堆棧
  • 指針堆棧中存放的是autoreleased對象的內(nèi)存地址 或者 POOL_SENTINEL的內(nèi)存地址
  • 內(nèi)部結(jié)構(gòu)是由若干個以page為結(jié)點的雙向鏈表組成,系統(tǒng)會在需要的時候動態(tài)地增加或刪除page節(jié)點,這里說的page就是下面即將說到的AutoreleasePoolPage對象

autoreleasepool的嵌套

  • 每產(chǎn)生一個autoreleasePool,就會產(chǎn)生一個哨兵對象,作為pool的邊界
  • pool的嵌套其實就是產(chǎn)生多個哨兵對象而已
  • pop的時候可以向前跨越若干個page,直到指定哨兵對象所在的page為止

AutoreleasePoolPage

  • 一個空的 AutoreleasePoolPage 的內(nèi)存結(jié)構(gòu)如下圖所示:

    AutoreleasePoolPage.png

    • magic用來校驗AutoreleasePoolPage的結(jié)構(gòu)是否完整
    • next指向下一個即將產(chǎn)生的autoreleased對象的存放位置(當next == begin()時,表示AutoreleasePoolPage為空;當next == end()時,表示AutoreleasePoolPage已滿
    • thread指向當前線程一個AutoreleasePoolPage只會對應(yīng)一個線程,但一個線程可以對應(yīng)多個AutoreleasePoolPage;
    • parent指向父結(jié)點,第一個結(jié)點的 parent 值為 nil;
    • child指向子結(jié)點,最后一個結(jié)點的 child 值為 nil;
    • depth代表深度,第一個page的depth為0,往后每遞增一個page,depth會加1;
    • hiwat代表 high water mark
  • 前面所說的autoreleasepool的內(nèi)部結(jié)構(gòu)是由若干個AutoreleasePoolPage為結(jié)點的雙向鏈表組成,這個雙向鏈表就是通過上述結(jié)構(gòu)中的parent指針child指針連接起來的

  • 每個AutoreleasePoolPage對象會開辟4KB內(nèi)存(也就是虛擬內(nèi)存一頁的大小),除了上面的實例變量所占空間,剩下的空間全部用來儲存autoreleased對象的內(nèi)存地址

  • 一個page的空間被占滿時,會新建一個page,通過parent指針child指針連接鏈表,之后的autoreleased對象會加入到新建的page中

NSThread、NSRunLoop 和 NSAutoreleasePool三者之間的關(guān)系

  • NSThread 和 NSRunLoop是一一對應(yīng)的關(guān)系
  • 在NSRunLoop對象的每個運行循環(huán)(event loop)開始前,系統(tǒng)會自動創(chuàng)建一個autoreleasepool,并在運行循環(huán)(event loop)結(jié)束時drain掉這個pool,同時釋放所有autoreleased對象
  • autoreleasepool只會對應(yīng)一個線程,每個線程可能會對應(yīng)多個autoreleasepool,比如autoreleasepool嵌套的情況

Autorelease返回值的快速釋放機制

  • ARC下,runtime有一套對autorelease返回值的優(yōu)化策略
  • 通過objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue的配合使用可以達到最優(yōu)化程序運行的目的
  • Thread Local Storage(TLS)線程局部存儲,在返回值返回前調(diào)用objc_autoreleaseReturnValue方法時,runtime會將這個返回值儲存在TLS中,然后直接返回返回值不調(diào)用autorelease),
  • 外部接收這個返回值時通過調(diào)用objc_retainAutoreleasedReturnValue發(fā)現(xiàn)TLS中已存在這個返回值,就直接返回(不調(diào)用retain),免去了對返回值的內(nèi)存管理,達到優(yōu)化目的

其他Autorelease相關(guān)知識點

  • 使用容器的block版本的枚舉器時,內(nèi)部會自動添加一個autoreleasePool
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) 
{ 
      // 這里被一個局部@autoreleasepool{ }包圍著
}];
  • 普通for循環(huán)for in循環(huán)沒有,所以,還是新版的block版本枚舉器更加方便,但是性能還是for in循環(huán)最高

  • 下面三種情況是需要我們手動添加autoreleasepool

    • 如果你編寫的程序不是基于 UI 框架的,比如:命令行工具;
    • for循環(huán)中遍歷產(chǎn)生大量autorelease變量時,就需要手動添加加局部autoreleasePool來進行手動干預(yù)
    • 如果你創(chuàng)建了一個子線程,一般會自定義繼承自NSOperation的操作,在main方法中要加上@autoreleasepool{...},這段代碼是在子線程上執(zhí)行是無法訪問主線程的自動釋放池的,所以得自己創(chuàng)建
    - (void)main
    {
        // 自己創(chuàng)建自動釋放池,如果這段代碼是在子線程上執(zhí)行是無法訪問主線程的自動釋放池的,所以得自己創(chuàng)建
        @autoreleasepool
        {
            // 代碼邏輯
        }
    }
    

面試題

  • autoreleasepool的實現(xiàn)原理

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容