內存管理下篇(強引用分析、AutoReleasePool)

  • 強引用分析

    • 示例代碼
      //B頁面中添加timer和對應的執行方法 A頁面就僅僅添加push到B頁面的代碼
      @property (nonatomic, strong) NSTimer       *timer;
      self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
      - (void)fireHome{
          num++;
          NSLog(@"hello word - %d",num);
      }
      複製代碼
    • 強引用出現的狀況及緣由分析
      先在B頁面建立一個timer,而後從A頁面pushB此時timer開始執行而後再pop回到A頁面,部分人可能會以爲此時timer會暫停執行,由於timerB頁面持有,pop回來以後B頁面也就銷燬了因此相應的timer也因該被銷燬,因此對應的應該是timer中止執行。可是結果其實否則。能夠看一下運行結果 iShot2021-06-07 16.00.04 (1).gif能夠發現pop回來以後 timer同樣還在執行。
      首先簡單粗略的分析一下緣由:猜想是循環引用形成了 B不能釋放,看一下下面的官方文檔 image.png官方文檔中明確說明了 timer會對 self進行強持有,而此時 self有持有 timer因此形成了循環引用,也就形成了 B頁面不能釋放,因此即便 pop計時器還在執行。
      在文章 Block的底層分析中咱們知道了循環引用的解決辦法,__weak去修飾 self,此時 self的引用計數不會加一,因此不會形成循環引用問題,在這裏不妨試一下用 __weak去修飾而後再看執行結果 iShot2021-06-07 17.20.29 (1).gif發現這個地方 __weak修飾並不能解決循環引用的問題。一樣的在文章Block的底層分析咱們知道,用 __weak修飾的話底層 block會走到 _Block_object_assign方法,發現 block底層其實僅僅存儲了對象的指針地址也就是 weakSelf的地址。這裏咱們先分別打印一下 self的引用計數和 __weak修飾以後的引用計數,而後在分別打印一下 selfweakSelf和這二者的地址 image.png 首先能夠肯定的是 __weak修飾的變量指向對象並不會形成引用計數加一的狀況,其次經過地址打印、值打印咱們能夠肯定的是 selfweakSelf是兩個變量指向了同一片的內存空間以下圖所示 未命名文件(37).png
      因此 block能經過存儲的 weakSelf的地址找到對象的地址從而獲取對象的屬性修改對象相關的屬性等。而且也可以解決循環引用的問題。 可是 timer就不同了,上圖的官方文檔咱們能夠知道,timer強持有的是對象,並非對象的指針地址了,因此 timer的引用臉就是
      timer -> weakSelf -> 對象
      最終仍是會找到對應的對象進行持有,而後呢 timer又被 runloop持有,引用鏈以下:
      runloop -> timer -> weakSelf -> 對象
      runloop的生命週期又很長(大於對象和 timer的生命週期)runloop沒有停那麼 timer就不會被釋放,進而致使 weakSelf以及對象都不會釋放. 也就致使了不一樣於 block的解決循環引用的方法也就是 __weak不能解決強持有的問題。
      結論:強持有致使就算用__weak修飾也會被持有對象,引用計數同樣會加一,因此只有釋放變量纔可以釋放對象
    • 強引用解決辦法
      • 退出前銷燬 timer
        前文分析問題的緣由咱們知道就是應爲 timer持有的是當前對象因此對象不能被釋放,因此解決辦法其實也很簡單就是pop出去的時候只須要釋放 timer就行。上文的官方文檔也有提到 image.png只要釋放 timer對象也就會被釋放。因此只須要在 didMoveToParentViewController方法中調用 [self.timer invalidate];self.timer = nil;就好了效果以下 iShot2021-06-08 09.20.02 (1).gif這樣強持有後不能釋放的問題也就解決了
      • timer回調方法判斷
        一樣的解決問題最根本的方法仍是釋放 timer可是除了 didMoveToParentViewController方法中釋放還能夠考慮專門建立一個添加 timer的類,在該類中新建一個方法,而後和傳入的方法作交換,該方法中須要判斷傳入的 target是否爲空了,若是不爲空則使用傳入的 target調用傳入的方法。若是爲空則釋放 timer。釋放 timer對應 target引用計數就會減一。若是減到0就會被正常釋放。一樣的也能夠解決問題具體代碼以下
        #import "LGTimerWapper.h"
          #import <objc/message.h>
        
          @interface LGTimerWapper()
          @property (nonatomic, weak) id target;
          @property (nonatomic, assign) SEL aSelector;
          @property (nonatomic, strong) NSTimer *timer;
        
          @end
        
          @implementation LGTimerWapper
        
          - (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
              if (self == [super init]) {
                  self.target     = aTarget; // vc
                  self.aSelector  = aSelector; // 方法 -- vc 釋放
        
                  if ([self.target respondsToSelector:self.aSelector]) { 
                      Method method    = class_getInstanceMethod([self.target class], aSelector);
                      const char *type = method_getTypeEncoding(method);
                      class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
        
                      self.timer      = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
                  }
              }
              return self;
          }
        
          void fireHomeWapper(LGTimerWapper *warpper){
        
              if (warpper.target) { // vc - dealloc
                  void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
                   lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
              }else{ // warpper.target
                  [warpper.timer invalidate];
                  warpper.timer = nil;
              }
          }
        
        
          - (void)lg_invalidate{
              [self.timer invalidate];
              self.timer = nil;
          }
        
          - (void)dealloc{
              NSLog(@"%s",__func__);
          }
        
          @end
        複製代碼
      • proxy 虛基類的方式
        在講解 Block底層分析中的解決循環引用的方法的時候也提到過 proxy這裏其實也相似,這裏使用 proxy的思想主要是想使用一箇中間者,這樣 timer不會再持有對象而是 proxy,因此對象的引用計數不會再加一,從而對象釋放的時候對應的 timerproxy也就釋放了也就解決了強持有的問題。具體代碼以下;
        #import "LGProxy.h"
        
          @interface LGProxy()
          @property (nonatomic, weak) id object;
          @end
        
          @implementation LGProxy
          + (instancetype)proxyWithTransformObject:(id)object{
              LGProxy *proxy = [LGProxy alloc];
              proxy.object = object;
              return proxy;
          }
        
          // 僅僅添加了weak類型的屬性還不夠,爲了保證中間件可以響應外部self的事件,須要經過消息轉發機制,讓實際的響應target仍是外部self,這一步相當重要,主要涉及到runtime的消息機制。
          // 轉移
          // 強引用 -> 消息轉發
        
          -(id)forwardingTargetForSelector:(SEL)aSelector {
              return self.object;
          }
        複製代碼
        image.png image.png
  • AutoReleasePool

    • 自動釋放池介紹

      image.png 從這個官方文檔中咱們能夠知道,在 Runloop開始的時候會自動建立一個自動釋放池,當 Runloop此次循環結束的時候,那麼就會銷燬自動釋放池,從而釋放全部 autorelease對象,固然若是在一個事務中須要建立多個臨時變量此時就能夠本身手動建立一個自動釋放池來管理這些對象能夠很大程度地減小內存峯值。(例如一個代碼塊中須要建立循環建立10000個 image對象而後渲染出來,此時徹底可使用自動釋放池,正常狀況下不使用自動釋放池的話會等到這個代碼塊執行完成以後才能釋放這10000個對象,而是用自動釋放池以後每次循環完成自動釋放池的代碼也執行完成那麼該對象也就會被釋放。這樣就減小了內存峯值) 4f421094329644eb9cea2ff982fc7b5a_tplv-k3u1fbpfcp-zoom-1.png 結合文檔和上圖的理解總結:xcode

      1. 每次用戶出發一個時間都會啓動一次 runloop,建立完事件以後會建立一個自動釋放池
      2. 這次循環中會將全部延遲釋放的對象也就是 autorelease對象放到自動釋放池中去
      3. 在一次完整的runloop結束以前,會向自動釋放池中全部對象發送release消息,而後銷燬自動釋放池
    • 新老xcode建立的項目中 main函數中使用自動釋放池的區別

      xcode11以前建立的項目是這樣的 image.png xcode11以後建立的工程是這樣的 image.png 能夠發現 xcode11以前整個程序都是放在自動釋放池中的,當 runloop啓動會再建立一個自動釋放池嵌套在 main函數的這個釋放池中,這樣使用的結果是 main函數自動釋放池中建立的對象只有程序結束以後才能被釋放,再看 xcode11以後建立的 main函數發現程序在自動釋放池的外面,因此在自動釋放池中建立的對象只要程序啓動就能被釋放,這樣節省了程序的內存markdown

    • Clang分析

      能夠將 main文件 clang一下看編譯後的源碼 image.png 發現底層其實就是一個 __AtAutoreleasePool對象。而後再全局搜索 __AtAutoreleasePool而且自動釋放池中的代碼是使用 {}包裹的 image.png不出意外的是個結構體,裏面有構造函數 objc_autoreleasePoolPush返回了 atautoreleasepoolobj對象,還有一個析構函數 objc_autoreleasePoolPop須要傳入 atautoreleasepoolobj對象,上文也說了自動釋放池的代碼是在一個做用域中的,因此開始的時候就會調用構造方法,做用域結束的時候就會調用析構方法也能夠經過斷點調試查看彙編代碼驗證此結論 image.pngapp

    • 源碼分析

      上文經過 clang查看編譯後的代碼得知自動吃其實也就是個對象,就是個結構體,其中有構造方法和析構方法,接下來就能夠經過源碼查詢構造和析構方法看源碼是如何實現的同時也能夠深刻探索自動釋放池這個對象函數

      • AutoreleasePoolPage
        源碼中全局搜索構造方法發現 image.png構造和析構方法其實都是調用的是 AutoreleasePoolPage中的方法點擊 AutoreleasePoolPage查看源碼 image.png發現自動釋放池就是經過AutoreleasePoolPage來實現的註釋中也說道了自動釋放池的實現方法大概意思以下:
        1. 線程的自動釋放池是指針的堆棧
        2. 每一個指針都是要釋放的對象,或者是POOL_BOUNDARY,它是自動釋放池的邊界。
        3. 池令牌是指向該池的POOL_BOUNDARY的指針。彈出池後,將釋放比哨點更熱的每一個對象
        4. 堆棧分爲兩個雙向連接的頁面列表。根據須要添加和刪除頁面。
        5. 線程本地存儲指向熱頁面,該頁面存儲新自動釋放的對象。
        首先看該類的定義: image.png 從這個結構中也能夠看出是個雙向鏈表應爲有父節點和子節點。 整個程序的運行中可能會有多個AutoreleasePoolPage對象,從定義中能夠看出AutoreleasePoolPage是以棧爲結點經過雙向鏈表的形式組合而成,每一個頁的大小是4096,再看AutoreleasePoolPageData結構 image.png發現一共 56字節因此通常狀況下共有 4096-56=4040字節存儲 autorelease對象也就是一共能夠存 4040/8=505個對象,可是從定義中知道還有一個POOL_BOUNDARY(注意哨兵對象只有在第一頁中存在)因此第一頁能夠存儲 504個對象剩下的能夠存儲 505個對象,這裏可已經過打印自動釋放池的狀況驗證(_objc_autoreleasePoolPrint方法打印自動釋放池的狀況) image.png 此時是建立了504個對象 image.png 多加一個對象則又建立了一頁,而且把新建立的頁設置成 hot,而後第二頁的第一個對象再也不是哨兵對象直接就是 autorelease對象 具體內存分佈圖以下: image.png
      • objc_autoreleasePoolPush源碼分析
        image.png image.png 先看建立頁面的源碼 image.png 這裏知道 AutoreleasePoolPage是經過構造方法建立的 image.png 再看 autoreleaseFullPage方法 image.png 這個方法就比較簡單了就是一個鏈表的查詢工做,查到了則設置成聚焦頁面並添加對象,沒查到則新建立一個頁面並插入到鏈表中,新頁面設置成聚焦頁面而後添加對象。 最後再看add方法 image.png,這裏就是將對象存到next指針,而後next++
        具體流程圖以下: 未命名文件(38).jpg
      • autorelease源碼分析
        image.png image.png image.png image.png image.png 跟到最後發現autorelease底層實現就是調用autoreleaseFast方法
      • objc_autoreleasePoolPop源碼分析

      image.png image.png image.png 再看 releaseUntil方法 image.png kill方法 image.png 具體流程圖以下: 未命名文件(39).jpgoop

    • 總結
      1. AutoreleasePool底層就是一個 AutoreleasePoolPage對象 AutoreleasePoolPage對象又是一個棧結構而且是個雙向兩邊(應爲每個 AutoreleasePoolPage都是有大小限制的超出了再添加對象則須要建立新的頁,因此是個雙向連接結構)
      2. 既然AutoreleasePool是個棧結構而且是雙向鏈表結構,因此 push可添加對象就是壓棧,棧壓滿了則建立新頁面對象壓棧到新頁面中去,而後將新頁面插入到鏈表結構中。 pop就是出棧而後釋放對象,釋放page
      3. AutoreleasePool會在每次 runloop啓動的時候自動建立一個自動釋放池,而後在這次循環結束的時候釋放自動釋放池,因此若是對象添加 __autoreleasing屬性修飾則將對象添加到了系統建立的自動釋放池中,那麼該對象的釋放也就是系統干預釋放了,也就是要等到這次 runloop結束以後釋放對象,
      4. AutoreleasePool還一種狀況是手動建立自動釋放池也是就是經過 @autoreleasepool建立自動釋放池,在該做用域中建立的 autorelease對象會放到手動建立的自動釋放池中此時該對象就會在手動建立的自動釋放池做用域結束以後就會被釋放,這樣作能夠下降內存峯值
相關文章
相關標籤/搜索