内存管理下篇(强引用分析、AutoReleasePool)

404 阅读11分钟
  • 强引用分析

    • 示例代码
      //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 结合文档和上图的理解总结:

      1. 每次用户出发一个时间都会启动一次 runloop,创建完事件之后会创建一个自动释放池
      2. 此次循环中会将所有延迟释放的对象也就是 autorelease对象放到自动释放池中去
      3. 在一次完整的runloop结束之前,会向自动释放池中所有对象发送release消息,然后销毁自动释放池
    • 新老xcode创建的项目中 main函数中使用自动释放池的区别

      xcode11之前创建的项目是这样的 image.png xcode11之后创建的工程是这样的 image.png 可以发现 xcode11之前整个程序都是放在自动释放池中的,当 runloop启动会再创建一个自动释放池嵌套在 main函数的这个释放池中,这样使用的结果是 main函数自动释放池中创建的对象只有程序结束之后才能被释放,再看 xcode11之后创建的 main函数发现程序在自动释放池的外面,所以在自动释放池中创建的对象只要程序启动就能被释放,这样节省了程序的内存

    • Clang分析

      可以将 main文件 clang一下看编译后的源码 image.png 发现底层其实就是一个 __AtAutoreleasePool对象。然后再全局搜索 __AtAutoreleasePool并且自动释放池中的代码是使用 {}包裹的 image.png不出意外的是个结构体,里面有构造函数 objc_autoreleasePoolPush返回了 atautoreleasepoolobj对象,还有一个析构函数 objc_autoreleasePoolPop需要传入 atautoreleasepoolobj对象,上文也说了自动释放池的代码是在一个作用域中的,所以开始的时候就会调用构造方法,作用域结束的时候就会调用析构方法也可以通过断点调试查看汇编代码验证此结论 image.png

    • 源码分析

      上文通过 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).jpg

    • 总结
      1. AutoreleasePool底层就是一个 AutoreleasePoolPage对象 AutoreleasePoolPage对象又是一个栈结构并且是个双向两边(应为每一个 AutoreleasePoolPage都是有大小限制的超出了再添加对象则需要创建新的页,所以是个双向链接结构)
      2. 既然AutoreleasePool是个栈结构并且是双向链表结构,所以 push可添加对象就是压栈,栈压满了则创建新页面对象压栈到新页面中去,然后将新页面插入到链表结构中。 pop就是出栈然后释放对象,释放page
      3. AutoreleasePool会在每次 runloop启动的时候自动创建一个自动释放池,然后在此次循环结束的时候释放自动释放池,所以如果对象添加 __autoreleasing属性修饰则将对象添加到了系统创建的自动释放池中,那么该对象的释放也就是系统干预释放了,也就是要等到此次 runloop结束之后释放对象,
      4. AutoreleasePool还一种情况是手动创建自动释放池也是就是通过 @autoreleasepool创建自动释放池,在该作用域中创建的 autorelease对象会放到手动创建的自动释放池中此时该对象就会在手动创建的自动释放池作用域结束之后就会被释放,这样做可以降低内存峰值