概述
对于安卓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的绘制流程:
所有View
的invalidate
,都会执行到 ViewRootImpl.java
的 requestLayout()
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接口,所以接下来该看的就是 FrameDisplayEventReceiver
的run
方法:
上图就是最终的执行代码,它是 Choregrapher.java
的 doFrame
方法。 它主要完成两大操作:
- 进行掉帧逻辑计算,并添加 性能分析的trace日志。
- 执行各种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
一样是平级关系,都会往 Choregrapher
的CallbackQueue
中插入一个callback
,只是他们的类型不同,postFrameCallback
是 animation事件
,而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的工作原理
最后,列举了一些常见的 可能导致屏幕丢帧的代码问题。