iOS底层-内存管理之强引用

iOS底层-内存管理之强引用在上一篇 iOS底层-内存管理之弱引用表 中我们对弱引用进行了详细的分析,与弱引用对应的是强引用。强引用会导致控制器不释放,本文将分析NSTimer的强引用,并提出几种方案解决不释放问题。

前言

在上一篇 iOS底层-内存管理之弱引用表 中我们对弱引用进行了详细的分析,与弱引用对应的是强引用。强引用会导致控制器不释放,本文将分析NSTimer的强引用,并提出几种方案解决不释放问题。

NSTimer的强引用分析

  • 首先看看如下NSTimer案例:

    // SecondViewController.m
    - (void)timerTest {
        __weak typeof(self)weakSelf = self;
        self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(addNumberAction) userInfo:nil repeats:true];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    }
    
    - (void)addNumberAction {
      num++;
      NSLog(@"++ %d ++", num);
    }
    
    - (void)dealloc {
        [self.timer invalidate];
        self.timer = nil;
        NSLog(@" second dealloc 🎉🎉🎉 ");
    }
    

    那么当界面pop时,会走dealloc么?结果不会走。在Developer Documentation中可以了解到,NSTimer在启动后,会对target进行强引用:

    截屏2021-11-04 22.53.43.png

    • 虽然target现在是个weak指针,但是它依然指向self内存,况且Runloop -> timer -> weakSelf -> self内存,也就相当于Runloop间接持有self,所以不释放。
      • 使用scheduledTimerWithTimeInterval方法也不会释放,它是是self ->timer -> weakSelf -> self内存不会释放
    • 这两种不释放的情况,如下图所示:

    截屏2021-11-05 09.25.48.png

解决方案

  • 下面将分析几种解决方案

一、NSTimer的block方式

  • 我们知道[NSTimer scheduledTimerWithTimeInterval: repeats: block:]方法不会产生强引用,只需要注意blockself的循环引用,就可以解决不释放问题。代码如下:

    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:true block:^(NSTimer * _Nonnull timer) {
        [weakSelf addNumberAction];
    }];
    

    此时block块中的weakSelf是临时变量,因此并没有对self进行强持有,所以界面会走dealloc方法。

二、pop时销毁NSTimer

  • 通过上面分析我们可以知道NSTimer会强引用target,如果在合适的时机销毁timer也就断开了这一层的强引用,进而就解决了不释放问题。
  • 销毁timer,我们通常会想到在viewWillDisappear的时候操作,不过这样会有一个问题,如果页面需要push此时的界面没有销毁。所以此时销毁了定时器,等回到界面时定时器任务停止了,就达不到我们想要的效果了。
  • 我们可以选择在界面pop时销毁定时器,pop判断有以下方式:
      1. 拿到当前navigationController.viewControllers数组,如果数组里找不到当前控制器,则为pop操作:
      - (void)viewWillDisappear:(BOOL)animated {
        [super viewWillDisappear:animated];
        NSArray *navigations = self.navigationController.viewControllers;
        if ([navigations indexOfObject:self] == NSNotFound) {  // pop
            [self.timer invalidate];
            self.timer = nil;
        }
      }
      
      1. 重写系统的didMoveToParentViewController方法,然后根据parent判断。didMoveToParentViewController方法会在页面加载后页面被移除后调用。判断方法如下:
      - (void)didMoveToParentViewController:(UIViewController *)parent {
          if (parent == nil) {
              [self.timer invalidate];
              self.timer = nil;
          }
      }
      

这两种方式都可以销毁NSTimer,进而解决强引用问题

三、中介模式

  • 中介模式实质就是让timer持有其他target,当timer执行时会找到targetIMP,进而执行方法,这样就不会对self进行强持有。具体代码如下:

    self.target = [NSObject alloc];
    class_addMethod([NSObject class], @selector(addNumberAction), (IMP)wushuang, "v@:");
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(addNumberAction) userInfo:nil repeats:true];
    
    void wushuang(id obj) {
        NSLog(@"__ %s __ %@", __func__, obj);
    }
    
    - (void)addNumberAction {
        num++;
        NSLog(@"正在打印 --- %d", num);
    }
    

    运行结果如下:

    截屏2021-11-05 15.19.52.png

    • 这种方式虽然可以修复self强引用问题,但是执行的是wushuang这个函数,要执行addNumberAction这个方法不太好处理,所以这个方法并不太好

