一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第12天,点击查看活动详情。
前言
iOS中常用的定时器有NSTimer、CADisplayLink、GCD定时器。前两者依赖runloop,某些方法创建时需要手动把定时器添加到runloop中,且如果是通过target:selector方式创建的,需要注意定时器会对target产生强引用。且由于是依赖runloop,如果某个runloop循环中有大量的耗时操作,则定时器的回调可能不会准确的在定时间隔内触发。GCD定时器是依赖于操作系统内核的,对定时精度有要求的,可以使用GCD定时器,且GCD定时器也不需要考虑上面所说的那些问题。
NSTimer
(NSTimer*)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation*)invocation repeats:(BOOL)yesOrNo; // 通过NSInvocation封装方法签名的方式创建定时器,需要手动添加定时器到runloop中,且手动调用fire方法才能触发
(NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; // 通过target:selector方式创建,需要手动添加定时器到runloop中,且手动调用fire方法才能触发
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep; // 指定date时间后开始执行,需要手动添加定时器到runloop中
// 其他scheduledTimerWithTimeInterval可以在创建的时候就开启定时器,不需要手动添加到runloop,和手动调用fire。至于block回调方式,因为iOS10之后才支持的,所以不推荐
以上方式创建的定时器都需要手动添加到runloop中才能执行,且会对target产生强引用,会存在target内存泄漏问题。解决方案就是创建一个代理类作为target,代理类弱引用原来的target。
@interface YCProxy : NSObject
- (instancetype)proxyWithTarget:(id)target;
@property (nonatomic, weak) id target;
@end
@implementation YCProxy
- (instancetype)proxyWithTarget:(id)target {
YCProxy *proxy = [[YCProxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
// 使用
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:[YCProxy proxyWithTarget:self] selector:@selector(test) userInfo:nil repeats:YES];
此方法可以解决强引用self,导致self无法释放的问题,但是需要注意在self释放的地方手动调用定时器的invalidate方法,否则因为定时强引用YCProxy对象,prxoy对象弱引用self,self释放的时候proxy的target为空,forwardingTargetForSelector方法放回nil,会继续向下执行方法转发,最终报“unrecognized selector sent to instance”。想要不报错,可以自行在Proxy对象中hook相关方法,做防crash处理,至于要不要做根据个人需求。
代理对象的另一种实现方式就是继承NSProxy,实现如下两个方法。NSPrxoy是与NSObject同一级别的一个基类。使用NSPxoxy的好处是,不会执行NSObject那一套消息查找机制,而是直接调用如下两个方法,效率相对来说更高。
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
CADisplayLink
/** Class representing a timer bound to the display vsync. **/
CADisplayLink是根据屏幕刷新率来做定时操作的,详细可以参考头文件。其也依赖runloop, 且也存在对target强引用的问题。不过CADisplayLink更多是用于一些动画、刷新率的计算。此处不再赘述。
GCD定时器
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); dispatch_source_set_event_handler(timer, ^{
NSLog(@"time call back");
});
dispatch_resume(timer);
self.timer = timer;
GCD定时基于系统内核,定时精度相对上面两种方式会更准确。且GCD定时器不存在对target强引用问题,不需要特别在释放的地方取消定时器。只是需要注意“self.timer = timer”,需要保住定时器的命,否则定时不会执行。并且dispatch_timer使用时有类似dispatch_once的代码块提示,使用起来可以说很方便,强烈推荐。
总结
综合上述情况,NSTimer、CADisplayLink都存在target强引用问题,且需要注意手动释放定时器。而GCD定时器完全没有这方面的问题。