前言
成為一名優(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)開始今天的探索之旅霞篡。
首先朗兵,我們?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í)行流程。
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ò)了入蛆,這里就不多贅述了响蓉。
在深入探索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是一種非侵入式的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)原理耕腾。
首先,我們調(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)破停。
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í)候青灼,才去使用這套方案暴心。
除了自動(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ī)方案就是在IPC的前后加上埋點(diǎn)插勤。但是沽瘦,這種方式不夠優(yōu)雅,而且农尖,在平常開發(fā)過(guò)程中我們經(jīng)常忘記某個(gè)埋點(diǎn)的真正用處析恋,同時(shí)它的維護(hù)成本也非常大。
接下來(lái)卤橄,我們講解一下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è)方案。
對(duì)于卡頓問(wèn)題檢測(cè)方案的建設(shè)肯污,主要是利用ARTHook去完善線下的檢測(cè)工具,盡可能地去Hook相對(duì)應(yīng)的操作,以暴露蹦渣、分析問(wè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)思路斟薇。
首先恕酸,我們可以通過(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方法的第一行前计。
通常垃杖,我們是通過(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。
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)控緯度葬荷。
對(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)控的線下方案笤闯。
這里我們直接使用TraceView去檢測(cè)即可,因?yàn)樗軌?b>清晰地記錄線程在具體的時(shí)間內(nèi)到底做了什么操作颗味,特別適合一段時(shí)間內(nèi)的盲區(qū)監(jiān)控超陆。
然后,我們來(lái)看下如何建立耗時(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抖格。
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è)拦英。
工具建設(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í)間感知到異常歉胶。
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è)方法感耙。
這個(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巩割。
從項(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)了一整套完善想括,多維度的解決方案陷谱。
我們的思路是來(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)確性衅檀。
首先哀军,針對(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ù)。
恭喜你禽最,如果你看到了這里腺怯,你會(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 信息解讀
7篮幢、nanoscope-An extremely accurate Android method tracing tool
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源碼
/ Chapter06
17昆箕、AndroidAdvanceWithGeektime
/ Chapter06-plus