anr 檢測(cè)

前言

成為一名優(yōu)秀的Android開發(fā)啡邑,需要一份完備的知識(shí)體系坯临,在這里蚂且,讓我們一起成長(zhǎng)為自己所想的那樣~隘梨。

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

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

2驰唬、卡頓單點(diǎn)問(wèn)題檢測(cè)方案

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

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

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

6辖佣、常見卡頓問(wèn)題解決方案總結(jié)

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

卡頓時(shí)間過(guò)長(zhǎng)搓逾,一定會(huì)造成應(yīng)用發(fā)生ANR卷谈。下面,我們就來(lái)從應(yīng)用的ANR分析與實(shí)戰(zhàn)來(lái)開始今天的探索之旅霞篡。

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

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

首先朗兵,我們?cè)賮?lái)回顧一下ANR的幾種常見的類型污淋,如下所示:

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

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

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

具體的時(shí)間定義我們可以在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

接下來(lái)歌憨,我們來(lái)看一下ANR的執(zhí)行流程。

ANR執(zhí)行流程

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

2漆改、然后心铃,我們的進(jìn)程就會(huì)接收到異常終止信息,并開始寫入進(jìn)程ANR信息挫剑,也就是當(dāng)時(shí)應(yīng)用的場(chǎng)景信息去扣,它包含了應(yīng)用所有的堆棧信息扑毡、CPU态蒂、IO等使用的情況。

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

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

在上面我們說(shuō)過(guò)袱蚓,當(dāng)應(yīng)用發(fā)生ANR時(shí)钞啸,會(huì)寫入當(dāng)時(shí)發(fā)生ANR的場(chǎng)景信息到文件中,那么癞松,我們可不可以通過(guò)這個(gè)文件來(lái)判斷是否發(fā)生了ANR呢爽撒?

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

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

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

那就是ANR-WatchDog幔崖,下面我就來(lái)詳細(xì)地介紹一下它食店。

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

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

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

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

1

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

new ANRWatchDog().start();

1

可以看到,它的初始化方式非常地簡(jiǎn)單渠退,同時(shí)忙迁,它內(nèi)部的實(shí)現(xiàn)也非常簡(jiǎn)單脐彩,整個(gè)庫(kù)只有兩個(gè)類,一個(gè)是ANRWatchDog动漾,另一個(gè)是ANRError丁屎。

接下來(lái)我們來(lái)看一下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類晨川,也就是它是一個(gè)線程,對(duì)于線程來(lái)說(shuō)删豺,最重要的就是其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、接著蓬蝶,聲明了一個(gè)默認(rèn)的超時(shí)間隔時(shí)間尘分,默認(rèn)的值為5000ms。

? ? long interval = _timeoutInterval;

? ? // 3丸氛、然后培愁,在while循環(huán)中通過(guò)_uiHandler去post一個(gè)_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);

? ? ? ? }

? ? ? ? // 接下來(lái),線程會(huì)sleep一段時(shí)間禾锤,默認(rèn)值為5000ms私股。

? ? ? ? try {

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

? ? ? ? } catch (InterruptedException e) {

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

? ? ? ? ? ? return ;

? ? ? ? }

? ? ? ? // 4、如果主線程沒有處理Runnable恩掷,即_tick的值沒有被賦值為0倡鲸,則說(shuō)明發(fā)生了ANR,第二個(gè)_reported標(biāo)志位是為了避免重復(fù)報(bào)道已經(jīng)處理過(guò)的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峭状、如果沒有主動(dòng)給ANR_Watchdog設(shè)置線程名,則會(huì)默認(rèn)會(huì)使用ANRError的NewMainOnly方法去處理ANR寸宏。

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

? ? ? ? ? ? }


