总结一下平时使用NSTimer碰到的几个小问题:
- 作为定时器使用时不准确的问题
- ScrollView滚动时timer不执行的问题
- 内存泄漏的问题
NSTimer准确性不高的原因
以上几个问题都跟RunLoop
有关系,一个 NSTimer
注册到RunLoop
后,RunLoop
会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop
为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度)
,标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
ScrollView滚动时timer不执行的原因
这里有个概念叫 CommonModes
:一个 Mode
可以将自己标记为Common
属性(通过将其 ModeName
添加到 RunLoop
的 commonModes
中)。每当 RunLoop
的内容发生变化时,RunLoop
都会自动将 _commonModeItems
里的 Source/Observer/Timer
同步到具有 Common
标记的所有Mode里。
应用场景举例:主线程的 RunLoop
里有两个预置的 Mode:kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
。这两个 Mode 都已经被标记为Common
属性。DefaultMode
是 App 平时所处的状态,TrackingRunLoopMode
是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode
时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode
,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 commonModeItems
中。commonModeItems
被 RunLoop 自动更新到所有具有Common
属性的 Mode 里去。
NSTimer导致内存泄漏的原因及解决方法
原因
-
NSTimer
作为局部变量使用,在添加到RunLoop
中的时会被RunLoop
强引用,导致Timer
本身不能被释放。 - NSTimer作为属性使用时只能声明成强引用(使用弱引用添加到RunLoop中会导致奔溃),在创建
timer
的过程中,timer
会强引用当前添加的target
,若target
是当前timer
所属的对象时,会造成循环引用的问题 - 当
timer
释放时所在线程跟timer
添加到的RunLoop
对应的线程不一致时释放会有问题
解决方法
在合适的时机调用invalidate
方法。苹果官方的描述如下:
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
invaliate
是唯一一个将timer
从NSRunLoop
对象移除的方法,如果我们是使用target
跟userinfo
对象创建的,那么同样会移除对这些对象强引用。
#import "TimerViewController.h"
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TimerViewController
- (void)dealloc {
NSLog(@"%@", NSStringFromSelector(_cmd));
}
- (void)viewDidLoad {
[super viewDidLoad];
[self propertyTimer];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
if ([self.timer isValid]) {
[self.timer invalidate];
}
}
#pragma mark - private method
- (void)propertyTimer {
self.timer = [NSTimer timerWithTimeInterval:5
target:self
selector:@selector(propertyTimerMethod)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
forMode:NSRunLoopCommonModes];
[self.timer fire];
}
- (void)propertyTimerMethod {
NSLog(@"%@", NSStringFromSelector(_cmd));
}
- (void)localTimer {
//局部变量 无法释放
NSTimer *timer = [NSTimer timerWithTimeInterval:5
target:self
selector:@selector(localTimerMethod)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSRunLoopCommonModes];
[timer fire];
}
- (void)localTimerMethod {
NSLog(@"%@", NSStringFromSelector(_cmd));
}