Android性能優(yōu)化-檢測App卡頓

移動APP性能評測-流暢度評測中,我們介紹了如何準確客觀評價APP的流暢度,最終采用SM指標來評價應用的流暢度,在知道如何評價流暢度之后,我們應該如何來檢測出APP中的UI卡頓就是我們面臨的一個新的問題;在Android性能優(yōu)化-App卡頓中介紹了Google官方提供的檢測卡頓的方法,除此之外還有那邊比較好的方法來檢測應用卡頓?目前主流的方法主要有:
1.利用UI線程Looper打印的日志,典型代表就是BlockCanary;
2.采用Choreographer;
BlockCanary:blockcanary是國內(nèi)開發(fā)者MarkZhai開發(fā)的一套性能監(jiān)控組件,它對主線程操作進行了完全透明的監(jiān)控,并能輸出有效的信息,幫助開發(fā)分析、定位到問題所在,迅速優(yōu)化應用;
BlockCanary核心原理:通過自定義一個Printer,設(shè)置到主線程ActivityThread的MainLooper中。MainLooper在dispatch消息前后都會調(diào)用Printer進行打印。從而獲取前后執(zhí)行的時間差值,判斷是否超過設(shè)置的閾值。如果超過,則會將記錄的棧信息及cpu信息發(fā)通知到前臺。和利用UI線程Looper打印日志原理一樣;
下面通過Blockcanary來簡單介紹它是如何來檢測應用卡頓的,然后簡單介紹通過Choreographer來檢測應用卡頓;

Blockcanary檢測APP卡頓

GitHub地址:BlockCanary
Blog in Chinese: BlockCanary.
blockcanary源碼學習隨筆
BlockCanary原理圖如下圖所示:

BlockCanary原理圖.png

其中最核心的兩步是在調(diào)用msg.target.dispatchMessage(msg),進行消息的分發(fā)前記錄時間T1,調(diào)用msg.target.dispatchMessage(msg)進行消息分發(fā)后記錄時間T2,如果T2-T1大于設(shè)置的卡頓閾值就會打印當前方法調(diào)用堆棧以及顯示其他相關(guān)提示或打印日志;
blockcanary充分的利用了Loop的機制,在MainLooper的loop方法中執(zhí)行dispatchMessage前后都會執(zhí)行printer的println進行輸出,并且提供了方法設(shè)置printer。通過分析前后打印的時差與閾值進行比對,從而判定是否卡頓。下面我們來看一下Looper中的loop方法;

Looper.java
    public static void loop() {
        // 獲取一個Looper對象
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        // 獲取Looper中的消息隊列
        final MessageQueue queue = me.mQueue;
        // 死循環(huán),對消息隊列里面的消息進行遍歷
        for (;;) {
            // 通過queue.next()取出消息,消息是在Handler.sendMessage方法中存到消息隊列里的
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                //用戶設(shè)置自己的Printer,在消息分發(fā)前調(diào)用Printer打印相關(guān)信息,此時獲取消息分發(fā)前的時間T1;
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                // 調(diào)用msg.target.dispatchMessage(msg),進行消息的分發(fā)。這里的msg.target就是發(fā)送這條消息的Handler對象。
                // 這樣Handler發(fā)送的消息最終又交回到它的dispatchMessage方法來處理。不同的是,Handler的dispatchMessage
                // 方法是在創(chuàng)建Handler時所使用的Looper中執(zhí)行的,這樣就成功將代碼邏輯切換到指定線程中去執(zhí)行了。
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logging != null) {
                //消息分發(fā)完成后,調(diào)用用戶自己設(shè)置的Printer.println()方法,此時獲取消息分發(fā)之后時間T2;
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
        }
    }
    
     /**
     * Control logging of messages as they are processed by this Looper.  If
     * enabled, a log message will be written to <var>printer</var>
     * at the beginning and ending of each message dispatch, identifying the
     * target Handler and message contents.
     *
     * @param printer A Printer object that will receive log messages, or
     * null to disable message logging.
     * 用戶可以設(shè)置自己的Printer,這樣在知道消息分發(fā)前后的時間,
     * 通過前后的時差與閾值進行對比,從而確定是否發(fā)生了卡頓
     */
    public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }

