前言

最近在做一些性能优化的工作,在这个工作之前,除了fps以外其他的性能指标我并不了解,所以就有了这篇文章。

帧率(FPS)

在说fps之前,我们先要搞清楚一些概念。

概念

帧是什么?很简单,就是视频或者动画中一个画面,许多个帧组合起来就是视频或者动画。

帧数

这个名词很直观,帧数就是生成帧的数量。如果一秒有60帧,那么2秒就是120帧。

帧率

帧率是用于测量显示帧数的量度,也就是我们所说的fps,它的计算方式如下引用。

帧率(Frame rate) = 帧数(Frames)/时间(Time),单位为帧每秒(f/s, frames per second, fps)

说明

fps也可以称为刷新率,它的值越高代表画面越连贯,当它太低时我们肉眼能感觉到不连贯。但是很多时候我们不仅在乎连贯性,也在乎流畅性。

引用https://zhuanlan.zhihu.com/p/48674410的例子如下,两者都是5 fps,都是一秒5帧,但是由于前者的帧数不平滑,导致实际上流畅度也不如后者,就是因为帧生成时间不够平滑导致的。

比如举个例子

第一组:第一帧与第二帧间隔了0.3秒,第二帧与第三针间隔了0.1秒,第三针与第四帧间隔了0.2秒,第四帧与第五帧间隔了0.4秒。

第二组:每一帧都间隔0.25秒

只有两者都达到最好才能形成最好的视觉效果,但是fps这一指标无法衡量流畅性,因此我们需要引入丢帧。

丢帧(Dropped Frame)

概念

丢帧一般指由于硬件不足以负荷画面刷新的频率,从而导致帧率过低所造成的画面出现停滞现象。它也可以称为跳帧或者掉帧(Skipped Frames)。

在解释丢帧之前,先让我们来回顾一遍屏幕显示画面的原理。引用来自YY大佬的iOS 保持界面流畅的技巧。简单来说就是准备一帧需要一行一行绘制,每换一行发一个叫HSync的信号,绘制完之后发一个YSync信号让CPU提交计算好的内容到GPU上进行显示。

img_0

首先从过去的 CRT 显示器原理说起。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。

在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。

所以为什么会丢帧呢?由于垂直同步的机制(强制绘制刷新率与屏幕刷新率相符,锁定1秒60帧),如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变,这样就发生了丢帧。示意图如下,可以发现第二帧越到了第三帧的时间内,这样第三帧显示的还是第二帧的内容就丢了一帧。

img_1

应用

那说了这么多,丢帧要怎么计算呢?其实很简单,我们已知一帧的时间是1/60 s也就是16.67 ms,那么只要想办法获取到前一帧和后一帧的时间戳就可以计算出丢帧的帧数。这在iOS中是很容易做到的事情,我们只要使用CADisplayLink即可。

计算公式如下:

丢帧帧数 = (后一帧时间戳 - 前一帧时间戳) / 一帧的时间

衡量丢帧

在衡量丢帧上,业界有许多方法,比如https://segmentfault.com/a/1190000005089412?from=from_parent_docs这篇文章中(虽然写的是Android,但也具备参考价值),Bugly就运用丢帧来计算多种指标。而我打算以时间为维度,多个(丢帧的次数之合 / 上报次数)为指标进行查看,可视化查询示意图如下。由于1~2帧的丢帧肉眼感受不明显,所以只统计了丢失3帧及以上的次数。

img_2

Demo

使用CADisplayLink来计算丢帧并用字典存起来,key是丢帧的帧数,value是该丢帧帧数丢失的次数。具体代码如下,完整代码可查看XIGSmoothnessMonitor,它也包含fps的计算。

- (void)start {
    self.lastDroppedTimestamp = 0;
    self.droppedFrameStatisticsInfo = [NSMutableDictionary dictionary];
    if (!self.displayLink) {
        self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(callback:)];
        if (@available(iOS 10.0, *)) {
            self.displayLink.preferredFramesPerSecond = 60;
        } else {
            self.displayLink.frameInterval = 1;
        }
    }
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)callback:(CADisplayLink *)displayLink {
    if (self.lastDroppedTimestamp == 0) {
        self.lastDroppedTimestamp = displayLink.timestamp * 1000;
    }
    
    //计算丢帧

    //获取当前回调时间戳 单位是ms
    NSTimeInterval droppedTimestamp = displayLink.timestamp * 1000;
    //获取当前一帧的时间 单位是ms
    NSTimeInterval duration = displayLink.duration * 1000;
    //根据公示计算丢帧帧数
    NSInteger droppedFrame = (droppedTimestamp - self.lastDroppedTimestamp) / duration;
    self.lastDroppedTimestamp = droppedTimestamp;
    
    //丢帧大于等于3的才进行统计
    if (droppedFrame >= 3) {
        NSString *droppedFrameKey = [NSString stringWithFormat:@"%zd", droppedFrame];
        if (self.droppedFrameStatisticsInfo[droppedFrameKey]) {
            NSInteger count = [self.droppedFrameStatisticsInfo[droppedFrameKey] integerValue];
            self.droppedFrameStatisticsInfo[droppedFrameKey] = @(count + 1);
        } else {
            self.droppedFrameStatisticsInfo[droppedFrameKey] = @(1);
        }
    }
}

参考文献

https://www.leiue.com/2128

https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/

https://zhuanlan.zhihu.com/p/48674410

https://juejin.im/post/5ec35cc55188256d92438174

https://segmentfault.com/a/1190000005089412


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

教你使用swift写编译器玩具(8) 下一篇