二十七、UI卡顿的分析和处理

概述

对于安卓app的UI性能分析,systrace是目前使用最广的工具。它能够帮助开发者分析多个模块的运行状态,以及详细信息,比如 SurfaceFlinger,View刷新机制等。

AndroidSDK 本身就提供了 systrace.py 脚本,可以设置数据的采集方式,并收集相关程序运行数据。最终生成一个html文件,供开发者分析程序性能问题。

基本使用

AndroidSdk中提供了 脚本文件位置在: Android/sdk/platform-tools/systrace

基本的使用命令为: python systrace.py --time=10 -o my_systrace.html

详细的参数如下:

由于这是python的脚本,使用之前,必须先在电脑上安装好 python环境,这个并非本文重点,不展开讨论。

上面的命令最终会生成一个 my_systrace.html 文件,需要使用 浏览器打开,推荐chrome 或者以 chrome为内核的其他浏览器。

比如上图,黄色的1处,就是UI渲染时不流畅的表现。绿色2处才正常。

由于分析这个html文件有一些门槛,需要提前对安卓屏幕渲染的流程有一定认知。所以下面将从基础开始讲。

屏幕渲染基础知识

CPU && GPU && 双缓冲机制

关键词:UI线程 , RenderThread线程 , SurfaceFlinger

前者用来进行计算,后者用来进行渲染。 我们在做自定义View的时候有提到过 硬件加速,开启硬件加速,就是用GPU直接来渲染组件。

通常情况下,View的 measure测量,layout布局,draw 绘制 3个流程都是由 CPU来完成的。另外,CPU还负责用户的输入事件的处理,View的动画等,这部分工作都是在UI线程中完成。

当CPU完成了 上面提到的所有工作之后,就会将 结果提交给 渲染线程RenderThread。 之后,工作就交接给了GPU,它会对 这些数据进行栅格化操作。并将数据缓存到 一个 buffer中。最后,手机屏幕从 buffer中读取数据,并显示出来。

实际上,真正对 Buffer中的数据进行合成处理的,是SurfaceFlinger.

仔细分析一下上面的流程,GPU不断向 buffer中存入数据,而屏幕不停地从 buffer中取得数据,这样就存在一个矛盾,就是有可能 GPU还没写入完毕,屏幕就来获取,取得的数据可能会不完整,就造成了屏幕上显示的内容错乱。

为了避免这种问题,安卓引入了双缓冲机制,利用两片缓存区,一个专门用来屏幕读取的FrameBuffer,一个专门供GPU写入数据的 BackBuffer, 在GPU写入完毕之后,通过swap事件,将两个缓存区的内容调换。

双缓冲机制,要求 定期交换 FrameBuffer和BackBuffer的数据,从而保证屏幕上显示最新内容。

当GPU正在向BackBuffer写入数据时,它会将 BackBuffer锁定。

此时,如果到达了 定期交换的时机,那么,本次swap事件将会被忽略。

直接导致的结果,就是屏幕上显示的是 上一个帧的数据。这也就是 我们常说的 丢帧

为了保证App能够流畅工作,我们需要保证 每一帧在16MS之内处理完成 CPU 和 GPU 的工作,包括绘制渲染等。完美的屏幕刷新应该如下图所示:

每一次 的屏幕刷新(swap事件)都发生在 CPU GPU 工作完成之后

VSYNC垂直同步机制

通过以上的双缓冲机制,大部分情况下已经能够满足屏幕刷新的体验要求。但是特殊情况下还是有例外。 这是因为,屏幕刷新率和GPU绘制的频率并不一定是一致的。

  • 屏幕刷新率
    • 指的是 手机屏幕每秒可以刷新多少次
    • 手机出厂之后,屏幕刷新率就确定了,无法改变
  • GPU绘制频率,
    • 指的是 GPU每秒可以绘制多少帧

上面说的特殊情况如下图:

GPU正在工作时,到达了屏幕刷新(swap 事件)的时间节点。这也就导致了丢帧。上图中第三帧显示的还是 老内容,无法使用到最新的数据。

这是由于,屏幕刷新的时机是一定的,但是 一个View的刷新时机,是可以通过代码去控制的,而我们开发者无法保证view刷新时机与 屏幕刷新的时机保持一致。

为了解决这个问题,安卓又引入了 Vsync(垂直同步)机制 :

每隔16MS,硬件层发出 vsync信号,应用层收到这个信号之后,就执行UI的渲染流程,同时 vsync也会触发 SurfaceFlinger从buffer中读取数据,进行合成,显示到屏幕上

简单来说就是: 将GPU 和 CPU 工作的开始时间与 屏幕刷新的时机强制同步

加入vsync之后,屏幕刷新的频率和vsync信号的频率一致,可以优化 刷新时机错位导致的丢帧 问题。

choregrapher 编舞者

有了vsync机制之后,如果我们有view要刷新数据,必须等到下一次vsync信号,才能执行刷新。

那么 软件层是如何 接收 硬件发出的Vsync信号并执行view的刷新操作的呢?

回顾View的绘制流程:

所有Viewinvalidate,都会执行到 ViewRootImpl.javarequestLayout()

requestLayout() 会执行 view的测量布局绘制流程,其核心代码就是在 scheduleTraversals()中. 看下图: 最核心的一句代码,mChoregrapher.postCallback(...)

