NSTimer 和 CADisplayLink 详解以及如何做一个测试屏幕FPS的小工具

昨天看到一篇文章,讲述的是 NSTimer 和 CADisplayLink 这两个定时器的区别。刚好有感而发,突然想到了测试屏幕FPS的小工具实现思路。

我们知道 NSTimer 和 CADisplayLink 都有定时的功效。区别在于 NSTimer 基于时间,而 CADisplayLink 基于帧率。我们设置 NSTimer 定时器的间隔为1/60秒执行任务的时候,实际效果就和 CADisplayLink 类似,因为 CADisplayLink 基于帧率执行,而iOS手机的刷新频率就是一秒60次,相当于每1/60秒刷新一次(默认),不管是NSTimer 还是 CADisplayLink,对于界面的刷新,他们的最小的执行间隔时间都不会小于 1/60 秒,因为屏幕刷新没那么快,就算小于这个时间,也是徒劳。

按照这个逻辑,我们想要实现每1/60秒做一次动作的话,使用 NSTimer 和 CADisplayLink 都可以实现。但是 NSTimer 的 timeInterval 属性是一个浮点数,我们可以随意调整想要的时间间隔,而 CADisplayLink 只能基于帧率执行动作,尽管它有一个名为 frameInterval 的整型属性(默认值为1,代表1帧刷新一次,修改为2则为2帧刷新一次,也就是1/30秒刷新一次)可以用来更改执行的频率,但是能更改的数值依然有限。但是 CADisplayLink 有它适合的使用场景,今天要介绍的测试FPS小工具就是其一。

NETimer的使用

如果要使用自定义数值的定时器,最好使用 NSTimer,对于重复的行为,它能让我们自定义任意的时间间隔 。就像这样:

{
  _timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
}

- (void)timerAction{
    NSLog(@"\n定时器  ---   %f",CACurrentMediaTime());
}
NSTimer执行在Runloop中

但是使用 NSTimer 并非没有缺陷。在程序主线程的 RunLoop 中,会处理一下事情:

  • 处理触摸事件
  • 发送和接受网络数据包
  • 执行使用GCD代码
  • 处理定时器执行行为
  • 屏幕重绘

这些时间被添加到runloop中之后,统统被称之为 dataSource 或 timeSource(也就是事件源) 。NSTimer 属于 timeSource ,因此,每次创建一个定时器,需要将之加入到对应线程中的runloop才能够被执行。这样就是为什么上述代码会有一段这个代码:

    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

NSRunLoop 有很多个mode,我们默认使用的是 NSDefaultRunLoopMode,除此之外,还有 NSRunLoopCommonModes 以及 UITrackingRunLoopMode
等mode,这两个mode分别用于不同的场景。我们有遇到过同一个界面中的NSTimer和UIScrollView不会同时执行,每当我们滑动UIScrollView的时候,NSTimer 就会暂停,原因是RunLoop的mode有优先级,当滑动UIScrollView,Runloop 切换为 UITrackingRunLoopMode,这时候,NSTimer 所处的NSDefaultRunLoopMode 被搁置,NSTimer也就暂停了,解决办法是将 NSTimer 加入到 NSRunLoopCommonModes 或者 UITrackingRunLoopMode中。

重复执行的NSTimer导致内存泄漏

NSTimer 加入到Runloop之后被执行的原理是,Runloop 中有一个和NSTimer可 toll-free-bridge 的类 —— CFRunLoopTimerRef,它直接和NSTimer混用,其中包含了一个定时器和回调函数。并持有NSTimer的target,以备后续调用。对于非重复的NStimer,执行完一次回调函数之后,会直接释放NSTimer,这样NSTimer也就不会持有targat,当target释放的时候,NSTimer 跟随释放,我们不需要做处理。但是,对于重复执行的NSTimer,NSTimer的target一直得不到释放,这样就会造成引用循环,导致内存泄漏。解决办法是在Targeth(UIView/UIViewController)消失之前,使用NSTimer的 invildata,将NSTimer对象停止,以解除引用关系。

NSTimer的使用弊端

NSTimer能够间隔时间重复执行某个函数,但是,它的时候的京都并不能保证。我们在一些老设备上,经常会看到NSTimer定时器的延迟和条数。比如我创建了一个每两秒执行一次的定时器,但是它不会精确在每两秒,它可能延后执行,这种情形给人一种跳动的感觉,在一般的数据处理还好。但是在动画的场景里,会显得动画极不流畅。

这个原因是什么呢?

我们将NSTimer放入Runloop的之后,如果runloop中只有一个该定时器的事件源,那么这个定时器将会精准的执行,可是显然,runloop要做的工作并不少,它内部还有其他各式的事件源,这些事件源有的是触摸事件,有的是数据发送事件,还有其他的定时器,它们都会经过一个有序的顺序执行, 如果某个在定时器之前的事件执行需要花费很长的时间,那么就会导致后面的定时器不能准时执行。这就造成了定时器执行时间的跳动问题。 这个跳动不会引起很大的问题,因为,在一个较大的时间内,定时器执行的次数是不会变的,套用一句话:定时器的执行可能迟到,但是不会不来。

