BlockCanary — 輕松找出Android App界面卡頓元兇

BlockCanary是我利用個(gè)人時(shí)間開發(fā)的Android平臺(tái)上的一個(gè)輕量的,非侵入式的性能監(jiān)控組件,應(yīng)用只需要簡(jiǎn)單地加幾行,提供一些該組件需要的上下文環(huán)境就可以在使用應(yīng)用的時(shí)候檢測(cè)主線程上的各種卡頓問題,并通過(guò)組件提供的各種信息分析出原因并進(jìn)行修復(fù)。

開源代碼:markzhai/AndroidPerformanceMonitor

背景

在復(fù)雜的項(xiàng)目環(huán)境中,由于歷史代碼龐大,業(yè)務(wù)復(fù)雜,包含各種第三方庫(kù),偶爾再來(lái)個(gè)jni調(diào)用,所以在出現(xiàn)了卡頓的時(shí)候,我們很難定位到底是哪里出現(xiàn)了問題,即便知道是哪一個(gè)Activity/Fragment,也仍然需要進(jìn)去里面一行一行看,動(dòng)輒數(shù)千行的類再加上跳來(lái)跳去調(diào)來(lái)調(diào)去的,結(jié)果就是不了了之隨它去了,實(shí)在不行了再優(yōu)化吧。于是一拖再拖,最后可能壓根就改不動(dòng)了,客戶端越來(lái)越卡。

事實(shí)上,很多情況下卡頓不是必現(xiàn)的,它們可能與機(jī)型、環(huán)境、操作等有關(guān),存在偶然性,即使發(fā)生了,再去查那如山般的logcat,也不一定能找到卡頓的原因,是我們自己的應(yīng)用導(dǎo)致的還是其他應(yīng)用搶占資源導(dǎo)致的?是哪些方法導(dǎo)致的?很難去回朔。有些機(jī)型自己修改了api導(dǎo)致的卡頓,還必須拿那臺(tái)機(jī)器才能去調(diào)試找原因。

BlockCanary就是來(lái)解決這個(gè)問題的。
告別打點(diǎn),告別Debug,哪里卡頓,一目了然。

介紹

BlockCanary對(duì)主線程操作進(jìn)行了完全透明的監(jiān)控,并能輸出有效的信息,幫助開發(fā)分析、定位到問題所在,迅速優(yōu)化應(yīng)用。其特點(diǎn)有:

  • 非侵入式,簡(jiǎn)單的兩行就打開監(jiān)控,不需要到處打點(diǎn),破壞代碼優(yōu)雅性。
  • 精準(zhǔn),輸出的信息可以幫助定位到問題所在(精確到行),不需要像Logcat一樣,慢慢去找。

目前包括了核心監(jiān)控輸出文件,以及UI顯示卡頓信息功能。僅支持Android端。

原理

熟悉Message/Looper/Handler系列的同學(xué)們一定知道Looper.java中這么一段:

private static Looper sMainLooper;  // guarded by Looper.class

...

