深刻探索Android卡頓優化(下)

前言

成爲一名優秀的Android開發,須要一份完備的知識體系,在這裏,讓咱們一塊兒成長爲本身所想的那樣~。

在上篇文章中,筆者帶領你們學習了卡頓優化分析方法與工具、自動化卡頓檢測方案及優化這兩塊內容。若是對這塊內容還不瞭解的同窗建議先看看《深刻探索Android卡頓優化(上)》。本篇,爲深刻探索Android卡頓優化的下篇。這篇文章包含的主要內容以下所示:html

  • 一、ANR分析與實戰
  • 二、卡頓單點問題檢測方案
  • 三、高效實現界面秒開
  • 四、優雅監控耗時盲區
  • 五、卡頓優化技巧總結
  • 六、常見卡頓問題解決方案總結
  • 七、卡頓優化的常見問題

卡頓時間過長,必定會形成應用發生ANR。下面,咱們就來從應用的ANR分析與實戰來開始今天的探索之旅。java

1、ANR分析與實戰

一、ANR介紹與實戰

首先,咱們再來回顧一下ANR的幾種常見的類型,以下所示:linux

  • 一、KeyDispatchTimeout:按鍵事件在5s的時間內沒有處理完成。
  • 二、BroadcastTimeout:廣播接收器在前臺10s,後臺60s的時間內沒有響應完成。
  • 三、ServiceTimeout:服務在前臺20s,後臺200s的時間內沒有處理完成。

具體的時間定義咱們能夠在AMS(ActivityManagerService)中找到:android

// How long we allow a receiver to run before giving up on it.
static final int BROADCAST_FG_TIMEOUT = 10*1000;
static final int BROADCAST_BG_TIMEOUT = 60*1000;

// How long we wait until we timeout on key dispatching.
static final int KEY_DISPATCHING_TIMEOUT = 5*1000;
複製代碼

接下來,咱們來看一下ANR的執行流程。git

ANR執行流程

  • 一、首先,咱們的應用發生了ANR。
  • 二、而後,咱們的進程就會接收到異常終止信息,並開始寫入進程ANR信息,也就是當時應用的場景信息,它包含了應用全部的堆棧信息、CPU、IO等使用的狀況。
  • 三、最後,會彈出一個ANR提示框,看你是要選擇繼續等待仍是退出應用,須要注意這個ANR提示框不必定會彈出,根據不一樣ROM,它的表現狀況也不一樣。由於有些手機廠商它會默認去掉這個提示框,以免帶來很差的用戶體驗。

分析完ANR的執行流程以後,咱們來分析下怎樣去解決ANR,究竟哪裏能夠做爲咱們的一個突破點。github

在上面咱們說過,當應用發生ANR時,會寫入當時發生ANR的場景信息到文件中,那麼,咱們可不能夠經過這個文件來判斷是否發生了ANR呢?算法

關於根據ANR log進行ANR問題的排查與解決的方式筆者已經在深刻探索Android穩定性優化的第三節ANR優化中講解過了,這裏就很少贅述了。shell

線上ANR監控方式

深刻探索Android穩定性優化的第三節ANR優化中我說到了使用FileObserver能夠監聽 /data/anr/traces.txt的變化,利用它能夠實現線上ANR的監控,可是它有一個致命的缺點,就是高版本ROM須要root權限,解決方案是隻能經過海外Google Play服務、國內Hardcoder的方式去規避。可是,這在國內顯然是不現實的,那麼,有沒有更好的實現方式呢?json

那就是ANR-WatchDog,下面我就來詳細地介紹一下它。c#

ANR-WatchDog項目地址

ANR-WatchDog是一種非侵入式的ANR監控組件,能夠用於線上ANR的監控,接下來,咱們就使用ANR-WatchDog來監控ANR。

首先,在咱們項目的app/build.gradle中添加以下依賴:

implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'
複製代碼

而後,在應用的Application的onCreate方法中添加以下代碼啓動ANR-WatchDog:

new ANRWatchDog().start();
複製代碼

能夠看到,它的初始化方式很是地簡單,同時,它內部的實現也很是簡單,整個庫只有兩個類,一個是ANRWatchDog,另外一個是ANRError。

接下來咱們來看一下ANRWatchDog的實現方式。

/**
* A watchdog timer thread that detects when the UI thread has frozen.
*/
public class ANRWatchDog extends Thread {
複製代碼

能夠看到,ANRWatchDog其實是繼承了Thread類,也就是它是一個線程,對於線程來講,最重要的就是其run方法,以下所示:

private static final int DEFAULT_ANR_TIMEOUT = 5000;

private volatile long _tick = 0;
private volatile boolean _reported = false;

private final Runnable _ticker = new Runnable() {
    @Override public void run() {
        _tick = 0;
        _reported = false;
    }
};

@Override
public void run() {
    // 一、首先,將線程命名爲|ANR-WatchDog|。
    setName("|ANR-WatchDog|");

    // 二、接着,聲明瞭一個默認的超時間隔時間,默認的值爲5000ms。
    long interval = _timeoutInterval;
    // 三、而後,在while循環中經過_uiHandler去post一個_ticker Runnable。
    while (!isInterrupted()) {
        // 3.1 這裏的_tick默認是0,因此needPost即爲true。
        boolean needPost = _tick == 0;
        // 這裏的_tick加上了默認的5000ms
        _tick += interval;
        if (needPost) {
            _uiHandler.post(_ticker);
        }

        // 接下來,線程會sleep一段時間,默認值爲5000ms。
        try {
            Thread.sleep(interval);
        } catch (InterruptedException e) {
            _interruptionListener.onInterrupted(e);
            return ;
        }

        // 四、若是主線程沒有處理Runnable,即_tick的值沒有被賦值爲0,則說明發生了ANR,第二個_reported標誌位是爲了不重複報道已經處理過的ANR。
        if (_tick != 0 && !_reported) {
            //noinspection ConstantConditions
            if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
                Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
                _reported = true;
                continue ;
            }

            interval = _anrInterceptor.intercept(_tick);
            if (interval > 0) {
                continue;
            }

            final ANRError error;
            if (_namePrefix != null) {
                error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
            } else {
                // 五、若是沒有主動給ANR_Watchdog設置線程名,則會默認會使用ANRError的NewMainOnly方法去處理ANR。
                error = ANRError.NewMainOnly(_tick);
            }
           
           // 六、最後會經過ANRListener調用它的onAppNotResponding方法,其默認的處理會直接拋出當前的ANRError,致使程序崩潰。 _anrListener.onAppNotResponding(error);
            interval = _timeoutInterval;
            _reported = true;
        }
    }
}
複製代碼