继续跟踪代码:

从上图红框内可以看出,我们的view刷新动作,最终会形成一个 事件,进入到一个 mCallbackQueue 队列中。

当 Choregrapher 接收到 vsync信号时,它会从 队列中将事件取出并执行。

那么问题就到了 choregrapher 怎么知道 vsync信号的到来的呢?

上图中,除了红框内的 队列处理之外,还有一句代码:scheduleFrameLocked(now); 它最终执行到了一个native (nativeScheduleVsync)方法。

这个native方法实际上就是向系统订阅了一次vsync信号。Android系统每隔16.6MS会发出一次Vsync信号,只有订阅了信号的app能够接收到。

回顾整个流程,我们去view.invalidate()想要刷新UI,Choregrapher 先帮我们注册一个事件,并且同时订阅一个 vsync信号,等到信号到来时,执行我们的UI刷新动作,由此保证刷新UI的时机与屏幕刷新的时机同步。

注意,订阅一次,只能收到一次,如果要收到多次,就只能订阅多次。如果 View存在动画特效,那么必须每个变化都会去订阅一次 Vsync信号。

还有一个未解之谜,那就是 Choregrapher 注册 vsync 信号之后,是在哪儿接收 vsync信号的。

答案就是:FrameDisplayEventReceiver 广播接收器。

上图中,onVsync方法会在vsync信号到来之后执行,执行的内容也很简单,就是 发送了一个 Message到 主线程的 handler中。

Message.obtain(mHandler,this)的源代码如下

由于传入的是第二个参数是 FrameDisplayEventReceiver.this,它实现了Runnable接口,所以接下来该看的就是 FrameDisplayEventReceiverrun方法:

上图就是最终的执行代码,它是 Choregrapher.javadoFrame方法。 它主要完成两大操作:

  1. 进行掉帧逻辑计算,并添加 性能分析的trace日志。
  2. 执行各种callback ,从这里可以看出,其实一帧中就做了这 INPUT,ANIMATION,TRAVERSAL 三件事。

Choregrapher小结

承上

承接了应用层的各种 callback输入,包括 input,animation,traversal绘制。但是它不会立即执行,而是先将他们存入一个 CallbackQueue队列中。

启下

它内部的 FrameDisplayEventReceiver负责接收Vsync信号,当收到信号之后,调用的doCallback,从 CallbackQueue中取出各种事件并执行,其中包括 负责 UI绘制的 traversalRunnable

骚操作

利用 Choregrapher 实现帧渲染监控

Choregrapher会在doFrame方法中执行各种callback,其中包括UI绘制的 traversalRunnable,并且向外部提供了 FrameCallback接口,可以用它来监听doFrame方法的执行过程。

我们可以在自己的app代码中,主动调用 Choregrapher.getInstance().postFrameCallback 来判断是否发生丢帧。

postFrameCallback 实际上 和 view的requestLayout一样是平级关系,都会往 ChoregrapherCallbackQueue中插入一个callback,只是他们的类型不同,postFrameCallbackanimation事件,而requestLayout发送的是 traversal事件

正常情况下,doFrame方法只会打印出 ~~doFrame~~,但是如果执行时间超过了16.6MS, 那么还会打印出 发生丢帧! 注意,由于每一次订阅vsync都只会接收到一次vsync信号,所以,最后一句,就是在递归订阅Vsync,始终保持 渲染丢帧的监控状态。

安卓利用同步屏障优化UI性能

View的 invalidate会向 Choregrapher发送 traversalCallback之前,会执行MessageQueue.postSyncBarrier() 设置同步屏障

handler中的Message分为同步消息和异步消息两类,比如下图就是一个异步消息。

通常情况下,Looper在不停的从MessageQueue中获取消息的时候不区分同步和异步,但是,如果设置了同步屏障,异步消息的优先级会提高,MessageQueue只会在next中提取出 异步消息。而 TraversalRunnable 会被封装到一个异步Message中,因此,View的绘制操作会被优先执行。

再来分析 systrace.html

在了解到本文的所有关于渲染的基础知识之后,我们再来分析 systrace.html

这是一个典型的由于animation动画引起的卡顿。 很明显,图中1处,这一帧之所以不流畅,是因为 inflate 和 decodeBitmap 消耗了太多时间。这里可以直接定位到 某个类的某一个方法在执行过长时间 的绘制 。

检索我们的代码,可以找到有问题的代码源头。从而进行优化。

上图中 inflate 方法在通过反射创建 View,decodeBitmap 在解析一个图片内容,前者可以通过 缓存View来优化,后者也可以通过缓存 bitmap到内存中来优化。

除了上面提到的 动画卡顿之外,还有:

  • 自定义View的draw方法耗时过长,

  • 自定义View的 measure 和 layout耗时过长

  • decodeBitmap 转换图片耗时过长

总结

本文讲述了大量的基础知识,主要包括:

  • CPU GPU 屏幕 的协同工作原理
  • 为了解决Buffer读取问题,引入了 双缓冲机制
  • 为了解决刷新时机与屏幕刷新时机不同步的问题,引入了 Vsync垂直同步机制
  • 详细讲述了 垂直同步机制的实现核心类 Choregrapher的工作原理

最后,列举了一些常见的 可能导致屏幕丢帧的代码问题。

相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6415 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js