草庐IT

Flutter帧率监控 | 由浅入深,详解获取帧率的那些事

编程黑板报 2023-03-28 原文

前言

做线上帧率监控上报时,少不了需要弄明白如何通过代码获取实时帧率的需求,这篇文章通过图解配合Flutter性能调试工具的方式一步步通俗易懂地让你明白获取帧率的基础知识,以后再也不愁看不懂调试工具上指标了。

说说 List<FrameTiming>

Flutter 中通过如下方式监听帧率,addTimingsCallback 涉及到帧调度知识,感兴趣可以看看这篇Flutter 帧调度过程

这里重点说说 List<FrameTiming>。

List<FrameTiming>从哪里来

addTimingsCallback 定义:

List<FrameTiming>可简单理解成:引擎层到框架层的帧数据流。

List<FrameTiming>何时有值

List<FrameTiming>则表示一系列实时帧信息。

如点击屏幕按钮,引擎将传递系列帧信息到框架层:“框架层,屏幕发送了变化,准备回调数据更新了!”。如果用户未操作,addTimesCallback 则不会回调。

因此 ,addTimesCallback(List<FrameTiming>)只有用户操作界面时参数才有值

List<FrameTiming>中帧存储顺序

List<FrameTiming>中 0 的位置是第一帧,last 是最新一帧。 最新的帧永远在最后面

再说说 FrameTiming

通过这个单词不难猜测 Frame 表示帧,加上 Timing 可以理解成实时变化的帧。FrameTiming 是一个用来存储实时帧信息的数据结构

FrameTiming 定义:

这里列了下我认为最重要的几个属性:

前置知识简单说明

理解上述属性前需了解渲染相关知识,不清楚的可以看看Vsync 机制卡顿产生原因

核心思想
图像内容展示到屏幕的过程需要 CPU 和 GPU 共同参与。CPU 负责计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。之后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。

FrameTiming 在帧中的表示

当在应用中操作时候,就会产生连续的帧,如图:

每两个柱形一起表示一帧:ui 表示 cpu 耗时,raster 表示 gpu 耗时。

每帧细化后如下图,其中标注 ①②③④ 对应 FrameTiming 中的四个主要属性。而其中:

  • ui 在 FrameTiming 中有对应衍生变量叫 buildDuration 。
  • Raster 在 FrameTiming 中用 RasterDuration 表示。

同时可推导出 FrameTiming 中相关衍生变量与上述重点关注属性关系:

④-① = totalSpan:同步信号开始到栅格化时间

②-① = vsyncOverhead:同步信号接受后到 ui 构建之间延迟。

③-② = buildDuration:ui 构建过程总时间。

④-③ = rasterDuration:栅格化过程总时间。

totalSpan 与 buildDuration+rasterDuration 关系

通过代码验证 Flutter 调试工具 PerformanceOverlay 中 Timing 每帧 ui 值和 ration 值与 vsyncstart、buildstart、buildFinish、rasterStart、rasterFinish 关系。

输出:

代码中,11 行是 ui 构建 + 栅格化时间,17 行是 totalSpan 时间, 22 行中是 vsyncOverhead + ui 构建 + 栅格化时间 这个值最终和才等于 totalSpan 值。

这里有个误区, 网上很少人关注 totalSpan 与 buildDuration+rasterDuration 关系,好像默认就是相等的。其实,totalSpan 不等于 Timing 中 ui + raster 值而是 Vsync 信号接受后构建之前延迟 vsyncOverhead+cpu 构建耗时 + gpu 耗时

通过上述案例和 totalSpan 定义很容易佐证这点:

如何获取帧率

核心思路

  1. 将原始帧数据 List降噪保留最新关注帧数。
  2. 通过公式 FPS≈ REFRESH_RATE * 实际绘制帧数 / 理论绘制帧数 。

如何降噪

  • 从原生数据中筛查最新关注帧数,其他都干掉。

    如下,通过栈方式调换了存储方式更容易操作,然后将栈中老的干掉只保留最新的关注 100 条。

  • 将位于不同帧的无效数据过滤掉。

    如下,以刷新率为 60 举例,如果一帧之间的时间 > 16.6 *2,该帧就位于不同帧中,因为一帧最大时间也就是 16.6ms。

如何计算

代码如下:

这里拆解下其中逻辑,方便理解。

有 5 帧,其中在实际绘制过程中 f① 和 f② 都是在正常时间范围内绘制,f③ 则会绘制耗时,跨越 2 帧。

假设 f①,f②,f③ 绘制总耗时为 P1, P2, P3 则:

  1. 理论绘制帧数 = (P1 / 16.6)+ 1 + (P2 / 16.6) + 1 + (P3 / 16.6) + 1 图中明显可以看到 P1 和 P2 < 16.6, 而 P3 > 16.6 *2 ,所有理论绘制帧数 = 0 +1 + 0 + 1 + 2 + 1 = 5。

  2. 实际绘制帧数 = 3 。

  3. 本来正常应该绘制 5 帧,但是实际绘制 3 帧,取比值表示实际绘制能力,根据 FPS≈ REFRESH*RATE * 实际绘制帧数 / 理论绘制帧数 。 即 FPS = 3 _ 60 / 5。

完整代码

效果展示

这就结束了?

上面代码在刷新率为 60HZ 的手机上每秒绘制帧时间为 16.6 是没有问题的,但是如果在其他帧率的手机上,比如 90HZ(OnePlus 7 Pro), 120HZ(Redmi K30)上就会存在问题。

  1. 代码中写死了 REFRESH_RATE = 60 。
  2. maxframes = 100 也有问题,如果在 60HZ 手机上取 100 帧绰绰有余,在 120HZ 手机上的话,每秒绘制 120 帧显然不够。