首先,在註釋1處,咱們將線程命名爲了|ANR-WatchDog|。接着,在註釋2處,聲明瞭一個默認的超時間隔時間,默認的值爲5000ms。而後,註釋3處,在while循環中經過_uiHandler去post一個_ticker Runnable。注意這裏的_tick默認是0,因此needPost即爲true。接下來,線程會sleep一段時間,默認值爲5000ms。在註釋4處,若是主線程沒有處理Runnable,即_tick的值沒有被賦值爲0,則說明發生了ANR,第二個_reported標誌位是爲了不重複報道已經處理過的ANR。若是發生了ANR,就會調用接下來的代碼,開始會處理debug的狀況,而後,咱們看到註釋5處,若是沒有主動給ANR_Watchdog設置線程名,則會默認會使用ANRError的NewMainOnly方法去處理ANR。ANRError的NewMainOnly方法以下所示:

/**
 * The minimum duration, in ms, for which the main thread has been blocked. May be more.
 */
public final long duration;

static ANRError NewMainOnly(long duration) {
    // 一、獲取主線程的堆棧信息
    final Thread mainThread = Looper.getMainLooper().getThread();
    final StackTraceElement[] mainStackTrace = mainThread.getStackTrace();

    // 二、返回一個包含主線程名、主線程堆棧信息以及發生ANR的最小時間值的實例。
    return new ANRError(new $(getThreadTitle(mainThread), mainStackTrace).new _Thread(null), duration);
}
複製代碼

能夠看到,在註釋1處,首先獲了主線程的堆棧信息,而後返回了一個包含主線程名、主線程堆棧信息以及發生ANR的最小時間值的實例。(咱們能夠改造其源碼在此時添加更多的卡頓現場信息,如CPU 使用率和調度信息、內存相關信息、I/O 和網絡相關的信息等等

接下來,咱們再回到ANRWatchDog的run方法中的註釋6處,最後這裏會經過ANRListener調用它的onAppNotResponding方法,其默認的處理會直接拋出當前的ANRError,致使程序崩潰。對應的代碼以下所示:

private static final ANRListener DEFAULT_ANR_LISTENER = new ANRListener() {
    @Override public void onAppNotResponding(ANRError error) {
        throw error;
    }
};
複製代碼

瞭解了ANRWatchDog的實現原理以後,咱們試一試它的效果如何。首先,咱們給MainActivity中的懸浮按鈕添加主線程休眠10s的代碼,以下所示:

@OnClick({R.id.main_floating_action_btn})
void onClick(View view) {
    switch (view.getId()) {
        case R.id.main_floating_action_btn:
            try {
                // 對應項目中的第170行
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            jumpToTheTop();
            break;
        default:
            break;
    }
}
複製代碼

而後,咱們從新安裝運行項目,點擊懸浮按鈕,發如今10s內都不能觸發屏幕點擊和觸摸事件,而且在10s以後,應用直接發生了崩潰。接着,咱們在Logcat過濾欄中輸入fatal關鍵字,找出致命的錯誤,log以下所示:

2020-01-18 09:55:53.459 29924-29969/? E/AndroidRuntime: FATAL EXCEPTION: |ANR-WatchDog|
Process: json.chao.com.wanandroid, PID: 29924
com.github.anrwatchdog.ANRError: Application Not Responding for at least 5000 ms.
Caused by: com.github.anrwatchdog.ANRError$$$_Thread: main (state = TIMED_WAITING)
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:373)
    at java.lang.Thread.sleep(Thread.java:314)
    // 1
    at json.chao.com.wanandroid.ui.main.activity.MainActivity.onClick(MainActivity.java:170)
    at json.chao.com.wanandroid.ui.main.activity.MainActivity_ViewBinding$1.doClick(MainActivity_ViewBinding.java:45)
    at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
    at android.view.View.performClick(View.java:6311)
    at android.view.View$PerformClick.run(View.java:24833)
    at android.os.Handler.handleCallback(Handler.java:794)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:173)
    at android.app.ActivityThread.main(ActivityThread.java:6653)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
 Caused by: com.github.anrwatchdog.ANRError$$$_Thread: AndroidFileLogger./storage/emulated/0/Android/data/json.chao.com.wanandroid/log/ (state = RUNNABLE)
複製代碼

能夠看到,發生崩潰的線程正是|ANR-WatchDog|。咱們重點關注註釋1,這裏發生崩潰的位置是在MainActivity的onClick方法,對應的行數爲170行,從前可知,這裏正是線程休眠的地方。

接下來,咱們來分析一下ANR-WatchDog的實現原理。

二、ANR-WatchDog原理

  • 首先,咱們調用了ANR-WatchDog的start方法,而後這個線程就會開始工做。
  • 而後,咱們經過主線程的Handler post一個消息將主線程的某個值進行一個加值的操做
  • post完成以後呢,咱們這個線程就sleep一段時間。
  • 在sleep以後呢,它就會來檢測咱們這個值有沒有被修改,若是這個值被修改了,那就說明咱們在主線程中執行了這個message,即代表主線程沒有發生卡頓,不然,則說明主線程發生了卡頓
  • 最後,ANR-WatchDog就會判斷髮生了ANR,拋出一個異常給咱們。

最後,ANR-WatchDog的工做流程簡圖以下所示:

image

上面咱們最後說到,若是檢測到主線程發生了卡頓,則會拋出一個ANR異常,這將會致使應用崩潰,顯然不能將這種方案帶到線上,那麼,有什麼方式可以自定義最後發生卡頓時的處理過程嗎?

其實ANR-WatchDog自身就實現了一個咱們自身也能夠去實現的ANRListener,經過它,咱們就能夠對ANR事件去作一個自定義的處理,好比將堆棧信息壓縮後保存到本地,並在適當的時間上傳到APM後臺。

三、小結

ANR-WatchDog是一種非侵入式的ANR監控方案,它可以彌補咱們在高版本中沒有權限去讀取traces.txt文件的問題,須要注意的是,在線上這兩種方案咱們須要結合使用。

在以前,咱們還講到了AndroidPerformanceMonitor,那麼它和ANR-WatchDog有什麼區別呢?

對於AndroidPerformanceMonitor來講,它是監控咱們主線程中每個message的執行,它會在主線程的每個message的先後打印一個時間戳,而後,咱們就能夠據此計算每個message的具體執行時間,可是咱們須要注意的是一個message的執行時間一般是很是短暫的,也就是很難達到ANR這個級別。而後咱們來看看ANR-WatchDog的原理,它是無論應用是如何執行的,它只會看最終的結果,即sleep 5s以後,我就看主線程的這個值有沒有被更改。若是說被改過,就說明沒有發生ANR,不然,就代表發生了ANR

根據這兩個庫的原理,咱們即可以判斷出它們分別的適用場景,對於AndroidPerformanceMonitor來講,它適合監控卡頓,由於每個message它執行的時間並不長。對於ANR-WatchDog來講,它更加適合於ANR監控的補充

此外,雖然ANR-WatchDog解決了在高版本系統沒有權限讀取 /data/anr/traces.txt 文件的問題,可是在Java層去獲取全部線程堆棧以及各類信息很是耗時,對於卡頓場景不必定合適,它可能會進一步加重用戶的卡頓。若是是對性能要求比較高的應用,能夠經過Hook Native層的方式去得到全部線程的堆棧信息,具體爲以下兩個步驟:

