Runloop 探秘(二)—— NSTimer 计时不准?

来源:互联网 发布:知乎ipad客户端 编辑:程序博客网 时间:2024/05/16 17:04

有这么一个场景,在界面中除了有 tableView,还有显示倒计时的 Label,当我们在滑动 tableView 的时候,倒计时就停止了,你是否遇到过这种问题?

首先来回顾一下 NSTimer 的相关知识。一般的做法是,在主线程(UI 线程)中创建 Timer。会有两种写法,如下所示:

// 第一种写法:NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];[timer fire];// 第二种写法:[NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

上述两种写法实质上市等价的,都会有上说所说的问题,第二种写法,默认也是将 timer 添加到 NSDefaultRunLoopMode 下的,并且会自动 fire。

说道 fire,这里做一下详细解释:它把触发的时间给提前了,但过10秒后定时器调用的方法还是会执行,即它只是提前触发定时器,而不影响之前的那个定时器设置的时间,就好比我们等不及要去看一场球赛,赶紧把车开快些一样,fire 的功能就像让我们快些到球场,但却不影响球赛开始的时间。当初始化定时器的 repeats 属性改为 NO 时,即不让它循环触发时,再 fire 的时候,触发一次,该定时器就被自动销毁了,以后再fire也不会触发了。

然而,在我们滑动 tableView 的时候 timerUpdate 方法并不会调用。是什么原因呢?

原因是当我们滑动 scrollView 时,主线程的 runloop 会切换到 UITrackingRunLoopMode 这个 Mode,执行也是 UITrackingRunLoopMode 下的任务,而 timer 是添加在 NSDefaultRunLoopMode 下的,所以 timer 任务并不会执行,只有当 UITrackingRunLoopMode 的任务执行完毕,runloop 切换到 NSDefaultRunLoopMode 后,才回继续执行 timer。

解决办法

解决方法一:更改 Runloop 运行 Mode

我们只需要在添加 timer 时,将 mode 设置为 NSRunLoopCommonModes 即可。

// 第一种写法    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];    [timer fire];// 第二种写法,因为是固定添加到 defaultMode 中,就不要用了

在笔者前面一篇博文中有说道,NSRunLoopCommonModes 是一个占位用的 Mode,不是一种真正的 Mode,其包含 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。

解决方法二:将 NSTimer 放到新的线程(非UI线程)中

NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerUpdate) object:nil];[subThread start];- (void)timerUpdate {    @autoreleasepool {        //在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode        timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(incrementCounter:) userInfo: nil repeats:YES];        //开始执行新线程的 Run Loop,如果不启动 run loop,timer 的事件是不会响应的        [[NSRunLoop currentRunLoop] run];    }  }

这样设置后,NSTimer 的事件影响就不会被冻结了。

这里出现了自动释放池 @autoreleasepool,顺便解释一下什么时候应该显示使用 Autorelease Pool。

  • autorelease 机制基于 UI framework。因此写非UI framework的程序时,需要自己管理对象生存周期。
  • autorelease 触发时机发生在下一次 runloop 的时候。因此如果在一个大的循环里不断创建 autorelease 对象,那么这些对象在下一次 runloop 回来之前将没有机会被释放,可能会耗尽内存。这种情况下,可以在循环内部显式使用 @autoreleasepool {} 将autorelease 对象释放。
for (item in BigSet){    @autoreleasepool {        //create large mem objects    }}
  • 自己创建的线程。Cocoa 的应用都会维护自己 autoreleasepool。需要显式添加autoreleasepool。

最后,做一个总结:

  1. 如果是在主线程中运行 timer,想要 timer 在某界面有视图滚动时,依然能正常运转,那么将 timer 添加到 RunLoop中 时,就需要设置 mode 为 NSRunLoopCommonModes。
  2. 如果是在子线程中运行 timer,那么将 timer 添加到 RunLoop 中后,Mode 设置为NSDefaultRunLoopMode 或 NSRunLoopCommonModes 均可,但是需要保证RunLoop 在运行,且其中有任务。
原创粉丝点击