如何获取帧率(改进版)

思路:通过通道获取各系统提供的刷新率获取方式,然后更新上述代码中的刷新率。

获取各系统帧率

在 Android 和 ios 平台提供了获取帧率的方法。

  • 对于 Android 通过 WindowManager 获取刷新率:

定义统一获取接口并实现(以安卓为例)

定义接口

最终修改点

  1. 最大帧率数修改成 120。
  2. fpsHZ 这个值通过插件动态获取。
  3. 时间间隔也同步修改下,也就是 16.6(60hz 的时候)。
  4. 最后 fps 计算公式中的刷新率同步修改成 fpsHZ。

总结

本文重点讲解了 FrameTiming 结构在帧显示过程中的对应关系,图解获取准确帧的算法,最后完善了获取帧的逻辑。

总体来说网上能搜到的我这里都有,在学习过程中遇到 FrameTiming 结构和帧率计算方法这两个点觉得不好理解,不够系统,就重点介绍争取深入浅出表达出来。不足之处还望各位大佬指出,谢谢!

如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力。

❤️ 本文原创听蝉 公众号:编程黑板报 欢迎关注原创技术文章第一时间推送 ❤️

PS: 文中所有源码获取方式:公众号后台回复 “fps”

参考链接

如何代码获取 Flutter APP 的 FPS - Yrom's

Flutter 如何更加准确地获取 FPS | 区长

Flutter 性能计算之流畅性 fps 计算 - 简书

allenymt/flutter_fps: flutter Fps 的两种监听方案

有关Flutter帧率监控 | 由浅入深,详解获取帧率的那些事的更多相关文章

  1. ruby - 简单获取法拉第超时 - 2

    有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url

  2. ruby - 从 Ruby 中的主机名获取 IP 地址 - 2

    我有一个存储主机名的Ruby数组server_names。如果我打印出来,它看起来像这样:["hostname.abc.com","hostname2.abc.com","hostname3.abc.com"]相当标准。我想要做的是获取这些服务器的IP(可能将它们存储在另一个变量中)。看起来IPSocket类可以做到这一点,但我不确定如何使用IPSocket类遍历它。如果它只是尝试像这样打印出IP:server_names.eachdo|name|IPSocket::getaddress(name)pnameend它提示我没有提供服务器名称。这是语法问题还是我没有正确使用类?输出:ge

  3. ruby - 获取模块中定义的所有常量的值 - 2

    我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c

  4. ruby-on-rails - 获取 inf-ruby 以使用 ruby​​ 版本管理器 (rvm) - 2

    我安装了ruby​​版本管理器,并将RVM安装的ruby​​实现设置为默认值,这样'哪个ruby'显示'~/.rvm/ruby-1.8.6-p383/bin/ruby'但是当我在emacs中打开inf-ruby缓冲区时,它使用安装在/usr/bin中的ruby​​。有没有办法让emacs像shell一样尊重ruby​​的路径?谢谢! 最佳答案 我创建了一个emacs扩展来将rvm集成到emacs中。如果您有兴趣,可以在这里获取:http://github.com/senny/rvm.el

  5. Ruby 从大范围中获取第 n 个项目 - 2

    假设我有这个范围:("aaaaa".."zzzzz")如何在不事先/每次生成整个项目的情况下从范围中获取第N个项目? 最佳答案 一种快速简便的方法:("aaaaa".."zzzzz").first(42).last#==>"aaabp"如果出于某种原因你不得不一遍又一遍地这样做,或者如果你需要避免为前N个元素构建中间数组,你可以这样写:moduleEnumerabledefskip(n)returnto_enum:skip,nunlessblock_given?each_with_indexdo|item,index|yieldit

  6. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  7. ruby - 没有类方法获取 Ruby 类名 - 2

    如何在Ruby中获取BasicObject实例的类名?例如,假设我有这个:classMyObjectSystem我怎样才能使这段代码成功?编辑:我发现Object的实例方法class被定义为returnrb_class_real(CLASS_OF(obj));。有什么方法可以从Ruby中使用它? 最佳答案 我花了一些时间研究irb并想出了这个:classBasicObjectdefclassklass=class这将为任何从BasicObject继承的对象提供一个#class您可以调用的方法。编辑评论中要求的进一步解释:假设你有对象

  8. ruby-on-rails - 如何在 Gem 中获取 Rails 应用程序的根目录 - 2

    是否可以在应用程序中包含的gem代码中知道应用程序的Rails文件系统根目录?这是gem来源的示例:moduleMyGemdefself.included(base)putsRails.root#returnnilendendActionController::Base.send:include,MyGem谢谢,抱歉我的英语不好 最佳答案 我发现解决类似问题的解决方案是使用railtie初始化程序包含我的模块。所以,在你的/lib/mygem/railtie.rbmoduleMyGemclassRailtie使用此代码,您的模块将在

  9. ruby - 如何使用 CarrierWave 从 S3 获取真实文件 - 2

    我有一个应用程序可以读取文件的内容并为其编制索引。我将它们存储在磁盘本身中,但现在我使用的是AmazonS3,因此以下方法不再适用。事情是这样的:defperform(docId)@document=Document.find(docId)if@document.file?#Youshould'tcreateanewversion@document.versionlessdo|doc|@document.file_content=Cloudoc::Extractor.new.extract(@document.file.file)@document.saveendendend@docu

  10. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

随机推荐