經過這種方式就大體模擬了系統打印 ANR 日誌的流程,可是因爲採用的是Hook方式,因此可能會產生一些異常甚至崩潰的狀況,這個時候就須要經過 fork 子進程方式去避免這種問題,並且使用 子進程去獲取堆棧信息的方式能夠作到徹底不卡住咱們主進程。

可是須要注意的是,fork 進程會致使進程號發生改變,此時須要經過指定 /proc/[父進程 id]的方式從新獲取應用主進程的堆棧信息

經過 Native Hook 的 方式咱們實現了一套「無損」獲取全部 Java 線程堆棧與詳細信息的卡頓監控體系。爲了下降上報數據量,建議只有主線程的 Java 線程狀態是 WAITING、TIME_WAITING 或者 BLOCKED 的時候,纔去使用這套方案

2、卡頓單點問題檢測方案

除了自動化的卡頓與ANR監控以外,咱們還須要進行卡頓單點問題的檢測,由於上述兩種檢測方案的並不能知足全部場景的檢測要求,這裏我舉一個小栗子:

好比我有不少的message要執行,可是每個message的執行時間
都不到卡頓的閾值,那自動化卡頓檢測方案也就不可以檢測出卡
頓,可是對用戶來講,用戶就以爲你的App就是有些卡頓。
複製代碼

除此以外,爲了創建體系化的監控解決方案,咱們就必須在上線以前將問題儘量地暴露出來

一、IPC單點問題檢測方案

常見的單點問題有主線程IPC、DB操做等等,這裏我就拿主線程IPC來講,由於IPC實際上是一個很耗時的操做,可是在實際開發過程當中,咱們可能對IPC操做沒有足夠的重視,因此,咱們常常在主程序中去作頻繁IPC操做,因此說,這種耗時它可能並不到你設定卡頓的一個閾值,接下來,咱們看一下,對於IPC問題,咱們應該去監測哪些指標。

  • 一、IPC調用類型:如PackageManager、TelephoneManager的調用。
  • 二、每個的調用次數與耗時。
  • 三、IPC的調用堆棧(代表哪行代碼調用的)、發生線程。

常規方案

常規方案就是在IPC的先後加上埋點。可是,這種方式不夠優雅,並且,在日常開發過程當中咱們常常忘記某個埋點的真正用處,同時它的維護成本也很是大

接下來,咱們講解一下IPC問題監測的技巧。

IPC問題監測技巧

在線下,咱們能夠經過adb命令的方式來進行監測,以下所示:

// 一、首先,對IPC操做開始進行監控
adb shell am trace-ipc start
// 二、而後,結束IPC操做的監控,同時,將監控到的信息存放到指定的文件當中
adb shell am trace-ipc stop -dump-file /data/local/tmp/ipc-trace.txt
// 三、最後,將監控到的ipc-trace導出到電腦查看
adb pull /data/local/tmp/ipc-trace.txt
複製代碼

而後,這裏咱們介紹一種優雅的實現方案,看過深刻探索Android佈局優化(上)的同窗可能知道這裏的實現方案無非就是ARTHook或AspectJ這兩種方案,這裏咱們須要去監控IPC操做,那麼,咱們應該選用哪一種方式會更好一些呢?(利用epic實現ARTHook)

要回答這個問題,就須要咱們對ARTHook和AspectJ這二者的思想有足夠的認識,對應ARTHook來講,其實咱們能夠用它來去Hook系統的一些方法,由於對於系統代碼來講,咱們沒法對它進行更改,可是咱們能夠Hook住它的一個方法,在它的方法體裏面去加上本身的一些代碼。可是,對於AspectJ來講,它只能針對於那些非系統方法,也就是咱們App本身的源碼,或者是咱們所引用到的一些jar、aar包。由於AspectJ其實是往咱們的具體方法裏面插入相對應的代碼,因此說,他不可以針對於咱們的系統方法去作操做,在這裏,咱們就須要採用ARTHook的方式去進行IPC操做的監控

在使用ARTHook去監控IPC操做以前,咱們首先思考一下,哪些操做是IPC操做呢?

好比說,咱們經過PackageManager去拿到咱們應用的一些信息,或者去拿到設備的DeviceId這樣的信息以及AMS相關的信息等等,這些其實都涉及到了IPC的操做,而這些操做都會經過固定的方式進行IPC,並最終會調用到android.os.BinderProxy,接下來,咱們來看看它的transact方法,以下所示:

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
複製代碼

這裏咱們僅僅關注transact方法的參數便可,第一個參數是一個行動編碼,爲int類型,它是在FIRST_CALL_TRANSACTION與LAST_CALL_TRANSACTION之間的某個值,第2、三個參數都是Parcel類型的參數,用於獲取和回覆相應的數據,第四個參數爲一個int類型的標記值,爲0表示一個正常的IPC調用,不然代表是一個單向的IPC調用。而後,咱們在項目中的Application的onCreate方法中使用ARTHook對android.os.BinderProxy類的transact方法進行Hook,代碼以下所示:

try {
        DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
                int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                        LogHelper.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName()
                                + "\n" + Log.getStackTraceString(new Throwable()));
                        super.beforeHookedMethod(param);
                    }
                });
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
複製代碼

從新安裝應用,便可看到以下的Log信息:

2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ WanAndroidApp$1.beforeHookedMethod  (WanAndroidApp.java:160)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │    LogHelper.i  (LogHelper.java:37)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ [WanAndroidApp.java | 160 | beforeHookedMethod] BinderProxy beforeHookedMethod BinderProxy
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ java.lang.Throwable
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at json.chao.com.wanandroid.app.WanAndroidApp$1.beforeHookedMethod(WanAndroidApp.java:160)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at com.taobao.android.dexposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:237)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at me.weishu.epic.art.entry.Entry64.onHookBoolean(Entry64.java:72)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:237)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at me.weishu.epic.art.entry.Entry64.booleanBridge(Entry64.java:86)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.ServiceManagerProxy.getService(ServiceManagerNative.java:123)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.ServiceManager.getService(ServiceManager.java:56)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.ServiceManager.getServiceOrThrow(ServiceManager.java:71)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.UiModeManager.<init>(UiModeManager.java:127)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.SystemServiceRegistry$42.createService(SystemServiceRegistry.java:511)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.SystemServiceRegistry$42.createService(SystemServiceRegistry.java:509)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.SystemServiceRegistry$CachedServiceFetcher.getService(SystemServiceRegistry.java:970)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.SystemServiceRegistry.getSystemService(SystemServiceRegistry.java:920)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ContextImpl.getSystemService(ContextImpl.java:1677)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.view.ContextThemeWrapper.getSystemService(ContextThemeWrapper.java:171)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.Activity.getSystemService(Activity.java:6003)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatDelegateImplV23.<init>(AppCompatDelegateImplV23.java:33)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatDelegateImplN.<init>(AppCompatDelegateImplN.java:31)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatDelegate.create(AppCompatDelegate.java:198)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatDelegate.create(AppCompatDelegate.java:183)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatActivity.getDelegate(AppCompatActivity.java:519)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:70)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at me.yokeyword.fragmentation.SupportActivity.onCreate(SupportActivity.java:38)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at json.chao.com.wanandroid.base.activity.AbstractSimpleActivity.onCreate(AbstractSimpleActivity.java:29)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at json.chao.com.wanandroid.base.activity.BaseActivity.onCreate(BaseActivity.java:37)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.Activity.performCreate(Activity.java:7098)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.Activity.performCreate(Activity.java:7089)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1215)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2895)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread.-wrap11(Unknown Source:0)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1616)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.Handler.dispatchMessage(Handler.java:106)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.os.Looper.loop(Looper.java:173)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at android.app.ActivityThread.main(ActivityThread.java:6653)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at java.lang.reflect.Method.invoke(Native Method)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
2020-01-22 19:52:47.657 10683-10683/json.chao.com.wanandroid I/WanAndroid-LOG: │ 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
複製代碼