通過設(shè)置Printer我們可以檢測msg.target.dispatchMessage(msg)執(zhí)行時間,這樣就可以知道部分UI線程是否有耗時操作了。
BlockCanary的LooperMonitor的println方法如下:

LooperMonitor
@Override
    public void println(String x) {
        if (!mPrintingStarted) {
            //dispatchMesage前執(zhí)行的println
            //記錄開始時間
            mStartTimestamp = System.currentTimeMillis();
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            mPrintingStarted = true;
            //開始采集棧及cpu信息,最終會調(diào)用Stacksampler.start()方法;
            startDump();
        } else {
            //dispatchMesage后執(zhí)行的println
            //獲取結(jié)束時間
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            //判斷耗時是否超過閾值
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
           //最終會調(diào)用Stacksampler.stop()方法;
            stopDump();
        }
    }
    //判斷是否超過閾值
    private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
    //回調(diào)監(jiān)聽
    private void notifyBlockEvent(final long endTime) {
        final long startTime = mStartTimestamp;
        final long startThreadTime = mStartThreadTimestamp;
        final long endThreadTime = SystemClock.currentThreadTimeMillis();
        HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
            @Override
            public void run() {
                mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
            }
        });
    }

其中在startDump方法最終會調(diào)用Stacksampler.start()方法;stopDump最終會調(diào)用Stacksampler.stop()方法;相關(guān)方法如下:

Stacksampler
    public void start() {
        //在mRunable進行信息采集;  
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
        //通過一個HandlerThread延時執(zhí)行了mRunnable
        HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
                BlockCanaryInternals.getInstance().getSampleDelay());
    }
    public void stop() {
        //取消handler消息,如果未超時就不會采集相關(guān)信息
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
    }

在開始進行msg.target.dispatchMessage(msg)消息分發(fā)前通過HandlerThread發(fā)送一個延時runable,在msg.target.dispatchMessage(msg)消息分發(fā)后會remove該runable,如果指定的時間消息分發(fā)沒有完成,說明應用發(fā)生了卡頓,這之后開始執(zhí)行mRunable,在mRunable進行相關(guān)信息采集及提示APP發(fā)生卡頓;以上就是BlockCanary監(jiān)測卡頓的核心原理;

利用Choreographer監(jiān)測APP卡頓

Android系統(tǒng)每隔16ms發(fā)出VSYNC信號,觸發(fā)對UI進行渲染。開發(fā)者可以使用Choreographer#postFrameCallback設(shè)置自己的callback與Choreographer交互,你設(shè)置的FrameCallCack(doFrame方法)會在下一個frame被渲染時觸發(fā)。理論上來說兩次回調(diào)的時間周期應該在16ms,如果超過了16ms我們則認為發(fā)生了卡頓,我們主要就是利用兩次回調(diào)間的時間周期來判斷,

    Choreographer.getInstance()
        .postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long l) {
                //移除消息
                Handler.removeMessage();
                //發(fā)送延時消息
                Hnadler.sendMessageAtTime(...)
                Choreographer.getInstance().postFrameCallback(this);
            }
    });

發(fā)送的延時消息在執(zhí)行的時間沒有被remove掉,說明發(fā)生了卡頓,這時候可以進行卡頓相關(guān)信息的采集,如果在渲染下一幀的時候該消息還沒有被處理,這時候?qū)⒃撓emove掉,此場景說明未發(fā)生卡頓;該檢測卡頓的思想和BlockCanary類似;
最后,我們可以結(jié)合上述原理以及自己需求開發(fā)出一個適合自己的卡頓監(jiān)測方案,也可以參考已有開源方案。

其它

為什么主線程Looper.loop進行消息分發(fā)耗時就代表APP卡頓?
答:為了保證應用的平滑性,每一幀渲染時間不能超過16ms,達到60幀每秒;如果UI渲染慢的話,就會發(fā)生丟幀,這樣用戶就會感覺到不連貫性,我們稱之為Jank(APP卡頓);VSync信號由SurfaceFlinger實現(xiàn)并定時發(fā)送(每16ms發(fā)送),Choreographer.FrameDisplayEventReceiver收到信號后,調(diào)用onVsync方法組織消息發(fā)送到主線程處理。Choreographer主要功能是當收到VSync信號時,去調(diào)用使用通過postCallBack設(shè)置的回調(diào)函數(shù),在postCallBack調(diào)用doFrame,在doFrame中渲染下一幀;FrameDisplayEventReceiver相關(guān)代碼如下:

