anr 檢測

前言

成為一名優(yōu)秀的Android開發(fā),需要一份完備的知識體系,在這里,讓我們一起成長為自己所想的那樣~。

在上篇文章中,筆者帶領(lǐng)大家學(xué)習(xí)了卡頓優(yōu)化分析方法與工具、自動化卡頓檢測方案及優(yōu)化這兩塊內(nèi)容。如果對這塊內(nèi)容還不了解的同學(xué)建議先看看《深入探索Android卡頓優(yōu)化(上)》。本篇,為深入探索Android卡頓優(yōu)化的下篇。這篇文章包含的主要內(nèi)容如下所示:

1、ANR分析與實(shí)戰(zhàn)

2、卡頓單點(diǎn)問題檢測方案

3、高效實(shí)現(xiàn)界面秒開

4、優(yōu)雅監(jiān)控耗時盲區(qū)

5、卡頓優(yōu)化技巧總結(jié)

6、常見卡頓問題解決方案總結(jié)

7、卡頓優(yōu)化的常見問題

卡頓時間過長,一定會造成應(yīng)用發(fā)生ANR。下面,我們就來從應(yīng)用的ANR分析與實(shí)戰(zhàn)來開始今天的探索之旅。

一、ANR分析與實(shí)戰(zhàn)

1、ANR介紹與實(shí)戰(zhàn)

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

1、KeyDispatchTimeout:按鍵事件在5s的時間內(nèi)沒有處理完成。

2、BroadcastTimeout:廣播接收器在前臺10s,后臺60s的時間內(nèi)沒有響應(yīng)完成。

3、ServiceTimeout:服務(wù)在前臺20s,后臺200s的時間內(nèi)沒有處理完成。

具體的時間定義我們可以在AMS(ActivityManagerService)中找到:

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

1

2

3

4

5

6

接下來,我們來看一下ANR的執(zhí)行流程。

ANR執(zhí)行流程

1、首先,我們的應(yīng)用發(fā)生了ANR。

2、然后,我們的進(jìn)程就會接收到異常終止信息,并開始寫入進(jìn)程ANR信息,也就是當(dāng)時應(yīng)用的場景信息,它包含了應(yīng)用所有的堆棧信息、CPU、IO等使用的情況。

3、最后,會彈出一個ANR提示框,看你是要選擇繼續(xù)等待還是退出應(yīng)用,需要注意這個ANR提示框不一定會彈出,根據(jù)不同ROM,它的表現(xiàn)情況也不同。因?yàn)橛行┦謾C(jī)廠商它會默認(rèn)去掉這個提示框,以避免帶來不好的用戶體驗(yàn)。

分析完ANR的執(zhí)行流程之后,我們來分析下怎樣去解決ANR,究竟哪里可以作為我們的一個突破點(diǎn)。

在上面我們說過,當(dāng)應(yīng)用發(fā)生ANR時,會寫入當(dāng)時發(fā)生ANR的場景信息到文件中,那么,我們可不可以通過這個文件來判斷是否發(fā)生了ANR呢?

關(guān)于根據(jù)ANR log進(jìn)行ANR問題的排查與解決的方式筆者已經(jīng)在深入探索Android穩(wěn)定性優(yōu)化的第三節(jié)ANR優(yōu)化中講解過了,這里就不多贅述了。

線上ANR監(jiān)控方式

深入探索Android穩(wěn)定性優(yōu)化的第三節(jié)ANR優(yōu)化中我說到了使用FileObserver可以監(jiān)聽 /data/anr/traces.txt的變化,利用它可以實(shí)現(xiàn)線上ANR的監(jiān)控,但是它有一個致命的缺點(diǎn),就是高版本ROM需要root權(quán)限,解決方案是只能通過海外Google Play服務(wù)、國內(nèi)Hardcoder的方式去規(guī)避。但是,這在國內(nèi)顯然是不現(xiàn)實(shí)的,那么,有沒有更好的實(shí)現(xiàn)方式呢?

那就是ANR-WatchDog,下面我就來詳細(xì)地介紹一下它。

ANR-WatchDog項(xiàng)目地址

ANR-WatchDog是一種非侵入式的ANR監(jiān)控組件,可以用于線上ANR的監(jiān)控,接下來,我們就使用ANR-WatchDog來監(jiān)控ANR。

首先,在我們項(xiàng)目的app/build.gradle中添加如下依賴:

implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'

1

然后,在應(yīng)用的Application的onCreate方法中添加如下代碼啟動ANR-WatchDog:

new ANRWatchDog().start();

1

可以看到,它的初始化方式非常地簡單,同時,它內(nèi)部的實(shí)現(xiàn)也非常簡單,整個庫只有兩個類,一個是ANRWatchDog,另一個是ANRError。

接下來我們來看一下ANRWatchDog的實(shí)現(xiàn)方式。

/**

* A watchdog timer thread that detects when the UI thread has frozen.

*/

public class ANRWatchDog extends Thread {

1

2

3

4

可以看到,ANRWatchDog實(shí)際上是繼承了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() {

? ? // 1、首先,將線程命名為|ANR-WatchDog|。

? ? setName("|ANR-WatchDog|");

? ? // 2、接著,聲明了一個默認(rèn)的超時間隔時間,默認(rèn)的值為5000ms。

? ? long interval = _timeoutInterval;

? ? // 3、然后,在while循環(huán)中通過_uiHandler去post一個_ticker Runnable。

? ? while (!isInterrupted()) {

? ? ? ? // 3.1 這里的_tick默認(rèn)是0,所以needPost即為true。

? ? ? ? boolean needPost = _tick == 0;

? ? ? ? // 這里的_tick加上了默認(rèn)的5000ms

? ? ? ? _tick += interval;

? ? ? ? if (needPost) {

? ? ? ? ? ? _uiHandler.post(_ticker);

? ? ? ? }

? ? ? ? // 接下來,線程會sleep一段時間,默認(rèn)值為5000ms。

? ? ? ? try {

? ? ? ? ? ? Thread.sleep(interval);

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? _interruptionListener.onInterrupted(e);

? ? ? ? ? ? return ;

? ? ? ? }

? ? ? ? // 4、如果主線程沒有處理Runnable,即_tick的值沒有被賦值為0,則說明發(fā)生了ANR,第二個_reported標(biāo)志位是為了避免重復(fù)報道已經(jīng)處理過的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 {

? ? ? ? ? ? ? ? // 5、如果沒有主動給ANR_Watchdog設(shè)置線程名,則會默認(rèn)會使用ANRError的NewMainOnly方法去處理ANR。

? ? ? ? ? ? ? ? error = ANRError.NewMainOnly(_tick);

? ? ? ? ? ? }


? ? ? ? ? // 6、最后會通過ANRListener調(diào)用它的onAppNotResponding方法,其默認(rèn)的處理會直接拋出當(dāng)前的ANRError,導(dǎo)致程序崩潰。 _anrListener.onAppNotResponding(error);

? ? ? ? ? ? interval = _timeoutInterval;

? ? ? ? ? ? _reported = true;

? ? ? ? }

? ? }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

