1, 感知卡顿
用户对卡顿的感知, 主要来源于界面的刷新. 而界面的性能主要是依赖于设备的UI渲染性能. 如果我们的UI设计过于复杂, 或是实现不够好, 设备又不给力, 界面就会像卡住了一样, 给用户卡顿的感觉.
1.1 16ms原则
在剖析卡顿的原因之前, 我们先来了解下Android中著名的"16ms"原则:
Android系统每隔16ms会发出VSYNC信号重绘我们的界面(Activity).
为什么是16ms, 因为Android设定的刷新率是60FPS(Frame Per Second), 也就是每秒60帧的刷新率, 约合16ms刷新一次.
就像是这样的:
这就意味着, 我们需要在16ms内完成下一次要刷新的界面的相关运算, 以便界面刷新更新. 然而, 如果我们无法在16ms内完成此次运算会怎样呢?
例如, 假设我们更新屏幕的背景图片, 需要24ms来做这次运算. 当系统在第一个16ms时刷新界面, 然而我们的运算还没有结束, 无法绘出图片. 当系统隔16ms再发一次VSYNC信息重绘界面时, 用户才会看到更新后的图片. 也就是说用户是32ms后看到了这次刷新(注意, 并不是24ms). 这就是传说中的丢帧(dropped frame):
丢帧给用户的感觉就是卡顿, 而且如果运算过于复杂, 丢帧会更多, 导致界面常常处于停滞状态, 卡到爆.
那么会有哪些常见的情况会导致运算超过16ms, 进而丢帧, 让用户觉得卡顿呢?
2, 卡顿原因分析及处理
一般来说, 会有以下几种情况导致卡顿这种性能问题, 我们逐一看下:
2.1 过于复杂的布局
上节有说, 界面性能取决于UI渲染性能. 我们可以理解为UI渲染的整个过程是由CPU和GPU两个部分协同完成的.
其中, CPU负责UI布局元素的Measure, Layout, Draw等相关运算执行. GPU负责栅格化(rasterization), 将UI元素绘制到屏幕上.
如果我们的UI布局层次太深, 或是自定义控件的onDraw中有复杂运算, CPU的相关运算就可能大于16ms, 导致卡顿.
这个时候, 我们需要借助Hierarchy Viewer这个工具来帮我们分析布局了. Hierarchy Viewer不仅可以以图形化树状结构的形式展示出UI层级, 还对每个节点给出了三个小圆点, 以指示该元素Measure, Layout, Draw的耗时及性能.
具体请参考App优化之Layout怎么摆.
2.2 过度绘制(Overdraw)
上节说的CPU方面的, 关于GPU的绘制, 如果我们的界面存在Overdraw, 也可能导致卡顿.
Overdraw: 用来描述一个像素在屏幕上多少次被重绘在一帧上.
通俗的说: 理想情况下, 每屏每帧上, 每个像素点应该只被绘制一次, 如果有多次绘制, 就是Overdraw, 过度绘制了.
2.2.1 调试Overdraw
Android系统提供了可视化的方案来让我们很方便的查看overdraw的现象:
在"系统设置"-->"开发者选项"-->"调试GPU过度绘制"中开启调试:
此时界面可能会有五种颜色标识:
- 原色: 没有overdraw
- 蓝色: 1次overdraw
- 绿色: 2次overdraw
- 粉色: 3次overdraw
- 红色: 4次及4次以上的overdraw
一般来说, 蓝色是可接受的, 是性能优的.
2.2.2 Overdraw的分析处理
上面有言, 所谓Overdraw, 就是在一个像素点上绘制了多次. 常见的就是:
- 绘制了多重背景.
- 绘制了不可见的UI元素.
还是以GithubApp这个App的代码为例调试, 打开应用, 展示是这样的:
可以看到是中间列表这块overdraw比较严重. 查看代码发现:
fragment_trending_container.xml中ViewPager设置了背景:
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:background="@color/md_white_1000"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
而ViewPager中的fragment又设置了背景:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refresh_layout"
android:background="@color/md_white_1000"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
</android.support.v4.widget.SwipeRefreshLayout
完整代码请查看Github上源码, 本文分析时commit截止到b01b5793.
删除外层ViewPager的背景再看:
可以发现中间列表区域已经不再是红色了, 但是也没有达到蓝色这个可以接受的层级. 这是因为我们的Activity默认情况下, theme会给window设置一个纯色的背景. 因为我们这里不想使用这个默认的背景,故而给layout加了一层背景, 导致了多重绘制背景.
当然我们也可以自定义主题, 将theme的window background设置成我们想要的, 而不在布局中设置.
可以通过如下方式去掉window的背景.
设置主题:
<item name="android:windowBackground">@null</item>
或是代码设置, 在onCreate中:
getWindow().setBackgroundDrawable(null);
此时我们看到的效果:
已基本达到优化水平.
以上旨在提供分析方法和思路.
Overdraw主要原因是背景的多重绘制, 或是不可见的View在背后绘制等, 但不仅限于此.
2.3 UI线程的复杂运算
如上文ANR相关分析中就说到UI线程的复杂运算会造成UI无响应, 当然更多的是造成UI响应停滞, 卡顿.
产生ANR已经是卡顿的极致了, 具体分析可以参看App优化之ANR详解一文.
关于运算阻塞导致的卡顿的分析, 可以使用Traceview这个工具.
具体Traceview的介绍, 以及实战分析, 可以参考App优化之提升你的App启动速度之理论基础和App优化之提升你的App启动速度之实例挑战.
在这里需要提下我们在性能分析工具中提到的StrictMode.
2.3.1 StrictMode的使用
StrictMode用来基于线程或VM设置一些策略, 一旦检测到策略违例, 控制台将输出一些警告,包含一个trace信息展示你的应用在何处出现问题.
通常用来检测主线程中的磁盘读写或网络访问等耗时操作.
在Application或是Activity的onCreate中开启StrictMode:
public void onCreate() {
if (BuildConfig.DEBUG) {
// 针对线程的相关策略
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build());
// 针对VM的相关策略
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}
如果你的线程出了问题, 控制台会有警告输出, 可以定位到代码.
相对简单, 在此就不多废话了.
解决UI线程的耗时操作方案, 可以参考ANR详解里面说到的那些线程模式.
2.4 频繁的GC
上面说的都是处理上的, CPU, GPU相关的. 实际上内存原因也可能会造成应用不流畅, 卡顿的.
说到这, 想起当年配台式机的三大件(CPU, 内存, 显示器)了. 貌似分析App性能也是这几大件啊 :)
为什么说频繁的GC会导致卡顿呢?
简而言之, 就是执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行, 故而如果程序频繁GC, 自然会导致界面卡顿.
以下内容参考自Android Performance Patterns:Memory Churn and Performance. 需FQ
导致频繁GC有两个原因:
- 内存抖动(Memory Churn), 即大量的对象被创建又在短时间内马上被释放.
- 瞬间产生大量的对象会严重占用Young Generation的内存区域, 当达到阀值, 剩余空间不够的时候, 也会触发GC. 即使每次分配的对象需要占用很少的内存,但是他们叠加在一起会增加Heap的压力, 从而触发更多的GC.
这些GC操作可能会造成上面说到的丢帧, 如下:
就会让用户感知到卡顿了.
一般来说瞬间大量产生对象一般是因为我们在代码的循环中new对象, 或是在onDraw中创建对象等. 所以说这些地方是我们尤其需要注意的...