Android 性能優(yōu)化體系詳解

一、應用保活與啟動優(yōu)化

1.1 Android 活動等級與?;畈呗?/h3>
  • Android 活動等級:5級
  • ?;钍侄?/strong>:
    • 使用 [JobScheduler](http://www.itdecent.cn/p/1f2103d3d2a2) 進行?;?。
  • 必要權限
    • 允許應用后臺運行;
    • 允許應用自啟動。

1.2 冷啟動耗時測量

在 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í)行。
  • 代碼示例
    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。在 DecorView attachToWindow 前,可通過 View.post() 將任務 Runnables 存放到 HandlerActionQueue 中。當 DecorView attachToWindow 時,會遍歷并執(zhí)行這些任務。
  • 關鍵源碼邏輯
    1. View.dispatchAttachedToWindow() 時,mAttachInfo 被賦值;
    2. 此后,View.post() 實際就是直接調(diào)用 Handler.post() 執(zhí)行任務;
    3. performResumeActivity() 在渲染之前先執(zhí)行,說明只有在 onResume() 或之前調(diào)用 View.post() 才有效。
  • 二次延遲技巧
    • View.post()Runnablerun() 方法中再延遲一個任務;
    • performTraversals() 調(diào)用順序看,該任務剛好在渲染完成后執(zhí)行。
  • 代碼示例
    getWindow().getDecorView().post(new Runnable() {
        @Override
        public void run() {
            sHandler.post(runnable);
        }
    });
    

方案四:解決冷啟動白屏/黑屏


二、ANR(Application Not Responding)機制

2.1 ANR 的四種觸發(fā)場景

  1. Service TimeOut:

    • Service 未在規(guī)定時間內(nèi)執(zhí)行完成。
    • 前臺服務: 20秒
    • 后臺服務: 200秒
  2. BroadcastQueue TimeOut:

    • 未在規(guī)定時間內(nèi)處理完廣播。
    • 前臺廣播: 10秒內(nèi)
    • 后臺廣播: 60秒內(nèi)
  3. ContentProvider TimeOut:

    • publish 在 10秒內(nèi)沒有完成。
  4. 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
  • 原理簡述
    1. RefWatcher.watch() 創(chuàng)建一個 KeyedWeakReference 用于觀察對象。
    2. 在后臺線程中,檢測引用是否被清除,并且是否沒有觸發(fā) GC。
    3. 如果引用仍然沒有被清除,則將堆棧信息保存為 .hprof 文件。
    4. HeapAnalyzerService 在獨立進程中啟動,使用 HAHA 庫解析 heap dump。
    5. 根據(jù) referenceKey 找到 KeyedWeakReference 并定位泄露的引用。
    6. 計算到 GC Roots 的最短強引用路徑,建立泄露鏈。
    7. 結果傳回 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)。
    • 最后,在 DisplayLeakActivityonResume 中讀取所有 .hprof.result 文件并顯示。
  • 參考資料

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 耗時方法定位


四、網(wǎng)絡優(yōu)化

4.1 測試與監(jiān)控工具

  • 測試工具
    • Network Profiler
    • Charles
    • 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)計重啟前的流量。

4.2 流量優(yōu)化方案

  1. 數(shù)據(jù)緩存
  2. 數(shù)據(jù)壓縮
    • Gzip
    • 壓縮請求頭
    • 合并請求
  3. 圖片壓縮
    • 縮略圖
    • WebP
    • Luban
  4. 網(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)定性和用戶體驗。

7.2 為什么需要兜底框架?

以下場景尤其需要這種可配置的崩潰攔截能力:

  1. 系統(tǒng)級崩潰

    • 例如,臭名昭著的 Android 7.x 版本中由 Toast 引發(fā)的 BadTokenException。這類問題源于系統(tǒng)底層,應用層難以規(guī)避。
  2. 第三方庫的“無痛”崩潰

    • 對于公司內(nèi)部廣泛使用但無法或不愿修改源碼的大型第三方框架(如 React Native),其 UI 操作(如動畫)可能在特定條件下拋出難以復現(xiàn)的異常。兜底框架可以避免因這些非核心路徑的崩潰導致整個 App 退出。
  3. 特殊資源型崩潰

    • 例如,因磁盤空間不足引發(fā)的 No space left on device 異常。兜底框架可以在捕獲此類異常時,主動清理應用的磁盤緩存,然后嘗試讓應用繼續(xù)運行,而不是直接崩潰。
  4. 其他未知場景

    • 為應對線上復雜多變的環(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)定性體系。


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

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

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