首先,在注釋1處,我們將線程命名為了|ANR-WatchDog|。接著,在注釋2處,聲明了一個默認(rèn)的超時間隔時間,默認(rèn)的值為5000ms。然后,注釋3處,在while循環(huán)中通過_uiHandler去post一個_ticker Runnable。注意這里的_tick默認(rèn)是0,所以needPost即為true。接下來,線程會sleep一段時間,默認(rèn)值為5000ms。在注釋4處,如果主線程沒有處理Runnable,即_tick的值沒有被賦值為0,則說明發(fā)生了ANR,第二個_reported標(biāo)志位是為了避免重復(fù)報道已經(jīng)處理過的ANR。如果發(fā)生了ANR,就會調(diào)用接下來的代碼,開始會處理debug的情況,然后,我們看到注釋5處,如果沒有主動給ANR_Watchdog設(shè)置線程名,則會默認(rèn)會使用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) {

? ? // 1、獲取主線程的堆棧信息

? ? final Thread mainThread = Looper.getMainLooper().getThread();

? ? final StackTraceElement[] mainStackTrace = mainThread.getStackTrace();

? ? // 2、返回一個包含主線程名、主線程堆棧信息以及發(fā)生ANR的最小時間值的實(shí)例。

? ? return new ANRError(new $(getThreadTitle(mainThread), mainStackTrace).new _Thread(null), duration);

}

1

2

3

4

5

6

7

8

9

10

11

12

13

可以看到,在注釋1處,首先獲了主線程的堆棧信息,然后返回了一個包含主線程名、主線程堆棧信息以及發(fā)生ANR的最小時間值的實(shí)例。(我們可以改造其源碼在此時添加更多的卡頓現(xiàn)場信息,如CPU 使用率和調(diào)度信息、內(nèi)存相關(guān)信息、I/O 和網(wǎng)絡(luò)相關(guān)的信息等等

接下來,我們再回到ANRWatchDog的run方法中的注釋6處,最后這里會通過ANRListener調(diào)用它的onAppNotResponding方法,其默認(rèn)的處理會直接拋出當(dāng)前的ANRError,導(dǎo)致程序崩潰。對應(yīng)的代碼如下所示:

private static final ANRListener DEFAULT_ANR_LISTENER = new ANRListener() {

? ? @Override public void onAppNotResponding(ANRError error) {

? ? ? ? throw error;

? ? }

};

1

2

3

4

5

了解了ANRWatchDog的實(shí)現(xiàn)原理之后,我們試一試它的效果如何。首先,我們給MainActivity中的懸浮按鈕添加主線程休眠10s的代碼,如下所示:

@OnClick({R.id.main_floating_action_btn})

void onClick(View view) {

? ? switch (view.getId()) {

? ? ? ? case R.id.main_floating_action_btn:

? ? ? ? ? ? try {

? ? ? ? ? ? ? ? // 對應(yīng)項(xiàng)目中的第170行

? ? ? ? ? ? ? ? Thread.sleep(10000);

? ? ? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? ? ? e.printStackTrace();

? ? ? ? ? ? }

? ? ? ? ? ? jumpToTheTop();

? ? ? ? ? ? break;

? ? ? ? default:

? ? ? ? ? ? break;

? ? }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

然后,我們重新安裝運(yùn)行項(xiàng)目,點(diǎn)擊懸浮按鈕,發(fā)現(xiàn)在10s內(nèi)都不能觸發(fā)屏幕點(diǎn)擊和觸摸事件,并且在10s之后,應(yīng)用直接發(fā)生了崩潰。接著,我們在Logcat過濾欄中輸入fatal關(guān)鍵字,找出致命的錯誤,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)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

可以看到,發(fā)生崩潰的線程正是|ANR-WatchDog|。我們重點(diǎn)關(guān)注注釋1,這里發(fā)生崩潰的位置是在MainActivity的onClick方法,對應(yīng)的行數(shù)為170行,從前可知,這里正是線程休眠的地方。

接下來,我們來分析一下ANR-WatchDog的實(shí)現(xiàn)原理。

2、ANR-WatchDog原理

首先,我們調(diào)用了ANR-WatchDog的start方法,然后這個線程就會開始工作。

然后,我們通過主線程的Handler post一個消息將主線程的某個值進(jìn)行一個加值的操作。

post完成之后呢,我們這個線程就sleep一段時間。

在sleep之后呢,它就會來檢測我們這個值有沒有被修改,如果這個值被修改了,那就說明我們在主線程中執(zhí)行了這個message,即表明主線程沒有發(fā)生卡頓,否則,則說明主線程發(fā)生了卡頓。

最后,ANR-WatchDog就會判斷發(fā)生了ANR,拋出一個異常給我們。

最后,ANR-WatchDog的工作流程簡圖如下所示:

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-eHUvBwng-1581908980196)(https://raw.githubusercontent.com/JsonChao/Awesome-Android-Performance/master/screenshots/anr_watch_dog_implement.png)]

上面我們最后說到,如果檢測到主線程發(fā)生了卡頓,則會拋出一個ANR異常,這將會導(dǎo)致應(yīng)用崩潰,顯然不能將這種方案帶到線上,那么,有什么方式能夠自定義最后發(fā)生卡頓時的處理過程嗎?

其實(shí)ANR-WatchDog自身就實(shí)現(xiàn)了一個我們自身也可以去實(shí)現(xiàn)的ANRListener,通過它,我們就可以對ANR事件去做一個自定義的處理,比如將堆棧信息壓縮后保存到本地,并在適當(dāng)?shù)臅r間上傳到APM后臺。

3、小結(jié)

ANR-WatchDog是一種非侵入式的ANR監(jiān)控方案,它能夠彌補(bǔ)我們在高版本中沒有權(quán)限去讀取traces.txt文件的問題,需要注意的是,在線上這兩種方案我們需要結(jié)合使用。

在之前,我們還講到了AndroidPerformanceMonitor,那么它和ANR-WatchDog有什么區(qū)別呢?

對于AndroidPerformanceMonitor來說,它是監(jiān)控我們主線程中每一個message的執(zhí)行,它會在主線程的每一個message的前后打印一個時間戳,然后,我們就可以據(jù)此計(jì)算每一個message的具體執(zhí)行時間,但是我們需要注意的是一個message的執(zhí)行時間通常是非常短暫的,也就是很難達(dá)到ANR這個級別。然后我們來看看ANR-WatchDog的原理,它是不管應(yīng)用是如何執(zhí)行的,它只會看最終的結(jié)果,即sleep 5s之后,我就看主線程的這個值有沒有被更改。如果說被改過,就說明沒有發(fā)生ANR,否則,就表明發(fā)生了ANR

