内存管理下篇(强引用分析、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对象会放到手动建立的自动释放池中此时该对象就会在手动建立的自动释放池做用域结束以后就会被释放,这样作能够下降内存峰值
相关文章
相关标签/搜索