? ? ? ? ? // 6宁炫、最后會(huì)通過(guò)ANRListener調(diào)用它的onAppNotResponding方法,其默認(rèn)的處理會(huì)直接拋出當(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處,聲明了一個(gè)默認(rèn)的超時(shí)間隔時(shí)間幽钢,默認(rèn)的值為5000ms歉备。然后,注釋3處匪燕,在while循環(huán)中通過(guò)_uiHandler去post一個(gè)_ticker Runnable蕾羊。注意這里的_tick默認(rèn)是0,所以needPost即為true帽驯。接下來(lái)龟再,線程會(huì)sleep一段時(shí)間,默認(rèn)值為5000ms尼变。在注釋4處利凑,如果主線程沒有處理Runnable,即_tick的值沒有被賦值為0嫌术,則說(shuō)明發(fā)生了ANR哀澈,第二個(gè)_reported標(biāo)志位是為了避免重復(fù)報(bào)道已經(jīng)處理過(guò)的ANR。如果發(fā)生了ANR度气,就會(huì)調(diào)用接下來(lái)的代碼割按,開始會(huì)處理debug的情況,然后蚯嫌,我們看到注釋5處哲虾,如果沒有主動(dòng)給ANR_Watchdog設(shè)置線程名丙躏,則會(huì)默認(rèn)會(huì)使用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晒旅、返回一個(gè)包含主線程名栅盲、主線程堆棧信息以及發(fā)生ANR的最小時(shí)間值的實(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處谈秫,首先獲了主線程的堆棧信息,然后返回了一個(gè)包含主線程名鱼鼓、主線程堆棧信息以及發(fā)生ANR的最小時(shí)間值的實(shí)例拟烫。(我們可以改造其源碼在此時(shí)添加更多的卡頓現(xiàn)場(chǎng)信息,如CPU 使用率和調(diào)度信息迄本、內(nèi)存相關(guān)信息硕淑、I/O 和網(wǎng)絡(luò)相關(guān)的信息等等

接下來(lái),我們?cè)倩氐紸NRWatchDog的run方法中的注釋6處,最后這里會(huì)通過(guò)ANRListener調(diào)用它的onAppNotResponding方法置媳,其默認(rèn)的處理會(huì)直接拋出當(dāng)前的ANRError于樟,導(dǎo)致程序崩潰。對(duì)應(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)原理之后拇囊,我們?cè)囈辉囁男Ч绾斡厍J紫龋覀兘oMainActivity中的懸浮按鈕添加主線程休眠10s的代碼寥袭,如下所示:

@OnClick({R.id.main_floating_action_btn})

void onClick(View view) {

? ? switch (view.getId()) {

? ? ? ? case R.id.main_floating_action_btn:

? ? ? ? ? ? try {

? ? ? ? ? ? ? ? // 對(duì)應(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ā)生了崩潰尝江。接著涉波,我們?cè)贚ogcat過(guò)濾欄中輸入fatal關(guān)鍵字,找出致命的錯(cuò)誤炭序,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方法窗声,對(duì)應(yīng)的行數(shù)為170行,從前可知辜纲,這里正是線程休眠的地方笨觅。

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

2见剩、ANR-WatchDog原理

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

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

post完成之后呢羹呵,我們這個(gè)線程就sleep一段時(shí)間。

在sleep之后呢疗琉,它就會(huì)來(lái)檢測(cè)我們這個(gè)值有沒有被修改冈欢,如果這個(gè)值被修改了,那就說(shuō)明我們?cè)谥骶€程中執(zhí)行了這個(gè)message盈简,即表明主線程沒有發(fā)生卡頓凑耻,否則犯戏,則說(shuō)明主線程發(fā)生了卡頓

最后拳话,ANR-WatchDog就會(huì)判斷發(fā)生了ANR先匪,拋出一個(gè)異常給我們。

最后弃衍,ANR-WatchDog的工作流程簡(jiǎn)圖如下所示:

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

上面我們最后說(shuō)到呀非,如果檢測(cè)到主線程發(fā)生了卡頓,則會(huì)拋出一個(gè)ANR異常镜盯,這將會(huì)導(dǎo)致應(yīng)用崩潰岸裙,顯然不能將這種方案帶到線上,那么速缆,有什么方式能夠自定義最后發(fā)生卡頓時(shí)的處理過(guò)程嗎降允?

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

3翅楼、小結(jié)

ANR-WatchDog是一種非侵入式的ANR監(jiān)控方案,它能夠彌補(bǔ)我們?cè)诟甙姹局袥]有權(quán)限去讀取traces.txt文件的問(wèn)題真慢,需要注意的是毅臊,在線上這兩種方案我們需要結(jié)合使用。

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

