iOS定时器的常规实现分析

一起养成写作习惯!这是我参与「掘金日新计划 · 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定时器完全没有这方面的问题。

猜你喜欢

转载自juejin.im/post/7086358330175651876
今日推荐