发布日期:2024-06-24 来源: 网络 阅读量()
Android性能优化不是一个能完全讲解清楚的题目。Android中的性能优化涉及的内容实在太过广泛,需要掌握的技术实在太多,且具体的项目所使用的优化方案也大不相同。想全面讲解性能优化,是万万不能的,实际上目前我学习到的还差得很远。 本专题内容包括对过往工作、技术学习的总结,以及对优化方向的思考与梳理。内容涵盖的点可能不够全面,其实也没必要做到全面,更多的是思考和实践。 系列预计分为五篇: 再加上之前的《启动优化》,基本上相对重要的Android性能优化的方向都会涉及到。 本篇就首先来介绍我认为在性能优化中地位仅次于包体积优化、启动速度优化的流畅度优化。 流畅度在本篇中是指 可滑动列表在滑动时的流畅度,流畅度越高则体验越好。流畅度优化,就是让列表滑动地更流畅,以期望带来像留存率、停留时长等业务指标的收益。 它和所谓的 布局优化、卡顿优化、绘制优化 还是有区别的:流畅度优化有确定的衡量指标——fps,fps越大则滑动时的体验越流畅。也就是说,流畅度优化是 有指标衡量的、且指标能反映用户直接体验好坏的 优化方向。 fps,每秒帧数,即帧率单位。可见文章《Android屏幕刷新机制》 像电商、新闻等典型app的核心页面都是一个可滑动的列表,用户滑动列表以浏览更多的商品或信息,那么滑动时的流畅程度是影响用户是否继续滑动的一个重要因素。手指滑动时 列表不跟手、滑动出现明显卡顿等这些问题 我们是需要极力避免的。 想要解决滑动流畅度问题、提升fps,需要掌握较多的技术点: 这里列举的是本篇强相关的技术点,性能优化本身是对涉及技术点地综合运用,需要具备扎实的基础知识。 在流畅度优化中所使用的工具最重要的有2个: 此外成熟的性能优化方案 除了实施优化外,还应包括 线上监控APM工具和防劣化方案,本篇不会涉及。 在很多介绍 布局优化、卡顿优化、绘制优化的文章中,提到的解决卡顿问题方案有很多: 这些方案在实际项目中也确实能带来不错的收益,但是在项目的流畅度优化中经实验对比却没有获得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进行合成,然后我们就可以在屏幕上看到这一帧了。 示意图: 渲染流程在Systrace图中的描述: 渲染流程的耗时可以通过工具——GPU呈现模型分析 来分析,这非常有助于耗时点寻找和分析。 绿色的横线是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呈现模型分析图有什么特点。 我们在onBindViewHolder做一个耗时操作: 滑动时如果被触发的 onBindViewHolder 的触发来自recycleView的prefetch,那么在接收到VSync信号后这一帧的doframe却被当前UI线程的Message—onBindViewHolder耗时耽误了执行,这个就是VSync延迟了,即这一帧占比最大的就是青色。 如果被触发的onBindViewHolder 来自doFrame中的InputCallback,那就是这一帧占比最大的就是深绿色。 我们在在item的根布局的onMeasure做耗时操作: 正常滑动时,滑进屏幕的一个item,它的onMeasure/onLayout是来自doFrame中的InputCallback、AnimationCallback,那就是深绿色,即第一根柱子。 第二根柱子,因为前一帧占用主线程时间较长,那这一帧的VSync就被延迟了,即青色,即第二根柱子。 onLayout中耗时和onMeasure表现一致。 即onLayout/onMeasure需要来自performTraversal。 刚进入页面后,首帧的每个item的 onLayout和onMeasure 都是来自doFrame的performTraversal。 在onBindViewHodler中延迟2秒更新文字,那2s后的item的onMeasure就来自TraversalCallback,即浅绿色。 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方法: 原因是:系统会尽可能地缓存显示列表。因此某些情况下,滚动、转换或动画会要求系统重新发送显示列表(即红色),但不必实际重新构建它(即重新捕获绘制命令)(即draw过程-深蓝色)。因此,您可能会看到“发出命令”条较高,但“绘制命令”条并不高(即红色高但深蓝色不高)。 上图是低端机的情况,可见对于低端机来说,每一帧红色都满了,对fps影响巨大。(实测低端机中50个drawCircle就会造成每帧都超出16.6ms。) 像canvas.saveLayer相关方法,一次调用就很耗费性能,请不要使用!重要! 3.5节与3.3节的不同: 总结:当你慢滑时,发现每一 帧 都超出16.6且红色占比很大,那么就可以判断是绘制命令的问题,需要去查itemView中的自定义view的draw相关方法。 怎么让黄色变长呢?GPU忙碌?暂未测试出~ 在实际项目首页的列表滑动fps优化时,发现在慢滑时:红色块占据一帧大部分耗时、且是所有帧的共性问题,如3.5节中一样,可见是绘制命令的问题。这就需要排查view绘制相关代码,尤其是自定义view。最后发现,在列表的item view中使用了较多的自定义圆角view: 实现绘制圆角的方案为 saveLayer+Xfermode混合模式,而此方案中的saveLayer方法则是重度绘制方法。 替换方案:使用setOutlineProvider系统方法即可: 修改后,每帧的红色条占比大幅降低,实际fps也大幅提升。 为啥有的优化操作不达预期,对FPS绝对值提升很有限? 先在慢滑状态下,查看GPU呈现模式工具,优先看多数帧的共同耗时点,再看非共性问题(例如进入新item时的帧耗时)。具体耗时点分析,可通过SysTrace分析 本篇重点介绍了渲染流程和对应的GPU呈现模式分析图,以及对应色条的理解。然后对滑动阶段各耗时场景进行的详细的分析,最后进行了优化实战。性能优化需要真实的实践,只有真正做过并取得了显著的收益才会有更深的理解。大家可以针对自己的项目看看有无流畅度的问题,尝试去分析和优化,看看是否能有显著的提升。 好了,本篇就到这里,欢迎留言讨论~ github:com.hfy.demo01.performance.fps.PerformanceLearningActivity 你的 点赞、评论,是对我的巨大鼓励! 欢迎关注我的?公众号? 胡飞洋?,文章更新可第一时间收到; 如果有问题或者想进群,号内有加我微信的入口,我拉你进技术讨论群。在技术学习、个人成长的道路上,我们一起前进!
3.2.2.1 页面首帧
3.2.2.2onBindViewHodler中延迟刷新的帧