在学习了 JVM、DVM、ART后,我们对Android关于运行时的一些机制(内存分配、垃圾回收、AOT、JIT)有所了解。
接下来就可以正式的性能调优了。
第一个学习的就是绘制优化,因为绘制和界面流畅度有关,这个优化的效果是最直观的。
1.绘制性能分析
要学会性能的分析,首先要知道绘制的原理,接着就是介绍性能绘制的工具:Profile GPU Rendering
、 Systrace
和 Traceview
。
1.1 绘制原理
View的绘制流程有 measure、layout和draw,他们主要运行在应用框架层。而真正将数据渲染到屏幕上的则是系统Native层的SurfaceFlinger
服务来完成的。
绘制过程主要由CPU来进行 Measure
、Layout
、Record
、Execute
的数据计算工作,GPU复制栅格化、渲染。
CPU和GPU是通过图形驱动层来进行连接,图形驱动层维护了一个队列,CPU将display list添加到该队列中,这样GPU就可以从这个队列中取出数据进行绘制。
画面的流畅度的表现就是帧数,正常情况下,人眼在观察60fps的画面是流畅的、能够被大脑很舒服的接受的。
如果屏幕的刷新率要保持在60fps,就要屏幕在1秒钟刷新60次,所以每 16.6667ms
就要刷新一次。
Android系统每隔16ms发出VSYNC信号
,触发对UI渲染,如果每次都能渲染成功,那么就能达成流畅的60fps。
VSYNC信号
是垂直同步(Vertical Synchronization)的缩写。每次接收到 VSYNC信号,CPU就计算各种帧数据,如果某个操作要花费超过16ms的时间,那么用户就要在 (totalTime / 16 + 1) * 16 的时间内看到同一帧画面。
比如一帧CPU处理了17ms,超了1ms,这就导致在 32ms的时间内看到的是同一帧画面。
产生卡顿的原因有很多,主要有以下几点:
- 布局Layout过于复杂,无法在16ms内完成渲染
- 统一时间动画执行次数过多,导致CPU或GPU负载过重
- View过度绘制,导致某些像素在同一帧被绘制很多次
- 在UI线程中做了稍微耗时的操作
- GC回收时暂停时间过长或者频繁的GC产生大量的暂停时间
为了解决上述的问题,除了我们要在写代码时注意外,也可以借助一些工具来分析和解决卡顿问题。
1.2 Profile GPU Rendering
Profile GPU Rendering是Android4.1系统提供的开发辅助功能,我们可以在开发者选项中打开这一个功能。
单击 Profile GPU Rendering -> On screen as bars(中文版是 GPU渲染模式分析 -> 在屏幕上显示为条形图):
其中横轴代表时间,纵轴代表某一帧的耗时。绿色的横线为警戒线,超过这条线则意味着市场超过了了16ms,尽量要保证垂直的彩色柱状图保持在绿线下面,这些垂直的彩色柱状图代表着一帧,不同颜色的彩色柱状图代表不同的含义。
,具体如下:
他们的含义为:
- Swap Buffers:表示处理的时间,和上面讲到的橙色一样。
- Command Issue:表示执行的时间,和上面讲到的红色一样
- Sync & Upload:表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片的大小。
- Draw:表示测量和绘制视图列表所需要的时间,和上面讲到的蓝色一样。
- Measure/Layout:表示布局的onMeasure与onLayout所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题。
- Animation:表示计算执行动画所需要花费的时间,包含的动画有ObjectAnimator,ViewPropertyAnimator,Transition等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等。
- Input Handling:表示系统处理输入事件所耗费的时间,粗略等于对事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作。
- Misc Time/Vsync Delay:表示在主线程执行了太多的任务,导致UI渲染跟不上VSYNC的信号而出现掉帧的情况。
Profile GPU Rendering可以找到渲染有问题的界面,但是想要修复的话,只依赖Profile GPU Rendering是不够的,可以用另外一个工具Hierarchy Viewer来查看布局层次和每个View所花的时间,这个工具会在后面介绍。
1.3 Systrace
Systrace是Android4.1新增的性能数据采样和分析工具,它可以帮助开发者手机Android关键子系统(SufaceFlinger、WMS等Framework部分关键模块、服务,View体系系统等)的运行信息。
Systrace的功能包括跟踪系统的I/O操作、内核工作队列、CPU负载以及Android各个子系统的运行状况等。
所以可以用其来分析 UI渲染。
关于Systrace的使用:
Systrace跟踪的设备要在Android4.1上使用,其中4.3版本前后有区别,因为现在很少有4.3之前的手机了,所以就讲4.3之后的使用方法。
Systrace可以再DDMS上使用,可以使用命令行来使用,也可以在代码中进行跟踪。接下来介绍3种方式
1.在DDMS种使用Systrace
因为Google认为DDMS已经不好用了,所以在Android Studio3.0就删除它了。
我们要去 Sdk\tools 下找到 monitor.bat,运行它就能看到Android Device Monitor了。
选中当前追踪的手机,点中图中框出的地方:
最上面的路径就是Systrace生成html的路径。
第二行就是跟踪的时间,可以自己设置。
点击ok后,就会去追踪当前应用。
2. 用命令行使用Systrace
Android提供一个Python脚本文件systrace.py,它位于Android SDK目录 /tools/systrace中,我们可以使用以下命令来使用Systrace:
C:\>cd C:\Users\msn\AppData\Local\Android\Sdk\platform-tools\systrace
C:\Users\msn\AppData\Local\Android\Sdk\platform-tools\systrace>python systrace.py --time=10 -o newtrace.html sched gfx view wm
然后我的就提示:Systrace does not support Python 3.7. Please use Python 2.7.
…
也就是说Android Studio通过命令行使用Python查看Systrace,要用2.7版本的…
3. 在代码中使用Systrace
可以使用Trace类对应用中的具体活动进行追踪。Android源码中也引入了Trace类,为了展示得更加直观,这里RecyclerView得源码:
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
this.startInterceptRequestLayout();
this.onEnterLayoutOrScroll();
//这里开始使用了Trace进行追踪
TraceCompat.beginSection("RV Scroll");
this.fillRemainingScrollValues(this.mState);
int consumedX = 0;
int consumedY = 0;
if (dx != 0) {
consumedX = this.mLayout.scrollHorizontallyBy(dx, this.mRecycler, this.mState);
}
if (dy != 0) {
consumedY = this.mLayout.scrollVerticallyBy(dy, this.mRecycler, this.mState);
}
//这里开始暂停了Trace追踪
TraceCompat.endSection();
this.repositionShadowingViews();
this.onExitLayoutOrScroll();
this.stopInterceptRequestLayout(false);
if (consumed != null) {
consumed[0] = consumedX;
consumed[1] = consumedY;
}
}
TraceCompat
类对 Trace
类进行了封装,其中 beginSection()
和 endSection()
方法之间的代码会被追踪,endSection()
只会结束最近的 beginSection()
,因此要保证这个两个方法调用的次数要相同。
用Chrome分析Systrace
通过前面的方法生成了trace.html文件后,我们用Chrome打开它,如下图所示:
我们可以使用W、S键进行放大和缩小,A、D键进行左右移动。接下来介绍上图中的各个区域:
1.Alert区域
这个区域会标记性能有问题的点,单击叹号图标就是查看某一个Alert的问题描述,如下图所示:
这里指出:Scheduling delay说明这个处理特定时间片的线程很长时间没有被CPU调度,因此这个线程花了很久才完成。为什么会出现这种状况呢?可能是因为开启了太多的线程和UI线程竞争CPU资源,导致UI线程迟迟不能执行。
2.CPU区域
CPU区域,每一行代表一个CPU核心和它执行任务的时间片,放大后会看到每个色块代表一个执行过程,色块的长度表示执行时间。
CPU0主要执行adbb线程和InputReader线程,CPU 1主要执行surfaceflinger线程和ordinartorlayout进程中的RnderThread线程,我们单击每个色块,都能看到它做了什么事情,用了多久。
3. 应用区域
Systrace会给出应用中的Frames分析,每一帧就是一个F圈圈,F圈圈有三种颜色,绿色表示渲染流畅,黄色和红色则代表渲染的时间超过了16ms,红色更严重一点。图中的红色F点击后会告诉我们原因。
4. Alerts总体分析
单击最右边的Alerts按钮会给出Alert的总体分析:
Alerts会给出Alert类型,以及出现的次数。有了这些总体的分析,方便开发者对该间断的绘制性能有一个大概的了解,便于进行下一步分析。
由于Systrace是以系统的角度返回一些信息的,只能为我们提供一个概览,它的深度是有限的,我们可以用它来进行粗略的检查,以便了解大概的情况,但是如果还要分析的更加详细,比如要找到是什么让CPU繁忙,某些方法的调用次数等,则还要借助另一个工具 Traceview。
1.4 Traceview
TraceView是 AndroidSdk中自带的数据采集和分析工具,一般来说通过TraceView,我们可以得到两种数据:
- 单次执行耗时的方法
- 执行次数多的方法
如何使用Traceview
要分析traceview,首先要得到一个 trace文件。获取trace文件有两种方式:
- DDMS中使用
在Android Device Monitor中选择相应的进程,并单击Start Method Profiling
按钮
然后对应用中需要监控的点进行操作
最后单击Stop Method Profiling
按钮,会自动跳到TraceView师徒 - 在代码中加入调试语句
Debug.startMethodTracing();
...
Debug.stopMethodTracing();
系统会在SD卡中生成trace文件,将trace文件导出并用SDK中的Traceview打开即可。当然这需要我们为应用程序加上写权限。
然后我们就能得到trace文件。
接着我们来分析一下trace文件:
2.布局优化
一个界面的测量和绘制是通过递归来完成的,减少布局的层数就会减少测量和绘制的时间,从而性能就会得到提升。
当然这只是布局优化的一方面。
在讲布局优化之前,我们先来学习两种布局优化的工具,分别是 Hierarchy Viewer和Android Lint
2.1 Hierarchy Viewer
Hierarchy Viewer是Android SDK自带的可视化的调试工具,用来检查布局嵌套和绘制的时间。
我们通过 Android Device Monitor,在其中选择Hierarchy Viewer如下图:
打开后会给出一个Hierarchy Viewer窗口,在Hierarchy Viewer窗口中有4个子窗口,它们的作用如下:
- Windows
当前设备所有界面列表 - Tree View
将当前Activity 的所有View的层次按照高层到低层从左到右显示出来 - Tree Overview
全局概览,以缩略的形式显示 - Layout View
整体布局图,以手机屏幕上真是的位置呈现出来。
使用注意:
在Android的官方文档中提到:
To preserve security, Hierarchy Viewer can only connect to devices running a developer version of the Android system.
即:出于安全考虑,Hierarchy Viewer只能连接Android开发版手机或是模拟器(准确地说,只有ro.secure参数等于0且ro.debuggable等于1的android系统)。Hierarchy Viewer在连接手机时,手机上必须启动一个叫View Server的客户端与其进行socket通信。而在商业手机上,是无法开启View Server的,故Hierarchy Viewer是无法连接到普通的商业手机。
所以我们在使用Android Device Monitor
去连接真机/没有配置的模拟器 会提示: Unable to get view server version from device xxxx....
网上有很多教程,主要是通过Android逆向去替换系统文件,让系统通过可以调试的验证。
这边我的做法是使用 ViewServer的开源框架,在导入后,在Activity中的使用方法是:
public class MyActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set content view, etc.
ViewServer.get(this).addWindow(this);
}
public void onDestroy() {
super.onDestroy();
ViewServer.get(this).removeWindow(this);
}
public void onResume() {
super.onResume();
ViewServer.get(this).setFocusedWindow(this);
}
}
然后再去重新打开Hierarchy Viewer,在手机中打开使用这个代码的Activity就能看到层级结构了:
在我们开发中加入这些代码,当界面完成时,我们可以将这些调试的代码注释掉。
通过单击每个view,我们可以知道view的 layout、mesure、draw的耗时。
一个合格的布局,它应该是层级合理的。即纵向多,横向少。
2.2 Android Lint
Android Lint是在ADT 16中提供的新工具,它是一个代码扫描工具,通过代码静态检查来发现代码出现的潜在问题,并给出优化建议。检查的范围主要有以下几点:
- 正确性:Correctness
- 安全性:Security
- 性能:Performance
- 可用性:Usablity
- 可达性:Accessiblility
- 国际化:Internationalization
其功能十分强大,这里我们只关注 XML布局检查,可以通过AS的 Analyze->Inspect Code 来配置检查的范围。
点击OK后,就会出现所有问题的采集,以及每个问题种类的个数:
通过点击每一项,我们就能知道哪里出现了问题,或者说哪里的代码/资源文件没有被引用到。
我们可以通过在 Setting -> Editor-> Inspections 中配置Lint检查的范围。
2.3 布局优化方法
布局优化的方法有很多,主要包括 合理运用布局、Inculde、Merge和ViewStub。
1.合理运用布局
这个比较笼统。举个例子:
在布局很复杂时,合理运用 RelativeLayout可以来替代 LinearLayout,能减少很多层布局。
但是如果嵌套很多,使用 LinearLayout,性能要比 RelativeLayout要好。
这个还是要实际开发中,我们借助 Hierarchy Viewer等工具去判断。
2.用Include来进行布局的复用
这个比较常见。我们在项目中经常会好多个页面用到同一个View,比如说顶部栏、底部栏。我们就可以使用 <include>
标签来进行复用。
3.用Merge标签去除多余层级
Merge意味着合并,在合适的场景使用Merge标签可以减少多余的层级。
<Merge>
一般和 <include>
复用。 不如这么说,使用include并没有怎么照顾到性能而更多的是代码的复用性。
真正让 <include>
在性能上有意义的就是 Merge标签。具体用法之前讲过,这里不讲了。
merge标签最好是替代 FrameLayout或者布局方向一致的LinearLayout
4.使用ViewStub来提高加载速度
在XML中我们有时候会用到 GONE或者INVISIBLE 来隐藏一个View,但是这样其实效率不高,因为系统仍然会解析他们。
可以使用 ViewStub
来解决这一个问题。ViewStub是一个轻量级的View,不可见且不占据布局位置。
当ViewStub调用 inflate()
或设置可见时,系统会加载 ViewStub指定的布局,然后将这个布局添加到ViewStub中,在对ViewStub调用 inflate方法或者设置可见之前,它是不占布局空间和系统资源的。它主要的目的是为目标视图占用一个位置。
所以使用ViewStub可以调高界面初始化的性能,从而提高界面的加载速度。
我们在xml中代码加入:
...
<ViewStub
android:id="@+id/viewstub"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout="@layout/titlebar"/>
...
然后再代码中使用:
ViewStub viewstub = findViewById(R.id.viewstub);
viewstub.infate();
viewstub.setVisibility(View.VISIBLE);
使用的时候要注意三点:
- ViewStub只能被加载一次,加载后ViewStub对象会被置空,这样在ViewStub引用的布局被加载后,就不能用ViewStub来控制引用的布局了。因此如果一个控件需要不断地显示和隐藏,还是要使用 View的Visiblity属性
- ViewStub不能嵌套Merge标签。
- ViewStub操作的是布局文件,如果只是想操作具体的View,还是要使用View的Visiblity属性。
2.4 避免GPU过度绘制
过度绘制的概念:
如果一个像素在同一帧内被绘制了多次,从而浪费了GPU和CPU的资源,这就是过度绘制。
过度绘制的原因:
- 在XML布局中,空间有重叠,而且都有设置背景
- View的onDraw() 在同一区域绘制多次。
在实际项目中,要避免过度绘制其实是很难的,但是过多的过度绘制会浪费很多资源,并且导致性能的问题,因此避免过度绘制是十分有必要的。我们可以使用Android系统中自带的工具来检测过度绘制,首先系统要4.1以上,然后去开发者选项中找 “调试GPU过度绘制选项”,就可以进入到GPU绘制模式:
这时,屏幕会出现多种颜色:
- 白色
没有过度绘制,每个像素在屏幕上只绘制了一次 - 蓝色
一次过度绘制,…两次 - 绿色
二次过度绘制,…三次 - 粉色
三次过度绘制,…四次 - 红色
每个像素被绘制了五次或五次以上。
合格的页面绘制是以白色和蓝色为主的,绿色区域不能超过整体的三分之一。
如何避免过度绘制:
- 移除不需要的background
- 在自定义的View的 onDraw(),用
canvas.clipRect
来指定绘制的区域,防止重叠的组件发生过度绘制。