根據(jù)這兩個庫的原理,我們便可以判斷出它們分別的適用場景,對于AndroidPerformanceMonitor來說,它適合監(jiān)控卡頓,因?yàn)槊恳粋€message它執(zhí)行的時間并不長。對于ANR-WatchDog來說,它更加適合于ANR監(jiān)控的補(bǔ)充。

此外,雖然ANR-WatchDog解決了在高版本系統(tǒng)沒有權(quán)限讀取 /data/anr/traces.txt 文件的問題,但是在Java層去獲取所有線程堆棧以及各種信息非常耗時,對于卡頓場景不一定合適,它可能會進(jìn)一步加劇用戶的卡頓。如果是對性能要求比較高的應(yīng)用,可以通過Hook Native層的方式去獲得所有線程的堆棧信息,具體為如下兩個步驟:

通過libart.so、dlsym調(diào)用ThreadList::ForEach方法,拿到所有的 Native 線程對象。

遍歷線程對象列表,調(diào)用Thread::DumpState方法。

通過這種方式就大致模擬了系統(tǒng)打印 ANR 日志的流程,但是由于采用的是Hook方式,所以可能會產(chǎn)生一些異常甚至崩潰的情況,這個時候就需要通過?fork 子進(jìn)程方式去避免這種問題,而且使用 子進(jìn)程去獲取堆棧信息的方式可以做到完全不卡住我們主進(jìn)程。

但是需要注意的是,fork 進(jìn)程會導(dǎo)致進(jìn)程號發(fā)生改變,此時需要通過指定 /proc/[父進(jìn)程 id]的方式重新獲取應(yīng)用主進(jìn)程的堆棧信息。

通過 Native Hook 的 方式我們實(shí)現(xiàn)了一套“無損”獲取所有 Java 線程堆棧與詳細(xì)信息的卡頓監(jiān)控體系。為了降低上報數(shù)據(jù)量,建議只有主線程的 Java 線程狀態(tài)是 WAITING、TIME_WAITING 或者 BLOCKED 的時候,才去使用這套方案。

二、卡頓單點(diǎn)問題檢測方案

除了自動化的卡頓與ANR監(jiān)控之外,我們還需要進(jìn)行卡頓單點(diǎn)問題的檢測,因?yàn)樯鲜鰞煞N檢測方案的并不能滿足所有場景的檢測要求,這里我舉一個小栗子:

比如我有很多的message要執(zhí)行,但是每一個message的執(zhí)行時間

都不到卡頓的閾值,那自動化卡頓檢測方案也就不能夠檢測出卡

頓,但是對用戶來說,用戶就覺得你的App就是有些卡頓。

1

2

3

除此之外,為了建立體系化的監(jiān)控解決方案,我們就必須在上線之前將問題盡可能地暴露出來。

1、IPC單點(diǎn)問題檢測方案

常見的單點(diǎn)問題有主線程IPC、DB操作等等,這里我就拿主線程IPC來說,因?yàn)镮PC其實(shí)是一個很耗時的操作,但是在實(shí)際開發(fā)過程中,我們可能對IPC操作沒有足夠的重視,所以,我們經(jīng)常在主程序中去做頻繁IPC操作,所以說,這種耗時它可能并不到你設(shè)定卡頓的一個閾值,接下來,我們看一下,對于IPC問題,我們應(yīng)該去監(jiān)測哪些指標(biāo)。

1、IPC調(diào)用類型:如PackageManager、TelephoneManager的調(diào)用。

2、每一個的調(diào)用次數(shù)與耗時。

3、IPC的調(diào)用堆棧(表明哪行代碼調(diào)用的)、發(fā)生線程。

常規(guī)方案

常規(guī)方案就是在IPC的前后加上埋點(diǎn)。但是,這種方式不夠優(yōu)雅,而且,在平常開發(fā)過程中我們經(jīng)常忘記某個埋點(diǎn)的真正用處,同時它的維護(hù)成本也非常大。

接下來,我們講解一下IPC問題監(jiān)測的技巧。

IPC問題監(jiān)測技巧

在線下,我們可以通過adb命令的方式來進(jìn)行監(jiān)測,如下所示:

// 1、首先,對IPC操作開始進(jìn)行監(jiān)控

adb shell am trace-ipc start

// 2、然后,結(jié)束IPC操作的監(jiān)控,同時,將監(jiān)控到的信息存放到指定的文件當(dāng)中

adb shell am trace-ipc stop -dump-file /data/local/tmp/ipc-trace.txt

// 3、最后,將監(jiān)控到的ipc-trace導(dǎo)出到電腦查看

adb pull /data/local/tmp/ipc-trace.txt

1

2

3

4

5

6

然后,這里我們介紹一種優(yōu)雅的實(shí)現(xiàn)方案,看過深入探索Android布局優(yōu)化(上)的同學(xué)可能知道這里的實(shí)現(xiàn)方案無非就是ARTHook或AspectJ這兩種方案,這里我們需要去監(jiān)控IPC操作,那么,我們應(yīng)該選用哪種方式會更好一些呢?

要回答這個問題,就需要我們對ARTHook和AspectJ這兩者的思想有足夠的認(rèn)識,對應(yīng)ARTHook來說,其實(shí)我們可以用它來去Hook系統(tǒng)的一些方法,因?yàn)閷τ谙到y(tǒng)代碼來說,我們無法對它進(jìn)行更改,但是我們可以Hook住它的一個方法,在它的方法體里面去加上自己的一些代碼。但是,對于AspectJ來說,它只能針對于那些非系統(tǒng)方法,也就是我們App自己的源碼,或者是我們所引用到的一些jar、aar包。因?yàn)锳spectJ實(shí)際上是往我們的具體方法里面插入相對應(yīng)的代碼,所以說,他不能夠針對于我們的系統(tǒng)方法去做操作,在這里,我們就需要采用ARTHook的方式去進(jìn)行IPC操作的監(jiān)控。

在使用ARTHook去監(jiān)控IPC操作之前,我們首先思考一下,哪些操作是IPC操作呢?

比如說,我們通過PackageManager去拿到我們應(yīng)用的一些信息,或者去拿到設(shè)備的DeviceId這樣的信息以及AMS相關(guān)的信息等等,這些其實(shí)都涉及到了IPC的操作,而這些操作都會通過固定的方式進(jìn)行IPC,并最終會調(diào)用到android.os.BinderProxy,接下來,我們來看看它的transact方法,如下所示:

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {

1

這里我們僅僅關(guān)注transact方法的參數(shù)即可,第一個參數(shù)是一個行動編碼,為int類型,它是在FIRST_CALL_TRANSACTION與LAST_CALL_TRANSACTION之間的某個值,第二、三個參數(shù)都是Parcel類型的參數(shù),用于獲取和回復(fù)相應(yīng)的數(shù)據(jù),第四個參數(shù)為一個int類型的標(biāo)記值,為0表示一個正常的IPC調(diào)用,否則表明是一個單向的IPC調(diào)用。然后,我們在項(xiàng)目中的Application的onCreate方法中使用ARTHook對android.os.BinderProxy類的transact方法進(jìn)行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();

? ? }