能夠看出,這裏彈出了應用中某一個IPC調用的全部堆棧信息。在這裏,具體是在AbstractSimpleActivity的onCreate方法中調用了ServiceManager的getService方法,它是一個IPC調用的方法。這樣,應用的IPC調用咱們就能很方便地捕獲到了。

你們能夠看到,經過這種方式咱們能夠很方便地拿到應用中全部的IPC操做,並能夠得到到IPC調用的類型、調用耗時、發生次數、調用的堆棧等等一系列信息。固然,除了IPC調用的問題以外,還有IO、DB、View繪製等一系列單點問題須要去創建與之對應的檢測方案。

二、卡頓問題檢測方案

對於卡頓問題檢測方案的建設,主要是利用ARTHook去完善線下的檢測工具,儘量地去Hook相對應的操做,以暴露、分析問題。這樣,才能更好地實現卡頓的體系化解決方案。

3、如何實現界面秒開?

界面的打開速度對用戶體驗來講是相當重要的,那麼如何實現界面秒開呢?

其實界面秒開就是一個小的啓動優化,其優化的思想能夠借鑑啓動速度優化與佈局優化的一些實現思路

一、界面秒開實現

首先,咱們能夠經過Systrace來觀察CPU的運行情況,好比有沒有跑滿CPU;而後,咱們在啓動優化中學習到的優雅異步以及優雅延遲初始化等等一些方案;其次,針對於咱們的界面佈局,咱們可使用異步Inflate、X2C、其它的繪製優化措施等等;最後,咱們可使用預加載的方式去提早獲取頁面的數據,以免網絡或磁盤IO速度的影響,或者也能夠將獲取數據的方法放到onCreate方法的第一行

那麼咱們如何去衡量界面的打開速度呢?

一般,咱們是經過界面秒開率去統計頁面的打開速度的,具體就是計算onCreate到onWindowFocusChanged的時間。固然,在某些特定的場景下,把onWindowFocusChanged做爲頁面打開的結束點並非特別的精確,那咱們能夠去實現一個特定的接口來適配咱們的Activity或Fragment,咱們能夠把那個接口方法做爲頁面打開的結束點

那麼,除了以上說到的一些界面秒開的實現方式以外,尚未更好的方式呢?

那就是Lancet。

二、Lancet

Lancet是一個輕量級的Android AOP框架,它具備以下優點:

  • 一、編譯速度快,支持增量編譯。
  • 二、API簡單,沒有任何多餘代碼插入apk。(這一點對應包體積優化時相當重要的)

而後,我來簡單地講解下Lancet的用法。Lancet自身提供了一些註解用於Hook,以下所示:

  • @Prxoy:一般是用於對系統API調用的Hook。
  • @Insert:常常用於操做App或者是Library當中的一些類。

接下來,咱們就是使用Lancet來進行一下實戰演練。

首先,咱們須要在項目根目錄的 build.gradle 添加以下依賴:

dependencies{
    classpath 'me.ele:lancet-plugin:1.0.5'
}
複製代碼

而後,在 app 目錄的'build.gradle' 添加:

apply plugin: 'me.ele.lancet'

dependencies {
    compileOnly 'me.ele:lancet-base:1.0.5'
}
複製代碼

接下來,咱們就可使用Lancet了,這裏咱們須要先新建一個類去進行專門的Hook操做,以下所示:

public class ActivityHooker {

    @Proxy("i")
    @TargetClass("android.util.Log")
    public static int i(String tag, String msg) {
        msg = msg + "JsonChao";
        return (int) Origin.call();
    }
}
複製代碼

上述的方法就是對android.util.Log的i方法進行Hook,並在全部的msg後面加上"JsonChao"字符串,注意這裏的i方法咱們須要從android.util.Log裏面將它的i方法複製過來,確保方法名和對應的參數信息一致;而後,方法上面的@TargetClass與@Proxy分別是指定對應的全路徑類名與方法名;最後,咱們須要經過Lancet提供的Origin類去調用它的call方法來實現返回原來的調用信息。完成以後,咱們從新運行項目,會出現以下log信息:

2020-01-23 13:13:34.124 7277-7277/json.chao.com.wanandroid I/MultiDex: VM with version 2.1.0 has multidex supportJsonChao
2020-01-23 13:13:34.124 7277-7277/json.chao.com.wanandroid I/MultiDex: Installing applicationJsonChao
複製代碼

能夠看到,log後面都加上了咱們預先添加的字符串,說明Hook成功了。下面,咱們就能夠用Lancet來統計一下項目界面的秒開率了,代碼以下所示:

public static ActivityRecord sActivityRecord;

static {
    sActivityRecord = new ActivityRecord();
}

