一.理解渲染机制与卡顿分析
我们知道CPU是负责计算,GPU负责渲染的,android的VSYNC机制是每16.6ms会同步一次信号,CPU + GPU 处理时间大于 16.66ms 就会引发跳帧,多次跳帧从视觉上就是卡顿,所以一般造成UI卡顿的原因主要有两种:
1.UI层面上布局层级嵌套过深、过度绘制;
2.系统资源引起的卡顿:内存不足引发频繁 GC、线程阻塞/锁阻塞(Binder、IO)
二.卡顿分析与优化手段
1.Layout Inspector 查看布局嵌套
Layout Inspector是androidstudio自带的查看布局嵌套的工具,我们可以通过它来查看布局嵌套,一般我们在开发中尽量不要做过多的嵌套布局,尽量使用merge、include这些标签来减少布局嵌套,因为层级越深,由于LayoutInfatexml布局时他走的是递归方法,过多的方法栈可能会导致内存问题;
2.systrace 工具的使用
systrace是一个系统级的性能检测工具,我们可以通过它来做一些更深层次的性能检测,一般情况下,我们用它定位下。
**方式1: DDMS中已经集成了Systrace,我们打开DDMS找到Systrace的按钮,就可以抓取相关信息了
**点击之后会弹出一个对话框,再点击ok就会生成相应的trace.html文件了
**方式2:利用命令行去生成trace.html
****systrace 工具存放在 sdk 目录 platform-tools/systrace/systracce.py,在使用前需要安装相关环境:
**1.下载一个python,建议下载python2.7**的版本;
****2.安装好python、配置好环境变量后,如果你使用该命令:
****python systrace.py --time=10 -o mytrace.html sched gfx view wm 可能还会报错:
**ImportError: No module named win32con,所以你需要安装six**库,然后在解压后的目录使用python安装:
****python setup.py install;
****3.ImportError: No module named win32con 问题,利用
**python -m pip install pypiwin32命令去下载安装一个就可以了。
**上面环境的配置完成之后,我们在代码中加入 Trace.beginSection() 和 Trace.endSection() 开始和停止记录。
****在开始的地方调用TraceCompat.beginSection("SystraceAppOnCreate"),在结束的地方调用TraceCompat.endSection();
****命令行进入到 systrace 目录启动 systrace.py,程序运行启动后回车导出 trace.html:
****cd sdk\platform-tools\systrace
****敲下命令:python systrace.py --time=10 -o mytrace.html sched gfx view wm -a pkgName
**就会生成traace.xml文件。如何查看这个文件,网上有很多文章,这里就不赘述了,一般主要查看下掉帧情况下,系统的状态,如cpu是否正常等状态。
**systrace支持的命令参考Android文档:
https://developer.android.google.cn/topic/performance/tracing/command-line?hl=zh-cn
**你也可以通过性能分析工具Systrace的使用详解这篇文章查看,具体的命令。
3.编舞者Choreograph检测
我们知道,Choreographer 是配合 Vsync 同步告知 CPU 开始测量布局 和 GPU 渲染绘制的工具类,所以我们通过Choreographer 可以很方便的计算每帧的耗时,也就能计算出是否存在跳帧。
public class ChoreographerHelper {
private static final String TAG = "ChoreographerHelper";
static long lastFrameTimeNanos = 0;
public static void start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//上次回调时间
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
return;
}
long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
if (diff > 16.6f) {
//掉帧数
int droppedCount = (int) (diff / 16.6);
if (droppedCount > 2) {
Log.w(TAG, "UI线程超时(超过16ms)当前:" + diff + "ms" + " , 丢帧:" + droppedCount);
}
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
}
在非生产环境可以使用 Choreographer 监控每一帧的状态和计算丢帧数量,我们可以将它开启后在 app 上运行,如果出现了丢帧数多的情况就能定位到某个界面会出现卡顿了。
丢 1 帧不算丢,丢 2 帧和底层运行时间也有关系,如果出现丢 3 帧但频率不高的情况一般不会卡,如果有超过 5 帧的就会卡顿。
4.使用looper快速定位
我们知道android是基于事件驱动的,而looper就是对管理轮询分发消息事件的,所以我们可以通过设置主线程的事件打印,来迅速定位卡顿问题,这个也是BlockCanary原理
public static void loop() {
...
for (;;) {
...
// 事件处理前,获取 Printer 打印
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
...
// 事件处理后,打印已处理
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}
我们也写一个自己的主线程定位卡顿小框架
object MyBlockCanary {
fun install() {
val logMonitor = LogMonitor()
//打印主线程的message
Looper.getMainLooper().setMessageLogging(logMonitor)
}
}
class LogMonitor :Printer{
private var mStackSampler: StackSampler
private var mPrintingStarted = false //是否已经开始
private var mStartTimestamp: Long = 0
// 卡顿阈值
private val mBlockThresholdMillis = (5 * 16.66).toLong()
//采样频率
private val mSampleInterval: Long = 1000
private var mLogHandler: Handler? = null
constructor() {
mStackSampler = StackSampler(mSampleInterval)
val handlerThread = HandlerThread("block-canary-io")
handlerThread.start()
mLogHandler = Handler(handlerThread.looper)
}
//主线程打印信息
override fun println(x: String?) {
//从if到else会执行 dispatchMessage,如果执行耗时超过阈值,输出卡顿信息
if (!mPrintingStarted) {
Log.d("block-canary","开始记录")
//记录开始时间
mStartTimestamp = System.currentTimeMillis()
mPrintingStarted = true
mStackSampler.startDump()
} else {
Log.d("block-canary","结束记录")
mPrintingStarted = false
val endTime = System.currentTimeMillis()
//出现卡顿 当记录时间超过5个刷新时 打印堆栈信息
if (isBlock(endTime)) {
notifyBlockEvent(endTime)
}
//停止堆栈打印
mStackSampler.stopDump()
}
}
private fun notifyBlockEvent(endTime: Long) {
Log.d("block-canary","mLogHandler:$mLogHandler")
mLogHandler?.post(Runnable { //获取卡顿时主线程堆栈
val stacks: List<String> = mStackSampler.getStacks(mStartTimestamp, endTime)
for (stack in stacks) {
Log.d("block-canary", stack)
}
})
}
private fun isBlock(endTime: Long): Boolean {
val costTime = endTime - mStartTimestamp
Log.d("block-canary","costTime:$costTime")
return costTime> mBlockThresholdMillis
}
}
class StackSampler(sampleInterval: Long) {
val SEPARATOR = "\r\n"
val TIME_FORMATTER: SimpleDateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS")
private var mHandler: Handler? = null
private val mStackMap: MutableMap<Long, String> = LinkedHashMap()
private val mMaxCount = 100 //最大栈保存数量
private var mSampleInterval: Long = sampleInterval //堆栈的采样频率
//是否需要采样
protected var mShouldSample = AtomicBoolean(false)
init {
val handlerThread = HandlerThread("block-canary-sampler")
handlerThread.start()
mHandler = Handler(handlerThread.looper)
}
/**
* 开始采样 执行堆栈
*/
fun startDump() {
//避免重复开始
if (mShouldSample.get()) {
return
}
mShouldSample.set(true)
mHandler?.removeCallbacks(mRunnable)
mHandler?.postDelayed(mRunnable, mSampleInterval)
}
fun stopDump() {
if (!mShouldSample.get()) {
return
}
mShouldSample.set(false)
mHandler?.removeCallbacks(mRunnable)
}
fun getStacks(startTime: Long, endTime: Long): List<String> {
val result = ArrayList<String>()
synchronized(mStackMap) {
for (entryTime in mStackMap.keys) {
if (entryTime in (startTime + 1)..<endTime) {
result.add(
TIME_FORMATTER.format(entryTime)
+ SEPARATOR
+ SEPARATOR
+ mStackMap[entryTime]
)
}
}
}
return result
}
private val mRunnable: Runnable = object : Runnable {
override fun run() {
val sb = StringBuilder()
val stackTrace = Looper.getMainLooper().thread.stackTrace
for (s in stackTrace) {
sb.append(s.toString()).append("\n")
}
synchronized(mStackMap) {
//最多保存100条堆栈信息
if (mStackMap.size == mMaxCount) {
mStackMap.remove(mStackMap.keys.iterator().next())
}
mStackMap.put(System.currentTimeMillis(), sb.toString())
}
if (mShouldSample.get()) {
mHandler?.postDelayed(this, mSampleInterval)
}
}
}
}
三.总结
UI卡顿核心原理就是在vsync垂直信号的16.6毫秒,我们的cpu计算和gpu绘制是否能完成,如果完成不了就会导致掉帧,如果掉帧较多,就会出现明显卡顿,一般情况下,我们可以通过减少布局嵌套,可以通过systrace来查看卡顿时系统的状态,如果说卡顿时cpu是stw状态,可能要考虑是线程、io、内存等导致的问题,我们也可以通过编舞者看下掉帧的具体情况,通过仿blockCanary定位具体的代码,总的来说,如果是本身代码问题,通过以上手段,基本能定位问题。