對(duì)于AndroidPerformanceMonitor來(lái)說(shuō)朗鸠,它是監(jiān)控我們主線程中每一個(gè)message的執(zhí)行蚯撩,它會(huì)在主線程的每一個(gè)message的前后打印一個(gè)時(shí)間戳,然后童社,我們就可以據(jù)此計(jì)算每一個(gè)message的具體執(zhí)行時(shí)間求厕,但是我們需要注意的是一個(gè)message的執(zhí)行時(shí)間通常是非常短暫的著隆,也就是很難達(dá)到ANR這個(gè)級(jí)別扰楼。然后我們來(lái)看看ANR-WatchDog的原理,它是不管應(yīng)用是如何執(zhí)行的美浦,它只會(huì)看最終的結(jié)果弦赖,即sleep 5s之后,我就看主線程的這個(gè)值有沒有被更改浦辨。如果說(shuō)被改過(guò)蹬竖,就說(shuō)明沒有發(fā)生ANR,否則,就表明發(fā)生了ANR币厕。

根據(jù)這兩個(gè)庫(kù)的原理列另,我們便可以判斷出它們分別的適用場(chǎng)景,對(duì)于AndroidPerformanceMonitor來(lái)說(shuō)旦装,它適合監(jiān)控卡頓页衙,因?yàn)槊恳粋€(gè)message它執(zhí)行的時(shí)間并不長(zhǎng)。對(duì)于ANR-WatchDog來(lái)說(shuō)阴绢,它更加適合于ANR監(jiān)控的補(bǔ)充店乐。

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

通過(guò)libart.so、dlsym調(diào)用ThreadList::ForEach方法篓足,拿到所有的 Native 線程對(duì)象伏穆。

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

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

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

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

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

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

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

都不到卡頓的閾值檀夹,那自動(dòng)化卡頓檢測(cè)方案也就不能夠檢測(cè)出卡

頓筋粗,但是對(duì)用戶來(lái)說(shuō),用戶就覺得你的App就是有些卡頓炸渡。

1

2

3

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

1暇唾、IPC單點(diǎn)問(wèn)題檢測(cè)方案

常見的單點(diǎn)問(wèn)題有主線程IPC、DB操作等等辰斋,這里我就拿主線程IPC來(lái)說(shuō)策州,因?yàn)镮PC其實(shí)是一個(gè)很耗時(shí)的操作,但是在實(shí)際開發(fā)過(guò)程中宫仗,我們可能對(duì)IPC操作沒有足夠的重視够挂,所以,我們經(jīng)常在主程序中去做頻繁IPC操作藕夫,所以說(shuō)孽糖,這種耗時(shí)它可能并不到你設(shè)定卡頓的一個(gè)閾值,接下來(lái)毅贮,我們看一下办悟,對(duì)于IPC問(wèn)題,我們應(yīng)該去監(jiān)測(cè)哪些指標(biāo)滩褥。

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

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

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

常規(guī)方案

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

接下來(lái)卤橄,我們講解一下IPC問(wèn)題監(jiān)測(cè)的技巧绿满。

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

在線下,我們可以通過(guò)adb命令的方式來(lái)進(jìn)行監(jiān)測(cè)窟扑,如下所示:

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

adb shell am trace-ipc start

// 2嚎货、然后橘霎,結(jié)束IPC操作的監(jiān)控,同時(shí)殖属,將監(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)方案外潜,看過(guò)深入探索Android布局優(yōu)化(上)的同學(xué)可能知道這里的實(shí)現(xiàn)方案無(wú)非就是ARTHook或AspectJ這兩種方案,這里我們需要去監(jiān)控IPC操作挠唆,那么处窥,我們應(yīng)該選用哪種方式會(huì)更好一些呢?

要回答這個(gè)問(wèn)題玄组,就需要我們對(duì)ARTHook和AspectJ這兩者的思想有足夠的認(rèn)識(shí)滔驾,對(duì)應(yīng)ARTHook來(lái)說(shuō),其實(shí)我們可以用它來(lái)去Hook系統(tǒng)的一些方法俄讹,因?yàn)閷?duì)于系統(tǒng)代碼來(lái)說(shuō)哆致,我們無(wú)法對(duì)它進(jìn)行更改,但是我們可以Hook住它的一個(gè)方法患膛,在它的方法體里面去加上自己的一些代碼摊阀。但是,對(duì)于AspectJ來(lái)說(shuō)踪蹬,它只能針對(duì)于那些非系統(tǒng)方法驹溃,也就是我們App自己的源碼,或者是我們所引用到的一些jar延曙、aar包豌鹤。因?yàn)锳spectJ實(shí)際上是往我們的具體方法里面插入相對(duì)應(yīng)的代碼,所以說(shuō)枝缔,他不能夠針對(duì)于我們的系統(tǒng)方法去做操作布疙,在這里,我們就需要采用ARTHook的方式去進(jìn)行IPC操作的監(jiān)控愿卸。

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

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

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

