设为首页添加收藏

您好! 欢迎来到广东某某建材科技有限公司

微博
扫码关注官方微博
微信
扫码关注官方微信
电话:400-123-4567

您的位置: 主页 > 杏鑫资讯 > 行业动态
行业动态

“终于懂了“系列:Android性能优化(一)流畅度优化—FPS提升实战

发布日期:2024-06-24 来源: 网络 阅读量(

Android性能优化不是一个能完全讲解清楚的题目。Android中的性能优化涉及的内容实在太过广泛,需要掌握的技术实在太多,且具体的项目所使用的优化方案也大不相同。想全面讲解性能优化,是万万不能的,实际上目前我学习到的还差得很远。

本专题内容包括对过往工作、技术学习的总结,以及对优化方向的思考与梳理。内容涵盖的点可能不够全面,其实也没必要做到全面,更多的是思考和实践。

系列预计分为五篇:

  • 《“终于懂了“系列:Android性能优化(〇)启动优化》
  • 《“终于懂了“系列:Android性能优化(一)流畅度优化—FPS提升实战》
  • 《“终于懂了“系列:Android性能优化(二)包体积优化—减包实战》
  • 《“终于懂了“系列:Android性能优化(三)内存优化》
  • 《“终于懂了“系列:Android性能优化(四)动态策略》
  • 《“终于懂了“系列:Android性能优化(五)降本增效与AB实验》

再加上之前的《启动优化》,基本上相对重要的Android性能优化的方向都会涉及到。

本篇就首先来介绍我认为在性能优化中地位仅次于包体积优化、启动速度优化的流畅度优化

流畅度在本篇中是指 可滑动列表在滑动时的流畅度,流畅度越高则体验越好。流畅度优化,就是让列表滑动地更流畅,以期望带来像留存率停留时长等业务指标的收益。

它和所谓的 布局优化卡顿优化绘制优化 还是有区别的:流畅度优化有确定的衡量指标——fps,fps越大则滑动时的体验越流畅。也就是说,流畅度优化是 有指标衡量的、且指标能反映用户直接体验好坏的 优化方向。

fps,每秒帧数,即帧率单位。可见文章《Android屏幕刷新机制》

像电商、新闻等典型app的核心页面都是一个可滑动的列表,用户滑动列表以浏览更多的商品或信息,那么滑动时的流畅程度是影响用户是否继续滑动的一个重要因素。手指滑动时 列表不跟手、滑动出现明显卡顿等这些问题 我们是需要极力避免的。

想要解决滑动流畅度问题、提升fps,需要掌握较多的技术点:

  • View工作原理,包括三大流程measure/layout/draw,自定义view等
  • 屏幕刷新机制,包括VSync、Choreographer、fps的计算
  • 渲染流程,包括UIThread与RenderThread、CPU与GPU 分别经过哪些步骤
  • 滑动列表RecycleView的原理,包括四级缓存、onCreateViewHolder与onBindViewHodler的调用时机等

这里列举的是本篇强相关的技术点,性能优化本身是对涉及技术点地综合运用,需要具备扎实的基础知识。

在流畅度优化中所使用的工具最重要的有2个:

  • Systrace,性能数据采样和分析工具,通过生成的Systrace文件可以帮助分析问题,是Android性能优化中必须掌握的工具。在文章《启动优化》中有介绍过,《Systrace系列》可以帮你全面深入学习Systrace。另外还有更方便的基于Systrac的btrace,高性能且支持自动注入自定义事件
  • GPU呈现模型分析(HWUI呈现模式分析),以滚动直方图的形式直观地显示渲染界面窗口帧所花费的时间

此外成熟的性能优化方案 除了实施优化外,还应包括 线上监控APM工具防劣化方案,本篇不会涉及。

在很多介绍 布局优化、卡顿优化、绘制优化的文章中,提到的解决卡顿问题方案有很多:

  • 减少view层级、异步加载view、使用x2c框架
  • 通过Looper设置Printer来监控并解决主线程耗时函数
  • 滑动时暂停后台下载任务/IO读写,例如在列表idle时才加载图片/视频
  • 对各种IPC结果进行缓存

这些方案在实际项目中也确实能带来不错的收益,但是在项目的流畅度优化中经实验对比却没有获得fps的较大提升。而最后使fps有大幅提升的方案是 解决所有帧的公共问题——重度绘制,也是本篇重点介绍的内容。

在需要的预备知识中,View工作原理、屏幕刷新机制 我之前有文章做了专门的介绍,网上关于RecycleView原理的文章也是比较多的。关于渲染流程则是一个被提及比较少的知识点,本节会整体介绍渲染流程,以及与GPU呈现模型分析图的关系。

《Android屏幕刷新机制》我们知道,屏幕上每一帧的渲染都要从 VSync开始,会先在UI线程处理 Input、Animations、Traversal(measure/layout/draw)事件,在draw中(现在Android默认开启GPU硬件加速)会产生用来描述绘制行为的DisplayList。

然后UI线程把DisplayList同步给渲染线程 RenderThread,RenderThread这里做一些优化的操作,到这里都是在CPU中完成。接着RenderThread把绘制信息提交给 GPU 进行绘制(这里会进行dequeueBuffer),当绘制完毕后通过 queueBuffer把Buffer放回到 BufferQueue里。最后在Vsync-sf时SurfaceFlinger会将Frame Buffer进行合成,然后我们就可以在屏幕上看到这一帧了。

image.png
  • 第一个阶段,其实主要做的就是构建DrawOp树(里面封装OpenGL渲染命令),同时,预处理分组一些相似命令,以便提高GPU处理效率,这个阶段主要是CPU在工作,不过这个阶段前期运行在UI线程,后期部分运行在RenderThread(渲染线程)。如下图
构建DrawOp树
  • 第二个阶段主要运行在渲染线程,CPU将数据同步给GPU,之后,通知GPU进行渲染,不过这里需要注意的是,CPU一般不会阻塞等待GPU渲染完毕,而是通知结束后就返回,除非GPU非常繁忙,来不及响应CPU的请求,没有给CPU发送通知,CPU才会阻塞等待。 CPU返回后,会直接将GraphicBuffer提交给SurfaceFlinger,告诉SurfaceFlinger进行合成,SF合成后提交显示,如此完成图像的渲染显示。

示意图:

渲染流程

渲染流程在Systrace图中的描述:

Systrace

渲染流程的耗时可以通过工具——GPU呈现模型分析 来分析,这非常有助于耗时点寻找和分析。

image.png

绿色的横线是16.6ms基准线;每一个竖条就代表一个帧的绘制流程,颜色块及其长度则是对应某个阶段所用的相对时间,具体如下:

颜色称谓从低向上青色、深绿色、浅绿色、深蓝色、浅蓝色、红色、黄色

  • VSync延迟:收到VSync信号到执行此次绘制的时间间隔。收到VSync信号后会post一个Message放入队列,当UI线程有耗时操作,那么handleMessage/doFrame就会被延迟。一般前一帧绘制较久,那么本帧就会被延迟

  • 输入和动画:编舞者doFrame中执行InputCallback、AnimationCallback的时间

  • 测量/布局:编舞者doFrame中执行TraversalCallback的的performMeasure/performLayout的时间

  • 绘制:编舞者doFrame中执行TraversalCallback的的performDraw的时间

  • 同步和上传:主线程与渲染线程同步渲染数据、将位图信息上传到 GPU 所花的时间。

  • 命令问题(发出命令):CPU-RenderThreader将绘制显示列表的命令发送给GPU所花的时间。之后,GPU才能根据这些OpenGL命令进行渲染。

  • 交换缓冲区:之前的GPU命令被发送完毕后,CPU一般会发送最后一个命令给GPU,告诉GPU当前命令发送完毕,可以处理,GPU一般而言需要返回一个确认的指令,不过,这里并不代表GPU渲染完毕,仅仅是通知CPU,GPU有空开始渲染而已,并未渲染完成,但是之后的问题APP端无需关心了,CPU可以继续处理下一帧的任务了。如果GPU比较忙,来不及回复通知,则CPU需要阻塞等待,直到收到通知,才会唤起当前阻塞的Render线程,继续处理下一条消息,这个阶段是在swapBuffers中完成的。

尽管此工具名为“GPU 渲染模式分析”,但所有受监控的进程实际上发生在 CPU 中。通过将命令提交到 GPU 来触发渲染,GPU 也会异步渲染屏幕。在某些情况下,GPU 可能会有太多工作要处理,因此您的 CPU 必须先等待一段时间,然后才能提交新命令。如果发生这种情况,您将看到橙色竖条和红色竖条上出现峰值,且命令提交将被阻止,直到 GPU 命令队列中腾出更多空间。

了解了渲染流程,以及对应的GPU呈现模型分析图,那么就来看看滑动列表在滑动时的现象。我们模拟各阶段的耗时,用来测试和深度理解。

首先看看正常无耗时的滑动列表,在滑动时的GPU呈现模型分析图(忽略图中右上角,看底部GPU呈现图即可):

正常的GPU呈现图

下面我们分别来看不同场景下对应的GPU呈现模型分析图有什么特点。

我们在onBindViewHolder做一个耗时操作:



滑动时如果被触发的 onBindViewHolder 的触发来自recycleView的prefetch,那么在接收到VSync信号后这一帧的doframe却被当前UI线程的Message—onBindViewHolder耗时耽误了执行,这个就是VSync延迟了,即这一帧占比最大的就是青色

如果被触发的onBindViewHolder 来自doFrame中的InputCallback,那就是这一帧占比最大的就是深绿色。

我们在在item的根布局的onMeasure做耗时操作:




image.png
image.png

正常滑动时,滑进屏幕的一个item,它的onMeasure/onLayout是来自doFrame中的InputCallback、AnimationCallback,那就是深绿色,即第一根柱子。

第二根柱子,因为前一帧占用主线程时间较长,那这一帧的VSync就被延迟了,即青色,即第二根柱子。

onLayout中耗时和onMeasure表现一致。

onLayout/onMeasure需要来自performTraversal

3.2.2.1 页面首帧

刚进入页面后,首帧的每个item的 onLayout和onMeasure 都是来自doFrame的performTraversal。

3.2.2.2onBindViewHodler中延迟刷新的帧

在onBindViewHodler中延迟2秒更新文字,那2s后的item的onMeasure就来自TraversalCallback,即浅绿色


20221213-211623 -big-original.gif

onLayout和onMeasure表现一致。

Item view的dispatchDraw:




正常滑动时,滑进屏幕的一个item,它的draw/onDraw/dispatchDraw来自从traversalCallback: traversalCallback->Recyleview.draw->Item.draw->Item.dispatchDraw,表现为深蓝色

完整的draw过程: 1. 画背景 2.画自己-- onDraw,自己实现 3.画子view-- dispatchDraw 4.画装饰,这里每一个耗时都会表现为深蓝色。

注意:如果是ViewGroup,要设置.setWillNotDraw(false),才会走完整的draw过程,否则只会走dispatchDraw。

如下图,列表中有很多图片时 浅蓝色区段 确实增大(在低端机上可能更为明显)。

draw/onDraw/dispatchDraw中有很多绘制命令,即多次调用canvas.drawXXX方法:



  • 可见 的红色部分都变长:每帧需要发出的命令都是3000个。(因为是影响到每一帧,所以需特别注意此情况)
  • 当新item出现时会有一帧 深蓝色变长:深绿色是因为,深蓝色是因为7.3

原因是:系统会尽可能地缓存显示列表。因此某些情况下,滚动、转换或动画会要求系统重新发送显示列表(即红色),但不必实际重新构建它(即重新捕获绘制命令)(即draw过程-深蓝色)。因此,您可能会看到“发出命令”条较高,但“绘制命令”条并不高(即红色高但深蓝色不高)。

上图是低端机的情况,可见对于低端机来说,每一帧红色都满了,对fps影响巨大。(实测低端机中50个drawCircle就会造成每帧都超出16.6ms。)

  • 这种情况,无论快滑还是慢滑,每帧都是满的。
  • 2.1-2.3中的场景,只影响即将出现item的一帧,慢滑时对整体fps影响较小,快滑时影响比较大。

canvas.saveLayer相关方法,一次调用就很耗费性能,请不要使用!重要!

3.5节与3.3节的不同

  • 3.3中是单纯draw方法的耗时,只影响即将出现item的那一帧
  • 而本节中的 高频绘制命令、重度绘制命令,会作用于每一 ****(虽然也是写在draw相关方法中)

总结:当你慢滑时,发现每一 都超出16.6且红色占比很大,那么就可以判断是绘制命令的问题,需要去查itemView中的自定义view的draw相关方法。

怎么让黄色变长呢?GPU忙碌?暂未测试出~

在实际项目首页的列表滑动fps优化时,发现在慢滑时:红色块占据一帧大部分耗时、且是所有帧的共性问题,如3.5节中一样,可见是绘制命令的问题。这就需要排查view绘制相关代码,尤其是自定义view。最后发现,在列表的item view中使用了较多的自定义圆角view:


实现绘制圆角的方案为 saveLayer+Xfermode混合模式,而此方案中的saveLayer方法则是重度绘制方法。

替换方案:使用setOutlineProvider系统方法即可:


修改后,每帧的红色条占比大幅降低,实际fps也大幅提升。

为啥有的优化操作不达预期,对FPS绝对值提升很有限?

  1. 假如一次滑动有100帧,有80帧每帧耗时40ms,有10帧耗时16.6ms以内,10帧耗时80ms,那么我们只把80ms的10个帧优化到40ms,那么对FPS均值的提升是有限的(尤其是快速滑动时)。例如只优化了onCreateViewHolder或者onBindViewHodler,那么只对出现新item的帧有影响,这只占很小的比例。
  2. 如果把40ms的80个帧优化到16.6ms以内,80ms的10个帧优化到40ms,那么对FPS均值的提升就是显著的。例如上面实战中,所有帧都有的绘制耗时,这影响到所有帧。

先在慢滑状态下,查看GPU呈现模式工具,优先看多数帧的共同耗时点,再看非共性问题(例如进入新item时的帧耗时)。具体耗时点分析,可通过SysTrace分析

  1. 慢滑,是因为避免两帧之间的干扰,若当前帧耗时较多,那么很可能会导致下一帧的VSync延迟
  2. 优先看多数帧的耗时点,是因为要优先解决帧耗时的共性问题 进而大幅提升FPS
  3. 使用Systrace打点来分析具体耗时的代码
  4. UIThread和RenderThread都需要分析

本篇重点介绍了渲染流程和对应的GPU呈现模式分析图,以及对应色条的理解。然后对滑动阶段各耗时场景进行的详细的分析,最后进行了优化实战。性能优化需要真实的实践,只有真正做过并取得了显著的收益才会有更深的理解。大家可以针对自己的项目看看有无流畅度的问题,尝试去分析和优化,看看是否能有显著的提升。

好了,本篇就到这里,欢迎留言讨论~

github:com.hfy.demo01.performance.fps.PerformanceLearningActivity

你的 点赞、评论,是对我的巨大鼓励!

欢迎关注我的?公众号? 胡飞洋?,文章更新可第一时间收到;

如果有问题或者想进群,号内有加我微信的入口,我拉你进技术讨论群。在技术学习、个人成长的道路上,我们一起前进!

平台注册入口