黑幕背后的Autorelease

st123456 10年前發布 | 12K 次閱讀 IOS iOS開發 移動開發

我是前言

Autorelease機制是iOS開發者管理對象內存的好伙伴,MRC中,調用[obj autorelease]來延遲內存的釋放是一件簡單自然的事,ARC下,我們甚至可以完全不知道Autorelease就能管理好內存。而在這背后,objc和編譯器都幫我們做了哪些事呢,它們是如何協作來正確管理內存的呢?刨根問底,一起來探究下黑幕背后的Autorelease機制。

Autorelease對象什么時候釋放?

這個問題拿來做面試題,問過很多人,沒有幾個能答對的。很多答案都是“當前作用域大括號結束時釋放”,顯然木有正確理解Autorelease機制。
在沒有手加Autorelease Pool的情況下,Autorelease對象是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入了自動釋放池Push和Pop

小實驗

__weak id reference = nil;

  • (void)viewDidLoad { [super viewDidLoad]; NSString *str = [NSString stringWithFormat:@"sunnyxx"]; // str是一個autorelease對象,設置一個weak的引用來觀察它 reference = str; }
  • (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; NSLog(@"%@", reference); // Console: sunnyxx }
  • (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"%@", reference); // Console: (null) } </code></pre> </td> </tr> </tbody> </table>

    這個實驗同時也證明了viewDidLoadviewWillAppear是在同一個runloop調用的,而viewDidAppear是在之后的某個runloop調用的。
    由于這個vc在loadView之后便add到了window層級上,所以viewDidLoadviewWillAppear是在同一個runloop調用的,因此在viewWillAppear中,這個autorelease的變量依然有值。

    當然,我們也可以手動干預Autorelease對象的釋放時機:

    - (void)viewDidLoad {
      [super viewDidLoad];
      @autoreleasepool {
          NSString *str = [NSString stringWithFormat:@"sunnyxx"];
      }
      NSLog(@"%@", str); // Console: (null)
    }
    

    Autorelease原理

    AutoreleasePoolPage

    ARC下,我們使用@autoreleasepool{}來使用一個AutoreleasePool,隨后編譯器將其改寫成下面的樣子:

    void *context = objc_autoreleasePoolPush();
    // {}中的代碼
    objc_autoreleasePoolPop(context);
    

    而這兩個函數都是對AutoreleasePoolPage的簡單封裝,所以自動釋放機制的核心就在于這個類。

    AutoreleasePoolPage是一個C++實現的類

    黑幕背后的Autorelease

    • AutoreleasePool并沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對應結構中的parent指針和child指針)
    • AutoreleasePool是按線程一一對應的(結構中的thread指針指向當前線程)
    • AutoreleasePoolPage每個對象會開辟4096字節內存(也就是虛擬內存一頁的大小),除了上面的實例變量所占空間,剩下的空間全部用來儲存autorelease對象的地址
    • 上面的id *next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置
    • 一個AutoreleasePoolPage的空間被占滿時,會新建一個AutoreleasePoolPage對象,連接鏈表,后來的autorelease對象在新的page加入

    所以,若當前線程中只有一個AutoreleasePoolPage對象,并記錄了很多autorelease對象地址時內存如下圖:

    黑幕背后的Autorelease

    圖中的情況,這一頁再加入一個autorelease對象就要滿了(也就是next指針馬上指向棧頂),這時就要執行上面說的操作,建立下一頁page對象,與這一頁鏈表連接完成后,新page的next指針被初始化在棧底(begin的位置),然后繼續向棧頂添加新對象。

    所以,向一個對象發送- autorelease消息,就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置

    釋放時刻

    每當進行一次objc_autoreleasePoolPush調用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象,值為0(也就是個nil),那么這一個page就變成了下面的樣子:

    黑幕背后的Autorelease

    objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)作為入參,于是:

    1. 根據傳入的哨兵對象地址找到哨兵對象所處的page
    2. 在當前page中,將晚于哨兵對象插入的所有autorelease對象都發送一次- release消息,并向回移動next指針到正確位置
    3. 補充2:從最新加入的對象一直向前清理,可以向前跨越若干個page,直到哨兵所在的page

    剛才的objc_autoreleasePoolPop執行后,最終變成了下面的樣子:

    黑幕背后的Autorelease

    嵌套的AutoreleasePool

    知道了上面的原理,嵌套的AutoreleasePool就非常簡單了,pop的時候總會釋放到上次push的位置為止,多層的pool就是多個哨兵對象而已,就像剝洋蔥一樣,每次一層,互不影響。

    【附加內容】

    Autorelease返回值的快速釋放機制

    值得一提的是,ARC下,runtime有一套對autorelease返回值的優化策略。
    比如一個工廠方法:

    + (instancetype)createSark {
      return [self new];
    }
    // caller
    Sark *sark = [Sark createSark];
    

    秉著誰創建誰釋放的原則,返回值需要是一個autorelease對象才能配合調用方正確管理內存,于是乎編譯器改寫成了形如下面的代碼:

    + (instancetype)createSark {
      id tmp = [self new];
      return objc_autoreleaseReturnValue(tmp); // 代替我們調用autorelease
    }
    // caller
    id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我們調用retain
    Sark *sark = tmp;
    objc_storeStrong(&sark, nil); // 相當于代替我們調用了release
    

    一切看上去都很好,不過既然編譯器知道了這么多信息,干嘛還要勞煩autorelease這個開銷不小的機制呢?于是乎,runtime使用了一些黑魔法將這個問題解決了。

    黑魔法之Thread Local Storage

    Thread Local Storage(TLS)線程局部存儲,目的很簡單,將一塊內存作為某個線程專有的存儲,以key-value的形式進行讀寫,比如在非arm架構下,使用pthread提供的方法實現:

    void* pthread_getspecific(pthread_key_t);
    int pthread_setspecific(pthread_key_t , const void *);
    

    說它是黑魔法可能被懂pthread的笑話- -

    在返回值身上調用objc_autoreleaseReturnValue方法時,runtime將這個返回值object儲存在TLS中,然后直接返回這個object(不調用autorelease);同時,在外部接收這個返回值的objc_retainAutoreleasedReturnValue里,發現TLS中正好存了這個對象,那么直接返回這個object(不調用retain)。
    于是乎,調用方和被調方利用TLS做中轉,很有默契的免去了對返回值的內存管理。

    于是問題又來了,假如被調方和主調方只有一邊是ARC環境編譯的該咋辦?(比如我們在ARC環境下用了非ARC編譯的第三方庫,或者反之)
    只能動用更高級的黑魔法。

    黑魔法之__builtin_return_address

    這個內建函數原型是char *__builtin_return_address(int level),作用是得到函數的返回地址,參數表示層數,如__builtin_return_address(0)表示當前函數體返回地址,傳1是調用這個函數的外層函數的返回值地址,以此類推。

    - (int)foo {
      NSLog(@"%p", __builtin_return_address(0)); // 根據這個地址能找到下面ret的地址
      return 1;
    }
    // caller
    int ret = [sark foo];
    

    看上去也沒啥厲害的,不過要知道,函數的返回值地址,也就對應著調用者結束這次調用的地址(或者相差某個固定的偏移量,根據編譯器決定)
    也就是說,被調用的函數也有翻身做地主的機會了,可以反過來對主調方干點壞事。
    回到上面的問題,如果一個函數返回前知道調用方是ARC還是非ARC,就有機會對于不同情況做不同的處理

    黑魔法之反查匯編指令

    通過上面的__builtin_return_address加某些偏移量,被調方可以定位到主調方在返回值后面的匯編指令

    // caller
    int ret = [sark foo];
    // 內存中接下來的匯編指令(x86,我不懂匯編,瞎寫的)
    movq ??? ???
    callq ???
    

    而這些匯編指令在內存中的值是固定的,比如movq對應著0x48。
    于是乎,就有了下面的這個函數,入參是調用方__builtin_return_address傳入值

    static bool callerAcceptsFastAutorelease(const void * const ra0) {
      const uint8_t *ra1 = (const uint8_t *)ra0;
      const uint16_t *ra2;
      const uint32_t *ra4 = (const uint32_t *)ra1;
      const void **sym;
      // 48 89 c7    movq  %rax,%rdi
      // e8          callq symbol
      if (*ra4 != 0xe8c78948) {
          return false;
      }
      ra1 += (long)*(const int32_t *)(ra1 + 4) + 8l;
      ra2 = (const uint16_t *)ra1;
      // ff 25       jmpq *symbol@DYLDMAGIC(%rip)
      if (*ra2 != 0x25ff) {
          return false;
      }
      ra1 += 6l + (long)*(const int32_t *)(ra1 + 2);
      sym = (const void **)ra1;
      if (*sym != objc_retainAutoreleasedReturnValue)
      {
          return false;
      }
      return true;
    }
    

    它檢驗了主調方在返回值之后是否緊接著調用了objc_retainAutoreleasedReturnValue,如果是,就知道了外部是ARC環境,反之就走沒被優化的老邏輯。

    其他Autorelease相關知識點

    使用容器的block版本的枚舉器時,內部會自動添加一個AutoreleasePool:

    [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
      // 這里被一個局部@autoreleasepool包圍著
    }];
    

    當然,在普通for循環和for in循環中沒有,所以,還是新版的block版本枚舉器更加方便。for循環中遍歷產生大量autorelease變量時,就需要手加局部AutoreleasePool咯。

    via:http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

 本文由用戶 st123456 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
  • sesese色