一、概述
在iOS开发中,我们会经常使用定时器,使用定时器可以完成每间隔一段时间,去执行我们所需要的任务。常见的定时器技术有下面三种,接下来我们依次介绍一下他们如何使用。
- NSTimer
- CADisplayLink
- GCD
二、NSTimer
2.1 假如有这么一个需求:
- 需求:应用启动后创建一个定时器,每隔1秒执行一次任务,点击控制器的View,停止定时器
- 我们这么来设计,Window的根控制器是UINavigationController,UINavigationController的根控制器放置一个UIViewController,UIViewController上放置一个UIButton,点击按钮跳转到一个我们执行任务的定时器,来开启定时器执行任务。
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建NSTimer对象
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(execute) userInfo:nil repeats:YES];
// 将timer添加到RunLoop启动定时器
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
// 点击控制器的View停止定时器
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.timer invalidate];
self.timer = nil;
}
// 执行的任务
- (void)execute {
NSLog(@"%s", __func__);
}
// 销毁时,清除timer
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s", __func__);
}
@end
- 上面的代码中的逻辑,进入控制器开启定时器,并执行任务,当点击控制器的view的时候,停止定时器,timer指向nil,点击返回按钮,控制器销毁,这样的逻辑没有任何问题,定时器可以停止,控制器也可以正常销毁。
- 但是!,如果作为用户,我没有点击控制器的view,而是直接点击返回按钮,你觉得定时器会停止吗?你可能会认为dealloc方法中不是写了,清除timer了吗?**但是!**你会发现,点击Back按钮,根本就没有走dealloc方法,所以可以断定控制器根本就没有销毁。timer还在快乐的运行着,而且控制器也没有销毁,产生了内存泄漏。
- 为什么呢?因为timer中的
target对控制器产生了强引用,而控制器对timer是强引用,timer对target是强引用。
2.2 解决方案
解决方案有2种:
- 方式一:使用block方式创建NSTimer
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self)weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf execute];
}];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- 方式二:创建一个代理类
LLProxy,继承自NSObject,创建一个弱引用的target,处理执行的任务
// LLProxy.h
@interface LLProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@end
// LLProxy.m
@interface LLProxy ()
@property (nonatomic, weak) id target;
@end
@implementation LLProxy
+ (instancetype)proxyWithTarget:(id)target {
LLProxy *proxy = [[LLProxy alloc] init];
proxy.target = target;
return proxy;
}
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LLProxy proxyWithTarget:self] selector:@selector(execute) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
-
代码写到这,直接运行,会报一个非常经典的错误
**-[LLProxy execute]: unrecognized selector sent to instance 0x600000474a60'** -
LLProxy类中找不到execute方法,我们可以把execute方法写到LLProxy类中,但是这不是我们想要的,我们还是希望execute方法在控制器中处理。这时候,我们就可以利用runtime的消息转发。
// LLProxy.m
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
三、CADisplayLink
- CADisplayLink和NSTimer类似,但是它是和屏幕的刷新率相关的。手机屏幕刷新,就会调用这个定时器。
- 下面的代码产生的问题和NSTimer是一样的,参考NSTimer即可
@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(execute)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.timer invalidate];
}
- (void)execute {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
}
- 因为
CADisplayLink是block方式创建的,所以想要解决上面的问题,只能使用代理类。参考NSTimer即可。
四、NSProxy
前面介绍了2种定时器可能遇到的问题和相应的解决方案。其实,苹果官方给我们提供一个类,专门来做代理的这么一个类NSProxy。
- 创建LLProxy类,继承NSProxy类
// LLProxy.h
@interface LLProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
- 这里注意创建LLProxy对象的方式,
LLProxy *proxy = [LLProxy alloc];
@interface LLProxy ()
@property (nonatomic, weak) id target;
@end
@implementation LLProxy
+ (instancetype)proxyWithTarget:(id)target {
LLProxy *proxy = [LLProxy alloc];
proxy.target = target;
return proxy;
}
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LLProxy proxyWithTarget:self] selector:@selector(execute) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.timer invalidate];
}
- (void)execute {
NSLog(@"%s", __func__);
}
- (void)dealloc {
[self.timer invalidate];
NSLog(@"%s", __func__);
}
- 上面的代码调用会直接崩溃,崩溃信息如下:意味着直接来到消息转发,省略了executef方法的查找过程,这样的话可以提高效率。
-[NSProxy methodSignatureForSelector:] called!'
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
NSProxy是专门用来做代理的,点进去看.h文件,你会发现他也是一个基类。
五、GCD创建定时器
无论使用NSTimer,还是使用CADisplayLink,本质都是添加到Runloop去处理定时器,这样就可能会产生一个问题,那就是Runloop很忙,所以没有及时处理定时器,就会导致,定时器时间不准确的问题,此时就可以使用GCD创建一个定时器,因为GCD是和内核相关的,无论从处理效率还是准确性都是比前2种好的。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"begin ----- ");
// timer 必须是强引用才能开启定时器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 延迟多久才开始执行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 0.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, 2.0 * NSEC_PER_SEC, 0);
// 这个位置必须使用弱引用,否则会产生循环引用
// block -> self -> timer -> block
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(self.timer, ^{
// 执行任务
[weakSelf execute];
});
// 开启定时器
dispatch_resume(self.timer);
}
- timer必须是强引用定时器才会生效
- dispatch_source_set_event_handler 方法中会产生循环引用,这里需要处理一下
实际开发中,建议使用GCD的 方式,来创建和处理定时器,符合奥运精神更高、更快、更强!GCD的方式,调用比较复杂不够面向对象,其实可以根据自己的方式封装一下了。我做了简单的封装,只提供.h文件,仅供参考,每个人的需求不太一样。
+ (void)timerWithTimeInterval:(NSTimeInterval)interval afterDelay:(NSTimeInterval)afterDelay repeate:(BOOL)repeat name:(NSString *)name queue:(dispatch_queue_t)queue block:(void(^)(void))block;
+ (void)cancelTimerWithName:(NSString *)name;