1

2

3

4

5

6

7

8

9

10

11

12

13

重新安裝應(yīng)用,即可看到如下的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)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

可以看出,這里彈出了應(yīng)用中某一個IPC調(diào)用的所有堆棧信息。在這里,具體是在AbstractSimpleActivity的onCreate方法中調(diào)用了ServiceManager的getService方法,它是一個IPC調(diào)用的方法。這樣,應(yīng)用的IPC調(diào)用我們就能很方便地捕獲到了。

大家可以看到,通過這種方式我們可以很方便地拿到應(yīng)用中所有的IPC操作,并可以獲得到IPC調(diào)用的類型、調(diào)用耗時、發(fā)生次數(shù)、調(diào)用的堆棧等等一系列信息。當(dāng)然,除了IPC調(diào)用的問題之外,還有IO、DB、View繪制等一系列單點(diǎn)問題需要去建立與之對應(yīng)的檢測方案。

2、卡頓問題檢測方案

對于卡頓問題檢測方案的建設(shè),主要是利用ARTHook去完善線下的檢測工具,盡可能地去Hook相對應(yīng)的操作,以暴露、分析問題。這樣,才能更好地實(shí)現(xiàn)卡頓的體系化解決方案。

三、如何實(shí)現(xiàn)界面秒開?

界面的打開速度對用戶體驗(yàn)來說是至關(guān)重要的,那么如何實(shí)現(xiàn)界面秒開呢?

其實(shí)界面秒開就是一個小的啟動優(yōu)化,其優(yōu)化的思想可以借鑒啟動速度優(yōu)化與布局優(yōu)化的一些實(shí)現(xiàn)思路。

1、界面秒開實(shí)現(xiàn)

首先,我們可以通過Systrace來觀察CPU的運(yùn)行狀況,比如有沒有跑滿CPU;然后,我們在啟動優(yōu)化中學(xué)習(xí)到的優(yōu)雅異步以及優(yōu)雅延遲初始化等等一些方案;其次,針對于我們的界面布局,我們可以使用異步Inflate、X2C、其它的繪制優(yōu)化措施等等;最后,我們可以使用預(yù)加載的方式去提前獲取頁面的數(shù)據(jù),以避免網(wǎng)絡(luò)或磁盤IO速度的影響,或者也可以將獲取數(shù)據(jù)的方法放到onCreate方法的第一行。

那么我們?nèi)绾稳ズ饬拷缑娴拇蜷_速度呢?

通常,我們是通過界面秒開率去統(tǒng)計(jì)頁面的打開速度的,具體就是計(jì)算onCreate到onWindowFocusChanged的時間。當(dāng)然,在某些特定的場景下,把onWindowFocusChanged作為頁面打開的結(jié)束點(diǎn)并不是特別的精確,那我們可以去實(shí)現(xiàn)一個特定的接口來適配我們的Activity或Fragment,我們可以把那個接口方法作為頁面打開的結(jié)束點(diǎn)

那么,除了以上說到的一些界面秒開的實(shí)現(xiàn)方式之外,還沒有更好的方式呢?

那就是Lancet。

2、Lancet

Lancet是一個輕量級的Android AOP框架,它具有如下優(yōu)勢:

1、編譯速度快,支持增量編譯。

2、API簡單,沒有任何多余代碼插入apk。(這一點(diǎn)對應(yīng)包體積優(yōu)化時至關(guān)重要的)

然后,我來簡單地講解下Lancet的用法。Lancet自身提供了一些注解用于Hook,如下所示:

@Prxoy:通常是用于對系統(tǒng)API調(diào)用的Hook。

@Insert:經(jīng)常用于操作App或者是Library當(dāng)中的一些類。

接下來,我們就是使用Lancet來進(jìn)行一下實(shí)戰(zhàn)演練。

首先,我們需要在項(xiàng)目根目錄的 build.gradle 添加如下依賴:

dependencies{

? ? classpath 'me.ele:lancet-plugin:1.0.5'

}

1

2

3

然后,在 app 目錄的’build.gradle’ 添加:

apply plugin: 'me.ele.lancet'

dependencies {

? ? compileOnly 'me.ele:lancet-base:1.0.5'

}

1

2

3

4

5

接下來,我們就可以使用Lancet了,這里我們需要先新建一個類去進(jìn)行專門的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();

? ? }

}

1

2

3

4

5

6

7

8

9

上述的方法就是對android.util.Log的i方法進(jìn)行Hook,并在所有的msg后面加上"JsonChao"字符串,注意這里的i方法我們需要從android.util.Log里面將它的i方法復(fù)制過來,確保方法名和對應(yīng)的參數(shù)信息一致;然后,方法上面的@TargetClass與@Proxy分別是指定對應(yīng)的全路徑類名與方法名;最后,我們需要通過Lancet提供的Origin類去調(diào)用它的call方法來實(shí)現(xiàn)返回原來的調(diào)用信息。完成之后,我們重新運(yùn)行項(xiàng)目,會出現(xiàn)如下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

1

2

可以看到,log后面都加上了我們預(yù)先添加的字符串,說明Hook成功了。下面,我們就可以用Lancet來統(tǒng)計(jì)一下項(xiàng)目界面的秒開率了,代碼如下所示:

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();

? ? // 調(diào)用當(dāng)前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();

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

上面,我們通過@TargetClass和@Insert兩個注解實(shí)現(xiàn)Hook了android.support.v7.app.AppCompatActivity的onCreate與onWindowFocusChanged方法。我們注意到,這里@Insert注解可以指定兩個參數(shù),其源碼如下所示:

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface Insert {

? ? String value();

? ? boolean mayCreateSuper() default false;

}

1

2

3

4

5

6

7

第二個參數(shù)mayCreateSuper設(shè)定為true則表明如果沒有重寫父類的方法,則會默認(rèn)去重寫這個方法。對應(yīng)到我們ActivityHooker里面實(shí)現(xiàn)的@Insert注解方法就是如果當(dāng)前的Activity沒有重寫父類的onCreate和

onWindowFocusChanged方法,則此時默認(rèn)會去重寫父類的這個方法,以避免因某些Activity不存在該方法而Hook失敗的情況。

然后,我們注意到@TargetClass也可以指定兩個參數(shù),其源碼如下所示:

@Retention(RetentionPolicy.RUNTIME)

@java.lang.annotation.Target({ElementType.TYPE, ElementType.METHOD})

public @interface TargetClass {

? ? String value();

? ? Scope scope() default Scope.SELF;

}

1

2

3

4

5

6

7

