NSTimer
是iOS
常见定时器。它经过特定时间间隔就会触发,将指定的消息发送到目标对象。定时器是线程通知自己做某件事的方法,定时器和runLoop
的特定的模式相关。如果定时器所在的模式当前未被runLoop
监视,那么定时器将不会开始,直到runLoop
运行在相应的模式下。如果runLoop
停止运行,那定时器也会停止动。
NSTimer
会对外界传递的target
进行强持有。如果只使用一次,会在本次使用之后自身销毁invalidate
,并且会对NSTimer
的那个target
进行release
操作。如果是多次重复调用,就需要我们自己手动进行invalidate
,否则NSTimer
会一直存在。
NSTimer
在那个线程创建就要在那个线程停止,否则资源不能正确的释放。
NSTimer
的API
按照是否需要手动将timer
放入定时器,我们可以把NSTimer
的方法分为两种:
- 需要手动加入
runLoop
:
+ (NSTimer *)timerWithTimeInterval:(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 *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;
上述几个方法需要将timer
放到runLoop
才能执行:
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
- 不需要手动放入
runLoop
:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
NSTimer
精准度问题
NSTimer
不是一个高精度的定时器,这是因为NSTimer
是依赖于runloop
,如果它当前所处的线程正在进行大数据处理,NSTimer
的执行就会等到这个大数据处理完之后。等待的过程可能会错过很多次NSTimer
的循环周期,但是NSTimer
并不会将前面错过的执行次数在后面都执行一遍,而是继续执行后面的循环。而且无论循环延迟多久,循环间隔都不会发生变化。
在有UIScrollView
或者其子类的控制器中使用NSTimer
,需要注意scrollView
的滑动操作会影响到NSTimer
。因为scrollView
在滑动的时候会将runloop
的模式从NSDefaultRunLoopMode
切换到UITrackingRunLoopMode
,这是NSTimer
就不会进行回调了。此时需要调用如下方法:
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
将NSTimer
的将runloop
的模式切换到NSRunLoopCommonModes
,这样才不会对其进行影响。
NSTimer
注意事项
使用多次循环的NSTimer
一定要进行销毁动作,否则会导致内存泄露问题。销毁的方法如下:
- (void)invalidate
target
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.
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.
从官方给出的文档可以看出,timer
对传入的target
是强引用,而invalidate
则是从runLoop
对象删除计时器的唯一方法,如果我们我们不调用该方法,就对导致这个强引用对象释放不掉,从而出现内存问题。需要特别注意的是,必须在设置计时器的线程调用该方法,如果从别的线程调用该方法,可能并不会从runLoop
删除timer
,会导致线程的异常。
下面我们看一个例子,我们从A
控制器push
到B
控制器,并在B
控制器实现以下代码:
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) int num;
- (void)fireTimer {
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)timerAction {
num++;
NSLog(@"==%d==",num);
}
运行程序,先从A
控制器进入B
控制器,然后再返回。我们可以发现,控制台依然在输出,timer
并没有被停止。这就是因为self
本身对timer
持有,而timer
也强引用了self
,而我们没有调用invalidate
来打破这个循环引用,timer
无法被释放销毁。
我们知道block
可以使用weakSelf
打破循环引用,那么此处我们将传入的self
改为weakSelf
是否可以呢?
__weak typeof(self) weakSelf = self;
运行程序,可以发现,timer
依然没有被释放销毁。在控制台调试一下self
和weakSelf
,
lldb) po self
0x7fa5b7c10770
(lldb) po weakSelf
0x7fa5b7c10770
(lldb) po &self
0x0000000103d70fc8
(lldb) po &weakSelf
0x00007ffeee377f68
可以得出,self
和weakSelf
其实是指向同一快空间的不同指针。timer
对weakSelf
的强持有是对weakSelf
这个对象的持有,其实也就是对self
的持有,而block
对外界对象的持有是对指针地址的持有,而weakSelf
的指针和self
的指针并不相同,所以block
使用weakSelf
可以打破循环引用,而timer
不能。
关于block
的分析可以参考block(二)-底层分析。
打破timer
对self
强持有的方法有以下几种
- 在
dealloc
中调用invalidate
方法,此方法有个缺陷就是如果控制器其他地方内存逻辑出现问题,可能会不走dealloc
方法。
- 在
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
- 在
didMoveToParentViewController
中调用invalidate
- 在
- (void)didMoveToParentViewController:(UIViewController *)parent{
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
}
}
- 使用一个中间层打破
timer
和target
之间的强引用。将timer
的响应方法交给中间层,而中间层处理不了,再通过消息转发告诉target
来进行处理。此方法还是需要使用invalidate
。
- 使用一个中间层打破
@interface TProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
@interface TProxy()
@property (nonatomic, weak) id object;
@end
@implementation TProxy
+ (instancetype)proxyWithTransformObject:(id)object{
TProxy *proxy = [TProxy alloc];
proxy.object = object;
return proxy;
}
// 仅仅添加了weak类型的属性还不够
// 为了保证中间件能够响应外部self的事件,需要通过消息转发机制,
// 让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// 转移
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
// VC
- (void)proxyTimer {
TProxy *proxy = [TProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}
使用NSProxy
的时候,官方给出的文档是继承自NSProxy
的类,可以直接实现methodSignatureForSelector
和forwardInvocation
来处理自身未实现的消息。这样会比直接走消息转发流程快一些。
// NSProxy已经实现,性能更高
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.object methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.object];
}
总结
- 由于
NSTimer
依赖于runloop
,其精度不高 - 使用
NSTimer
需要注意是其中有些方法需要我们手动添加到runloop
才能执行。 - 使用
NSTimer
必须要调用invalidate
,否则会出现内存问题。
网友评论