@Insert(value = "onCreate",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
protected void onCreate(Bundle savedInstanceState) {
    sActivityRecord.mOnCreateTime = System.currentTimeMillis();
    // 調用當前Hook類方法中原先的邏輯
    Origin.callVoid();
}

@Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
public void onWindowFocusChanged(boolean hasFocus) {
    sActivityRecord.mOnWindowsFocusChangedTime = System.currentTimeMillis();
    LogHelper.i(getClass().getCanonicalName() + " onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
    Origin.callVoid();
}
複製代碼

上面,咱們經過@TargetClass和@Insert兩個註解實現Hook了android.support.v7.app.AppCompatActivity的onCreate與onWindowFocusChanged方法。咱們注意到,這裏@Insert註解能夠指定兩個參數,其源碼以下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Insert {
    String value();

    boolean mayCreateSuper() default false;
}
複製代碼

第二個參數mayCreateSuper設定爲true則代表若是沒有重寫父類的方法,則會默認去重寫這個方法。對應到咱們ActivityHooker裏面實現的@Insert註解方法就是若是當前的Activity沒有重寫父類的onCreate和 onWindowFocusChanged方法,則此時默認會去重寫父類的這個方法,以避免因某些Activity不存在該方法而Hook失敗的狀況

而後,咱們注意到@TargetClass也能夠指定兩個參數,其源碼以下所示:

@Retention(RetentionPolicy.RUNTIME)
@java.lang.annotation.Target({ElementType.TYPE, ElementType.METHOD})
public @interface TargetClass {
    String value();

    Scope scope() default Scope.SELF;
}
複製代碼

第二個參數scope指定的值是一個枚舉,可選的值以下所示:

public enum Scope {

    SELF,
    DIRECT,
    ALL,
    LEAF
}
複製代碼

對於Scope.SELF,它表明僅匹配目標value所指定的一個匹配類;對於DIRECT,它表明匹配value所指定的類的一個直接子類;若是是Scope.ALL,它就代表會去匹配value所指定的類的全部子類,而咱們上面指定的value值爲android.support.v7.app.AppCompatActivity,由於scope指定爲了Scope.ALL,則說明會去匹配AppCompatActivity的全部子類。而最後的Scope.LEAF 表明匹配 value 指定類的最終子類,由於java是單繼承,因此繼承關係是樹形結構,因此這裏表明了指定類爲頂點的繼承樹的全部葉子節點。

最後,咱們設定了一個ActivityRecord類去記錄onCreate與onWindowFocusChanged的時間戳,以下所示:

public class ActivityRecord {

    /**
    * 避免沒有僅執行onResume就去統計界面打開速度的狀況,如息屏、亮屏等等
    */
    public boolean isNewCreate;

    public long mOnCreateTime;
    public long mOnWindowsFocusChangedTime;
}
複製代碼

經過sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime獲得的時間即爲界面的打開速度,最後,從新運行項目,會獲得以下log信息:

2020-01-23 14:12:16.406 15098-15098/json.chao.com.wanandroid I/WanAndroid-LOG: │ [null | 57 | json_chao_com_wanandroid_aop_ActivityHooker_onWindowFocusChanged] json.chao.com.wanandroid.ui.main.activity.SplashActivity onWindowFocusChanged cost 257
2020-01-23 14:12:18.930 15098-15098/json.chao.com.wanandroid I/WanAndroid-LOG: │ [null | 57 | json_chao_com_wanandroid_aop_ActivityHooker_onWindowFocusChanged] json.chao.com.wanandroid.ui.main.activity.MainActivity onWindowFocusChanged cost 608
複製代碼

從上面的log信息,咱們就能夠知道 SplashActivity 和 MainActivity 的界面打開速度分別是257ms和608ms。

最後,咱們來看下界面秒開的監控緯度。

三、界面秒開監控緯度

對於界面秒開的監控緯度,主要分爲如下三個方面:

  • 整體耗時
  • 生命週期耗時
  • 生命週期間隔耗時

首先,咱們會監控界面打開的總體耗時,也就是onCreate到onWindowFocusChanged這個方法的耗時;固然,若是咱們是在一個特殊的界面,咱們須要更精確的知道界面打開的一個時間,這個咱們能夠用自定義的接口去實現。其次,咱們也須要去監控生命週期的一個耗時,如onCreate、onStart、onResume等等。最後,咱們也須要去作生命週期間隔的耗時監控,這點常常被咱們所忽略,好比onCreate的結束到onStart開始的這一段時間,也是有時間損耗的,咱們能夠監控它是否是在一個合理的範圍以內。經過這三個方面的監控緯度,咱們就可以很是細粒度地去檢測頁面秒開各個方面的狀況

4、優雅監控耗時盲區

儘管咱們在應用中監控了不少的耗時區間,可是仍是有一些耗時區間咱們尚未捕捉到,如onResume到列表展現的間隔時間,這些時間在咱們的統計過程當中很容易被忽視,這裏咱們舉一個小栗子:

咱們在Activity的生命週期中post了一個message,那這個message極可能其中
執行了一段耗時操做,那你知道這個message它的具體執行時間嗎?這個message其實
頗有可能在列表展現以前就執行了,若是這個message耗時1s,那麼列表的展現
時間就會延遲1s,若是是200ms,那麼咱們設定的自動化卡頓檢測就沒法
發現它,那麼列表的展現時間就會延遲200ms。
複製代碼

其實這種場景很是常見,接下來,咱們就在項目中來進行實戰演練。

首先,咱們在MainActivity的onCreate中加上post消息的一段代碼,其中模擬了延遲1000ms的耗時操做,代碼以下所示:

// 如下代碼是爲了演示Msg致使的主線程卡頓
    new Handler().post(() -> {
        LogHelper.i("Msg 執行");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
複製代碼

接着,咱們在RecyclerView對應的Adapter中將列表展現的時間打印出來,以下所示:

if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
        mHasRecorded = true;
        helper.getView(R.id.item_search_pager_group).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                helper.getView(R.id.item_search_pager_group).getViewTreeObserver().removeOnPreDrawListener(this);
                LogHelper.i("FeedShow");
                return true;
            }
        });
    }
複製代碼

最後,咱們從新運行下項目,看看二者的執行時間,log信息以下:

2020-01-23 15:21:55.076 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ [MainActivity.java | 108 | lambda$initEventAndData$1$MainActivity] Msg 執行
2020-01-23 15:21:56.264 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ [null | 57 | json_chao_com_wanandroid_aop_ActivityHooker_onWindowFocusChanged] json.chao.com.wanandroid.ui.main.activity.MainActivity onWindowFocusChanged cost 1585
2020-01-23 15:21:57.207 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ ArticleListAdapter$1.onPreDraw  (ArticleListAdapter.java:93)
2020-01-23 15:21:57.208 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │    LogHelper.i  (LogHelper.java:37)
2020-01-23 15:21:57.208 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-01-23 15:21:57.208 19091-19091/json.chao.com.wanandroid I/WanAndroid-LOG: │ [ArticleListAdapter.java | 93 | onPreDraw] FeedShow
複製代碼

從log信息中能夠看到,MAinActivity的onWindowFocusChanged方法延遲了1000ms才被調用,與此同時,列表頁時延遲了1000ms才展現出來。也就是說,post的這個message消息是執行在界面、列表展現以前的。由於任何一個開發都有可能在某一個生命週期或者是某一個階段以及一些第三方的SDK裏面,回去作一些handler post的相關操做,這樣,他的handler post的message的執行,頗有可能在咱們的界面或列表展現以前就被執行,因此說,出現這種耗時的盲區是很是廣泛的,並且也很差排查,下面,咱們分析下耗時盲區存在的難點。

一、耗時盲區監控難點

首先,咱們能夠經過細化監控的方式去獲取耗時的一些盲區,可是咱們殊不知道在這個盲區中它執行了什麼操做。其次,對於線上的一些耗時盲區,咱們是沒法進行排查的。

