前两天大佬随口问了句关于iOS中的定时器?我说一般就用GCD和NSTimer这两个吧。接着大佬就问有遇到失准的情况么?额...NSTimer用起来不当可能会不准!为什么?其他的呢?
关于iOS中的定时器常用如下:
- NSTimer
- GCD (dispatch_source_t 、 dispatch_after)
- NSObject - performSelector的delay方法
- CADisplayLink
如果说前三个都知道和使用过的话,那第四个真的有点陌生。好吧了解下还有模拟NSTimer不准的情况以及如何使它变准和怎么样使用定时器能更加精准。
CADisplayLink
//一个计时器对象,允许应用程序将其绘图与显示的刷新速度同步。
A timer object that allows your application to synchronize its drawing to the refresh rate of the display.
CADisplayLink和NSTimer的使用方式差不多,同样也是创建一个计时器的对象,绑定的target上的selector;但CADisplayLink更适合做界面的重绘,比如视频播放。它有几个使用的方法介绍一下
- timestamp :屏幕显示的上一帧的时间戳这个属性一般用来计算下一帧中应该显示的内容。
- frameInterval :每隔定义的帧调用(默认值是1)此属性在iOS 10.0中被弃用替代属性preferredFramesPerSecond
- duration :屏幕刷新更新之间的时间间隔 (readonly)
CADisplayLink *displayLink_;
displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateTime)];
[displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
// 每隔1帧调用一次(默认值)
[displayLink_ setPreferredFramesPerSecond:1];
//释放
[displayLink_ invalidate];
displayLink_ = nil;
NSTimer不准,为什么?怎么解决?
什么操作为什么会不准?因为和NSRunLoop的Mode有关,我们默认创建NSTimer的Mode是NSDefaultRunLoopMode,这个级别比较低而我们滚动tableView的时候Mode的级别,这样就导致NSDefaultRunLoopMode被挂起定时器而失准。我们只需设置下定时器的Model即可
- NSDefaultRunLoopMode -> 标准优先级
- NSRunLoopCommonModes -> 高优先级
- UITrackingRunLoopMode -> 用于UIScrollView/UITableView和其他的控件的滚动或动画
/* 处理非NSConnection对象的输入源的模式,这是最常用的运行循环模式 */
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode;
/* 使用此值作为模式添加到运行循环中的对象,被声明为“公共”模式集成员的所有运行循环模式监视 */
FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
解决
NSTimer *aTimer_;
[[NSRunLoop mainRunLoop] addTimer:aTimer_ forMode:NSRunLoopCommonModes];
如上说到CADisplayLink和NSTimer创建很像,所有CADisplayLink我们也需要手动改变其所建对象的Mode级别。
如上滚动操作,同样的代码5s定时,因为没有设置Mode导致了定时器的失准(多1秒延迟)
这边很多人觉得定时器的三种方法未把performSelector的delay方法算进去,这是因为performSelector-delay本质上就是对NSTimer进行封装而产生的方法
- 注意点performSelector是单线程方法也就是会按照顺序执行如下
[self performSelector:@selector(delayTime) withObject:nil afterDelay:1]; //单线程
NSLog(@"滴答");
sleep(2);
- (void)delayTime{
NSLog(@"滴答");
}
2018-07-18 20:04:29.237032+0800 CDDTimerDemo[62599:4875841] 滴答
2018-07-18 20:04:31.238052+0800 CDDTimerDemo[62599:4875841] 滴答
顺序 --> 滴答 - > sleep - > 滴答
GCD(dispatch_source_t 、 dispatch_after)
- dispatch_after
__weak typeof(self)weakSelf = self;
NSLog(@"滴答");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf delayTime];//方法调用
});
- dispatch_source_t
dispatch_source_t timer_;
NSLog(@"滴答");
__block float secondTime = 1.0; //设置每秒执行
__block float countDownTime = 10.0; //设置倒计时时间
timer_ = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(timer_,dispatch_walltime(NULL, 0),secondTime * NSEC_PER_SEC, 0); //每秒执行
__weak typeof(self)weakSelf = self;
dispatch_source_set_event_handler(timer_, ^{
if(countDownTime <= 0){ //倒计时结束,关闭
dispatch_source_cancel(timer_);
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf delayTime]; //方法调用
});
}else{
countDownTime = countDownTime - secondTime;
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"滴答 %f",countDownTime);
});
}
});
dispatch_resume(timer_);
就如上截图来看其实误差值很小很小几乎可以忽略不计
总结来说:在定时器的使用上如果很简易的延迟处理建议用GCD的dispatch_after建议方便。如果和界面的重绘有关、屏幕刷新(帧)、动画更新、视频播放有关的建议用CADisplayLink。定时时间内的循环事件操作建议用dispatch_source_t、或NSTimer。当然GCD在使用上更具健壮性。
Demo中顺便写了一个悬浮球支持透明度渐变、透明度的自定义、输出当前屏幕的位置点击事件(代理)、是否依附屏幕边角,和距离父视图的边距等属性设置
实现方式就是自定义View添加手势和代理。
/** 代理<监听当前屏幕的所在位置和点击事件> */
@property (nonatomic, weak) id delegate;
/* 是否关闭吸附边角 默认开启 */
@property (assign , nonatomic)BOOL isCloseAdsorption;
/* 是否开启定时渐变 默认关闭 */
@property (assign , nonatomic)BOOL isOpenGradient;
/* 自定义View的圆角 设置开启 不设置则关闭 */
@property (assign , nonatomic)CGFloat ballCorners;
/* 边距 */
@property (nonatomic , assign) UIEdgeInsets ballEdgeInset;
/* 自定义View渐变时间 */
@property (assign , nonatomic)CGFloat ballATimer;
/* 自定义View渐变alpha */
@property (assign , nonatomic)CGFloat ballAlpha;
#pragma mark - 打开和关闭
- (void)dcCloseFloatingBall;
- (void)dcOpenFloatingBall;
#pragma mark - 移除
- (void)dcRemoveFloatingBall;
CDDFloatingBall *floatBall = [[CDDFloatingBall alloc] initWithFrame:CGRectMake(self.view.frame.size.width - 55, 100, 55, 55)];
floatBall.ballCorners = 12;
floatBall.isOpenGradient = YES; //是否开启渐变
floatBall.backgroundColor = [UIColor lightGrayColor];
self.floatBall = floatBall;
floatBall.delegate = self;
// floatBall.ballEdgeInset = UIEdgeInsetsMake(64, 0, 0, 0); //边距
// floatBall.ballAlpha = 0.4; //透明度
// floatBall.isCloseAdsorption = YES; //是否依附边角
floatBall.ballATimer = 3.0; //透明渐变的时间
[[UIApplication sharedApplication].keyWindow addSubview:floatBall];