/**
 * 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;
    }
}

即整個(gè)應(yīng)用的主線程,只有這一個(gè)looper,不管有多少handler,最后都會(huì)回到這里。

如果再細(xì)心一點(diǎn)會(huì)發(fā)現(xiàn)在Looper的loop方法中有這么一段

public static void loop() {
    ...

    for (;;) {
        ...

        // 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);
        }

        ...
    }
}

是的,就是這個(gè)Printer - mLogging,它在每個(gè)message處理的前后被調(diào)用,而如果主線程卡住了,不就是在dispatchMessage里卡住了嗎?

核心流程圖:


flow

該組件利用了主線程的消息隊(duì)列處理機(jī)制,通過(guò)

Looper.getMainLooper().setMessageLogging(mainLooperPrinter);

并在mainLooperPrinter中判斷start和end,來(lái)獲取主線程dispatch該message的開始和結(jié)束時(shí)間,并判定該時(shí)間超過(guò)閾值(如2000毫秒)為主線程卡慢發(fā)生,并dump出各種信息,提供開發(fā)者分析性能瓶頸。


...
@Override
public void println(String x) {
    if (!mStartedPrinting) {
        mStartTimeMillis = System.currentTimeMillis();
        mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
        mStartedPrinting = true;
    } else {
        final long endTime = System.currentTimeMillis();
        mStartedPrinting = false;
        if (isBlock(endTime)) {
            notifyBlockEvent(endTime);
        }
    }
}

private boolean isBlock(long endTime) {
    return endTime - mStartTimeMillis > mBlockThresholdMillis;
}
...

說(shuō)到此處,想到是不是可以用mainLooperPrinter來(lái)做更多事情呢?既然主線程都在這里,那只要parse出app包名的第一行,每次打印出來(lái),是不是就不需要打點(diǎn)也能記錄出用戶操作路徑? 再者,比如想做onClick到頁(yè)面創(chuàng)建后的耗時(shí)統(tǒng)計(jì),是不是也能用這個(gè)原理呢? 之后可以試試看這個(gè)思路(目前存在問題是獲取線程堆棧是定時(shí)3秒取一次的,很可能一些比較快的方法操作一下子完成了沒法在stacktrace里面反映出來(lái))。

功能

BlockCanary會(huì)在發(fā)生卡頓(通過(guò)MonitorEnv的getConfigBlockThreshold設(shè)置)的時(shí)候記錄各種信息,輸出到配置目錄下的文件,并彈出消息欄通知(可關(guān)閉)。

簡(jiǎn)單的使用如在開發(fā)、測(cè)試、Monkey的時(shí)候,Debug包啟用

  • 開發(fā)可以通過(guò)圖形展示界面直接看信息,然后進(jìn)行修復(fù)
  • 測(cè)試可以把log丟給開發(fā),也可以通過(guò)卡慢詳情頁(yè)右上角的更多按鈕,分享到各種聊天軟件(不要懷疑,就是抄的LeakCanary)
  • Monkey生成一堆的log,找個(gè)專人慢慢過(guò)濾記錄下重要的卡慢吧

還可以通過(guò)Release包用戶端定時(shí)開啟監(jiān)控并上報(bào)log,后臺(tái)匹配堆棧過(guò)濾同類原因,提供給開發(fā)更大的樣本環(huán)境來(lái)優(yōu)化應(yīng)用。

本項(xiàng)目提供了一個(gè)友好的展示界面,供開發(fā)測(cè)試直接查看卡慢信息(基于LeakCanary的界面修改)。

dump的信息包括:

  • 基本信息:安裝包標(biāo)示、機(jī)型、api等級(jí)、uid、CPU內(nèi)核數(shù)、進(jìn)程名、內(nèi)存、版本號(hào)等
  • 耗時(shí)信息:實(shí)際耗時(shí)、主線程時(shí)鐘耗時(shí)、卡頓開始時(shí)間和結(jié)束時(shí)間
  • CPU信息:時(shí)間段內(nèi)CPU是否忙,時(shí)間段內(nèi)的系統(tǒng)CPU/應(yīng)用CPU占比,I/O占CPU使用率
  • 堆棧信息:發(fā)生卡慢前的最近堆棧,可以用來(lái)幫助定位卡慢發(fā)生的地方和重現(xiàn)路徑

sample如下圖,可以精確定位到代碼中哪一個(gè)類的哪一行造成了卡慢。


blockcanary log sample

總結(jié)

BlockCanary作為一個(gè)Android組件,目前還有局限性,因?yàn)槠湓谝粋€(gè)完整的監(jiān)控系統(tǒng)中只是一個(gè)生產(chǎn)者,還需要對(duì)應(yīng)的消費(fèi)者去分析日志,比如歸類排序,以便看出哪些卡慢更有修復(fù)價(jià)值,需要優(yōu)先處理;又比如需要過(guò)濾機(jī)型,有些奇葩機(jī)型的問題造成的卡慢,到底要不要去修復(fù)是要斟酌的。扯遠(yuǎn)一點(diǎn)的話,像是埋點(diǎn)除了統(tǒng)計(jì)外,完全還能用來(lái)做鏈路監(jiān)控,比如一個(gè)完整的流程是A -> B -> D -> E, 但是某個(gè)時(shí)間節(jié)點(diǎn)突然A -> B -> D后沒有到達(dá)E,這時(shí)候監(jiān)控平臺(tái)就可以發(fā)出預(yù)警,讓開發(fā)人員及時(shí)定位。很多監(jiān)控方案都需要C/S兩端的配合。

目前阿里內(nèi)多個(gè)Android項(xiàng)目接入并使用BlockCanary來(lái)優(yōu)化Android應(yīng)用的性能。

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

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

  • 今天面試的時(shí)候被問到怎么解決App界面卡頓問題,之前的做法是用hierarchy viewer去看一下view的繪...
    jimmy_Hu閱讀 775評(píng)論 0 1
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,828評(píng)論 25 709
  • Tips 因?yàn)樽约荷婕暗墓ぷ鲀?nèi)容范圍比較廣,經(jīng)常需要切換,所以就有了這個(gè)工具記錄。主要的領(lǐng)域是關(guān)于產(chǎn)品的,但是也人...
    雨果僧閱讀 599評(píng)論 0 1
  • 我小學(xué)時(shí)代在我媽工作的小學(xué)讀書,小學(xué)在鎮(zhèn)子上,所以每個(gè)周末,我們都要做將近一個(gè)小時(shí)的車。這么長(zhǎng)的車程,又不睡...
    飛鳥與浮云閱讀 380評(píng)論 5 11

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