扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
在开发项目的时候,需要一个计时器来做读秒操作。要求在页面切换的时候,重新进入页面仍然可以继续读秒。但是,当页面pop出来的时候,定时器会自动销毁掉,重新进入页面的时候已经无法继续进行读秒了。
十多年的扎赉诺尔网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。网络营销推广的优势是能够根据用户设备显示端的尺寸不同,自动调整扎赉诺尔建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。成都创新互联公司从事“扎赉诺尔网站设计”,“扎赉诺尔网站推广”以来,每个客户项目都认真落实执行。
iOS中常用的定时器有三种,分别是NSTime,CADisplayLink和GCD。其本质都是通过RunLoop来实现,但GCD通过其调度机制大大提高了性能。GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理。dispatch类似生产者消费者模式,通过监听系统内核对象,在生产者生产数据后自动通知相应的dispatch队列执行,后者充当消费者。通过系统级调用,更加精准。
//–––––––––––––––––––––单例.h––––––––––––––––––––––––
#import
@interface CaptchaTimerManager :NSObject
@property (nonatomic,assign)__blockint timeout;
+ (id)sharedTimerManager;
- (void)countDown;
@end
//–––––––––––––––––––––单例.m––––––––––––––––––––––––
#import"CaptchaTimerManager.h"
@implementation CaptchaTimerManager
+ (id)sharedTimerManager{
static CaptchaTimerManager *manager =nil;
staticdispatch_once_t onceToken;
dispatch_once(onceToken, ^{
if (manager ==nil) {
manager = [[selfalloc]init];
}
});
return manager;
}
- (void)countDown{
if (_timeout 0) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0,queue);
dispatch_source_set_timer(_timer,dispatch_walltime(NULL,0),1.0*NSEC_PER_SEC,0); //每秒执行
dispatch_source_set_event_handler(_timer, ^{
if(_timeout=0){//倒计时结束,关闭
dispatch_source_cancel(_timer);
}else{
_timeout--;
}
});
dispatch_resume(_timer);
}
}
@end
//–––––––––––––––––––––调用––––––––––––––––––––––––
#import "CaptchaTimerManager.h"
@property (weak,nonatomic) IBOutletUIButton *getNUmber;
@property (nonatomic,assign) int timeout;
- (IBAction)getNumberButton:(UIButton *)sender {
_getNUmber.enabled =NO;
_timeout =10; //倒计时时间
[selftimerCountDown];
}
-(void)viewWillAppear:(BOOL)animated{
CaptchaTimerManager *manager = [CaptchaTimerManagersharedTimerManager];
int temp = manager.timeout;
if (temp 0) {
_timeout= temp;//倒计时时间
_getNUmber.enabled =NO;
[selftimerCountDown];
}else{
_getNUmber.enabled =YES;
}
}
- (void)viewWillDisappear:(BOOL)animated{
[superviewWillDisappear:animated];
if (self.timeout 0) {
CaptchaTimerManager *manager = [CaptchaTimerManagersharedTimerManager];
if (manager.timeout ==0) {
manager.timeout =_timeout;
[manager countDown];
}
_timeout = 0;//置为0,释放controller
}
}
//控制器里的计时器
- (void)timerCountDown {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0,queue);
dispatch_source_set_timer(_timer,dispatch_walltime(NULL,0),1.0*NSEC_PER_SEC,0); //每秒执行
dispatch_source_set_event_handler(_timer, ^{
if(_timeout=0){//倒计时结束,关闭
dispatch_source_cancel(_timer);
dispatch_async(dispatch_get_main_queue(), ^{
//这里写倒计时结束button的处理
});
}else{
dispatch_async(dispatch_get_main_queue(), ^{
//这里写倒计时期间button的处理(重设button的tiitle、用户交互等)
if (_timeout==1) {
self.title =@"输入手机号";
_getNUmber.enabled =YES;
} else {
self.title = [NSStringstringWithFormat:@"%d",_timeout];
}
});
_timeout--;
}
});
dispatch_resume(_timer);
}
总结自下面两篇文章
iOS的几种定时器及区别
iOS 单例计时器(页面切换仍然计时)
demo地址 GitHub - littlePerson/SingletonTimer
欢迎指正批评!
《iOS Core Animation: Advanced Techniques》- 性能调优篇
当我们想开发一个基于时间流逝运动的动画时,首先会想到使用NSTimer计时器,但是这里不推荐使用这个类,我们看下NSTimer是怎么工作的。
iOS上每个线程都管理一个Runloop。对于主线程的Runloop,每一次循环都会做以下操作:
当设置了一个NSTimer计时器,这个任务会被插入任务队列中,但是它只会在上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。
我们可以通过一些途径来优化:
CADisplayLink和NSTimer的接口很相似,但是和NSTimer用秒作为及时单位不同,它使用属性 frameInterval 指定间隔多少帧后执行,用CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑。
但要知道即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散任务或者事件(例如资源紧张的后台程序,GPU渲染进程)可能会导致动画偶尔地丢帧。
添加到Runloop的任务都有一个指定优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,而且当UI很活跃的时候的确会暂停一些别的任务。
一个典型的例子就是当是用UIScrollview滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,所以标准的NSTimer和网络请求就不会启动。
我们可以同时加入NSDefaultRunLoopMode和UITrackingRunLoopMode来保证动画不会被滑动或者其他IO行为打断
动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程我们称它为渲染服务。在iOS5之前叫SpringBoard(同时管理着iOS的主屏),在iOS6之后叫做BackBoard。
Core Animation运行一段动画的过程:
CPU处理:
GPU处理:
所以我们真正能控制和优化的,只有在CPU处理布局和显示阶段,但是我们提交到IPC的渲染行为是可以被优化的,下面介绍CPU行为上的优化方法
视图布局计算会消耗掉部分时间,特别是使用AutoLayout。以60FPS作为一个iOS流畅度的黄金标准,那么将要求布局在0.0166667s内完成,而AutoLayout基于Cassowary算法会计算大量线性等式和不等式,下图(图片来自互联网)做了一个简单的布局对比,当视图数达到50个,AutoLayout将会出现性能瓶颈。Facebook的 yoga 框架允许你在iOS开发中使用FlexBox布局,同样来自Facebook的 AsyncDisplayKit 框架也引入了FlexBox优化布局的性能开销
懒加载只有在视图需要加载时才会去加载,这样的做法对内存占用和启动速度都要好处,但是在完成初始化操作前,你的动画都会被延迟。所以可以对动画必要视图进行优先初始化,而非傻傻的懒加载
当实现了视图中的 -drawRect: 方法,或者CALayerDelegate的 -drawLayer:inContext: 方法,就会在绘制前产生一个可估算的性能开销,CoreAnimation需要在内存中开辟一个等大小的寄宿图用于绘制,CoreGraphics绘制会十分缓慢,绘制结束还需通过通过IPC将图片数据上传到BackBoard,这也是为什么非不得已都不建议使用软件绘图,并且不要实现 -drawRect: 方法,尽管可能它是空方法。CoreAniamtion为图形绘制提供了专有图层,并提供了硬件加速,总体上都比Core Graphics更快,同时他们也避免了创造一个寄宿图
PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多,直接或间接使用UIImageView,或者将图片绘制到CoreGraphics都需要对图片解压缩,对于一个较大的图片,都会占用一定的时间。这一步虽然不可避免,但是我们可以把这个操作放到后台线程,先把图片绘制到CGBitmapContext中,然后从Bitmap直接创建图片。
主流的网络图片库都用了这样的方式,我们来看下SDWebImage的网络Res解析类,在获取到网络资源后,直接在子线程对图片绘制解码:
之前我们说过,CoreGraphic绘图是有较大性能开销的,那么如果一定要使用软件绘图,那么我们在封装的时候,可以提供同步和异步的绘制方法,非常幸运,CoreGraphic提供的方法都是线程安全的,例如提供一个绘制色块图片的方法
有时候要用CAShapeLayer并不能完全代替CoreGraphics,比如创建一个绘图应用时。当我们绘制的轨迹越复杂,绘制的越多,就会越卡顿,帧数将会下降。这是由于每次移动手指绘制时,都会重绘之前的轨迹,即使场景大部分都没有改变
为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作「脏区域」。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是「脏矩形」,如果你可以高效确定指定系统需要重绘的脏矩形位置,那么可以调用 -setNeedsDisplayInRect: 来避免不必要的绘制而非调用 -setNeedsDisplay 。
这里有一个例子,例如当我们创建了一个画笔,触碰屏幕则会将画笔size的矩形绘制到图层上,由于我们明确知道画笔的尺寸,那么在用户绘制时每次拖拽所产生的「脏矩形」我们都是可以准确计算的,然后告诉GPU我们只需要重绘画笔矩形而非重绘整个画布
GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出一个图层是否被遮挡也是相当复杂并且会消耗处理器资源。同样,重叠多个透明视图(图层)消耗的资源也是相当客观的。所以为了加速处理进程,不到必须时刻不要使用透明图层。任何情况下,你应该这样做:
这样做可以使计算过程加速,在CPU处理阶段,Core Animation就可以处理好并抛弃那些完全被遮盖的图层
当图层被指定为在未预合成之前不能直接在屏幕中绘制时,离屏渲染会被唤醒,这意味着图层必须在被显示之前在一个屏幕外上下文中被渲染,而对于GPU来说,这样的操作对性能是有较大损耗的。
会产生离屏渲染的操作:
例如当一个列表视图中出现大量圆角视图快速滑动时,可以观察到GPU资源已经占满,而CPU资源消耗很少。这是由于CPU已经计算完所有图层信息提交IPC,而GPU负担了大量的离屏渲染任务
优化的方案首先是避免圆角和 maskToBounds 一起使用,非必须不适用图层蒙版,若无法避免那么就将性能开销转嫁给CPU
这里有几种处理方式,一种是对于需要切圆角的图片,不要使用 CALayer.corner 在图层上裁剪,而是在获取到图片资源后在子线程提交前再进行一次对图片裁切的异步绘制;第二种使用图层栅格化 CALayer.shouldRasterize 转化为位图
是否取消,要看你自己的需求!
调用一次计时器方法:
myTimer = [NSTimer scheduledTimerWithTimeInterval:1.5 target:self selector:@selector(scrollTimer) userInfo:nil repeats:NO];
//不重复,只调用一次。timer运行一次就会自动停止运行
重复调用计时器方法:
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(function:) userInfo:nil repeats:YES];
//每1秒运行一次function方法。
注意:将计数器的repeats设置为YES的时候,self的引用计数会加1。因此可能会导致self(即viewController)不能release,所以,必须在viewWillAppear的时候,将计数器timer停止,否则可能会导致内存泄露。
停止timer的运行,但这个是永久的停止:(注意:停止后,一定要将timer赋空,否则还是没有释放。不信?你自己试试~)
//取消定时器
[timer invalidate];
timer = nil;
要想实现:先停止,然后再某种情况下再次开启运行timer,可以使用下面的方法:
首先关闭定时器不能使用上面的方法,应该使用下面的方法:
//关闭定时器
[myTimer setFireDate:[NSDate distantFuture]];
然后就可以使用下面的方法再此开启这个timer了:
//开启定时器
[myTimer setFireDate:[NSDate distantPast]];
例子:比如,在页面消失的时候关闭定时器,然后等页面再次打开的时候,又开启定时器。
(主要是为了防止它在后台运行,暂用CPU)可以使用下面的代码实现:
//页面将要进入前台,开启定时器
-(void)viewWillAppear:(BOOL)animated
{
//开启定时器
[scrollView.myTimer setFireDate:[NSDate distantPast]];
}
//页面消失,进入后台不显示该页面,关闭定时器
-(void)viewDidDisappear:(BOOL)animated
{
//关闭定时器
[scrollView.myTimer setFireDate:[NSDate distantFuture]];
}
一、NSTimer
1.创建方法
TimerInterval : 执行之前等待的时间。比如设置成1.0,就代表1秒后执行方法
target : 需要执行方法的对象。
selector : 需要执行的方法
repeats : 是否需要循环
2.释放方法
3.特点
存在延迟:不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。
必须加入Runloop:使用上面的创建方式,会自动把timer加入MainRunloop的NSDefaultRunLoopMode中。如果使用以下方式创建定时器,就必须手动加入Runloop:
二、CADisplayLink
什么是CADisplayLink
CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink 对象,把它添加到一个runloop中,并给它提供一个 target 和selector 在屏幕刷新的时候调用。
一但 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的selector,这时target可以读到 CADisplayLink 的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小等等。在添加进runloop的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一下,我们在动画的过程中,runloop被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行CADisplayLink的调用,从而造成动画过程的卡顿,使动画不流畅。
duration属性提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。我们可以使用这个时间来计算出下一帧要显示的UI的数值。但是 duration只是个大概的时间,如果CPU忙于其它计算,就没法保证以相同的频率执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。frameInterval属性是可读可写的NSInteger型值,标识间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。如果每帧都调用一次的话,对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval 设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。我们通过pause属性开控制CADisplayLink的运行。当我们想结束一个CADisplayLink的时候,应该调用-(void)invalidate从runloop中删除并删除之前绑定的 targetselector
另外CADisplayLink 不能被继承。
1.创建方法
2.停止方法
当把CADisplayLink对象add到runloop中后,selector就能被周期性调用,类似于重复的NSTimer被启动了;执行invalidate操作时,CADisplayLink对象就会从runloop中移除,selector调用也随即停止,类似于NSTimer的invalidate方法。
3.特点:
屏幕刷新时调用:CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。CADisplayLink以特定模式注册到runloop后,每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。所以通常情况下,按照iOS设备屏幕的刷新率60次/秒 延迟:iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。但如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会。 如果CPU过于繁忙,无法保证屏幕60次/秒的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。 使用场景:从原理上可以看出,CADisplayLink适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。4. 重要属性 frameInterval NSInteger类型的值,用来设置间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。 duration readOnly的CFTimeInterval值,表示两次屏幕刷新之间的时间间隔。需要注意的是,该属性在target的selector被首次调用以后才会被赋值。selector的调用间隔时间计算方式是:调用间隔时间 = duration × frameInterval。
三、GCD
1.执行一次
swift版本:
DispatchSourceTimer
间隔定时器, 相当于repeats设置为true的Timer.
初始化
设置timer参数
示例: 获取验证码60s倒计时
注意事项: 下面两种操作会造成程序崩溃, 原因是: gcdTimer执行了suspend()操作后, 是不可以被直接释放的, 如果想关闭一个执行了suspend()操作的计时器, 需要先执行resume(), 再执行cancel(), 最后置nil.
CADisplayLink 与 NSTimer 有什么不同?
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。
CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。给非UI对象添加动画效果我们知道动画效果就是一个属性的线性变化,比如UIView 动画的 EasyInEasyOut 。通过数值按照不同速率的变化我们能生成更接近真实世界的动画效果。我们也可以利用这个特性来使一些其他属性按照我们期望的曲线变化。比如当播放视频时关掉视频的声音我可以通过CADisplayLink来实现一个 EasyOut的渐出效果:先快速的降低音量,在慢慢的渐变到静音。
注意
通常来讲:iOS设备的刷新频率事60HZ也就是每秒60次。那么每一次刷新的时间就是1/60秒 大概16.7毫秒。当我们的frameInterval值为1
的时候我们需要保证的是 CADisplayLink调用的 target 的函数计算时间不应该大于 16.7否则就会出现严重的丢帧现象。在mac应用中我们使用的不是CADisplayLink而是 CVDisplayLink它是基于C接口的用起来配置有些麻烦但是用起来还是很简单的。
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(downLoadDicomFile) userInfo:nil repeats:YES];
看看你的interval设置的值是多少?这个代表多长时间调用一次selector。
创建一个计时器就行了。
例:
验证60秒
int timeTick;
NSTimer *timer;
timeTick = 61;//60秒倒计时
timer=[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeFireMethod) userInfo:nil repeats:YES];
_but.enabled = NO;
-(void)timeFireMethod
{
timeTick--;
if(timeTick==0){
[timer invalidate];
_but.enabled = YES;
[_but setTitle:@"获取验证码" forState:UIControlStateNormal];
}else
{
NSString *str = [NSString stringWithFormat:@"%d秒",timeTick];
[_but setTitle:str forState:UIControlStateNormal];
}
}
上面代码就是实现了一个倒计时60秒的功能。
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流