1

這里我們僅僅關(guān)注transact方法的參數(shù)即可,第一個(gè)參數(shù)是一個(gè)行動(dòng)編碼,為int類型,它是在FIRST_CALL_TRANSACTION與LAST_CALL_TRANSACTION之間的某個(gè)值媳否,第二、三個(gè)參數(shù)都是Parcel類型的參數(shù)庄撮,用于獲取和回復(fù)相應(yīng)的數(shù)據(jù),第四個(gè)參數(shù)為一個(gè)int類型的標(biāo)記值毙籽,為0表示一個(gè)正常的IPC調(diào)用洞斯,否則表明是一個(gè)單向的IPC調(diào)用。然后惧财,我們?cè)陧?xiàng)目中的Application的onCreate方法中使用ARTHook對(duì)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)用中某一個(gè)IPC調(diào)用的所有堆棧信息厅翔。在這里,具體是在AbstractSimpleActivity的onCreate方法中調(diào)用了ServiceManager的getService方法搀突,它是一個(gè)IPC調(diào)用的方法刀闷。這樣,應(yīng)用的IPC調(diào)用我們就能很方便地捕獲到了仰迁。

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

2检盼、卡頓問(wèn)題檢測(cè)方案

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

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

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

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

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

首先恕酸,我們可以通過(guò)Systrace來(lái)觀察CPU的運(yùn)行狀況堪滨,比如有沒有跑滿CPU;然后蕊温,我們?cè)趩?dòng)優(yōu)化中學(xué)習(xí)到的優(yōu)雅異步以及優(yōu)雅延遲初始化等等一些方案袱箱;其次,針對(duì)于我們的界面布局义矛,我們可以使用異步Inflate发笔、X2C、其它的繪制優(yōu)化措施等等凉翻;最后了讨,我們可以使用預(yù)加載的方式去提前獲取頁(yè)面的數(shù)據(jù),以避免網(wǎng)絡(luò)或磁盤IO速度的影響制轰,或者也可以將獲取數(shù)據(jù)的方法放到onCreate方法的第一行前计。

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

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

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

那就是Lancet。

2、Lancet

Lancet是一個(gè)輕量級(jí)的Android AOP框架皇忿,它具有如下優(yōu)勢(shì):

1畴蹭、編譯速度快,支持增量編譯鳍烁。

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

然后糊闽,我來(lái)簡(jiǎn)單地講解下Lancet的用法。Lancet自身提供了一些注解用于Hook爹梁,如下所示:

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

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

接下來(lái)姚垃,我們就是使用Lancet來(lái)進(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

接下來(lái)掂墓,我們就可以使用Lancet了,這里我們需要先新建一個(gè)類去進(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

上述的方法就是對(duì)android.util.Log的i方法進(jìn)行Hook君编,并在所有的msg后面加上"JsonChao"字符串,注意這里的i方法我們需要從android.util.Log里面將它的i方法復(fù)制過(guò)來(lái)绍昂,確保方法名和對(duì)應(yīng)的參數(shù)信息一致啦粹;然后,方法上面的@TargetClass與@Proxy分別是指定對(duì)應(yīng)的全路徑類名與方法名窘游;最后唠椭,我們需要通過(guò)Lancet提供的Origin類去調(diào)用它的call方法來(lái)實(shí)現(xiàn)返回原來(lái)的調(diào)用信息。完成之后忍饰,我們重新運(yùn)行項(xiàng)目贪嫂,會(huì)出現(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ù)先添加的字符串艾蓝,說(shuō)明Hook成功了力崇。下面,我們就可以用Lancet來(lái)統(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

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

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface Insert {

? ? String value();

? ? boolean mayCreateSuper() default false;

}

1

2

3

4

5

6

7

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

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

然后讶踪,我們注意到@TargetClass也可以指定兩個(gè)參數(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

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

public enum Scope {

? ? SELF,

? ? DIRECT,

? ? ALL,

? ? LEAF

}

1

2

3

4

5

6

7

對(duì)于Scope.SELF乳讥,它代表僅匹配目標(biāo)value所指定的一個(gè)匹配類柱查;對(duì)于DIRECT,它代表匹配value所指定的類的一個(gè)直接子類雏婶;如果是Scope.ALL物赶,它就表明會(huì)去匹配value所指定的類的所有子類白指,而我們上面指定的value值為android.support.v7.app.AppCompatActivity留晚,因?yàn)閟cope指定為了Scope.ALL,則說(shuō)明會(huì)去匹配AppCompatActivity的所有子類告嘲。而最后的Scope.LEAF 代表匹配 value 指定類的最終子類错维,因?yàn)閖ava是單繼承,所以繼承關(guān)系是樹形結(jié)構(gòu)橄唬,所以這里代表了指定類為頂點(diǎn)的繼承樹的所有葉子節(jié)點(diǎn)赋焕。

最后,我們?cè)O(shè)定了一個(gè)ActivityRecord類去記錄onCreate與onWindowFocusChanged的時(shí)間戳仰楚,如下所示:

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

通過(guò)sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime得到的時(shí)間即為界面的打開速度僧界,最后侨嘀,重新運(yùn)行項(xiàng)目,會(huì)得到如下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咬腕。

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

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

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

總體耗時(shí)

生命周期耗時(shí)

生命周期間隔耗時(shí)

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

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

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

我們?cè)贏ctivity的生命周期中post了一個(gè)message呻征,那這個(gè)message很可能其中

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

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