第二個參數(shù)scope指定的值是一個枚舉,可選的值如下所示:

public enum Scope {

? ? SELF,

? ? DIRECT,

? ? ALL,

? ? LEAF

}

1

2

3

4

5

6

7

對于Scope.SELF,它代表僅匹配目標(biāo)value所指定的一個匹配類;對于DIRECT,它代表匹配value所指定的類的一個直接子類;如果是Scope.ALL,它就表明會去匹配value所指定的類的所有子類,而我們上面指定的value值為android.support.v7.app.AppCompatActivity,因?yàn)閟cope指定為了Scope.ALL,則說明會去匹配AppCompatActivity的所有子類。而最后的Scope.LEAF 代表匹配 value 指定類的最終子類,因?yàn)閖ava是單繼承,所以繼承關(guān)系是樹形結(jié)構(gòu),所以這里代表了指定類為頂點(diǎn)的繼承樹的所有葉子節(jié)點(diǎn)。

最后,我們設(shè)定了一個ActivityRecord類去記錄onCreate與onWindowFocusChanged的時間戳,如下所示:

public class ActivityRecord {

? ? /**

? ? * 避免沒有僅執(zhí)行onResume就去統(tǒng)計(jì)界面打開速度的情況,如息屏、亮屏等等

? ? */

? ? public boolean isNewCreate;

? ? public long mOnCreateTime;

? ? public long mOnWindowsFocusChangedTime;

}

1

2

3

4

5

6

7

8

9

10

通過sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime得到的時間即為界面的打開速度,最后,重新運(yùn)行項(xiàng)目,會得到如下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

1

2

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

最后,我們來看下界面秒開的監(jiān)控緯度。

3、界面秒開監(jiān)控緯度

對于界面秒開的監(jiān)控緯度,主要分為以下三個方面:

總體耗時

生命周期耗時

生命周期間隔耗時

首先,我們會監(jiān)控界面打開的整體耗時,也就是onCreate到onWindowFocusChanged這個方法的耗時;當(dāng)然,如果我們是在一個特殊的界面,我們需要更精確的知道界面打開的一個時間,這個我們可以用自定義的接口去實(shí)現(xiàn)。其次,我們也需要去監(jiān)控生命周期的一個耗時,如onCreate、onStart、onResume等等。最后,我們也需要去做生命周期間隔的耗時監(jiān)控,這點(diǎn)經(jīng)常被我們所忽略,比如onCreate的結(jié)束到onStart開始的這一段時間,也是有時間損耗的,我們可以監(jiān)控它是不是在一個合理的范圍之內(nèi)。通過這三個方面的監(jiān)控緯度,我們就能夠非常細(xì)粒度地去檢測頁面秒開各個方面的情況。

四、優(yōu)雅監(jiān)控耗時盲區(qū)

盡管我們在應(yīng)用中監(jiān)控了很多的耗時區(qū)間,但是還是有一些耗時區(qū)間我們還沒有捕捉到,如onResume到列表展示的間隔時間,這些時間在我們的統(tǒng)計(jì)過程中很容易被忽視,這里我們舉一個小栗子:

我們在Activity的生命周期中post了一個message,那這個message很可能其中

執(zhí)行了一段耗時操作,那你知道這個message它的具體執(zhí)行時間嗎?這個message其實(shí)

很有可能在列表展示之前就執(zhí)行了,如果這個message耗時1s,那么列表的展示

時間就會延遲1s,如果是200ms,那么我們設(shè)定的自動化卡頓檢測就無法

發(fā)現(xiàn)它,那么列表的展示時間就會延遲200ms。

1

2

3

4

5

其實(shí)這種場景非常常見,接下來,我們就在項(xiàng)目中來進(jìn)行實(shí)戰(zhàn)演練。

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

// 以下代碼是為了演示Msg導(dǎo)致的主線程卡頓

? ? new Handler().post(() -> {

? ? ? ? LogHelper.i("Msg 執(zhí)行");

? ? ? ? try {

? ? ? ? ? ? Thread.sleep(1000);

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? });

1

2

3

4

5

6

7

8

9

接著,我們在RecyclerView對應(yīng)的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;

? ? ? ? ? ? }

? ? ? ? });

? ? }

1

2

3

4

5

6

7

8

9

10

11

最后,我們重新運(yùn)行下項(xiàng)目,看看兩者的執(zhí)行時間,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 執(zhí)行

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

1

2

3

4

5

6

從log信息中可以看到,MAinActivity的onWindowFocusChanged方法延遲了1000ms才被調(diào)用,與此同時,列表頁時延遲了1000ms才展示出來。也就是說,post的這個message消息是執(zhí)行在界面、列表展示之前的。因?yàn)槿魏我粋€開發(fā)都有可能在某一個生命周期或者是某一個階段以及一些第三方的SDK里面,回去做一些handler post的相關(guān)操作,這樣,他的handler post的message的執(zhí)行,很有可能在我們的界面或列表展示之前就被執(zhí)行,所以說,出現(xiàn)這種耗時的盲區(qū)是非常普遍的,而且也不好排查,下面,我們分析下耗時盲區(qū)存在的難點(diǎn)。

1、耗時盲區(qū)監(jiān)控難點(diǎn)

首先,我們可以通過細(xì)化監(jiān)控的方式去獲取耗時的一些盲區(qū),但是我們卻不知道在這個盲區(qū)中它執(zhí)行了什么操作。其次,對于線上的一些耗時盲區(qū),我們是無法進(jìn)行排查的。

這里,我們先來看看如何建立耗時盲區(qū)監(jiān)控的線下方案。

2、耗時盲區(qū)監(jiān)控線下方案

這里我們直接使用TraceView去檢測即可,因?yàn)樗軌?b>清晰地記錄線程在具體的時間內(nèi)到底做了什么操作,特別適合一段時間內(nèi)的盲區(qū)監(jiān)控。

然后,我們來看下如何建立耗時盲區(qū)監(jiān)控的線上方案。

3、耗時盲區(qū)監(jiān)控線上方案

我們知道主線程的所有方法都是通過message來執(zhí)行的,還記得在之前我們學(xué)習(xí)了一個庫:AndroidPerformanceMonitor,我們是否可以通過這個mLogging來做盲區(qū)檢測呢?通過這個mLogging確實(shí)可以知道我們主線程發(fā)生的message,但是通過mLogging無法獲取具體的調(diào)用棧信息,因?yàn)樗?b>獲取的調(diào)用棧信息都是系統(tǒng)回調(diào)回來的,它并不知道當(dāng)前的message是被誰拋出來的,所以說,這個方案并不夠完美。

