android帧的绘制过程以及fps的获取

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android帧的绘制过程以及fps的获取相关的知识,希望对你有一定的参考价值。

参考技术A 帧的渲染过程中一些关键组件的流程图

任何可以产生图形信息的组件都统称为图像的生产者,比如OpenGL ES, Canvas 2D, 和 媒体解码器等。

SurfaceFlinger是最常见的图像消费者,Window Manager将图形信息收集起来提供给SurfaceFlinger,SurfaceFlinger接受后经过合成再把图形信息传递给显示器。同时,SurfaceFlinger也是唯一一个能够改变显示器内容的服务。SurfaceFlinger使用OpenGL和Hardware Composer来生成surface.

某些OpenGL ES 应用同样也能够充当图像消费者,比如相机可以直接使用相机的预览界面图像流,一些非GL应用也可以是消费者,比如ImageReader 类。

Window Manager是一个用于控制window的系统服务,包含一系列的View。每个Window都会有一个surface,Window Manager会监视window的许多信息,比如生命周期、输入和焦点事件、屏幕方向、转换、动画、位置、转换、z-order等,然后将这些信息(统称window metadata)发送给SurfaceFlinger,这样,SurfaceFlinger就能将window metadata合成为显示器上的surface。

为硬件抽象层(HAL)的子系统。SurfaceFlinger可以将某些合成工作委托给Hardware Composer,从而减轻OpenGL和GPU的工作。此时,SurfaceFlinger扮演的是另一个OpenGL ES客户端,当SurfaceFlinger将一个缓冲区或两个缓冲区合成到第三个缓冲区时,它使用的是OpenGL ES。这种方式会比GPU更为高效。

一般应用开发都要将UI数据使用Activity这个载体去展示,典型的Activity显示流程为:

一般app而言,在任何屏幕上起码有三个layer:

那么android是如何使用这两种合成机制的呢?这里就是Hardware Composer的功劳。处理流程为:

借用google一张图说明,可以将上面讲的很多概念展现,很清晰。地址位于 https://source.android.com/devices/graphics/

即 Frame Rate,单位 fps,是指 gpu 生成帧的速率,如 33 fps,60fps,越高越好。
但是对于快速变化的游戏而言,你的FPS很难一直保持同样的数值,他会随着你所看到的显示卡所要描画的画面的复杂程度而变化。

安卓系统中有 2 种 VSync 信号:

如上图,CPU/GPU 向 Buffer 中生成图像,屏幕从 Buffer 中取图像、刷新后显示。这是一个典型的生产者——消费者模型。理想的情况是帧率和刷新频率相等,每绘制一帧,屏幕显示一帧。而实际情况是,二者之间没有必然的大小关系,如果没有锁来控制同步,很容易出现问题。

所谓”撕裂”就是一种画面分离的现象,这样得到的画像虽然相似但是上半部和下半部确实明显的不同。这种情况是由于帧绘制的频率和屏幕显示频率不同步导致的,比如显示器的刷新率是75Hz,而某个游戏的FPS是100. 这就意味着显示器每秒更新75次画面,而显示卡每秒更新100次,比你的显示器快33%。

两个缓存区分别为 Back Buffer 和 Frame Buffer。GPU 向 Back Buffer 中写数据,屏幕从 Frame Buffer 中读数据。VSync 信号负责调度从 Back Buffer 到 Frame Buffer 的复制操作,可认为该复制操作在瞬间完成。

双缓冲的模型下,工作流程这样的:

应用和SurfaceFlinger的渲染回路必须同步到硬件的VSYNC,在一个VSYNC事件中,显示器将显示第N帧,SurfaceFlinger合成第N+1帧,app合成第N+2帧。

