美文网首页iOS进阶面试好文
10-内存管理中NSTimer常见问题

10-内存管理中NSTimer常见问题

作者: 光强_上海 | 来源:发表于2020-06-21 19:55 被阅读0次

    我们在平时的项目开发过程中,经常会使用到NSTimer来创建定时器,但是在使用过程中有时我们又会遇到以下几个问题:

    • 主线程中NSTimer创建的定时器不工作
    • 异步子线程中创建的timer不工作
    • 滚动列表时,NSTimer失效不工作,停止滚动timer恢复工作
    • NSTimer创建的定时器,当前控制器对象销毁了,但是此时Timer还在工作,没有销毁,造成了循环引用
    • NSTimer创建的定时器不准,例如设置的是1秒执行一次,最终发现有时不是一秒执行1次

    我们先来了解下iOS中NSTimer常用的API有哪些:

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    

    我们在平时的开发过程中,常用到的NSTimer类方法就上面这6个函数

    下面我们先来探究下NSTimer不工作的问题,示例代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        NSTimer *timer1 = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
    
        NSTimer *timer2 = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"2222");
        }];
        
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        invocation.target = self;
        invocation.selector = @selector(doTask);
        NSTimer *timer3 =  [NSTimer timerWithTimeInterval:1 invocation:invocation repeats:YES];
    }
    
    - (void)doTask {
        NSLog(@"1111");
    }
    

    从上面创建的3个timer的运行结果来看,这三个timer都没有工作,这又是为何尼,我们从timerWithTimeInterval:开头的函数注释可以得知,初始化出来的timer需要添加到runloop中才能正常使用

    Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.

    我们将timer添加到runloop中,修改代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        NSTimer *timer1 = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
        // 将timer添加到runloop
        [[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode];
        
        NSTimer *timer2 = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"2222");
        }];
        // 将timer添加到runloop
        [[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode];
        
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        invocation.target = self;
        invocation.selector = @selector(doTask2);
        NSTimer *timer3 =  [NSTimer timerWithTimeInterval:1 invocation:invocation repeats:YES];
        
        // 将timer添加到runloop
        [[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSDefaultRunLoopMode];
    }
    
    - (void)doTask {
        NSLog(@"1111");
    }
    
    - (void)doTask2 {
        NSLog(@"333");
    }
    

    将timer添加到runloop中后,这三个timer就能正常打印工作了

    接下来我们将timerWithTimeInterval:替换成scheduledTimerWithTimeInterval:,这时我们发现不将timer添加到runloop中,这时timer也都能正常工作,这又是为何尼?,我们看下scheduledTimerWithTimeInterval:开头的函数的注释可知,scheduledTimerWithTimeInterval:开头的函数创建的timer,底层已近将此timer添加到当前runloop中,不需要我们重复添加

    Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
        
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"444");
        }];
        
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        invocation.target = self;
        invocation.selector = @selector(doTask2);
        [NSTimer scheduledTimerWithTimeInterval:1 invocation:invocation repeats:YES];
    }
    
    - (void)doTask {
        NSLog(@"111");
    }
    
    - (void)doTask2 {
        NSLog(@"333");
    }
    

    我们从GNU中的源码也可以看到,scheduledTimerWithTimeInterval:内部确实将timer添加到runloop,源码如下:

    /**
     * Create a timer which will fire after ti seconds and, if f is YES,
     * every ti seconds thereafter. On firing, the target object will be
     * sent a message specified by selector and with the timer as its
     * argument.<br />
     * This timer will automatically be added to the current run loop and
     * will fire in the default run loop mode.
     */
    + (NSTimer*) scheduledTimerWithTimeInterval: (NSTimeInterval)ti
                         target: (id)object
                       selector: (SEL)selector
                       userInfo: (id)info
                        repeats: (BOOL)f
    {
      id t = [[self alloc] initWithFireDate: nil
                       interval: ti
                     target: object
                       selector: selector
                       userInfo: info
                    repeats: f];
                    
        // 将timer添加到runloop中
      [[NSRunLoop currentRunLoop] addTimer: t forMode: NSDefaultRunLoopMode];
      RELEASE(t);
      return t;
    }
    

    接下来我们再来看看在异步子线程中创建的timer不工作的问题,示例代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{        
            NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
        });
    }
    
    - (void)doTask {
        NSLog(@"111");
    }
    

    我们运行项目发现没有执行打印语句,timer没有工作,这是因为在异步子线程中默认是没有runloop的,不能将timer添加到runloop中,所以我们需要在子线程中创建一个runloop,修改代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
            [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        });
    }
    
    - (void)doTask {
        NSLog(@"111");
    }
    

    我们在异步线程中创建好runloop后,运行项目,发现还是没有打印,这又是为啥尼,这是因为在子线程中创建的runloop,我们必须手动调用run方法来启动这个runloop,所以我们修改代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
            [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
            // 启动runloop
            [[NSRunLoop currentRunLoop] run];
        });
    }
    
    - (void)doTask {
        NSLog(@"111"); // 111
    }
    

    滚动列表(继承自UIScrollView的控件)导致NSTimer失效的问题,前几个章节讲解runloop的应用时有讲解


    接下来我们再来看看NSTimer造成的循环引用问题,示例代码如下:

    @interface ViewController ()
    
    @property (nonatomic, strong) CADisplayLink *displayLink;
    @property (nonatomic, strong) NSTimer *timer;
    @end
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
            
        self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(doTask)];
        // 将displayLink添加到runloop中
        [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        
        self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doTask2) userInfo:nil repeats:YES];
        // 将timer添加到runloop中
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    }
    
    - (void)doTask {
        NSLog(@"111");
    }
    
    - (void)doTask2 {
        NSLog(@"333");
    }
    
    - (void)dealloc {
        [self.displayLink invalidate];
        [self.timer invalidate];
        
        NSLog(@"%s", __func__);
    }
    

    CADisplayLink

    CADisplayLink也是一种定时器,定时器平均每秒刷新60次(60FPS为屏幕刷帧频率)

    这里我们拿CADisplayLinkNSTimer一块分析,因为它们都会产生循环引用,并且原因也一样

    当我们返回当前控制器时,我们发现当前控制器已经销毁,但是定时器任然在工作,这是因为当前控制器self强引用着timer,然而timer内部的实现又将传递进去的target参数对象进行了持有(retain操作),这样就导致了循环引用

    timer内部对target对象进行了持有,也就是进行了retain操作,使target对象的引用计数器+1,这个我们可以通过timerWithTimeInterval:target:selector:userInfo:repeats:对应的GNU源码查看了解到,GNU源码如下:

    - (id) initWithFireDate: (NSDate*)fd
               interval: (NSTimeInterval)ti
             target: (id)object
               selector: (SEL)selector
               userInfo: (id)info
            repeats: (BOOL)f
    {
      if (ti <= 0.0)
        {
          ti = 0.0001;
        }
      if (fd == nil)
        {
          _date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
            initWithTimeIntervalSinceNow: ti];
        }
      else
        {
          _date = [fd copyWithZone: NSDefaultMallocZone()];
        }
        
         // 从这里可以看到,在timer内部,对传递进来的`target`对象进行了retain操作,也就是在timer内部对`target`对象进行了强引用
      _target = RETAIN(object);
      
      _selector = selector;
      _info = RETAIN(info);
      if (f == YES)
        {
          _repeats = YES;
          _interval = ti;
        }
      else
        {
          _repeats = NO;
          _interval = 0.0;
        }
      return self;
    }
    
    
    /**
     * Create a timer which will fire after ti seconds and, if f is YES,
     * every ti seconds thereafter. On firing, the target object will be
     * sent a message specified by selector and with the timer as its
     * argument.<br />
     * NB. To make the timer operate, you must add it to a run loop.
     */
    + (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
                    target: (id)object
                  selector: (SEL)selector
                  userInfo: (id)info
                   repeats: (BOOL)f
    {
      return AUTORELEASE([[self alloc] initWithFireDate: nil
                           interval: ti
                             target: object
                           selector: selector
                           userInfo: info
                            repeats: f]);
    }
    

    查看GNU源码,我们可以知道,timerWithTimeInterval:target:selector:userInfo:repeats:函数底层最终会调用initWithFireDate:interval:target:selector:userInfo:repeats:函数,在这个函数内部有执行_target = RETAIN(object);,所以会产生循环引用

    那么怎么解决这个循环引用问题尼,我们将Target参数的self强指针改为弱指针__weak typeof(self) weakSelf = self是否可以尼,示例代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        __weak typeof(self) weakSelf = self;
        
        self.displayLink = [CADisplayLink displayLinkWithTarget:weakSelf selector:@selector(doTask)];
        // 将displayLink添加到runloop中
        [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        
        self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(doTask2) userInfo:nil repeats:YES];
        // 将timer添加到runloop中
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    }
    

    我们发现将self改为weakSelf并不能解决循环引用问题,这是因为__weak是用来解决block代码块内部的循环引用问题的,用在此处并没有作用,那我们该怎么解决这种循环引用尼?

    对于NSTimer来说,我们可以选择使用timerWithTimeInterval:repeats:block:,这时就可以在block内部使用weakSelf来解决循环引用

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        __weak typeof(self) weakSelf = self;
        
        self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf doTask2];
        }];
        
        // 将timer添加到runloop中
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    }
    

    但是CADisplayLink定时器没有带block的这种用法,那还是得想办法解决target:selector:这种用法的循环引用,这时我们可以使用代理的方式来解决,我们创建一个target的代理对象,将实现doTask方法的目标对象转移给TimerProxy代理对象,测试代码如下:

    TimerProxy

    @interface TimerProxy : NSObject
    
    // 弱引用
    @property (nonatomic, weak) id target;
    
    + (instancetype)proxyWithTarget:(id)target;
    @end
    
    @implementation TimerProxy
    
    + (instancetype)proxyWithTarget:(id)target {
        TimerProxy *proxy = [[TimerProxy alloc] init];
        proxy.target = target;
        return proxy;
    }
    
    // 消息转发
    - (id)forwardingTargetForSelector:(SEL)aSelector {
         // 将aSelector的实现转发给self.target对象实现
        return self.target;
    }
    
    - (void)dealloc {
        NSLog(@"%s", __func__);
    }
    @end
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
            
        self.displayLink = [CADisplayLink displayLinkWithTarget:[TimerProxy proxyWithTarget:self] selector:@selector(doTask)];
        // 将displayLink添加到runloop中
        [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    
        self.timer = [NSTimer timerWithTimeInterval:1 target:[TimerProxy proxyWithTarget:self] selector:@selector(doTask2) userInfo:nil repeats:YES];
        // 将timer添加到runloop中
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    }
    
    - (void)doTask {
        NSLog(@"111");
    }
    
    - (void)doTask2 {
        NSLog(@"333");
    }
    
    - (void)dealloc {
        [self.displayLink invalidate];
        [self.timer invalidate];
        
        NSLog(@"%s", __func__);
    }
    

    我们再次运行程序,退出当前控制器,发现定时器和控制器都能正常的销毁了,这里使用弱引用的target就解决了循环引用问题


    接下来我们再来看下NSTimerCADisplayLink创建的定时器存在不准确的问题。NSTimerCADisplayLink定时器不准是因为timer需要在runloop环境下工作,然而runloop的运行循环并不能保证每一个循环所用的时间都是相同的,可能某一个循环所用时间0.2s,或者0.2s,或者0.3s,或者0.5s,这时如果timer定时器的时间间隔是1s,但是此时runloop需要循环0.2s+0.2s+0.3s+0.5s才能执行一次定时器任务,但是这时的时间就是1.2s了,与定时器的1s就有一些误差了,所以说导致了timer不准

    我们可以选择使用GCD来创建一个定时器,GCD创建的定时器不依赖与runloop的运行环境,所以就更加准确一些,示例代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
            
        // 创建一个队列,如果是`dispatch_get_main_queue`那么定时器就会在主线程中执行,如果我们需要定时器在子线程中执行,我们可以创建一个队列`dispatch_queue_create("queun", DISPATCH_QUEUE_SERIAL)`
        dispatch_queue_t queue = dispatch_get_main_queue();
        
        // 创建定时器
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        
        // 设置几秒开始定时
        uint64_t startTime = 0;
        
        // 设置定时器的时间间隔
        uint64_t interval = 1;
        
        dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, startTime * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
        
        // 设置定时器执行任务
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"1111");
        });
        
        // 启动定时器
        dispatch_resume(timer);
        
        // 这里需要主要,创建完gcd的定时器,我们需要使用一个强指针指向这个定时器
        self.gcdTimer = timer;
    }
    

    讲解示例Demo地址:https://github.com/guangqiang-liu/10-NSTimer

    更多文章

    相关文章

      网友评论

        本文标题:10-内存管理中NSTimer常见问题

        本文链接:https://www.haomeiwen.com/subject/wtaaxktx.html