時(shí)間就會(huì)延遲1s,如果是200ms攒岛,那么我們?cè)O(shè)定的自動(dòng)化卡頓檢測(cè)就無(wú)法

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

1

2

3

4

5

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

首先挠进,我們?cè)贛ainActivity的onCreate中加上post消息的一段代碼色乾,其中模擬了延遲1000ms的耗時(shí)操作,代碼如下所示:

// 以下代碼是為了演示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

接著领突,我們?cè)赗ecyclerView對(duì)應(yīng)的Adapter中將列表展示的時(shí)間打印出來(lái)暖璧,如下所示:

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í)行時(shí)間澎办,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)用,與此同時(shí)局蚀,列表頁(yè)時(shí)延遲了1000ms才展示出來(lái)麦锯。也就是說(shuō),post的這個(gè)message消息是執(zhí)行在界面琅绅、列表展示之前的扶欣。因?yàn)槿魏我粋€(gè)開發(fā)都有可能在某一個(gè)生命周期或者是某一個(gè)階段以及一些第三方的SDK里面,回去做一些handler post的相關(guān)操作千扶,這樣料祠,他的handler post的message的執(zhí)行,很有可能在我們的界面或列表展示之前就被執(zhí)行澎羞,所以說(shuō)髓绽,出現(xiàn)這種耗時(shí)的盲區(qū)是非常普遍的,而且也不好排查妆绞,下面顺呕,我們分析下耗時(shí)盲區(qū)存在的難點(diǎn)。

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

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

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

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

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

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

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

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

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

那么焦履,最終的耗時(shí)盲區(qū)監(jiān)控的一個(gè)線上方案就是使用一個(gè)統(tǒng)一的Handler,定制了它的兩個(gè)方法雏逾,一個(gè)是sendMessageAtTime嘉裤,另外一個(gè)是dispatchMessage方法。因?yàn)閷?duì)于發(fā)送message栖博,不管調(diào)用哪個(gè)方法最終都會(huì)調(diào)用到一個(gè)是sendMessageAtTime這個(gè)方法屑宠,而處理message呢,它最終會(huì)調(diào)用dispatchMessage方法仇让。然后典奉,我們需要定制一個(gè)gradle插件,來(lái)實(shí)現(xiàn)自動(dòng)化的接入我們定制好的handler丧叽,通過(guò)這種方式卫玖,我們就能在編譯期間去動(dòng)態(tài)地替換所有使用Handler的父類為我們定制好的這個(gè)handler。這樣踊淳,在整個(gè)項(xiàng)目中假瞬,所有的sendMessage和handleMessage都會(huì)經(jīng)過(guò)我們的回調(diào)方法。接下來(lái)迂尝,我們來(lái)進(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將會(huì)是我們項(xiàng)目中所有Handler的一個(gè)父類芦劣。在注釋1處,我們?cè)趕endMessageAtTime這個(gè)方法里面判斷如果message發(fā)送成功说榆,將會(huì)把當(dāng)前message對(duì)象對(duì)應(yīng)的調(diào)用棧信息都保存到一個(gè)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