那么,我們是否可以通過AOP的方式去切Handler方法呢?比如sendMessage、sendMessageDeleayd方法等等,這樣我們就可以知道發(fā)生message的一個堆棧,但是這種方案也存在著一個問題,就是它不清楚準(zhǔn)確的執(zhí)行時間,我們切了這個handler的方法,僅僅只知道它具體是在哪個地方被發(fā)的和它所對應(yīng)的堆棧信息,但是無法獲取準(zhǔn)確的執(zhí)行時間。如果我們想知道在onResume到列表展示之間執(zhí)行了哪些message,那么通過AOP的方式也無法實(shí)現(xiàn)。

那么,最終的耗時盲區(qū)監(jiān)控的一個線上方案就是使用一個統(tǒng)一的Handler,定制了它的兩個方法,一個是sendMessageAtTime,另外一個是dispatchMessage方法。因?yàn)閷τ诎l(fā)送message,不管調(diào)用哪個方法最終都會調(diào)用到一個是sendMessageAtTime這個方法,而處理message呢,它最終會調(diào)用dispatchMessage方法。然后,我們需要定制一個gradle插件,來實(shí)現(xiàn)自動化的接入我們定制好的handler,通過這種方式,我們就能在編譯期間去動態(tài)地替換所有使用Handler的父類為我們定制好的這個handler。這樣,在整個項(xiàng)目中,所有的sendMessage和handleMessage都會經(jīng)過我們的回調(diào)方法。接下來,我們來進(jìn)行一下實(shí)戰(zhàn)演練。

首先,我這里給出定制好的全局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) {

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

上面的GlobalHandler將會是我們項(xiàng)目中所有Handler的一個父類。在注釋1處,我們在sendMessageAtTime這個方法里面判斷如果message發(fā)送成功,將會把當(dāng)前message對象對應(yīng)的調(diào)用棧信息都保存到一個ConcurrentHashMap中,GetDetailHandlerHelper類的代碼如下所示:

public class GetDetailHandlerHelper {

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

? ? public static ConcurrentHashMap<Message, String> getMsgDetail() {

? ? ? ? return sMsgDetail;

? ? }

}

1

2

3

4

5

6

7

8

這樣,我們就能夠知道這個message它是被誰發(fā)送過來的。然后,在dispatchMessage方法里面,我們可以計(jì)算拿到其處理消息的一個耗時,并在注釋2處將這個耗時保存到一個jsonObject對象中,同時,我們也可以通過GetDetailHandlerHelper類的ConcurrentHashMap對象拿到這個message對應(yīng)的堆棧信息,并在注釋3處將它們輸出到log控制臺上。當(dāng)然,如果是線上監(jiān)控,則會把這些信息保存到本地,然后選擇合適的時間去上傳。最后,我們還可以在方法體里面做一個判斷,我們設(shè)置一個閾值,比如閾值為20ms,超過了20ms就把這些保存好的信息上報到APM后臺。

在前面的實(shí)戰(zhàn)演練中,我們使用了handler post的方式去發(fā)送一個消息,通過gradle插件將所有handler的父類替換為我們定制好的GlobalHandler之后,我們就可以優(yōu)雅地去監(jiān)控應(yīng)用中的耗時盲區(qū)了。

對于實(shí)現(xiàn)全局替換handler的gradle插件,除了使用AspectJ實(shí)現(xiàn)之外,這里推薦一個已有的項(xiàng)目:DroidAssist。

然后,重新運(yùn)行項(xiàng)目,關(guān)鍵的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"

1

2

3

從以上信息我們不僅可以知道m(xù)essage執(zhí)行的時間,還可以從對應(yīng)的堆棧信息中得到發(fā)送message的位置,這里的位置是MainActivity的107行,也就是new Handler().post()這一行代碼。使用這種方式我們就可以知道在列表展示之前到底執(zhí)行了哪些自定義的message,我們一眼就可以知道哪些message其實(shí)是不符合我們預(yù)期的,比如說message的執(zhí)行時間過長,或者說這個message其實(shí)可以延后執(zhí)行,這個我們都可以根據(jù)實(shí)際的項(xiàng)目和業(yè)務(wù)需求進(jìn)行相應(yīng)地修改。

4、耗時盲區(qū)監(jiān)控方案總結(jié)

耗時盲區(qū)監(jiān)控是我們卡頓監(jiān)控中不可或缺的一個環(huán)節(jié),也是卡頓監(jiān)控全面性的一個重要保障。而需要注意的是,TraceView僅僅適用于線下的一個場景,同時對于TraceView來說,它可以用于監(jiān)控我們系統(tǒng)的message。而最后介紹的動態(tài)替換的方式其實(shí)是適合于線上的,同時,它僅僅監(jiān)控應(yīng)用自身的一個message。

五、卡頓優(yōu)化技巧總結(jié)

1、卡頓優(yōu)化實(shí)踐經(jīng)驗(yàn)

如果應(yīng)用出現(xiàn)了卡頓現(xiàn)象,那么可以考慮以下方式進(jìn)行優(yōu)化:

首先,對于耗時的操作,我們可以考慮異步或延遲初始化的方式,這樣可以解決大多數(shù)的問題。但是,大家一定要注意代碼的優(yōu)雅性。

對于布局加載優(yōu)化,可以采用AsyncLayoutInflater或者是X2C的方式來優(yōu)化主線程IO以及反射導(dǎo)致的消耗,同時,需要注意,對于重繪問題,要給與一定的重視。

此外,內(nèi)存問題也可能會導(dǎo)致應(yīng)用界面的卡頓,我們可以通過降低內(nèi)存占用的方式來減少GC的次數(shù)以及時間,而GC的次數(shù)和時間我們可以通過log查看。

然后,我們來看看卡頓優(yōu)化的工具建設(shè)。

2、卡頓優(yōu)化工具建設(shè)

工具建設(shè)這塊經(jīng)常容易被大家所忽視,但是它的收益卻非常大,也是卡頓優(yōu)化的一個重點(diǎn)。首先,對于系統(tǒng)工具而言,我們要有一個認(rèn)識,同時一定要學(xué)會使用它,這里我們再回顧一下。

對于Systrace來說,我們可以很方便地看出來它的CPU使用情況。另外,它的開銷也比較小。

對于TraceView來說,我們可以很方便地看出來每一個線程它在特定的時間內(nèi)做了什么操作,但是TraceView它的開銷相對比較大,有時候可能會被帶偏優(yōu)化方向。

同時,需要注意,StrictMode也是一個非常強(qiáng)大的工具。

然后,我們介紹了自動化工具建設(shè)以及優(yōu)化方案。我們介紹了兩個工具,AndroidPerformanceMonitor以及ANR-WatchDog。同時針對于AndroidPerformanceMonitor的問題,我們采用了高頻采集,以找出重復(fù)率高的堆棧這樣一種方式進(jìn)行優(yōu)化,在學(xué)習(xí)的過程中,我們不僅需要學(xué)會怎樣去使用工具,更要去理解它們的實(shí)現(xiàn)原理以及各自的使用場景。

同時,我們對于卡頓優(yōu)化工具的建設(shè)也做了細(xì)化,對于單點(diǎn)問題,比如說IPC監(jiān)控,我們通過Hook的手段來做到盡早的發(fā)現(xiàn)問題。對于耗時盲區(qū)的監(jiān)控,我們在線上采用的是替換Handler的方式來監(jiān)控所有子線程message執(zhí)行的耗時以及調(diào)用堆棧。

最后,我們來看一下卡頓監(jiān)控的指標(biāo)。我們會計(jì)算應(yīng)用整體的卡頓率,ANR率、界面秒開率以及交換時間、生命周期時間等等。在上報ANR信息的同時,我們也需要上報環(huán)境和場景信息,這樣不僅方便我們在不同版本之間進(jìn)行橫向?qū)Ρ?/b>,同時,也可以結(jié)合我們的報警平臺在第一時間感知到異常。

