在日常的开发中,定时器的使用是不可或缺的,在iOS中主要使用NSTimer
,CADisplayLink
以及dispatch_source_t
来实现定时器,那么我们该如何以正确的姿态来使用我们的定时器呢?
NSTimer, CADisplayLink
问题:定时器无法释放
在导航控制器中创建定时器,每秒执行一次.
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *link;
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) dispatch_source_t dispatchTimer;
@end
- (void)viewDidLoad {
[super viewDidLoad];
// NSTimer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// CADisplayLink
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayAction)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)timerAction{
NSLog(@"timerAction->%s",__func__);
}
- (void)displayAction{
NSLog(@"displayAction->%s",__func__);
}
- (void)dealloc{
NSLog(@"已销毁");
if (self.timer != nil) {
[self.timer invalidate];
self.timer = nil;
}
}
现象:
当点击导航栏左上角的Home按钮后,定时器依然在疯狂的输出,并且控制器中的dealloc
方法并未执行.
原因:
出现这样的现象原因是在我们初始化以上两种定时器时需要传入target
一个id
类型的参数.在NSTimer
,CADisplayLink
内部如果有一个strong
类型的属性接收这个参数就会出现强引用的现象,那么就会导致Timer强引用着控制器,控制器强引用Timer导致循环引用的现象.
解决方案:
- 方案一 适用于
NSTimer
iOS提供了除了上述的创建方法外还提供了block
的初始化方式.
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self)WeakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%s",__func__);
[WeakSelf timerAction];
}];
}
- (void)timerAction{
NSLog(@"timerAction->%s",__func__);
}
- (void)dealloc{
NSLog(@"已销毁");
if (self.timer != nil) {
[self.timer invalidate];
self.timer = nil;
}
}
这个时候当你点击导航栏上左侧的Home初你会看到定时器停止了,dealloc
也被执行了
- 方案二 适用于
NSTimer
,CADisplayLink
,创建一个中间的代理对象来解决这个问题.
问题的关键是NSTimer
,CADisplayLink
,内部用了强引用的指针指向了我们传入的对象,由于苹果是不开源的我们无法修改,所以我们可以创建一个中间的代理对象来解决这个尴尬的处境.
@interface MLProxy ()
@property (nonatomic, weak) id target;
@end
@implementation MLProxy
+ (instancetype)createProxyWeithTarget:(id)target{
MLProxy *proxy = [self alloc];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.proxy = [MLProxy createProxyWeithTarget:self];
// NSTimer
// 由于iOS的消息机制(objc_msgSend()), 系统会对self.proxy 发送一个scheduledTimerWithTimeInterval的消息由于MLProxyb并没有实现该方法,就会执行Runtime的消息转发机制。
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}
这时在尝试一下,可以看到一切正常了,原理:中间代理的出现,就不会出现循环引用的现象了,只要控制器一旦销毁,所有的引用都会解除,所有的对象也都会被释放。
注意点:
NSTimer
,CADisplayLink
,在底层是运用的RunLoop
(运行循环)机制实现的.
当在RunLoop处理很多耗时事务时可能会导致定时器的不准确。
- 假设定时器每秒执行一次。
- 假设RunLoop是每毫秒执行完一个运行循环。
在每进行下一次循环时都会查看当前定时器执行到了多少时间。
当时间正好执行到1秒时,此刻在RunLoop的这次循环中突然事务很多有可能到1.1秒才执行到定时器,那么就是导致定时器迟了0.1秒才执行,那么定时器不准确的现象就会出现了.(以上假设有待验证,大神勿喷)。
问题:综上所述,我们在日常开发中到底有没有更准确的、方便的、注意点少的方法来使用我们的定时器呢?
答案:有的,那就是使用我们GCD中的dispatch_source_t
.原因是GCD中的dispatch_source_t
定时器不依赖RunLoop。
dispatch_source_t
- (void)viewDidLoad {
[super viewDidLoad];
self.dispatchTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 从此刻开始,每一秒执行一次
dispatch_source_set_timer(self.dispatchTimer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.dispatchTimer, ^{
NSLog(@"%s",__func__);
});
dispatch_resume(self.dispatchTimer);
}
突然看到这一堆代码,好像也不是那么容易,但是现在的XCode很智能,只要你敲下dispatch_source timer
就会看到下图展示,直接给你生成上述一堆代码。
接下来就可以愉快的使用定时器了.