NSTimer
NSTimer是iOS中最常用的定时器。其通过Runloop来实现,一般情况下比较准确。但是当前循环耗时操作较多时,会出现延迟问题。同时,也受所加入的RunLoop的RunLoopMode影响。
使用:
selector方式
/// 构造并开启(启动NSTimer本质上是将其加入RunLoop中)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
/// 构造但不开启
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
上面两个方法的使用:
//方法1
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
//方法2
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
block方式
/// 构造并开启(启动NSTimer本质上是将其加入RunLoop中)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
/// 构造但不开启
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
上面两个方法的使用:
//方法1
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%s block ",__func__);
}];
//方法2
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%s block ",__func__);
}];
[[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSDefaultRunLoopMode];
停止
//定时器的释放一定要先将其终止,而后才能销毁对象
- (void)invalidate;
//立即执行(fire)
//我们对定时器设置了延时之后,有时需要让它立刻执行,可以使用fire方法:
- (void)fire;
CADisplayLink
CADisplayLink是基于屏幕刷新的周期,所以其一般很准时,每秒刷新60次。其本质也是通过RunLoop,所以不难看出,当RunLoop选择其他模式或被耗时操作过多时,仍旧会造成延迟。
其使用步骤为 创建CADisplayLink->添加至RunLoop中->终止->销毁。代码如下
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
- (void)invalidate;
使用:
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerTest)];
self.timer2.preferredFramesPerSecond = 1; //每秒1次
[self.timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
同时,由于其是基于屏幕刷新的,所以也度量单位是每帧,其提供了根据屏幕刷新来设置间隔的frameInterval属性,其决定于屏幕刷新多少帧时调用一次该方法,默认为1,即1/60秒调用一次。
在日常开发中,适当使用CADisplayLink甚至有优化作用。比如对于需要动态计算进度的进度条,由于起进度反馈主要是为了UI更新,那么当计算进度的频率超过帧数时,就造成了很多无谓的计算。如果将计算进度的方法绑定到CADisplayLink上来调用,则只在每次屏幕刷新时计算进度,优化了性能。MBProcessHUD则是利用了这一特性.
存在的问题
使用NSTimer和CADisplayLink,如果开发者不小心,可能会定时器无法销毁,导致内存泄漏问题。
实例代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)timerTest
{
NSLog(@"%s ",__func__);
}
- (void)dealloc
{
[self.timer invalidate];
NSLog(@"%s ",__func__);
}
会发现,当Controller关闭后,定时器还是持续打印,这就是定时器并没有及时的销毁。
出现这种情况得还有:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
self.timer2 = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerTest)];
self.timer2.preferredFramesPerSecond = 1;
[self.timer2 addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
这里要说明一下,采用block方式的定时器,会在Controller关闭后,自动销毁.
原因何在?
timer的target强引用self,同时self又强引用着timer,两者互相引用,造成了循环引用
解决方法:
第一种:
采用block方式
第二种:
自定义一个中间类充当target,并且这个类弱引用着self,即可
这个中间类就是NSProxy
NSProxy是一个抽象类,必须继承实例化其子类才能使用。
NSProxy效率较高,有方法就直接调用,若没有此方法会直接进入消息转发阶段,没有缓存查找,消息发送,动态方法解析
什么是NSProxy:
- NSProxy是一个抽象的基类,是根类,与NSObject类似
- NSProxy和NSObject都实现了协议
- 提供了消息转发的通用接口
下面我们就自定义类MyProxy继承与MyProxy
@interface MyProxy : MyProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation MyProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
MyProxy *proxy = [MyProxy alloc];
proxy.target = target;
return proxy;
}
//借用消息转发 使得原来类还能调用自己的sel
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
现在我们再使用selector方式创建定时器时:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MyProxy proxyWithTarget:self] selector:@selector(doTask) userInfo:nil repeats:YES];
运行就会发现,当Controller关闭时,timer也会被销毁,不会造成循环引用。
同样,CADisplayLink也是这样。
当然开发中还有一种方式,也是借助中间类类解决循环引用问题,代码如下:
.h
@interface NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
target:(**id**)aTarget
selector:(**SEL**)aSelector
userInfo:(**id**)userInfo
repeats:(**BOOL**)repeats;
@end
.m
@interface TimerWeakObject : NSObject
///中间对象的弱引用指针
@property (nonatomic,weak) id target;
///定时器到时之后的一个回调方法
@property (nonatomic,assign) SEL selector;
///中间对象的弱引用指针
@property (nonatomic,weak) NSTimer *timer;
- (void)fire:(NSTimer *)timer;
@end
@implementation TimerWeakObject
/*对它所持有的target进行判断,若target存在,判断它是否响应选择器,
如果响应则执行对应的回调方法
否则就把timer置为无效,就可以达到Runloop对Timer强引用的释放,同时Timer也会对弱引用对象进行释放
*/
- (void)fire:(NSTimer *)timer
{
if (self.target) {
if ([self.target respondsToSelector:self.selector]) {
[self.target performSelector:self.selector withObject:timer.userInfo];
}
}else{
[self.timer invalidate];
}
}
- (void)dealloc
{
NSLog(@"TimerWeakObject dealloc...");
}
@end
@implementation NSTimer (WeakTimer)
/*
创建中间对象,把我们传进分类中的aTarget和aSelector指派给中间对象,
然后调用系统的NSTimer方法去创建NSTimer,
同时指定Timer的回调事件是中间对象的fire方法,
然后再fire方法中再对实际对象回调方法进行调用
*/
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats{
TimerWeakObject *object = [[TimerWeakObject alloc] init];
object.target = aTarget;
object.selector = aSelector;
object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
return object.timer;
}
@end
使用定时器要这么复杂(借助其他类),难道就没有更简单一点了吗?
答案肯定是有的,那就是使用gcd的方式创建定时器,而且gcd的定时器不依赖与NSRunLoop,原理上来说会更加准确(在当前循环耗时操作较多时或者页面有点卡顿的时候)
gcd
GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理,通过系统级调用,更加精准
//创建定时器对象 gcd可以指定队列:即可以在主线程上也可以在子线程上完成任务
self.timer3 = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
/// 参数: 1 定时器 2 任务开始时间 3任务的间隔 4可接受的误差时间,设置0即不允许出现误差
dispatch_source_set_timer(self.timer3, DISPATCH_TIME_NOW, 1.0*NSEC_PER_SEC, 0.0*NSEC_PER_SEC);
//设置定时器任务
dispatch_source_set_event_handler(self.timer3, ^{
NSLog(@"%s ",__func__);
});
// 启动任务,GCD计时器创建后需要手动启动
dispatch_resume(_gcdTimer);
gcd还有一种设置定时器任务的方式
dispatch_source_set_event_handler_f(self.timer3, gcdtimerTest);
不过设置这种方式,销毁的时候要在dealloc方法中调用:
dispatch_suspend(self.timer3);
封装定时器
这三种定时器中最准确的还是GCD,但是在使用的时候,会写很多代码,所以简单的封装一下。
(NSTimer受runloop影响,每圈的运行时间不确定,所以NSTimer不够精准)
关键点:
1、用一个字典存储每一个定时器,在取消的时候,根据定时器的
key找到相应的定时器2、多线程会造成线程不安全,对字典读写操作的时候需要
加锁
@interface MBTimer : NSObject
+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (void)cancelTask:(NSString *)name;
@end
@implementation MBTimer
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore_ = dispatch_semaphore_create(1);
});
}
+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
// 队列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置时间
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定时器的唯一标识
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore_);
// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重复的任务
[self cancelTask:name];
}
});
// 启动定时器
dispatch_resume(timer);
return name;
}
+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!target || !selector) return nil;
return [self execTask:^{
if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
} start:start interval:interval repeats:repeats async:async];
}
+ (void)cancelTask:(NSString *)name
{
if (name.length == 0) return;
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}
dispatch_semaphore_signal(semaphore_);
}
@end
使用:
@property (copy, nonatomic) NSString *task;
//block方式:
// self.task = [MBTimer execTask:^{
// NSLog(@"%s ",__func__);
// } start:0.5 interval:1.0 repeats:YES async:NO];
//selector方式
self.task = [MBTimer execTask:self selector:@selector(timerTest) start:0.5 interval:1.0 repeats:YES async:NO];
//取消任务
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[MBTimer cancelTask:self.task];
}
以上若有错误,欢迎指正。转载请注明出处。