這樣寸认,我們就能夠知道這個(gè)message它是被誰(shuí)發(fā)送過(guò)來(lái)的。然后串慰,在dispatchMessage方法里面偏塞,我們可以計(jì)算拿到其處理消息的一個(gè)耗時(shí),并在注釋2處將這個(gè)耗時(shí)保存到一個(gè)jsonObject對(duì)象中邦鲫,同時(shí)灸叼,我們也可以通過(guò)GetDetailHandlerHelper類的ConcurrentHashMap對(duì)象拿到這個(gè)message對(duì)應(yīng)的堆棧信息,并在注釋3處將它們輸出到log控制臺(tái)上庆捺。當(dāng)然古今,如果是線上監(jiān)控,則會(huì)把這些信息保存到本地滔以,然后選擇合適的時(shí)間去上傳捉腥。最后,我們還可以在方法體里面做一個(gè)判斷你画,我們?cè)O(shè)置一個(gè)閾值抵碟,比如閾值為20ms,超過(guò)了20ms就把這些保存好的信息上報(bào)到APM后臺(tái)坏匪。

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

對(duì)于實(shí)現(xiàn)全局替換handler的gradle插件,除了使用AspectJ實(shí)現(xiàn)之外粒竖,這里推薦一個(gè)已有的項(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í)行的時(shí)間,還可以從對(duì)應(yīng)的堆棧信息中得到發(fā)送message的位置沿彭,這里的位置是MainActivity的107行朽砰,也就是new Handler().post()這一行代碼。使用這種方式我們就可以知道在列表展示之前到底執(zhí)行了哪些自定義的message喉刘,我們一眼就可以知道哪些message其實(shí)是不符合我們預(yù)期的瞧柔,比如說(shuō)message的執(zhí)行時(shí)間過(guò)長(zhǎng),或者說(shuō)這個(gè)message其實(shí)可以延后執(zhí)行睦裳,這個(gè)我們都可以根據(jù)實(shí)際的項(xiàng)目和業(yè)務(wù)需求進(jìn)行相應(yīng)地修改造锅。

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

耗時(shí)盲區(qū)監(jiān)控是我們卡頓監(jiān)控中不可或缺的一個(gè)環(huán)節(jié)廉邑,也是卡頓監(jiān)控全面性的一個(gè)重要保障哥蔚。而需要注意的是倒谷,TraceView僅僅適用于線下的一個(gè)場(chǎng)景,同時(shí)對(duì)于TraceView來(lái)說(shuō)糙箍,它可以用于監(jiān)控我們系統(tǒng)的message渤愁。而最后介紹的動(dòng)態(tài)替換的方式其實(shí)是適合于線上的,同時(shí)深夯,它僅僅監(jiān)控應(yīng)用自身的一個(gè)message抖格。

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

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

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

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

對(duì)于布局加載優(yōu)化,可以采用AsyncLayoutInflater或者是X2C的方式來(lái)優(yōu)化主線程IO以及反射導(dǎo)致的消耗饰剥,同時(shí)殊霞,需要注意,對(duì)于重繪問(wèn)題汰蓉,要給與一定的重視绷蹲。

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

然后若厚,我們來(lái)看看卡頓優(yōu)化的工具建設(shè)拦英。

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

工具建設(shè)這塊經(jīng)常容易被大家所忽視测秸,但是它的收益卻非常大疤估,也是卡頓優(yōu)化的一個(gè)重點(diǎn)。首先霎冯,對(duì)于系統(tǒng)工具而言铃拇,我們要有一個(gè)認(rèn)識(shí),同時(shí)一定要學(xué)會(huì)使用它沈撞,這里我們?cè)倩仡櫼幌隆?/p>

對(duì)于Systrace來(lái)說(shuō)慷荔,我們可以很方便地看出來(lái)它的CPU使用情況。另外缠俺,它的開銷也比較小显晶。

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

同時(shí),需要注意倦春,StrictMode也是一個(gè)非常強(qiáng)大的工具户敬。

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

同時(shí)骂维,我們對(duì)于卡頓優(yōu)化工具的建設(shè)也做了細(xì)化惹资,對(duì)于單點(diǎn)問(wèn)題,比如說(shuō)IPC監(jiān)控航闺,我們通過(guò)Hook的手段來(lái)做到盡早的發(fā)現(xiàn)問(wèn)題褪测。對(duì)于耗時(shí)盲區(qū)的監(jiān)控,我們?cè)诰€上采用的是替換Handler的方式來(lái)監(jiān)控所有子線程message執(zhí)行的耗時(shí)以及調(diào)用堆棧潦刃。

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

六汛兜、常見卡頓問(wèn)題解決方案總結(jié)

1、CPU資源爭(zhēng)搶引發(fā)的卡頓問(wèn)題如何解決通今?

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

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

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

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

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

