Android UI 卡頓及ANR檢測原理

一:背景

眾所周知,Android不允許在UI線程中做耗時的操作,否則有可能發(fā)生ANR的可能,默認(rèn)情況下,在Android中Activity的最長執(zhí)行時間是5秒,BroadcastReceiver的最長執(zhí)行時間則是10秒。如果操作這個時長,則會產(chǎn)生ANR。而本文所要介紹的主要內(nèi)容是檢測UI線程中的耗時操作,從而能夠定位一些老代碼中的各種耗時的操作,作為性能優(yōu)化的依據(jù)。

二:常見方案

1 通過UI 線程looper
2 通過Choreographer

2.1 通過UI 線程looper的打印日志

在github上也有許多開源庫是基于該原理,比較有代表行的有
AndroidPerformanceMonitor
ANR-WatchDog

下面以AndroidPerformanceMonitor為例子來進(jìn)行原理的介紹
AndroidPerformanceMonitor
?通過如下代碼可以看到,只存在一個主線程的Looper,所有通過主線程Handler發(fā)送的消息,都會發(fā)送到這里。

/**
     * Initialize the current thread as a looper, marking it as an
     * application's main looper. The main looper for your application
     * is created by the Android environment, so you should never need
     * to call this function yourself.  See also: {@link #prepare()}
     */
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

    /**
     * Returns the application's main looper, which lives in the main thread of the application.
     */
    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }

繼續(xù)仔細(xì)觀察Looper的源碼可以發(fā)現(xiàn),msg.target.dispatchMessage(msg); 這句用來進(jìn)行消息的分發(fā)處理,在處理前后分別會打印日志,如果我們在消息處理之前計一個時,在消息處理之后計算一個值,如果這兩者的閾值,大于了我們設(shè)定的門限,那么就可以認(rèn)為卡頓。

    public static void loop() {
        final Looper me = myLooper();
                ...
     
     for (;;) {
            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
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            msg.target.dispatchMessage(msg);

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
          }
              ...
        }
    }

但是MainLooper里面的默認(rèn)打印的消息,并沒有記錄時間,這時,我們需要通過Looper.setMessageLogging來設(shè)置自定義的 Printer 。

public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }

在BlockCanary中 定義了自定義的Printer
class LooperMonitor implements Printer
其println()方法邏輯就是消息處理之后各記錄時間值,計算這兩者的閾值,判斷是否block。

@Override
    public void println(String x) {
        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
            return;
        }
        if (!mPrintingStarted) {
            mStartTimestamp = System.currentTimeMillis();
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            mPrintingStarted = true;
            startDump();
        } else {
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
            stopDump();
        }
    }

流程圖


flow.png
2.2 通過UI 線程looper循環(huán)發(fā)送消息

比較有代表性的作品

ANR-WatchDog
實現(xiàn)十分的簡單,主要的類就是一個Thread, 通過使用主線程的Handler,不斷的發(fā)送任務(wù), 使得變量_tick的值不斷的增加,然后再去做判斷,_tick的值是否能得到,如果_tick的值沒有變,則認(rèn)為UI線程已經(jīng)卡頓.

private final Runnable _ticker = new Runnable() {
        @Override public void run() {
            _tick = (_tick + 1) % Integer.MAX_VALUE;
        }
    };
@Override
    public void run() {
        setName("|ANR-WatchDog|");

        int lastTick;
        int lastIgnored = -1;
        while (!isInterrupted()) {
            lastTick = _tick;
            _uiHandler.post(_ticker);
            try {
                  //睡眠一段時間,確保_tick的值更新后再做判斷。
                Thread.sleep(_timeoutInterval);
            }
            catch (InterruptedException e) {
                _interruptionListener.onInterrupted(e);
                return ;
            }

            // If the main thread has not handled _ticker, it is blocked. ANR.
            if (_tick == lastTick) {
                if (!_ignoreDebugger && Debug.isDebuggerConnected()) {
                    if (_tick != lastIgnored)
                        Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
                    lastIgnored = _tick;
                    continue ;
                }

                ANRError error;
                if (_namePrefix != null)
                    error = ANRError.New(_namePrefix, _logThreadsWithoutStackTrace);
                else
                    error = ANRError.NewMainOnly();
                _anrListener.onAppNotResponding(error);
                return;
            }
        }
}
通過Choreographer

Takt
Android系統(tǒng)從4.1(API 16)開始加入Choreographer這個類來控制同步處理輸入(Input)、動畫(Animation)、繪制(Draw)三個UI操作。其實UI顯示的時候每一幀要完成的事情只有這三種。

丟幀

Choreographer接收顯示系統(tǒng)的時間脈沖(垂直同步信號-VSync信號),在下一個frame渲染時控制執(zhí)行這些操作。
Choreographer.getInstance().postFrameCallback(new FPSFrameCallback());
把你的回調(diào)添加到Choreographer之中,那么在下一個frame被渲染的時候就會回調(diào)你的callback.
通過判斷兩次回調(diào)doFrame執(zhí)行的時間差,來判斷是否發(fā)生ANR
以Takt為例。一下為其核心代碼,主要代碼都添加了注釋。

@Override 
public void doFrame(long frameTimeNanos) {
    long currentTimeMillis = TimeUnit.NANOSECONDS.toMillis(frameTimeNanos);

    if (frameStartTime > 0) {
      // take the span in milliseconds
      final long timeSpan = currentTimeMillis - frameStartTime;
         //渲染次數(shù)+1
         framesRendered++;  
 if (timeSpan > interval) {
       //超過閾值,計算刷新頻率
        final double fps = framesRendered * 1000 / (double) timeSpan;

        frameStartTime = currentTimeMillis;
        framesRendered = 0;

        for (Audience audience : listeners) {
//回調(diào)處理
          audience.heartbeat(fps);
        }
      }
    } else {
      //第一次 frameStartTime=0;
      frameStartTime = currentTimeMillis;
    }
      choreographer.postFrameCallback(this);
}

三:參考資料

BlockCanary — 輕松找出Android App界面卡頓元兇
Cockroach
Choreographer源碼分析
Android應(yīng)用ANR分析

最后編輯于
?著作權(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)容