在使用定时器时,如果选择使用的NStimer
、CADisplayLink
来实现,比如:
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(test1) userInfo:nil repeats:YES];
self.displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(test2)];
这样写会存在的问题是NSTimer
或CADisplayLink
内部会强引用它的target
,导致循环引用。
不用其他定时器方案替换的解决方法是:使用自定义的NSProxy
子类对象作为target。这个是挺多第三方库(比如YYText、FLAnimatedImage)也采用的解决方式,这种方式不需要关注什么时候调用[timer invalidate].
另一个方法是需要在合适的时候(非dealloc方法里)进行调用[timer invalidate], 原因如下:
// 源码: 会对_target和_info做release
- (void) invalidate
{
_invalidated = YES;
if (_target != nil)
{
DESTROY(_target);
}
if (_info != nil)
{
DESTROY(_info);
}
}
一、NSProxy
类介绍
这个类是与基类NSObject
平级的基类,是专门用来处理消息转发的,相比NSObject对象的消息转发,这个类的效率更高,因为通过系统NSProxy源码分析可以知道,它只用两步来完成转发,省去了从父类中查找的过程,直接会进入消息转发。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector;
- (void)forwardInvocation:(NSInvocation *)invocation;
二、创建NSProxy
子类及调用代码
-
NSProxy
子类
@interface XXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation XXProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
MJProxy *proxy = [MJProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
NSMethodSignature *signature = [self.target methodSignatureForSelector:sel];
return signature ?: [NSObject methodSignatureForSelector:@selector(init)];
// 在signature =nil时传nsobject过去是为了报错正确
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
- NSTimer使用方法
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:[XXProxy proxyWithTarget:self]
selector:@selector(timerTest)
userInfo:nil
repeats:YES];
可以参考第三方库里面的NSProxy,但测试及源码查看之后还是这里的为准。
三、NSTimer和CADisplayLink、GCD创建子线程后添加timer三种比较
- NSTimer在使用时如果遇到了runloop执行了耗时的操作如下,会出现timer被延时触发的问题,所以timer是相对不精准的,适合于对时间精确度要求不那么高的情况(如轮播图)。
CFAbsoluteTime refTime = CFAbsoluteTimeGetCurrent();
NSTimer *timer = [NSTimer timerWithTimeInterval:5.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire %f",CFAbsoluteTimeGetCurrent() - refTime);
}];
timer.tolerance = 0.5;
[timer fire];// 立即触发一次
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"before busy %f", CFAbsoluteTimeGetCurrent() - refTime);
NSInteger j;
for (long i = 0; i< 1000000000; i++) {
j = i*3;
}
NSLog(@"after busy %f", CFAbsoluteTimeGetCurrent() - refTime);
});
打印如下:
2020-10-24 09:08:57.333 KVO[5366:421594] timer fire 0.000056
2020-10-24 09:09:01.334 KVO[5366:421594] before busy 4.001167
2020-10-24 09:09:03.503 KVO[5366:421594] after busy 6.170045
2020-10-24 09:09:03.503 KVO[5366:421594] timer fire 6.170465
2020-10-24 09:09:07.337 KVO[5366:421594] timer fire 10.004278
2020-10-24 09:09:12.339 KVO[5366:421594] timer fire 15.006128
可见timer的触发被延时到了after busy之后。
- CADisplayLink是跟屏幕刷新率相关的定时器,可以设置
preferredFramesPerSecond
属性每秒多少次触发调用selector, 在当前线程未执行耗时任务时是非常精准的;但如果当前线程的卡顿,会导致CADisplayLink卡顿。CADisplayLink适合用于跟屏幕刷新率相关的操作。
+ (void)beginDisplayLink{
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(test:)];
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
link.preferredFramesPerSecond = 2;// 1秒打印两次
}
+ (void)test:(CADisplayLink *)link{
NSLog(@"CADisplayLink触发-----");
// 2秒后加耗时任务卡顿线程
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSInteger j;
for (long i = 0; i< 1000000000; i++) {
j = i*3;
}
});
}
打印结果:
2020-10-24 10:17:48.167 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:48.652 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:49.153 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:49.652 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:50.153 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:52.527 KVO[5617:453444] CADisplayLink触发-----
由上面的时间戳可以看出,前5次打印来看是非常精准的,但加了耗时操作卡顿线程会导致selector触发延时。
- GCD创建子线程,在子线程中添加timer,这种情况是非常精准的,不会受其他线程卡顿的影响,适合于对时间精确度敏感的时候(比如发送短信验证码时).
+ (void)GCDSubThreadTimer{
__block NSTimeInterval timeOut = 5;
dispatch_source_t source_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(source_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(source_timer, ^{
if (timeOut <= 0) {
dispatch_source_cancel(source_timer);// 在不使用时手动关闭这个定时器事件源
}else{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"倒计时:%f", timeOut);
});
timeOut--;
}
});
dispatch_resume(source_timer);
}
打印:
2020-10-24 10:52:55.164 KVO[5787:472530] 倒计时:5.000000
2020-10-24 10:52:56.165 KVO[5787:472530] 倒计时:4.000000
2020-10-24 10:52:57.164 KVO[5787:472530] 倒计时:3.000000
2020-10-24 10:52:58.165 KVO[5787:472530] 倒计时:2.000000
2020-10-24 10:52:59.164 KVO[5787:472530] 倒计时:1.000000
网友评论