一、應用保活與啟動優(yōu)化
1.1 Android 活動等級與?;畈呗?/h3>
-
Android 活動等級:5級
-
?;钍侄?/strong>:
- 使用
[JobScheduler](http://www.itdecent.cn/p/1f2103d3d2a2) 進行?;?。
-
必要權限:
- 允許應用后臺運行;
- 允許應用自啟動。
1.2 冷啟動耗時測量
- 使用
[JobScheduler](http://www.itdecent.cn/p/1f2103d3d2a2)進行?;?。
- 允許應用后臺運行;
- 允許應用自啟動。
在 Android Studio Logcat 中過濾關鍵字 “Displayed”,可查看如下日志:
2019-07-03 01:49:46.748 1678-1718/? I/ActivityManager: Displayed com.tencent.qqmusic/.activity.AppStarterActivity: +12s449ms
- 日志末尾的
12s449ms即為冷啟動耗時。
1.3 冷啟動優(yōu)化方案
方案一:利用 IdleHandler 延遲初始化
-
原理:
IdleHandler列表中的任務只有在MessageQueue隊列為空時才會執(zhí)行,即所在線程任務已執(zhí)行完、處于空閑狀態(tài)時。 -
代碼示例:
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { // 頁面啟動所需耗時初始化 doSomething(); return false; // 執(zhí)行一次后移除 } });
方案二:在 onWindowFocusChanged 中通過 Handler 延后任務

-
原因:直接在
onWindowFocusChanged中執(zhí)行任務,其打點時間早于系統(tǒng)日志 “Displayed”;通過Handler.post()延后一個任務,可確保在 “Displayed” 日志之后執(zhí)行。 -
調(diào)用流程分析:
- 渲染調(diào)用
requestLayout()會增加任務監(jiān)聽; - 只有
SurfaceFlinger渲染信號返回時才會觸發(fā)渲染; - 因此延后一個任務,剛好在其之后執(zhí)行。
- 渲染調(diào)用
-
代碼示例:
public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (onCreateFlag && hasFocus) { onCreateFlag = false; sHandler.post(new Runnable() { @Override public void run() { doSomething(); } }); } }
方案三:通過 DecorView.post() 延遲任務
-
原理:
View內(nèi)部維護了一個HandlerActionQueue。在DecorViewattachToWindow前,可通過View.post()將任務Runnables存放到HandlerActionQueue中。當DecorViewattachToWindow時,會遍歷并執(zhí)行這些任務。 -
關鍵源碼邏輯:
- 在
View.dispatchAttachedToWindow()時,mAttachInfo被賦值; - 此后,
View.post()實際就是直接調(diào)用Handler.post()執(zhí)行任務; -
performResumeActivity()在渲染之前先執(zhí)行,說明只有在onResume()或之前調(diào)用View.post()才有效。
- 在
-
二次延遲技巧:
- 在
View.post()的Runnable的run()方法中再延遲一個任務; - 從
performTraversals()調(diào)用順序看,該任務剛好在渲染完成后執(zhí)行。
- 在
-
代碼示例:
getWindow().getDecorView().post(new Runnable() { @Override public void run() { sHandler.post(runnable); } });
方案四:解決冷啟動白屏/黑屏
-
方法:使用透明主題。
<activity android:name=".MainActivity" android:theme="@style/TranslucentTheme" /> - 參考資料:
二、ANR(Application Not Responding)機制
2.1 ANR 的四種觸發(fā)場景
-
Service TimeOut:
- Service 未在規(guī)定時間內(nèi)執(zhí)行完成。
- 前臺服務: 20秒
- 后臺服務: 200秒
-
BroadcastQueue TimeOut:
- 未在規(guī)定時間內(nèi)處理完廣播。
- 前臺廣播: 10秒內(nèi)
- 后臺廣播: 60秒內(nèi)
-
ContentProvider TimeOut:
-
publish在 10秒內(nèi)沒有完成。
-
-
Input Dispatching timeout:
- 5秒內(nèi)未響應鍵盤輸入、觸摸屏幕等事件。
【重要澄清】
Activity 的生命周期回調(diào)阻塞并不在觸發(fā) ANR 的場景里,因此不會直接觸發(fā) ANR。但是,死循環(huán)阻塞了主線程后,如果系統(tǒng)再發(fā)生上述四種事件之一,就無法在規(guī)定時間內(nèi)處理,從而間接觸發(fā) ANR。
2.2 ANR 產(chǎn)生機制詳解
1. 輸入事件超時 (5s)
a. InputDispatcher 發(fā)送 key 事件給對應進程的 Focused Window。若出現(xiàn)以下情況則發(fā)生 ANR:
- 對應的 window 不存在;
- 處于暫停態(tài);
- 通道(input channel)占滿、未注冊或異常;
- 5s 內(nèi)沒有處理完一個事件。
b.InputDispatcher發(fā)送MotionEvent事件有個例外: - 當對應 Touched Window 的 input waitQueue 中有超過 0.5s 的事件,inputDispatcher 會暫停該事件,并等待 5s。
- 如果仍舊沒有收到 window 的 ‘finish’ 事件,則觸發(fā) ANR。
c. 下一個事件到達,發(fā)現(xiàn)有一個超時事件才會觸發(fā) ANR。
2. 廣播類型超時(前臺15s,后臺60s)
a. 靜態(tài)注冊的廣播和有序廣播會 ANR,動態(tài)注冊的非有序廣播并不會 ANR。
b. 廣播發(fā)送時,會判斷該進程是否存在,不存在則創(chuàng)建,創(chuàng)建進程的耗時也算在超時時間里。
c. 只有當進程存在前臺顯示的 Activity 才會彈出 ANR 對話框,否則會直接殺掉當前進程。
d. 當 onReceive 執(zhí)行超過閾值(前臺15s,后臺60s),將產(chǎn)生 ANR。
e. 如何發(fā)送前臺廣播:Intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
3. 服務超時(前臺20s,后臺200s)
a. Service 的以下方法都會觸發(fā) ANR:
-
onCreate(),onStartCommand(),onStart(),onBind(),onRebind(),onTaskRemoved(),onUnbind(),onDestroy().
b. 前臺 Service 超時時間為 20s,后臺 Service 超時時間為 200s。
c. 如何區(qū)分前臺、后臺執(zhí)行:當前 APP 處于用戶態(tài),此時執(zhí)行的 Service 則為前臺執(zhí)行。
d. 用戶態(tài)定義:有前臺 activity、有前臺廣播在執(zhí)行、有 foreground service 執(zhí)行。
4. ContentProvider 類型
a. ContentProvider 創(chuàng)建發(fā)布超時并不會 ANR。
b. 使用 ContentProviderClient 來訪問 ContentProvider 可以自主選擇觸發(fā) ANR,超時時間自己定:
client.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
5. Activity 生命周期超時會不會 ANR?
- 經(jīng)測試并不會。
2.3 導致 ANR 的根本原因
1. 應用層導致 ANR(耗時操作)
a. 函數(shù)阻塞:如死循環(huán)、主線程 IO、處理大數(shù)據(jù)。
b. 鎖出錯:主線程等待子線程的鎖。
c. 內(nèi)存緊張:系統(tǒng)分配給應用的內(nèi)存有上限,長期內(nèi)存緊張會導致頻繁內(nèi)存交換,進而導致操作超時。
2. 系統(tǒng)導致 ANR
a. CPU 被搶占:例如前臺在玩游戲,可能導致后臺廣播被搶占 CPU。
b. 系統(tǒng)服務無法及時響應:如獲取系統(tǒng)聯(lián)系人,系統(tǒng)服務(Binder 機制)服務能力有限,可能長時間不響應。
c. 其他應用占用大量內(nèi)存。
2.4 參考資料
三、內(nèi)存優(yōu)化
3.1 內(nèi)存泄漏排查
-
工具:
LeakCanary -
原理簡述:
-
RefWatcher.watch()創(chuàng)建一個KeyedWeakReference用于觀察對象。 - 在后臺線程中,檢測引用是否被清除,并且是否沒有觸發(fā) GC。
- 如果引用仍然沒有被清除,則將堆棧信息保存為
.hprof文件。 -
HeapAnalyzerService在獨立進程中啟動,使用 HAHA 庫解析 heap dump。 - 根據(jù)
referenceKey找到KeyedWeakReference并定位泄露的引用。 - 計算到 GC Roots 的最短強引用路徑,建立泄露鏈。
- 結果傳回 app 進程,通過通知展示。
-
-
官方簡化解釋:
- 在
Activity執(zhí)行完onDestroy()后,將其放入WeakReference中,并與ReferenceQueue關聯(lián)。 - 檢查
ReferenceQueue中是否有該對象,如果沒有,執(zhí)行 GC 后再次檢查。 - 若仍無,則判定為內(nèi)存泄露,并用 HAHA 庫分析 heap dump。
- 在
-
工作流程細節(jié):
- LeakCanary 在判定有內(nèi)存泄漏時,首先會生成一個內(nèi)存快照文件(
.hprof文件),通常有 10+MB。 - 然后根據(jù)
referenceKey找出泄漏實例,在快照堆中使用 BFS 找到實例所在節(jié)點,并反向生成最小引用鏈。 - 生成引用鏈后,將其保存在
AnalysisResult對象中,然后寫入.hprof.result文件(僅幾十 KB)。 - 最后,在
DisplayLeakActivity的onResume中讀取所有.hprof.result文件并顯示。
- LeakCanary 在判定有內(nèi)存泄漏時,首先會生成一個內(nèi)存快照文件(
- 參考資料:
3.2 內(nèi)存抖動(Memory Churn)
- 問題現(xiàn)象:沒有內(nèi)存泄漏,但依然發(fā)生 OOM(Out Of Memory)。
- 根本原因:多半是內(nèi)存抖動在作祟。
-
影響:
- 導致程序莫名卡頓,甚至 Crash 和 OOM。
-
產(chǎn)生機制:
- 內(nèi)存的頻繁分配和回收導致內(nèi)存不穩(wěn)定。
-
典型癥狀:
- 頻繁 GC;
- 內(nèi)存曲線呈鋸齒狀。
-
卡頓原理:
- 頻繁的 GC 會導致 GC 線程在采集垃圾時掛起主線程及其他工作線程,造成用戶操作無響應。
-
OOM 原理:
- 頻繁申請和回收內(nèi)存會產(chǎn)生大量內(nèi)存碎片;
- 內(nèi)存不連續(xù),導致在創(chuàng)建需要連續(xù)內(nèi)存空間的對象(如大數(shù)組、長字符串)時失敗,引發(fā) OOM。
3.3 Bitmap 優(yōu)化
3.4 穩(wěn)定性優(yōu)化
3.5 耗時方法定位
- 工具/方法:讓你的Android應用快速定位耗時方法
四、網(wǎng)絡優(yōu)化
4.1 測試與監(jiān)控工具
-
測試工具:
Network ProfilerCharles-
Stetho(可以鏈接 Android 和 Chrome)
-
線上監(jiān)控:
-
OkHttp 事件監(jiān)聽器:
- 自定義事件監(jiān)聽器;
-
GlideModule(監(jiān)控 Glide 加載圖片); - 最大并發(fā)請求數(shù);
- 區(qū)分前后臺流量。
-
流量統(tǒng)計:
-
NetworkStatsManager:- 可獲取某時段或不同網(wǎng)絡類型的流量消耗;
- 不足:需要用戶開啟“查看使用情況”權限,用戶體驗差。
-
TrafficStats:- 統(tǒng)計手機上次重啟后的流量消耗;
- 局限:無法統(tǒng)計重啟前的流量。
-
-
OkHttp 事件監(jiān)聽器:
4.2 流量優(yōu)化方案
- 數(shù)據(jù)緩存
-
數(shù)據(jù)壓縮:
- Gzip
- 壓縮請求頭
- 合并請求
-
圖片壓縮:
- 縮略圖
- WebP
- Luban
-
網(wǎng)絡請求質(zhì)量優(yōu)化:
- HttpDNS
- Http 協(xié)議版本優(yōu)化
4.3 參考資料
五、性能分析工具
5.1 主流工具集
5.2 內(nèi)存類別詳解(Profiler 視角)
- Java: 從 Java 或 Kotlin 代碼中分配的對象的內(nèi)存。
- Native: 從 C 或 C++ 代碼中分配的對象的內(nèi)存。即使 App 未使用 C++,也可能看到此內(nèi)存,因為 Android 框架使用 Native 內(nèi)存處理圖像等任務。
- Graphics: 用于圖形緩沖區(qū)隊列的內(nèi)存,包括 GL 表面、GL 紋理等。(注意:這是與 CPU 共享的內(nèi)存,非專用 GPU 內(nèi)存)
- Stack: 應用程序中 Native 和 Java 棧使用的內(nèi)存,與線程數(shù)相關。
- Code: 應用程序用于代碼和資源的內(nèi)存,如 dex 字節(jié)碼、編譯后的代碼、庫和字體。
- Other: 應用程序使用的、系統(tǒng)無法分類的內(nèi)存。
- Allocated: 應用程序分配的 Java/Kotlin 對象的數(shù)量(不包含 C/C++ 對象)。
【注意】
當前應用程序中,native 內(nèi)存統(tǒng)計值可能會偏大,因為分析工具自身的內(nèi)存(多達 10MB)也被計入。在未來版本中,這些數(shù)字將被過濾掉。
5.3 其他工具
-
Perfetto:
- Android 10 后引入,適用 9.0 以上機型。
-
Emmagee:
- 網(wǎng)易出品,已不維護,7.0 之后版本不支持。
- 監(jiān)控維度:PSS 內(nèi)存占用比、CPU 使用率、流量、電量、溫度等。
- wetest:商用產(chǎn)品。
-
GT:
- 騰訊出品的手機端工具。
- MAT 工具:
-
通用提醒:
- 所有性能測試工具本身都需要占用資源,會影響測試結果。
5.4 參考資料
六、綜合性能優(yōu)化實踐
6.1 啟動優(yōu)化案例
6.2 必知必會清單
七、Android 穩(wěn)定性:可遠程配置化的 Looper 兜底框架
7.1 崩潰處理機制的核心原理
在 Android 應用中,當一個未被捕獲的異常(即崩潰)被拋出時,系統(tǒng)會調(diào)用 Thread#dispatchUncaughtException(throwable) 方法進行處理。
-
默認行為:
- 在進程初始化階段,
RuntimeInit#commonInit方法會注入一個默認的UncaughtExceptionHandler,即KillApplicationHandler。 - 如果開發(fā)者沒有實現(xiàn)自定義的
UncaughtExceptionHandler,那么dispatchUncaughtException最終會走到KillApplicationHandler中,直接殺死當前進程,從而產(chǎn)生一次用戶可感知的崩潰。
- 在進程初始化階段,
-
自定義兜底邏輯:
- 通過實現(xiàn)自定義的
UncaughtExceptionHandler,我們可以在應用真正崩潰退出前,攔截異常并執(zhí)行自定義邏輯(如上報、清理、嘗試恢復等),從而提升應用的穩(wěn)定性和用戶體驗。
- 通過實現(xiàn)自定義的
7.2 為什么需要兜底框架?
以下場景尤其需要這種可配置的崩潰攔截能力:
-
系統(tǒng)級崩潰:
- 例如,臭名昭著的 Android 7.x 版本中由 Toast 引發(fā)的
BadTokenException。這類問題源于系統(tǒng)底層,應用層難以規(guī)避。
- 例如,臭名昭著的 Android 7.x 版本中由 Toast 引發(fā)的
-
第三方庫的“無痛”崩潰:
- 對于公司內(nèi)部廣泛使用但無法或不愿修改源碼的大型第三方框架(如 React Native),其 UI 操作(如動畫)可能在特定條件下拋出難以復現(xiàn)的異常。兜底框架可以避免因這些非核心路徑的崩潰導致整個 App 退出。
-
特殊資源型崩潰:
- 例如,因磁盤空間不足引發(fā)的
No space left on device異常。兜底框架可以在捕獲此類異常時,主動清理應用的磁盤緩存,然后嘗試讓應用繼續(xù)運行,而不是直接崩潰。
- 例如,因磁盤空間不足引發(fā)的
-
其他未知場景:
- 為應對線上復雜多變的環(huán)境,提供一個通用的、可動態(tài)調(diào)整的崩潰防護層。
7.3 可遠程配置化能力
一個強大的兜底框架必須具備動態(tài)、精細化的控制能力。這可以通過網(wǎng)絡下發(fā)配置來實現(xiàn),允許運維或開發(fā)人員在不發(fā)版的情況下,對特定崩潰進行策略調(diào)整。
可配置的維度包括:
-
throwable class name:異常的全類名(如java.lang.NullPointerException)。 -
throwable message:異常的詳細信息。 -
throwable stacktrace:完整的堆棧跟蹤信息。 -
Android version:發(fā)生崩潰的設備 Android 版本。 -
app version:發(fā)生崩潰的應用版本號。 -
model/brand:發(fā)生崩潰的設備型號和品牌。
通過組合以上條件,可以實現(xiàn)非常精準的崩潰攔截策略。例如:“僅在 Android 7.0 的華為 P10 上,對 BadTokenException 且消息包含 'Unable to add window' 的崩潰進行靜默處理”。
7.4 實現(xiàn)與參考
【補充】
此兜底方案是對傳統(tǒng) Crash Report(如 Bugly、Firebase Crashlytics)的有效補充。后者側重于崩潰后的信息收集與分析,而前者則側重于崩潰發(fā)生時的實時干預與恢復,兩者結合可構建更健壯的應用穩(wěn)定性體系。