這裏,咱們先來看看如何創建耗時盲區監控的線下方案。

二、耗時盲區監控線下方案

這裏咱們直接使用TraceView去檢測便可,由於它可以清晰地記錄線程在具體的時間內到底作了什麼操做,特別適合一段時間內的盲區監控。

而後,咱們來看下如何創建耗時盲區監控的線上方案。

三、耗時盲區監控線上方案

咱們知道主線程的全部方法都是經過message來執行的,還記得在以前咱們學習了一個庫:AndroidPerformanceMonitor,咱們是否能夠經過這個mLogging來作盲區檢測呢?經過這個mLogging確實能夠知道咱們主線程發生的message,可是經過mLogging沒法獲取具體的調用棧信息,由於它所獲取的調用棧信息都是系統回調回來的,它並不知道當前的message是被誰拋出來的,因此說,這個方案並不夠完美。

那麼,咱們是否能夠經過AOP的方式去切Handler方法呢?好比sendMessage、sendMessageDeleayd方法等等,這樣咱們就能夠知道發生message的一個堆棧,可是這種方案也存在着一個問題,就是它不清楚準確的執行時間,咱們切了這個handler的方法,僅僅只知道它具體是在哪一個地方被髮的和它所對應的堆棧信息,可是沒法獲取準確的執行時間。若是咱們想知道在onResume到列表展現之間執行了哪些message,那麼經過AOP的方式也沒法實現。

那麼,最終的耗時盲區監控的一個線上方案就是使用一個統一的Handler,定製了它的兩個方法,一個是sendMessageAtTime,另一個是dispatchMessage方法。由於對於發送message,無論調用哪一個方法最終都會調用到一個是sendMessageAtTime這個方法,而處理message呢,它最終會調用dispatchMessage方法。而後,咱們須要定製一個gradle插件,來實現自動化的接入咱們定製好的handler,經過這種方式,咱們就能在編譯期間去動態地替換全部使用Handler的父類爲咱們定製好的這個handler。這樣,在整個項目中,全部的sendMessage和handleMessage都會通過咱們的回調方法。接下來,咱們來進行一下實戰演練。

首先,我這裏給出定製好的全局Handler類,以下所示:

public class GlobalHandler extends Handler {

    private long mStartTime = System.currentTimeMillis();

    public GlobalHandler() {
        super(Looper.myLooper(), null);
    }

    public GlobalHandler(Callback callback) {
        super(Looper.myLooper(), callback);
    }

    public GlobalHandler(Looper looper, Callback callback) {
        super(looper, callback);
    }

    public GlobalHandler(Looper looper) {
        super(looper);
    }

    @Override
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        boolean send = super.sendMessageAtTime(msg, uptimeMillis);
        // 1
        if (send) {
            GetDetailHandlerHelper.getMsgDetail().put(msg, Log.getStackTraceString(new Throwable()).replace("java.lang.Throwable", ""));
        }
        return send;
    }

    @Override
    public void dispatchMessage(Message msg) {
        mStartTime = System.currentTimeMillis();
        super.dispatchMessage(msg);

        if (GetDetailHandlerHelper.getMsgDetail().containsKey(msg)
            && Looper.myLooper() == Looper.getMainLooper()) {
            JSONObject jsonObject = new JSONObject();
            try {
                // 2
                jsonObject.put("Msg_Cost", System.currentTimeMillis() - mStartTime);
                jsonObject.put("MsgTrace", msg.getTarget() + " " + GetDetailHandlerHelper.getMsgDetail().get(msg));

                // 3
                LogHelper.i("MsgDetail " + jsonObject.toString());
                GetDetailHandlerHelper.getMsgDetail().remove(msg);
            } catch (Exception e) {
            }
        }
    }
}
複製代碼

上面的GlobalHandler將會是咱們項目中全部Handler的一個父類。在註釋1處,咱們在sendMessageAtTime這個方法裏面判斷若是message發送成功,將會把當前message對象對應的調用棧信息都保存到一個ConcurrentHashMap中,GetDetailHandlerHelper類的代碼以下所示:

public class GetDetailHandlerHelper {

    private static ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();

    public static ConcurrentHashMap<Message, String> getMsgDetail() {
        return sMsgDetail;
    }
}
複製代碼

這樣,咱們就可以知道這個message它是被誰發送過來的。而後,在dispatchMessage方法裏面,咱們能夠計算拿到其處理消息的一個耗時,並在註釋2處將這個耗時保存到一個jsonObject對象中,同時,咱們也能夠經過GetDetailHandlerHelper類的ConcurrentHashMap對象拿到這個message對應的堆棧信息,並在註釋3處將它們輸出到log控制檯上。固然,若是是線上監控,則會把這些信息保存到本地,而後選擇合適的時間去上傳。最後,咱們還能夠在方法體裏面作一個判斷,咱們設置一個閾值,好比閾值爲20ms,超過了20ms就把這些保存好的信息上報到APM後臺。

在前面的實戰演練中,咱們使用了handler post的方式去發送一個消息,經過gradle插件將全部handler的父類替換爲咱們定製好的GlobalHandler以後,咱們就能夠優雅地去監控應用中的耗時盲區了。

對於實現全局替換handler的gradle插件,除了使用AspectJ實現以外,這裏推薦一個已有的項目:DroidAssist

而後,從新運行項目,關鍵的log信息以下所示:

MsgDetail {"Msg_Cost":1001,"MsgTrace":"Handler (com.json.chao.com.wanandroid.performance.handler.GlobalHandler) {b0d4d48} \n\tat 
com.json.chao.com.wanandroid.performance.handler.GlobalHandler.sendMessageAtTime(GlobalHandler.java:36)\n\tat
json.chao.com.wanandroid.ui.main.activity.MainActivity.initEventAndData$__twin__(MainActivity.java:107)\n\tat"
複製代碼

從以上信息咱們不只能夠知道message執行的時間,還能夠從對應的堆棧信息中獲得發送message的位置,這裏的位置是MainActivity的107行,也就是new Handler().post()這一行代碼。使用這種方式咱們就能夠知道在列表展現以前到底執行了哪些自定義的message,咱們一眼就能夠知道哪些message實際上是不符合咱們預期的,好比說message的執行時間過長,或者說這個message其實能夠延後執行,這個咱們均可以根據實際的項目和業務需求進行相應地修改

四、耗時盲區監控方案總結

耗時盲區監控是咱們卡頓監控中不可或缺的一個環節,也是卡頓監控全面性的一個重要保障。而須要注意的是,TraceView僅僅適用於線下的一個場景,同時對於TraceView來講,它能夠用於監控咱們系統的message。而最後介紹的動態替換的方式實際上是適合於線上的,同時,它僅僅監控應用自身的一個message。

5、卡頓優化技巧總結

一、卡頓優化實踐經驗

若是應用出現了卡頓現象,那麼能夠考慮如下方式進行優化:

  • 首先,對於耗時的操做,咱們能夠考慮異步或延遲初始化的方式,這樣能夠解決大多數的問題。可是,你們必定要注意代碼的優雅性。
  • 對於佈局加載優化,能夠採用AsyncLayoutInflater或者是X2C的方式來優化主線程IO以及反射致使的消耗,同時,須要注意,對於重繪問題,要給與必定的重視。
  • 此外,內存問題也可能會致使應用界面的卡頓,咱們能夠經過下降內存佔用的方式來減小GC的次數以及時間,而GC的次數和時間咱們能夠經過log查看。

而後,咱們來看看卡頓優化的工具建設。

二、卡頓優化工具建設

工具建設這塊常常容易被你們所忽視,可是它的收益卻很是大,也是卡頓優化的一個重點。首先,對於系統工具而言,咱們要有一個認識,同時必定要學會使用它,這裏咱們再回顧一下。

  • 對於Systrace來講,咱們能夠很方便地看出來它的CPU使用狀況。另外,它的開銷也比較小。
  • 對於TraceView來講,咱們能夠很方便地看出來每個線程它在特定的時間內作了什麼操做,可是TraceView它的開銷相對比較大,有時候可能會被帶偏優化方向。
  • 同時,須要注意,StrictMode也是一個很是強大的工具。

而後,咱們介紹了自動化工具建設以及優化方案。咱們介紹了兩個工具,AndroidPerformanceMonitor以及ANR-WatchDog。同時針對於AndroidPerformanceMonitor的問題,咱們採用了高頻採集,以找出重複率高的堆棧這樣一種方式進行優化,在學習的過程當中,咱們不只須要學會怎樣去使用工具,更要去理解它們的實現原理以及各自的使用場景。

同時,咱們對於卡頓優化工具的建設也作了細化,對於單點問題,好比說IPC監控,咱們經過Hook的手段來作到儘早的發現問題。對於耗時盲區的監控,咱們在線上採用的是替換Handler的方式來監控全部子線程message執行的耗時以及調用堆棧

最後,咱們來看一下卡頓監控的指標。咱們會計算應用總體的卡頓率,ANR率、界面秒開率以及交換時間、生命週期時間等等。在上報ANR信息的同時,咱們也須要上報環境和場景信息,這樣不只方便咱們在不一樣版本之間進行橫向對比,同時,也能夠結合咱們的報警平臺在第一時間感知到異常

6、常見卡頓問題解決方案總結

一、CPU資源爭搶引起的卡頓問題如何解決?

此時,咱們的應用不只應該控制好核心功能的CPU消耗,也須要儘可能減小非核心需求的CPU消耗。

二、要注意Android Java中提供的哪些低效的API?

好比List.removeall方法,它內部會遍歷一次須要過濾的消息列表,在已經存在循環列表的狀況下會形成CPU資源的冗餘使用,此時應該去優化相關的算法,避免使用List.removeall這個方法。

三、如何減小圖形處理的CPU消耗?

這個時候咱們須要使用神器renderscript來圖形處理的相關運算,將CPU轉換到GPU。關於renderscript的背景知識能夠看看筆者以前寫的深刻探索Android佈局優化(下)

四、硬件加速長中文字體渲染時形成的卡頓如何解決?

此時只能關閉文本TextView的硬件加速,以下所示:

textView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
複製代碼

當開啓了硬件加速進行長中文字體的渲染時,首先會調用ViewRootImpl.draw()方法,最後會調用GLES20Canvas.nDrawDisplayList()方法開始經過JNI調整到Native層。在這個方法裏,會繼續調用OpenGLRenderer.drawDisplayList()方法,它經過調用DisplayList的replay方法,以回放前面錄製的DisplayList執行繪製操做

DisplayList的replay方法會遍歷DisplayList中保存的每個操做。其中渲染字體的操做名是DrawText,當遍歷到一個DrawText操做時,會調用OpenGLRender::drawText方法區渲染字體。最終,會在OpenGLRender::drawText方法裏去調用Font::render()方法渲染字體,而在這個方法中有一個很關鍵的操做,即獲取字體緩存。咱們都知道每個中文的編碼都是不一樣的,所以中文的緩存效果很是不理想,可是對於英文而言,只須要緩存26個字母就能夠了。在Android 4.1.2版本以前對文本的Buffer設置太小,因此狀況比較嚴重,若是你的應用在其它版本的渲染性能尚可,就能夠僅僅把Android 4.0.x的硬件加速關閉,代碼以下所示:

// AndroidManifest中
<Applicaiton
        ...
        android:hardwareAccelerated="@bool/hardware_acceleration">
        
// value-v1四、value-v15中設置相應的Bool
值便可
<bool name="hardware_acceleration">false</bool>
複製代碼

此外,硬件渲染還有一些其它的問題在使用時須要注意,具體爲以下所示:

  • 一、在軟件渲染的狀況下,若是須要重繪某個父View的全部子View,只須要調用這個Parent View的invalidate()方法便可,但若是開啓了硬件加速,這麼作是行不通的,須要遍歷整個子View並調用invalidate()。
  • 二、在軟件渲染的狀況下,會經常使用Bitmap重用的方式來節省內存,可是若是開啓了硬件加速,這將會無效。
  • 三、當開啓硬件加速的UI在前臺運行時,須要耗費額外的內存。當硬件加速的UI切換到後臺時,上述額外內存有可能不會釋放,這大多存在於Android 4.1.2版本中。
  • 四、長或寬大於2048像素的Bitmap沒法繪製,顯示爲一片透明。緣由是OpenGL的材質大小上限爲2048 * 2048,所以對於超過2048像素的Bitmap,須要將其切割成2048 * 2048之內的圖片塊,最後在顯示的時候拼起來。
  • 五、當UI中存在過渡繪製時,可能會發生花屏,通常來講繪製少於5層不會出現花屏現象,若是有大塊紅色區域就要十分當心了。
  • 六、須要注意,關於LAYER_TYPE_SOFTWARE,雖然不管在App打開硬件加速或沒有打開硬件加速的時候,都會經過軟件繪製Bitmap做爲離屏緩存,但區別在於打開硬件加速的時候,Bitmap最終還會經過硬件加速方式drawDisplayList去渲染這個Bitmap。

7、卡頓優化的常見問題

一、你是怎麼作卡頓優化的?

從項目的初期到壯大期,最後再到成熟期,每個階段都針對卡頓優化作了不一樣的處理。各個階段所作的事情以下所示:

  • 一、系統工具定位、解決
  • 二、自動化卡頓方案及優化
  • 三、線上監控及線下監測工具的建設

我作卡頓優化也是經歷了一些階段,最初咱們的項目當中的一些模塊出現了卡頓以後,我是經過系統工具進行了定位,我使用了Systrace,而後看了卡頓週期內的CPU情況,同時結合代碼,對這個模塊進行了重構,將部分代碼進行了異步和延遲,在項目初期就是這樣解決了問題。

可是呢,隨着咱們項目的擴大,線下卡頓的問題也愈來愈多,同時,在線上,也有卡頓的反饋,可是線上的反饋卡頓,咱們在線下難以復現,因而咱們開始尋找自動化的卡頓監測方案,其思路是來自於Android的消息處理機制,主線程執行任何代碼都會回到Looper.loop方法當中,而這個方法中有一個mLogging對象,它會在每一個message的執行先後都會被調用,咱們就是利用這個先後處理的時機來作到的自動化監測方案的。同時,在這個階段,咱們也完善了線上ANR的上報,咱們採起的方式就是監控ANR的信息,同時結合了ANR-WatchDog,做爲高版本沒有文件權限的一個補充方案。

在作完這個卡頓檢測方案以後呢,咱們還作了線上監控及線下檢測工具的建設,最終實現了一整套完善,多維度的解決方案。

二、你是怎麼樣自動化的獲取卡頓信息?

咱們的思路是來自於Android的消息處理機制,主線程執行任何代碼它都會走到Looper.loop方法當中,而這個函數當中有一個mLogging對象,它會在每一個message處理先後都會被調用,而主線程發生了卡頓,那就必定會在dispatchMessage方法中執行了耗時的代碼,那咱們在這個message執行以前呢,咱們能夠在子線程當中去postDelayed一個任務,這個Delayed的時間就是咱們設定的閾值,若是主線程的messaege在這個閾值以內完成了,那就取消掉這個子線程當中的任務,若是主線程的message在閾值以內沒有被完成,那子線程當中的任務就會被執行,它會獲取到當前主線程執行的一個堆棧,那咱們就能夠知道哪裏發生了卡頓。

通過實踐,咱們發現這種方案獲取的堆棧信息它不必定是準確的,由於獲取到的堆棧信息它極可能是主線程最終執行的一個位置,而真正耗時的地方其實已經執行完成了,因而呢,咱們就對這個方案作了一些優化,咱們採起了高頻採集的方案,也就是在一個週期內咱們會屢次採集主線程的堆棧信息,若是發生了卡頓,那咱們就將這些卡頓信息壓縮以後上報給APM後臺,而後找出重複的堆棧信息,這些重複發生的堆棧大機率就是卡頓發生的一個位置,這樣就提升了獲取卡頓信息的一個準確性。

三、卡頓的一整套解決方案是怎麼作的?

首先,針對卡頓,咱們採用了線上、線下工具相結合的方式,線下工具咱們須要儘量早地去暴露問題,而針對於線上工具呢,咱們側重於監控的全面性、自動化以及異常感知的靈敏度。

同時呢,卡頓問題還有不少的難題。好比說有的代碼呢,它不到你卡頓的一個閾值,可是執行過多,或者它錯誤地執行了不少次,它也會致使用戶感官上的一個卡頓,因此咱們在線下經過AOP的方式對常見的耗時代碼進行了Hook,而後對一段時間內獲取到的數據進行分析,咱們就能夠知道這些耗時的代碼發生的時機和次數以及耗時狀況。而後,看它是否是知足咱們的一個預期,不知足預期的話,咱們就能夠直接到線下進行修改。同時,卡頓監控它還有不少容易被忽略的一個盲區,好比說生命週期的一個間隔,那對於這種特定的問題呢,咱們就採用了編譯時註解的方式修改了項目當中全部Handler的父類,對於其中的兩個方法進行了監控,咱們就能夠知道主線程message的執行時間以及它們的調用堆棧。

對於線上卡頓,咱們除了計算App的卡頓率、ANR率等常規指標以外呢,咱們還計算了頁面的秒開率、生命週期的執行時間等等。並且,在卡頓發生的時刻,咱們也儘量多地保存下來了當前的一個場景信息,這爲咱們以後解決或者復現這個卡頓留下了依據。

8、總結

恭喜你,若是你看到了這裏,你會發現要作好應用的卡頓優化的確不是一件簡單的事,它須要你有成體系的知識構建基底。最後,咱們再來回顧一下面對卡頓優化,咱們已經探索的如下九大主題:

  • 一、卡頓優化分析方法與工具:背景介紹、卡頓分析方法之使用shell命令分析CPU耗時、卡頓優化工具。
  • 二、自動化卡頓檢測方案及優化:卡頓檢測方案原理、AndroidPerformanceMonitor實戰及其優化。
  • 三、ANR分析與實戰:ANR執行流程、線上ANR監控方式、ANR-WatchDog原理。
  • 四、卡頓單點問題檢測方案:IPC單點問題檢測方案、卡頓問題檢測方案。
  • 五、如何實現界面秒開?:界面秒開實現、Lancet、界面秒開監控緯度。
  • 六、優雅監控耗時盲區:耗時盲區監控難點以及線上與線下的監控方案。
  • 七、卡頓優化技巧總結:卡頓優化實踐經驗、卡頓優化工具建設。
  • 8︎、常見卡頓問題解決方案總結
  • 九、卡頓優化的常見問題

相信看到這裏,你必定收穫滿滿,可是要記住,方案再好,也只有本身動手去實踐,才能真正地掌握它。只有重視實踐,充分運用感性認知潛能,在項目中磨鍊本身,纔是正確的學習之道。在實踐中,在某些關鍵動做上刻意練習,也會取得事半功倍的效果。

參考連接:

一、國內Top團隊大牛帶你玩轉Android性能分析與優化 第6章 卡頓優化

二、極客時間之Android開發高手課 卡頓優化

三、《Android移動性能實戰》第四章 CPU

四、《Android移動性能實戰》第七章 流暢度

五、Android dumpsys cpuinfo 信息解讀

六、如何清楚易懂的解釋「UV和PV"的定義?

七、nanoscope-An extremely accurate Android method tracing tool

八、DroidAssist-A lightweight Android Studio gradle plugin based on Javassist for editing bytecode in Android.

九、lancet-A lightweight and fast AOP framework for Android App and SDK developers

十、MethodTraceMan-用於快速找到高耗時方法,定位解決Android App卡頓問題

十一、Linux環境下進程的CPU佔用率

十二、使用 ftrace

1三、profilo-A library for performance traces from production

1四、ftrace 簡介

1五、atrace源碼

1六、AndroidAdvanceWithGeektime / Chapter06

1七、AndroidAdvanceWithGeektime / Chapter06-plus

讚揚

若是這個庫對您有很大幫助,您願意支持這個項目的進一步開發和這個項目的持續維護。你能夠掃描下面的二維碼,讓我喝一杯咖啡或啤酒。很是感謝您的捐贈。謝謝!


Contanct Me

● 微信:

歡迎關注個人微信:bcce5360

● 微信羣:

微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~

About me

很感謝您閱讀這篇文章,但願您能將它分享給您的朋友或技術羣,這對我意義重大。

但願咱們能成爲朋友,在 Github掘金上一塊兒分享知識。

相關文章
相關標籤/搜索