使用VSYNC同步可以保证延迟的一致性,减少了app和SurfaceFlinger的错误,以及显示在各个阶段之间的偏移。然而,前提是app和SurfaceFlinger每帧时间的变化并不大。因此,从输入到显示的延迟至少有两帧。
为了解决这个问题,您可以使用VSYNC偏移量来减少输入到显示的延迟,其方法为将app和SurfaceFlinger的合成信号与硬件的VSYNC关联起来。因为通常app的合成耗时是小于两帧的(33ms左右)。
VSYNC偏移信号细分为以下3种,它们都保持相同的周期和偏移向量:

注意,当 VSync 信号发出时,如果 GPU/CPU 正在生产帧数据,此时不会发生复制操作。屏幕进入下一个刷新周期时,从 Frame Buffer 中取出的是“老”数据,而非正在产生的帧数据,即两个刷新周期显示的是同一帧数据。这是我们称发生了“掉帧”(Dropped Frame,Skipped Frame,Jank)现象。

第一列t1: when the app started to draw (开始绘制图像的瞬时时间)
第二列t2: the vsync immediately preceding SF submitting the frame to the h/w (VSYNC信令将软件SF帧传递给硬件HW之前的垂直同步时间),也就是对应上面所说的软件Vsync
第三列t3: timestamp immediately after SF submitted that frame to the h/w (SF将帧传递给HW的瞬时时间,及完成绘制的瞬时时间)

每dumpsys SurfaceFlinger一次计算汇总出一个fps,计算规则为:
frame的总数N:127行中的非0行数
绘制的时间T:设t=当前行t2 - 上一行的t2,求出所有行的和∑t
fps=N/T (要注意时间转化为秒)

一次dumpsys SurfaceFlinger会输出127帧的信息,但是这127帧可能是这个样子:

如果t3-t1>16.7ms,则认为发生一次卡顿

设目标fps为target_fps,目标每帧耗时为target_ftime=1000/target_fps
从以下几个维度衡量流畅度:

参考文章:

http://windrunnerlihuan.com/2017/05/21/VSync%E4%BF%A1%E5%8F%B7/

如何在 chrome devtools 中获取 FPS

【中文标题】如何在 chrome devtools 中获取 FPS【英文标题】:How to get the FPS in chrome devtools 【发布时间】:2018-01-03 14:45:50 【问题描述】:

我想检索测量的性能记录的平均 fps。

到目前为止,我只能通过将鼠标悬停在这样的帧上来获得每帧的持续时间和 fps:

或通过选择框架:

要得到所有帧的平均fps,我必须手动将它们一一相加,这很不方便。


例如,Firefox devtools 在面板的右上角显示平均 fps。

【问题讨论】:

页面本身只有一个单独的 FPS 仪表覆盖层,您可以在 devtools bottom drawer -> Rendering -> FPS meter 中启用它。 @drawer 但这只会显示当前的 fps 而不是平均值,还是我遗漏了什么? 显然没有实现。另见crbug.com/627925 如果您需要此功能,请评论并为 wOxxOm 链接到的 Crbug 问题加注星标。目前它被归档为低优先级。 【参考方案1】:

您可以使用 devtools-for-devtools。

    将 devtools 切换到分离窗口模式(单击 devtools 设置图标,单击“取消停靠”图标)。下次您只需按 Ctrl-Shift-D 即可切换模式。 按 Ctrl-Shift-i 调用 devtools-for-devtools

显示所有帧的 FPS:

UI.panels.timeline._flameChart._model._frameModel._frames.slice(1).map(f => +(1000 / f.duration).toFixed(1))

显示平均 FPS:

+UI.panels.timeline._flameChart._model._frameModel._frames.slice(1).map(f => 1000 / f.duration).reduce((avg, fps, i) => (avg*i + fps) / (i+1), 0).toFixed(1)

您可以将此代码保存为 devtools Snippets panel 中的 sn-ps 并在上述步骤 2 之后调用它。

【讨论】:

这太棒了!我不知道我可以在 devtools 窗口中使用 devtools。 对于那些可能对在哪里添加它感到困惑的人,打开第二个 DevTool 后,将上面的代码粘贴到控制台中。【参考方案2】:

感谢@wOxxOm 指出如何在the answer above 中为 DevTools 访问 DevTools。

但是,计算平均 FPS 的代码并不完全正确。例如,如果有一帧渲染需要一秒钟,那么该帧的 fps 为 1。如果有另一个帧需要 (1000 / 60) 毫秒来渲染,那么该帧的 fps 为 60。原始代码将给出这两个帧的平均 fps 为 (60 + 1) / 2,这是不正确的。

正确的平均 fps 是总帧数除以总时长。在这个例子中,它是2 / (1 + 1 / 60) fps。

实现这一点的一种方法是:

function averageFps() 
    let frames = UI.panels.timeline._flameChart._model._frameModel._frames;
    let duration = (frames[frames.length - 1].endTime - frames[0].startTime) / 1000;
    let average = frames.length / duration

    return +average.toFixed(2);

【讨论】:

【参考方案3】:

请注意,API 似乎对此略有更改,因此现在完成此操作的新代码是:

let frames = UI.panels.timeline.flameChart.model.frameModelInternal.frames;

let frameSet = [];
let startTimeMs = UI.panels.timeline.flameChart.model.window().left;
let endTimeMs = UI.panels.timeline.flameChart.model.window().right;

let minFPS = 1000;
let maxFPS = -1;
let totalFPS = 0;

for (let frameIdx in frames) 
    let frame = frames[frameIdx];
    if (frame.startTime >= startTimeMs && endTimeMs >= frame.endTime) 
        frameSet.push(frame);
        let frameRate = (16.0/frame.duration) * 60;
      
        if (maxFPS < frameRate) 
          maxFPS = frameRate;
                

        if (minFPS > frameRate) 
          minFPS = frameRate;
        

        totalFPS += frameRate;
    


console.log(`Total Frames: $frameSet.length`);
console.log(`Min FPS: $minFPS`);
console.log(`Max FPS: $maxFPS`);
console.log(`Average FPS: $totalFPS / frameSet.length`);

【讨论】:

有人知道这个UI API 上是否有文档吗? 我喜欢你的 sn-p,但是对于 97.0.4692.99 版的 Chrome,我仍然需要使用 API 的下划线版本。这个新的 API 什么时候开始发挥作用? @Chris Blackwell 让我检查一下我的 Chrome 版本并回复你。 @ChrisBlackwell 看起来我在 Ubuntu Linux(实际上是 PopOS,但它基于 Ubuntu)上使用的是 96.0.4664.110 版本。我想知道我是否有一个 old API。我会看看我是否可以调查一下。【参考方案4】:

更新了@Daniel Le 考虑当前选定范围的解决方案

var startTime = UI.panels.timeline._flameChart._model._window.left;
var endTime = UI.panels.timeline._flameChart._model._window.right;

var frames = UI.panels.timeline._flameChart._model._frameModel._frames
  .filter(frame => (frame.startTime > startTime) && (frame.endTime < endTime));

var duration = (frames[frames.length - 1].endTime - frames[0].startTime) / 1000;
var average = frames.length / duration

console.log(+average.toFixed(2));

【讨论】:

【参考方案5】:

更新代码: 显示所有帧的 FPS:

UI.panels.timeline.flameChart.model
.frameModel().frames.slice(1).map(f => +(1000 / f.duration).toFixed(1))

显示平均 FPS:

UI.panels.timeline.flameChart.model.frameModel()
.frames.slice(1).map(f => 1000 / f.duration)
.reduce((avg, fps, i) => (avg*i + fps) / (i+1), 0).toFixed(1)

【讨论】:

以上是关于android帧的绘制过程以及fps的获取的主要内容,如果未能解决你的问题,请参考以下文章

如何在 chrome devtools 中获取 FPS

unity---gameScreen 的Stats参数

iOS之性能优化·列表异步绘制

在python中绘制功率谱

即使帧的任务是 8ms 也没有达到 60fps

绘制折线捕捉到道路Android谷歌地图应用程序