4只酥、硬件加速長(zhǎng)中文字體渲染時(shí)造成的卡頓如何解決褥实?

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

textView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

1

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

DisplayList的replay方法會(huì)遍歷DisplayList中保存的每一個(gè)操作怎棱。其中渲染字體的操作名是DrawText,當(dāng)遍歷到一個(gè)DrawText操作時(shí)绷跑,會(huì)調(diào)用OpenGLRender::drawText方法區(qū)渲染字體拳恋。最終,會(huì)在OpenGLRender::drawText方法里去調(diào)用Font::render()方法渲染字體砸捏,而在這個(gè)方法中有一個(gè)很關(guān)鍵的操作谬运,即獲取字體緩存。我們都知道每一個(gè)中文的編碼都是不同的垦藏,因此中文的緩存效果非常不理想梆暖,但是對(duì)于英文而言,只需要緩存26個(gè)字母就可以了掂骏。在Android 4.1.2版本之前對(duì)文本的Buffer設(shè)置過(guò)小轰驳,所以情況比較嚴(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

此外,硬件渲染還有一些其它的問(wèn)題在使用時(shí)需要注意勤哗,具體為如下所示:

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

2、在軟件渲染的情況下缴挖,會(huì)常常使用Bitmap重用的方式來(lái)節(jié)省內(nèi)存袋狞,但是如果開啟了硬件加速,這將會(huì)無(wú)效映屋。

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

4砌梆、長(zhǎng)或?qū)挻笥?048像素的Bitmap無(wú)法繪制,顯示為一片透明贬循。原因是OpenGL的材質(zhì)大小上限為2048 * 2048咸包,因此對(duì)于超過(guò)2048像素的Bitmap,需要將其切割成2048 * 2048以內(nèi)的圖片塊杖虾,最后在顯示的時(shí)候拼起來(lái)烂瘫。

5、當(dāng)UI中存在過(guò)渡繪制時(shí)奇适,可能會(huì)發(fā)生花屏坟比,一般來(lái)說(shuō)繪制少于5層不會(huì)出現(xiàn)花屏現(xiàn)象,如果有大塊紅色區(qū)域就要十分小心了嚷往。

6葛账、需要注意,關(guān)于LAYER_TYPE_SOFTWARE皮仁,雖然無(wú)論在App打開硬件加速或沒有打開硬件加速的時(shí)候注竿,都會(huì)通過(guò)軟件繪制Bitmap作為離屏緩存茄茁,但區(qū)別在于打開硬件加速的時(shí)候,Bitmap最終還會(huì)通過(guò)硬件加速方式drawDisplayList去渲染這個(gè)Bitmap巩割。

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

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

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

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

2嗦嗡、自動(dòng)化卡頓方案及優(yōu)化

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

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

但是呢窑滞,隨著我們項(xiàng)目的擴(kuò)大琼牧,線下卡頓的問(wèn)題也越來(lái)越多,同時(shí)哀卫,在線上巨坊,也有卡頓的反饋,但是線上的反饋卡頓聊训,我們?cè)诰€下難以復(fù)現(xiàn)抱究,于是我們開始尋找自動(dòng)化的卡頓監(jiān)測(cè)方案,其思路是來(lái)自于Android的消息處理機(jī)制带斑,主線程執(zhí)行任何代碼都會(huì)回到Looper.loop方法當(dāng)中鼓寺,而這個(gè)方法中有一個(gè)mLogging對(duì)象,它會(huì)在每個(gè)message的執(zhí)行前后都會(huì)被調(diào)用勋磕,我們就是利用這個(gè)前后處理的時(shí)機(jī)來(lái)做到的自動(dòng)化監(jiān)測(cè)方案的妈候。同時(shí),在這個(gè)階段挂滓,我們也完善了線上ANR的上報(bào)苦银,我們采取的方式就是監(jiān)控ANR的信息,同時(shí)結(jié)合了ANR-WatchDog,作為高版本沒有文件權(quán)限的一個(gè)補(bǔ)充方案幔虏。

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

2、你是怎么樣自動(dòng)化的獲取卡頓信息瑟蜈?