六、常見卡頓問題解決方案總結(jié)

1、CPU資源爭搶引發(fā)的卡頓問題如何解決?

此時,我們的應(yīng)用不僅應(yīng)該控制好核心功能的CPU消耗,也需要盡量減少非核心需求的CPU消耗。

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

比如List.removeall方法,它內(nèi)部會遍歷一次需要過濾的消息列表,在已經(jīng)存在循環(huán)列表的情況下會造成CPU資源的冗余使用,此時應(yīng)該去優(yōu)化相關(guān)的算法,避免使用List.removeall這個方法。

3、如何減少圖形處理的CPU消耗?

這個時候我們需要使用神器renderscript來圖形處理的相關(guān)運(yùn)算,將CPU轉(zhuǎn)換到GPU。關(guān)于renderscript的背景知識可以看看筆者之前寫的深入探索Android布局優(yōu)化(下)。

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

此時只能關(guān)閉文本TextView的硬件加速,如下所示:

textView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

1

當(dāng)開啟了硬件加速進(jìn)行長中文字體的渲染時,首先會調(diào)用ViewRootImpl.draw()方法,最后會調(diào)用GLES20Canvas.nDrawDisplayList()方法開始通過JNI調(diào)整到Native層。在這個方法里,會繼續(xù)調(diào)用OpenGLRenderer.drawDisplayList()方法,它通過調(diào)用DisplayList的replay方法,以回放前面錄制的DisplayList執(zhí)行繪制操作

DisplayList的replay方法會遍歷DisplayList中保存的每一個操作。其中渲染字體的操作名是DrawText,當(dāng)遍歷到一個DrawText操作時,會調(diào)用OpenGLRender::drawText方法區(qū)渲染字體。最終,會在OpenGLRender::drawText方法里去調(diào)用Font::render()方法渲染字體,而在這個方法中有一個很關(guān)鍵的操作,即獲取字體緩存。我們都知道每一個中文的編碼都是不同的,因此中文的緩存效果非常不理想,但是對于英文而言,只需要緩存26個字母就可以了。在Android 4.1.2版本之前對文本的Buffer設(shè)置過小,所以情況比較嚴(yán)重,如果你的應(yīng)用在其它版本的渲染性能尚可,就可以僅僅把Android 4.0.x的硬件加速關(guān)閉,代碼如下所示:

// AndroidManifest中

<Applicaiton

? ? ? ? ...

? ? ? ? android:hardwareAccelerated="@bool/hardware_acceleration">


// value-v14、value-v15中設(shè)置相應(yīng)的Bool

值即可

<bool name="hardware_acceleration">false</bool>

1

2

3

4

5

6

7

8

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

1、在軟件渲染的情況下,如果需要重繪某個父View的所有子View,只需要調(diào)用這個Parent View的invalidate()方法即可,但如果開啟了硬件加速,這么做是行不通的,需要遍歷整個子View并調(diào)用invalidate()。

2、在軟件渲染的情況下,會常常使用Bitmap重用的方式來節(jié)省內(nèi)存,但是如果開啟了硬件加速,這將會無效。

3、當(dāng)開啟硬件加速的UI在前臺運(yùn)行時,需要耗費(fèi)額外的內(nèi)存。當(dāng)硬件加速的UI切換到后臺時,上述額外內(nèi)存有可能不會釋放,這大多存在于Android 4.1.2版本中。

4、長或?qū)挻笥?048像素的Bitmap無法繪制,顯示為一片透明。原因是OpenGL的材質(zhì)大小上限為2048 * 2048,因此對于超過2048像素的Bitmap,需要將其切割成2048 * 2048以內(nèi)的圖片塊,最后在顯示的時候拼起來。

5、當(dāng)UI中存在過渡繪制時,可能會發(fā)生花屏,一般來說繪制少于5層不會出現(xiàn)花屏現(xiàn)象,如果有大塊紅色區(qū)域就要十分小心了。

6、需要注意,關(guān)于LAYER_TYPE_SOFTWARE,雖然無論在App打開硬件加速或沒有打開硬件加速的時候,都會通過軟件繪制Bitmap作為離屏緩存,但區(qū)別在于打開硬件加速的時候,Bitmap最終還會通過硬件加速方式drawDisplayList去渲染這個Bitmap。

七、卡頓優(yōu)化的常見問題

1、你是怎么做卡頓優(yōu)化的?

從項(xiàng)目的初期到壯大期,最后再到成熟期,每一個階段都針對卡頓優(yōu)化做了不同的處理。各個階段所做的事情如下所示:

1、系統(tǒng)工具定位、解決

2、自動化卡頓方案及優(yōu)化

3、線上監(jiān)控及線下監(jiān)測工具的建設(shè)

我做卡頓優(yōu)化也是經(jīng)歷了一些階段,最初我們的項(xiàng)目當(dāng)中的一些模塊出現(xiàn)了卡頓之后,我是通過系統(tǒng)工具進(jìn)行了定位,我使用了Systrace,然后看了卡頓周期內(nèi)的CPU狀況,同時結(jié)合代碼,對這個模塊進(jìn)行了重構(gòu),將部分代碼進(jìn)行了異步和延遲,在項(xiàng)目初期就是這樣解決了問題。

但是呢,隨著我們項(xiàng)目的擴(kuò)大,線下卡頓的問題也越來越多,同時,在線上,也有卡頓的反饋,但是線上的反饋卡頓,我們在線下難以復(fù)現(xiàn),于是我們開始尋找自動化的卡頓監(jiān)測方案,其思路是來自于Android的消息處理機(jī)制,主線程執(zhí)行任何代碼都會回到Looper.loop方法當(dāng)中,而這個方法中有一個mLogging對象,它會在每個message的執(zhí)行前后都會被調(diào)用,我們就是利用這個前后處理的時機(jī)來做到的自動化監(jiān)測方案的。同時,在這個階段,我們也完善了線上ANR的上報,我們采取的方式就是監(jiān)控ANR的信息,同時結(jié)合了ANR-WatchDog,作為高版本沒有文件權(quán)限的一個補(bǔ)充方案。