从这里看出,NSTimer不适合做合成动画有关的定时器,因为它的执行时间跳动将给动画带来卡顿的感觉。

首先我们要明白,屏幕刷新的频率在每秒60次,也就是说,系统在每一次绘制之前都有 1/60 的时间去计算即将绘制的内容,然后渲染到视图上,如果没有及时绘制完成,那么就会等到下一次绘制的时间点上将图像铺上。 一般简单的绘制,会在这个时间内完成, 我们能看到流畅的画面,如果绘制的图像过于复杂,那就可能需要超过1/60秒的时间进行绘制,这时候会出现下一帧的画面显示的还是上一个时间点的绘制内容,也就是出现画面暂停,在我们肉眼看来就是卡顿了。
定时器的执行时间跳动会让1/60秒执行的内容出现漏洞,它很容易因为时间跳动错过了一次系统的刷新,这个跟图像渲染无关,跟执行的时间点有关,即使我们精确定义定时器每 1/60 秒执行一次也因为执行时间跳动而错过帧率。

CADisplayLink的使用

上面有提到,NSTimer并不适合做动画有关的计时器。而CADisplayLink几乎是为这个而生的。

CADisplayLink 的执行间隔不是具体的时间,而是在每一帧执行前会调用它的执行。如果屏幕的帧率为60的话,也就是每1/60秒执行一次。这样避免了一个问题,那就是如果因为图像的绘制耗时太久,错过了某个时间点的渲染,但是没关系,CADisplayLink 不会因此而乱掉,我们可以设置。

当然,不管是 CADisplayLink 和 NSTimer,都不能完全避免帧率不均导致的屏幕卡顿或者变慢。 我们要更加流畅的显示画面的时候,最好还是手动计算每一帧之间的时间间隔,然后在每帧被渲染之前调用执行。这个思路有人已经实现,这里就不详细说了。

使用CADisplayLink的帧前调用特性制作一个FPS测试

CADisplayLink 的特性是每一帧画面之前调用,利用这个特性我们可以实现一个简单的FPS测试工具。基本原理很简单,统计每一秒中 CADisplayLink 的执行次数就可以了。正常的状态,这个值为60。如果出现画面卡顿的时候,这个值会降低。我们要制作的工具就是实时的显示当前的FPS值。代码如下:
.h 文件

#import <UIKit/UIKit.h>

@interface FPSWhatchDog : UIView

+ (void)showFPSLabel;
+ (void)dismissFPSLabel;

@end

.m 文件

#import "FPSWhatchDog.h"

@interface FPSWhatchDog ()

@end

@implementation FPSWhatchDog
{
    CADisplayLink *_dispalyLink;
    UILabel *_fpsLabel;
    NSInteger _fps;
    NSTimeInterval _lastTime;
}

+ (id)shareInstance{
    static FPSWhatchDog* tool = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        tool = [[FPSWhatchDog alloc] init];
    });
    return tool;
}

+ (void)showFPSLabel{
    FPSWhatchDog *dog = [FPSWhatchDog shareInstance];
    [dog showLabel];
    [dog openDispalyLink];
}

+ (void)dismissFPSLabel{
    FPSWhatchDog *dog = [FPSWhatchDog shareInstance];
    [dog invaildDisplayLink];
    [dog removeLabel];
}

- (void)showLabel{
    _fpsLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 80, 30)];
    _fpsLabel.text = @"60 FPS";
    _fpsLabel.textColor = [UIColor greenColor];
    _fpsLabel.backgroundColor = [UIColor darkGrayColor];
    _fpsLabel.layer.cornerRadius = 15.0;
    _fpsLabel.center = CGPointMake([UIScreen mainScreen].bounds.size.width/2.0, 85);
    _fpsLabel.textAlignment = NSTextAlignmentCenter;
    UIWindow *keyWindow = [[UIApplication sharedApplication] windows].firstObject;
    _fpsLabel.layer.zPosition = 100;  //浮在最上面
    [keyWindow addSubview:_fpsLabel];
}

- (void)openDispalyLink{
    _fps = 0;
    _lastTime = (NSTimeInterval)CACurrentMediaTime(); //获取当前APP开启时间
    _dispalyLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(countFPS)];
    [_dispalyLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}


- (void)invaildDisplayLink{
    [_dispalyLink invalidate];
}

- (void)removeLabel{
    [_fpsLabel removeFromSuperview];
}

- (void)countFPS{
    if (_lastTime + 0.5 <= (NSTimeInterval)CACurrentMediaTime()) {
        _lastTime = (NSTimeInterval)CACurrentMediaTime();
        _fpsLabel.text = [NSString stringWithFormat:@"%d FPS",(int)_fps * 2];
        _fps = 0;
    }else{
        _fps++;
    }
}

@end

整个类暴露的只有两个方法,一个是调用方法,另一个是移除方法。
下面是我在5S模拟器下测试的效果,测试内容是在tableView上的每一个Cell呈现的时候,更改cell 上的几张图片,然后滑动,发现卡顿现象。这时候显示的FPS值会反馈出来。


2018-03-31 17_17_47.gif

代码量很少,Demo不贴了,直接copy过去就行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容