四、自定义NSTimer

  • 自定义NSTimer的主要思想是将定时器要执行的方法放在self(控制器),然后将timer核心方法写在自定义timer类,再结合中介模式objc_msgSend调用self(控制器)中的核心方法,这样也不会对self(控制器)产生强引用。

  • 具体实现如下:

    // SecondViewController.m
    @interface SecondViewController ()
    @property (nonatomic, strong) WSTimerWrapper *wrapperTimer;
    @end
    
    - (void)addNumberAction {
        num++;
        NSLog(@"正在打印 --- %d", num);
    }
    
    - (void)timerTest {
        self.wrapperTimer = [[WSTimerWrapper alloc] ws_timerWithTimeInterval:1 target:self selector:@selector(addNumberAction) userInfo:nil repeats:true];
    }
    
    // WSTimerWrapper.h
    @interface WSTimerWrapper : NSObject
    - (instancetype)ws_timerWithTimeInterval:(NSTimeInterval)ti
                                      target:(id)aTarget
                                    selector:(SEL)aSelector
                                    userInfo:(nullable id)userInfo
                                     repeats:(BOOL)yesOrNo;
    @end
    
    // WSTimerWrapper.m
    @interface WSTimerWrapper ()
    
    @property (nonatomic, weak) id aTarget;
    @property (nonatomic, strong) NSTimer *timer;
    @property (nonatomic, assign) SEL aSelector;
    @end
    
    @implementation WSTimerWrapper
    
    - (instancetype)ws_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
        if (self == [super init]) {
            self.aTarget = aTarget;
            self.aSelector = aSelector;
          
            if ([self.aTarget respondsToSelector:self.aSelector]) {
                Method method = class_getInstanceMethod([self.aTarget class], aSelector);
                const char *type = method_getTypeEncoding(method);
                class_addMethod([self class], aSelector, (IMP)wsAddNum, type);
              
                self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
            }
        }
        return self;
    }
    
    void wsAddNum(WSTimerWrapper *wrapper) {
        if (wrapper.aTarget) {
            void (*ws_msgSend)(void *, SEL, id) = (void *)objc_msgSend;
            ws_msgSend((__bridge void *)(wrapper.aTarget), wrapper.aSelector, wrapper.timer);
        } else {
            [wrapper.timer invalidate];
            wrapper.timer = nil;
        }
    }
    
    - (void)dealloc {
        NSLog(@" WSTimerWrapper 🍉🍉🍉 ");
    }
    
    @end
    

    代码的核心流程图解如下:

    截屏2021-11-05 16.11.56.png

    • 从图解中可以看出timer并没有对vc产生强持有,当vc销毁时由于target不存在了,所以就会调用销毁定时器代码

五、NSProxy虚基类

  • NSProxy是一个抽象的超类,和NSObject是同级的,它的Api比较简单:

    截屏2021-11-05 16.37.20.png
    NSObject也有消息转发,但是要经过一些列的条件最后才是消息慢转发,而NSProxy调用转发逻辑则要简单的多。具体实现如下:

    // SecondViewController.m
    @interface SecondViewController ()
    @property (nonatomic, strong) NSTimer        *timer;
    @property (nonatomic, strong) WSProxy        *proxy;
    @end
    
    - (void)timerTest {
        self.proxy = [WSProxy proxyWithTransformTarget:self];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(addNumberAction) userInfo:nil repeats:true];
    }
    
    - (void)addNumberAction {
        num++;
        NSLog(@"正在打印 --- %d", num);
    }
    
    // WSProxy.h
    @interface WSProxy : NSProxy
    + (instancetype)proxyWithTransformTarget:(id)target; 
    @end
    
    // WSProxy.m
    @interface WSProxy ()
    @property (nonatomic, weak) id object;
    @end
    
    @implementation WSProxy
    + (instancetype)proxyWithTransformTarget:(id)target {
        WSProxy *proxy = [WSProxy alloc];
        proxy.object = target;
        return  proxy;
    }
    // 消息快转发
    //- (id)forwardingTargetForSelector:(SEL)aSelector {
    // return self.object;
    //}
    
    // 消息慢转发
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        if (self.object) {
            NSLog(@"+++ %@ +++", NSStringFromSelector(sel));
        } else {
            NSLog(@"error : Signature has no object ~");
        }
        return [self.object methodSignatureForSelector:sel];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {
        if (self.object) {
            [invocation invokeWithTarget:self.object];
        } else {
            NSLog(@"error : invocation has no Target ~");
        }
    }
    @end
    
    • 定时器强引用的是WSProxy对象,而WSProxy弱引用VC,然后WSProxy不能执行sel,于是将sel方法转发给vc,这样就完成了timer的调用,且不会产生对vc的强持有
    • 代码中无论是消息的慢转发还是快转发都可以达到消除timervc的强持有。

今天的文章iOS底层-内存管理之强引用分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22465.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注