在做完這個卡頓檢測方案之后呢,我們還做了線上監(jiān)控及線下檢測工具的建設(shè),最終實(shí)現(xiàn)了一整套完善,多維度的解決方案。

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

我們的思路是來自于Android的消息處理機(jī)制,主線程執(zhí)行任何代碼它都會走到Looper.loop方法當(dāng)中,而這個函數(shù)當(dāng)中有一個mLogging對象,它會在每個message處理前后都會被調(diào)用,而主線程發(fā)生了卡頓,那就一定會在dispatchMessage方法中執(zhí)行了耗時的代碼,那我們在這個message執(zhí)行之前呢,我們可以在子線程當(dāng)中去postDelayed一個任務(wù),這個Delayed的時間就是我們設(shè)定的閾值,如果主線程的messaege在這個閾值之內(nèi)完成了,那就取消掉這個子線程當(dāng)中的任務(wù),如果主線程的message在閾值之內(nèi)沒有被完成,那子線程當(dāng)中的任務(wù)就會被執(zhí)行,它會獲取到當(dāng)前主線程執(zhí)行的一個堆棧,那我們就可以知道哪里發(fā)生了卡頓。

經(jīng)過實(shí)踐,我們發(fā)現(xiàn)這種方案獲取的堆棧信息它不一定是準(zhǔn)確的,因?yàn)楂@取到的堆棧信息它很可能是主線程最終執(zhí)行的一個位置,而真正耗時的地方其實(shí)已經(jīng)執(zhí)行完成了,于是呢,我們就對這個方案做了一些優(yōu)化,我們采取了高頻采集的方案,也就是在一個周期內(nèi)我們會多次采集主線程的堆棧信息,如果發(fā)生了卡頓,那我們就將這些卡頓信息壓縮之后上報給APM后臺,然后找出重復(fù)的堆棧信息,這些重復(fù)發(fā)生的堆棧大概率就是卡頓發(fā)生的一個位置,這樣就提高了獲取卡頓信息的一個準(zhǔn)確性。

3、卡頓的一整套解決方案是怎么做的?

首先,針對卡頓,我們采用了線上、線下工具相結(jié)合的方式,線下工具我們冊中醫(yī)藥盡可能早地去暴露問題,而針對于線上工具呢,我們側(cè)重于監(jiān)控的全面性、自動化以及異常感知的靈敏度。

同時呢,卡頓問題還有很多的難題。比如說有的代碼呢,它不到你卡頓的一個閾值,但是執(zhí)行過多,或者它錯誤地執(zhí)行了很多次,它也會導(dǎo)致用戶感官上的一個卡頓,所以我們在線下通過AOP的方式對常見的耗時代碼進(jìn)行了Hook,然后對一段時間內(nèi)獲取到的數(shù)據(jù)進(jìn)行分析,我們就可以知道這些耗時的代碼發(fā)生的時機(jī)和次數(shù)以及耗時情況。然后,看它是不是滿足我們的一個預(yù)期,不滿足預(yù)期的話,我們就可以直接到線下進(jìn)行修改。同時,卡頓監(jiān)控它還有很多容易被忽略的一個盲區(qū),比如說生命周期的一個間隔,那對于這種特定的問題呢,我們就采用了編譯時注解的方式修改了項(xiàng)目當(dāng)中所有Handler的父類,對于其中的兩個方法進(jìn)行了監(jiān)控,我們就可以知道主線程message的執(zhí)行時間以及它們的調(diào)用堆棧。

對于線上卡頓,我們除了計(jì)算App的卡頓率、ANR率等常規(guī)指標(biāo)之外呢,我們還計(jì)算了頁面的秒開率、生命周期的執(zhí)行時間等等。而且,在卡頓發(fā)生的時刻,我們也盡可能多地保存下來了當(dāng)前的一個場景信息,這為我們之后解決或者復(fù)現(xiàn)這個卡頓留下了依據(jù)。

八、總結(jié)

恭喜你,如果你看到了這里,你會發(fā)現(xiàn)要做好應(yīng)用的卡頓優(yōu)化的確不是一件簡單的事,它需要你有成體系的知識構(gòu)建基底。最后,我們再來回顧一下面對卡頓優(yōu)化,我們已經(jīng)探索的以下九大主題:

1、卡頓優(yōu)化分析方法與工具:背景介紹、卡頓分析方法之使用shell命令分析CPU耗時、卡頓優(yōu)化工具。

2、自動化卡頓檢測方案及優(yōu)化:卡頓檢測方案原理、AndroidPerformanceMonitor實(shí)戰(zhàn)及其優(yōu)化。

3、ANR分析與實(shí)戰(zhàn):ANR執(zhí)行流程、線上ANR監(jiān)控方式、ANR-WatchDog原理。

4、卡頓單點(diǎn)問題檢測方案:IPC單點(diǎn)問題檢測方案、卡頓問題檢測方案。

5、如何實(shí)現(xiàn)界面秒開?:界面秒開實(shí)現(xiàn)、Lancet、界面秒開監(jiān)控緯度。

6、優(yōu)雅監(jiān)控耗時盲區(qū):耗時盲區(qū)監(jiān)控難點(diǎn)以及線上與線下的監(jiān)控方案。

7、卡頓優(yōu)化技巧總結(jié):卡頓優(yōu)化實(shí)踐經(jīng)驗(yàn)、卡頓優(yōu)化工具建設(shè)。

8?、常見卡頓問題解決方案總結(jié)

9、卡頓優(yōu)化的常見問題

相信看到這里,你一定收獲滿滿,但是要記住,方案再好,也只有自己動手去實(shí)踐,才能真正地掌握它。只有重視實(shí)踐,充分運(yùn)用感性認(rèn)知潛能,在項(xiàng)目中磨煉自己,才是正確的學(xué)習(xí)之道。在實(shí)踐中,在某些關(guān)鍵動作上刻意練習(xí),也會取得事半功倍的效果。

參考鏈接:

1、國內(nèi)Top團(tuán)隊(duì)大牛帶你玩轉(zhuǎn)Android性能分析與優(yōu)化 第6章 卡頓優(yōu)化

2、極客時間之Android開發(fā)高手課 卡頓優(yōu)化

3、《Android移動性能實(shí)戰(zhàn)》第四章 CPU

4、《Android移動性能實(shí)戰(zhàn)》第七章 流暢度

5、Android dumpsys cpuinfo 信息解讀

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

7、nanoscope-An extremely accurate Android method tracing tool

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

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

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

11、Linux環(huán)境下進(jìn)程的CPU占用率

12、使用 ftrace

13、profilo-A library for performance traces from production

14、ftrace 簡介

15、atrace源碼

16、AndroidAdvanceWithGeektime

/ Chapter06

17、AndroidAdvanceWithGeektime

/ Chapter06-plus

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

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

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