性能优化最佳实践#UI卡顿优化

104 阅读6分钟

性能优化最佳实践#启动优化

性能优化最佳实践#UI卡顿优化

性能优化最佳实践#内存优化

性能优化最佳实践#Crash机制

性能优化最佳实践#ANR优化

性能优化最佳实践#体积包优化

一.理解渲染机制与卡顿分析

我们知道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定位具体的代码,总的来说,如果是本身代码问题,通过以上手段,基本能定位问题。