iOS定时器循环引用分析及完美解决方案

目录

1.NSTimer导致的循环引用分析
2.NSTimer循环引用解决思路误区
3.NSTimer循环引用解决方案
4.NSTimer不准确的问题探究及解决


1.NSTimer导致的循环引用分析
  • CADisplayLink(频率能达到屏幕刷新率的定时器类)也和NSTimer一样会有此问题,这里为了方便只使用NSTimer去讲解。

如下面代码 在控制器创建一个NSTimer定时器:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
    NSLog(@"%s", __func__);
}
- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}
@end

当我们退出控制器会发现dealloc没有被调用,timerTest还是一直在执行,即明显的发生了内存泄漏,原因如下:

  • timer是ViewController的成员,即ViewController对timer是强引用;
  • NSTimer创建会对target传入对象产生强引用,此时我们传入了self(即是ViewController),即NSTimer对self也是强引用,关系如下图


    截屏2021-02-25 下午7.33.01.png

    所以他们两者相互被强引用,即发生了循环引用,dealloc 永远不会被执行,timer 也永远不会被释放,造成内存泄漏。

2.NSTimer循环引用解决思路误区

当我们遇到循环引用大多数第一反应就是使用weak解决,我们来看一下

  • 一:把timer改成弱引用让self对timer是弱引用
@property (weak, nonatomic) NSTimer *timer;

虽然self对timer是弱引用,但是timer销毁在dealloc方法里面执行,而dealloc又需要timer销毁才能执行,即两者相互等待,循环引用问题依旧存在。

  • 二:使用__weak self,让timer弱引用self

weak关键字适用于block,当block引用了块外的变量时,会根据修饰变量的关键字来决定是强引用还是弱引用,如果变量使用weak关键字修饰,那block会对变量进行弱引用,如果没有__weak关键字,那就是强引用。
  但是NSTimer的 scheduledTimerWithTimeInterval:target方法内部不会判断修饰target的关键字,所以这里传self 和 weakSelf是没区别的,其内部会对target进行强引用,还是会产生循环引用。

  • 三 : 在“适当的时机释放定时器”
    比如在页面即将消失的时候:
- (void) viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }

在某些情况下,这种做法是可以解决问题的,但是有时却会引起其他问题,比如控制器push到下一个控制器,viewDidDisappear执行后,timer被释放,此时再pop回来,timer已经不复存在了。
所以,这种"方案"并不是合理的。
  优化上面的方法这个时候可以采用配对使用在 viewWillAppear 开timer启,在 viewWillDisappear 关闭timer:

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    if (!self.timer) {
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
    }
}
-(void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

虽然能解决问题,但是在项目中,每次使用定时器都写这么一堆,显得不够优雅,维护起来也比较麻烦。而且有的业务场景这么使用依旧会产生各种问题。

NSTimer循环引用解决方案:

1、使用block的创建方式解决:

iOS10中,NSTimer的API新增了block方法,我们可以直接使用这种方式创建定时器结合weak使用即可

    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerTest];
    }];

2、给self添加中间对象TimerPoxy
  引入一个中间对象TimerPoxy,TimerPoxy弱引用 self,然后 TimerPoxy 传入NSTimer。即self 强引用NSTimer,NSTimer强引用 TimerPoxy,TimerPoxy 弱引用 self,这样没有相互强引用,则不会造成循环引用。

打破相互强引用.png
  • TimerPoxy的实现:
    TimerProxy.h
#import 
@interface TimerProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

TimerProxy.m

#import "TimerProxy.h"
@implementation TimerProxy
+ (instancetype)proxyWithTarget:(id)target
{
    TimerProxy *proxy = [[TimerProxy alloc] init];
    proxy.target = target;
    return proxy;
}

/*
 因为这里并没有实现定时器的timerTest方法,
timerTest方法是在ViewController里面实现的,
所以这里我们要利用消息转发机制,把方法交给ViewController去实现
 **/
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
@end
  • (id)forwardingTargetForSelector:(SEL)aSelector是什么?
      消息转发,简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
      本文中由于当前target是TimerProxy,但是TimerProxy没有实现timerTest方法(当然也不需要它实现),让系统去找target实例的方法实现,也就是去找ViewController中的方法实现。
  • ViewController中的实现:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:[TimerProxy proxyWithTarget:self]
                                                selector:@selector(timerTest)
                                                userInfo:nil repeats:YES];
}
- (void)timerTest
{
    NSLog(@"%s", __func__);
}
- (void)dealloc
{
    NSLog(@"%s", __func__);
    [self.timer invalidate];
}
@end

这样就实现上面图示的内容,解决了循环引用的问题。

优化解决方案:

  • 不使用NSObject类实现,使用NSProxy类去实现。

上面我们用NSObject实现转发定时器的过程大概是:
当前类查找timerTest实现方法 ---> NSObject 类找timerTest实现方法 --->消息转发至ViewController查找实现方法。
而NSProxy一般就是用来做消息转发的,大概过程:
直接把实现方法转发ViewController。
这样对比下来,NSProxy比NSObject效率高且节省资源

NSProxy实现:

#import 
@interface TimerProxys : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "TimerProxys.h"
@implementation TimerProxys
+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy对象不需要调用init,因为它本来就没有init方法
    TimerProxys *proxy = [TimerProxys alloc];
    proxy.target = target;
    return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}
@end

到此定时器的循环引用问题,已经完美解决啦~

4.NSTimer不准确的问题探究及解决

为什么NSTimer会不准确?
1、NSTimer定时器依赖于RunLoop,我们知道RunLoop每循环一次的时间是基于任务量的,即每一次的时间都不一定相同,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时。
2、模式的切换,当创建的timer被加入到NSDefaultRunLoopMode时,此时如果有滑动UIScrollView的操作,runLoop 的mode会切换为TrackingRunLoopMode,而MainRunLoop处于 UITrackingRunLoopMode 的模式下,是不会处理 NSDefaultRunLoopMode 的消息(因为它们的RunLoop Mode不一样),所以timer会暂时停止;

对于精度要求不高的场景我们使用NSTimer没太大影响,对于上面2的滑动UIScrollView的导致定时器停止的问题下面一行代码即可解决:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

  • 解决方案:
    使用GCD定时器,因为GCD定时器是基于系统内核的,所以不会受其他因素影响,基本使用方法这里不作讲解,github有相关封装demon,有需要自取。

本文代码demon地址:https://github.com/yizhixiafancai/timerAbout

点个赞再走呗~

你可能感兴趣的:(iOS定时器循环引用分析及完美解决方案)