我們的思路是來(lái)自于Android的消息處理機(jī)制烟逊,主線程執(zhí)行任何代碼它都會(huì)走到Looper.loop方法當(dāng)中,而這個(gè)函數(shù)當(dāng)中有一個(gè)mLogging對(duì)象铺根,它會(huì)在每個(gè)message處理前后都會(huì)被調(diào)用宪躯,而主線程發(fā)生了卡頓,那就一定會(huì)在dispatchMessage方法中執(zhí)行了耗時(shí)的代碼位迂,那我們?cè)谶@個(gè)message執(zhí)行之前呢访雪,我們可以在子線程當(dāng)中去postDelayed一個(gè)任務(wù),這個(gè)Delayed的時(shí)間就是我們?cè)O(shè)定的閾值囤官,如果主線程的messaege在這個(gè)閾值之內(nèi)完成了冬阳,那就取消掉這個(gè)子線程當(dāng)中的任務(wù),如果主線程的message在閾值之內(nèi)沒有被完成党饮,那子線程當(dāng)中的任務(wù)就會(huì)被執(zhí)行肝陪,它會(huì)獲取到當(dāng)前主線程執(zhí)行的一個(gè)堆棧,那我們就可以知道哪里發(fā)生了卡頓刑顺。

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

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

首先哀军,針對(duì)卡頓沉眶,我們采用了線上、線下工具相結(jié)合的方式杉适,線下工具我們冊(cè)中醫(yī)藥盡可能早地去暴露問(wèn)題谎倔,而針對(duì)于線上工具呢,我們側(cè)重于監(jiān)控的全面性淘衙、自動(dòng)化以及異常感知的靈敏度传藏。

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

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

八垫蛆、總結(jié)

恭喜你禽最,如果你看到了這里腺怯,你會(huì)發(fā)現(xiàn)要做好應(yīng)用的卡頓優(yōu)化的確不是一件簡(jiǎn)單的事,它需要你有成體系的知識(shí)構(gòu)建基底川无。最后呛占,我們?cè)賮?lái)回顧一下面對(duì)卡頓優(yōu)化,我們已經(jīng)探索的以下九大主題:

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

2帜篇、自動(dòng)化卡頓檢測(cè)方案及優(yōu)化:卡頓檢測(cè)方案原理、AndroidPerformanceMonitor實(shí)戰(zhàn)及其優(yōu)化诫咱。

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

4、卡頓單點(diǎn)問(wèn)題檢測(cè)方案:IPC單點(diǎn)問(wèn)題檢測(cè)方案掏呼、卡頓問(wèn)題檢測(cè)方案坏快。

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

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

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

8?窄锅、常見卡頓問(wèn)題解決方案總結(jié)

9创千、卡頓優(yōu)化的常見問(wèn)題

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

參考鏈接:

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

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

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

4、《Android移動(dòng)性能實(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-用于快速找到高耗時(shí)方法,定位解決Android App卡頓問(wèn)題

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

12赋续、使用 ftrace

13、profilo-A library for performance traces from production

14另患、ftrace 簡(jiǎn)介

15纽乱、atrace源碼

16、AndroidAdvanceWithGeektime

/ Chapter06

17昆箕、AndroidAdvanceWithGeektime

/ Chapter06-plus

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸦列,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鹏倘,更是在濱河造成了極大的恐慌薯嗤,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纤泵,死亡現(xiàn)場(chǎng)離奇詭異骆姐,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)捏题,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門玻褪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人公荧,你說(shuō)我怎么就攤上這事带射。” “怎么了循狰?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵窟社,是天一觀的道長(zhǎng)券勺。 經(jīng)常有香客問(wèn)我,道長(zhǎng)桥爽,這世上最難降的妖魔是什么朱灿? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮钠四,結(jié)果婚禮上盗扒,老公的妹妹穿的比我還像新娘。我一直安慰自己缀去,他們只是感情好侣灶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缕碎,像睡著了一般褥影。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咏雌,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天凡怎,我揣著相機(jī)與錄音,去河邊找鬼赊抖。 笑死统倒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的氛雪。 我是一名探鬼主播房匆,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼报亩!你這毒婦竟也來(lái)了浴鸿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤弦追,失蹤者是張志新(化名)和其女友劉穎岳链,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骗卜,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宠页,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寇仓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片举户。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖遍烦,靈堂內(nèi)的尸體忽然破棺而出俭嘁,到底是詐尸還是另有隱情,我是刑警寧澤服猪,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布供填,位于F島的核電站拐云,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏近她。R本人自食惡果不足惜叉瘩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望粘捎。 院中可真熱鬧薇缅,春花似錦、人聲如沸攒磨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)娩缰。三九已至灸撰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拼坎,已是汗流浹背浮毯。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泰鸡,地道東北人亲轨。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鸟顺,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子器虾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容