Choreographer.java
    /**
     * FrameDisplayEventReceiver繼承自DisplayEventReceiver接收底層的VSync信號開始處理UI過程。
     * VSync信號由SurfaceFlinger實現(xiàn)并定時發(fā)送。FrameDisplayEventReceiver收到信號后,
     * 調(diào)用onVsync方法組織消息發(fā)送到主線程處理。這個消息主要內(nèi)容就是run方法里面的doFrame了,
     * 這里mTimestampNanos是信號到來的時間參數(shù)。
     */
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
            super(looper, vsyncSource);
        }

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            // 發(fā)送Runnable(callback參數(shù)即當前對象FrameDisplayEventReceiver)到FrameHandler,請求執(zhí)行doFrame
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            // 此處mHandler為FrameHandler,該Handler對應的Looper是主線程的Looper
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
    }

在mHandler.sendMessageAtTime發(fā)送消息之后,最終會在主線程的Looper.loop()方法中調(diào)用msg.target.dispatchMessage(msg);Looper.loop相關(guān)代碼可以參考在本文上邊進行查看;然后在Handler.dispatchMeassange分發(fā)消息,如下所示:

Handler.java
    public void dispatchMessage(Message msg) {
        // Message的callback實際上就是Handler的post方法所傳遞的Runnable參數(shù)
        // 這里首先檢查是否有由Runnable封裝的消息,如果有,首先處理;
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            // 其次處理mCallback
            if (mCallback != null) {
                // 如果mCallback的handleMessage方法返回true,那么handler中的handleMessage方法是不會被執(zhí)行的
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    private static void handleCallback(Message message) {
        //在此執(zhí)行FrameDisplayEventReceiver中的run方法,最終執(zhí)行doFrame渲染下一幀;
        message.callback.run();
    }

通過以上流程可以發(fā)現(xiàn),Android渲染每一幀都是通過消息機制來實現(xiàn)的,最終都會在主線Looper.loop()方法中開始渲染下一幀,因為Looper.loop方法在進行消息分發(fā)時是串行執(zhí)行的,這樣如果上一個消息分發(fā)時間過長即msg.target.dispatchMessage(msg)執(zhí)行時間過長,就會導致在VSYNC到來時進行下一幀渲染延遲執(zhí)行,就不能保證該幀在16ms內(nèi)完成渲染,從而導致丟幀;所以主線程Looper.loop方法中msg.target.dispatchMessage(msg)執(zhí)行時間過長就會導致APP卡頓;因此通過檢測msg.target.dispatchMessage(msg)執(zhí)行時間就可以檢測APP卡頓;
Android消息機制的重要性
1.在卡頓監(jiān)測會用到消息機制;主要是發(fā)送一個延時消息來監(jiān)測是否,在執(zhí)行時間內(nèi)沒有remove該消息就代碼APP發(fā)生卡頓;
2.ANR監(jiān)測也是通過發(fā)送一個延時消息來監(jiān)測是否發(fā)生ANR;ANR是APP卡頓的極端情況;
3.View監(jiān)測事件是否長按也用到消息機制,在發(fā)生Down的時候會發(fā)送一個延時消息,在Up的時候會將該消息Remove掉,如果指定的時間沒有發(fā)生UP就會觸發(fā)長按事件;
4.Choreographer在渲染每一幀的時候也是通過發(fā)送一個消息,然后在Looper.loop中處理下一個消息時才會去渲染下一幀;
5.Activity生命周期的控制也是在ActivityThread發(fā)送不同的消息來切換Activity生命周期;
6.消息機制可以將一個任務(wù)切換到其它指定的線程,如AsyncTask;
以上這些場景都用到Android消息機制,還有很多其他未知的場景可能也會用到Android消息機制,所以消息機制在Android中具有很重要的地位;

參考資料
鴻洋:Android UI性能優(yōu)化 檢測應用中的UI卡頓
BlockCanary GitHub地址
Blog in Chinese: BlockCanary.
blockcanary源碼學習隨筆
Android Choreographer 源碼分析 講的很好很重要

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容