使用指南

優先提升記憶體效率:Android 17 的必要步驟

10 分鐘小故事
3 作者
Alice Yuan, Ajesh Pai, Fung Lam

雖然應用程式效能通常等同於流暢的 UI 和快速的啟動時間,但記憶體是建構這些可見指標的無聲基礎。眾所皆知,裝置記憶體的重要性與日俱增。我們不僅在 Android 17 中大幅改善 Android 記憶體最佳化功能,還提供工具和 API 支援,協助您在今年稍晚因應更嚴格的記憶體要求。

為確保裝置穩定,Android 17 以上版本將根據裝置的總 RAM,強制限制應用程式記憶體。如果應用程式超出這些限制,Android 會終止程序,且不會提供相關聯的堆疊追蹤記錄。

除了強制終止外,未經最佳化的記憶體用量也會導致使用者體驗變差。當應用程式接近堆積記憶體限制時,會觸發頻繁的垃圾回收作業,導致 UI 明顯停頓。此外,當裝置可用記憶體不足時,系統會急忙回收頁面,導致 CPU 負載過重、UI 延遲和電池耗電。如果記憶體不足的情況過於嚴重,可能會導致記憶體不足終止 (LMK) 事件,突然終止背景程序,並導致應用程式冷啟動緩慢,以及遺失使用者狀態。

如要建構高效能應用程式並避免強制終止,建議您採用下列記憶體最佳化策略:

  1. 使用 R8 盡量最佳化位元碼
  2. 最佳化圖片載入程序
  3. 使用 Android Studio 偵測及修正記憶體流失問題
  4. 應用程式離開可見狀態時修剪記憶體
  5. 使用 ProfilingManager 進行進階記憶體觀測

我們也提供這篇網誌文章的簡短影片版本,歡迎觀看!

瞭解 Android 17 應用程式記憶體限制

Android 17 將導入應用程式記憶體限制,避免「惡意行為人」破壞使用者整部裝置的多工處理體驗和穩定性。

以下是促成這項架構變更的原因:

  • 防止連鎖終止程序:當應用程式在保留特殊權限狀態 (例如執行前景服務) 時變得臃腫或發生記憶體外洩,一開始會受到系統的低記憶體終止程序 (LMK) 保護。如果這個應用程式持續成長並占用大量 RAM,LMK 就必須終止數十個較小的快取應用程式和背景工作,才能回收空間供記憶體占用者使用。
  • 保留多工處理和使用者狀態:當系統被迫清除快取應用程式,以配合單一洩漏程序時,多工處理體驗會大幅降低。使用者返回先前快取的應用程式時,會遇到緩慢的冷啟動,而不是近乎即時的暖啟動。這種效率不彰的情況會增加 CPU 負擔,並加速耗盡電池電量。這也可能破壞使用者在最近使用應用程式中的情境,例如捲動位置、導覽堆疊和遊戲進度。

如要判斷應用程式工作階段是否受到這些限制影響,可以在 ApplicationExitInfo 中呼叫 getDescription()。如果系統套用限制,則結束原因會回報為 REASON_OTHER,且說明字串會包含「MemoryLimiter:AnonSwap」。您也可以使用 TRIGGER_TYPE_ANOMALY,透過觸發條件式剖析,在達到記憶體上限時自動擷取堆積傾印。此外,Android 團隊正積極努力,在 Google Play 管理中心向開發人員提供更多現場記憶體指標。

我們也擴充了記憶體限制文件,納入本機偵錯指令,讓您在本機環境中模擬記憶體限制,並驗證應用程式在任何記憶體限制強制執行下的行為。

使用 R8 盡量最佳化位元碼

如要有效減少應用程式的記憶體用量,請啟用 R8 最佳化工具。R8 會將類別、方法和欄位縮減為較短的名稱,並移除未使用的程式碼和資源,藉此減少執行期間所需的常駐程式碼量,大幅縮減應用程式的記憶體用量。

R8 會盡量減少常駐程式碼,縮減記憶體用量,降低 LMK 終止風險。因此暖啟動的頻率會高於冷啟動。此外,簡化的位元碼可減少主執行緒 CPU 負擔,直接降低 ANR 發生率,進而提升使用者體驗的流暢度。舉例來說,數位銀行 Monzo 啟用完整的 R8 最佳化功能後,ANR 率降低了 35%,冷啟動率提升了 30%,整體應用程式大小則減少了 9%。

pic1-IO26_113_TSV-monzo-casestudy.jpg
數位銀行 Monzo 啟用完整的 R8 最佳化功能,成效指標提升了 35%。

如要在 build.gradle 檔案中正確設定 R8,請按照下列步驟操作:

  • 設定 isShrinkResources = trueisMinifyEnabled = true
  • 請使用 proguard-android-optimize.txt,而非舊版 proguard-android.txt,因為後者實際上會阻止最佳化,且 Android Gradle 外掛程式 9 不再支援。
  • 從「gradle.properties」移除「android.enableR8.fullMode = false」。

如果您在程式碼集中使用反射,請新增「保留規則」,防止 R8 最佳化這些程式碼部分。請務必縮小保留規則的範圍,盡可能進行最佳化。

如要盡可能提高最佳化程度,請務必在保留規則檔案中遵循下列最佳做法。

  • 移除 -dontoptimize-dontshrink-dontobfuscate 等全域選項,避免 R8 無法最佳化整個程式碼集
  • 移除會妨礙最佳化 Android 元件 (例如 Activity、Services、Views 或 Broadcast receivers) 的保留規則。
  • 縮小套件範圍,只保留特定類別或方法。

如要查看更多最佳做法,請參閱 保留規則說明文件

程式庫開發人員 R8 最佳做法

如果您是程式庫開發人員,請將消費者需要的規則嚴格放置在 consumer-rules file 中,並將程式庫的內部保護規則保留在 proguard-rules.pro 檔案中。如要進一步瞭解如何最佳化程式庫,請參閱「程式庫作者的最佳化做法」。

R8 設定分析器

如要稽核 R8 最佳化作業,請使用設定分析器 設定分析器會顯示目前的最佳化狀態,以及 模糊處理、最佳化和縮減分數。您也可以透過設定分析器,瞭解每個保留規則會阻止多少類別、方法或欄位進行最佳化。請調整這些廣泛的套件保留規則,盡可能提升最佳化成效。

您也可以使用設定分析器,找出涵蓋其他保留規則的保留規則、多餘的保留規則,以及未使用的保留規則。

pic2-r8-config-analyzer.png
「設定分析器」會顯示目前的最佳化狀態,以及混淆、最佳化和縮減分數。

R8 代理程式技能

您也可以搭配使用 R8 代理程式技能與 Android Studio 代理程式或其他 AI 工具,解決設定錯誤問題並修正規則,進而提升應用程式效能。 (AI 輔助技能的洞察資料需要經過技術驗證)

最佳化圖片載入程序

點陣圖通常是應用程式記憶體中最大的常見物件。這是圖片載入程序的最後階段,系統會將 JPEG 或 PNG 等壓縮檔解碼為原始像素資料,以供顯示。也就是說,100 KB 的壓縮圖片可能會膨脹成數 MB 的 RAM,因為記憶體用量取決於圖片的像素尺寸和色深。由於點陣圖作業經常位於繪製影格的重要路徑上,因此未經過最佳化的圖片會導致記憶體嚴重膨脹,並造成 UI 卡頓。

Google 建議針對以 Kotlin 為主的專案使用圖片載入程式庫 Coil,特別是使用 Jetpack Compose 開發時;針對以 Java 為主的應用程式,則建議使用 Glide

採用這五項最佳做法

  1. 降低圖片取樣率:如果手動載入點陣圖,請避免將大型圖片載入小型縮圖檢視畫面;請使用 inSampleSize 載入較小的版本。Glide 和 Coil 預設會縮減圖片取樣,您可以使用 DownsampleStrategyImageLoader 分別設定這項縮減取樣策略。
  2. 裁剪: 請勿直接在圖片檔案中嵌入邊框,以免出現上下黑邊 (例如建立透明邊框來擴大圖片尺寸)。請勿直接加入這些邊框,而是使用 InsetDrawable,或在包含點陣圖的 View 或 Composable 中直接套用邊框間距。
  3. 設定:選擇合適的像素格式,在記憶體和品質之間取得平衡。不需要透明度時,請使用 RGB_565,這種格式使用的記憶體是預設 ARGB_8888 格式的一半。在 Glide 中,您可以使用 DecodeFormat 設定這項功能;在 Coil 中,則可以使用 bitmapConfig 屬性。
  4. 優先使用向量可繪項目:對於基本幾何資產,請使用 ShapeDrawable,做為解碼柵格化點陣圖的輕量替代方案。透過 XML 定義這些資產後,即可確保資產在所有螢幕密度中順暢縮放,同時有效消除資源驅動的記憶體膨脹。
  5. 重複使用:如果應用程式手動管理點陣圖,為了盡量減少記憶體抖動,當不再需要點陣圖時,應用程式應呼叫 bitmap.recycle() 並立即捨棄 Bitmap 參照。如果您使用 Glide 或 Coil 等圖片載入程式庫,請將點陣圖傳回程式庫的管理集區。由於集區會為日後的記憶體需求提供現有緩衝區,因此可有效避免新分配作業的負擔。

詳情請參閱「最佳化圖片效能」說明文件。

Android Studio 工具

您也可以使用 Android Studio Narwhal 4.1 消除多餘的點陣圖。以下是五個簡單步驟,可協助您找出這些檔案:

  1. 在 Android Studio 中開啟「分析器」分頁
  2. 按一下「記憶體快照資料」 (或「分析記憶體用量」),然後點選「記錄」,即可取得應用程式目前記憶體狀態的快照。
  3. 掃描分析結果,找出 黃色警告三角形 ⚠️,Android Studio 會使用這個符號標示多次儲存的重複點陣圖。或者,您也可以前往剖析器標頭,選擇「篩選依據:」,然後選取「重複點陣圖」設定。
  4. 按一下任何標記的項目,開啟「點陣圖預覽」窗格,即可查看重複違規的圖片。
  5. 運用這項視覺確認資訊,找出程式碼中多餘的載入邏輯,並實作更完善的快取策略。
pic3-IO26_113_TSV -dup-bitmaps-cropped.jpg
使用 Android Studio 分析器時,請在記憶體快照資料中尋找黃色警告三角形 ⚠️。

使用 Android Studio 偵測及修正記憶體流失問題

在 Android 中,如果程式碼在物件生命週期結束後,仍保留物件的參照,就會發生記憶體流失。這會導致垃圾收集器 (GC) 無法回收該記憶體,最終導致效能緩慢或 OutOfMemoryError (OOM)。

Android Studio Panda 3 具有專屬的 LeakCanary 分析器工作,可讓開發人員直接在 IDE 中分析即時記憶體流失情形,並對應追蹤記錄。

Android Studio 中的 LeakCanary 分析器工作會主動將記憶體流失分析作業從裝置移至開發機器,因此與裝置端記憶體流失分析相比,記憶體流失分析階段的效能大幅提升。

pic4-android-studio-leaks.png
 LeakCanary 記憶體流失分析,並搭配「前往宣告」進行偵錯

此外,現在 IDE 會根據實際情況分析記憶體洩漏問題,並與原始碼完全整合,提供前往宣告等實用程式碼連結功能,大幅減少調查及修正記憶體洩漏問題時的阻礙和所需時間。  

常見的記憶體洩漏示例

如果物件在記憶體中存留的時間超出預期,就會發生記憶體流失。這通常是因為:

  • 保留不再使用的片段、活動或檢視區塊參照。
  • 管理 Context 參照時發生錯誤。
  • 無法正確取消註冊觀察器、監聽器和接收器。
  • 建立繫結至生命週期較短元件的物件靜態參照。

以下列舉幾個例子:

情境以 Compose 為基礎的範例以檢視畫面為基礎的範例
洩漏內容

範例:
將 LocalContext.current 傳遞至 ViewModel

修正方式:
將依附於 Context 的邏輯保留在 UI 層。對於非 UI 層,請重構為使用依附元件插入,或使用 Kotlin 流程觀察 UI 狀態。

範例:
在伴生物件或靜態變數中儲存 Activity

修正方式:
請勿保留 UI 元件的靜態參照。重構為使用依附元件插入,或使用 Kotlin Flow 觀察 UI 狀態。

洩漏監聽器

範例:
使用 DisposableEffect 啟動監聽器,但將 onDispose 留空。

修正:
onDispose 區塊內執行取消註冊和清除邏輯

範例:
註冊 SensorManager 更新,但忘記取消註冊。

修正方式:
onStop()onDestroy() 生命週期中手動呼叫 unregisterListener()

洩漏檢視區塊

範例:
AndroidView 中保留對舊版 View 的參照,但沒有發布策略。


修正:
使用 AndroidView 可組合函式的 release 區塊,清理舊版 View

示例:
Fragment 遭到刪除後,仍保留對檢視區塊繫結物件的參照。

 

修正方式:在 onDestroyView() 生命週期方法中,將繫結變數設為 null

應用程式離開可見狀態時修剪記憶體

Android 可收回應用程式的記憶體,在必須釋放記憶體供重要工作使用時,也可以完全終止應用程式。詳情請參閱「記憶體管理總覽」。當應用程式對使用者不可見時,Android 通常會從應用程式回收記憶體,例如捨棄記憶體中的部分應用程式程式碼和資料頁面,或壓縮堆積分配。使用者繼續使用應用程式時,如果應用程式嘗試存取已回收的記憶體,作業系統會視需要將該記憶體換回。這種交換行為可能很慢,並導致應用程式中出現非預期的卡頓或延遲。

如果讓 OS 決定要從應用程式回收哪些記憶體,您可能會發現 OS 回收的記憶體在應用程式恢復執行後不久就會用到。因此,應用程式可以主動捨棄稍後可依需求以低成本重新產生的記憶體配置。如要這麼做,請實作 ComponentCallbacks2 介面。您可以在 ActivityFragmentService,甚至是自訂 Application 類別中實作 onTrimMemory。在 Application 類別中使用這項功能,可有效管理全域快取。

系統提供的 onTrimMemory() 回呼方法會將生命週期或記憶體相關事件通知應用程式,讓應用程式能主動減少記憶體用量。

就記憶體生命週期管理而言,您的實作項目應完全著重於 TRIM_MEMORY_UI_HIDDENTRIM_MEMORY_BACKGROUND。自 Android 14 起,系統已停止傳送其他舊版常數的通知,這些常數已在 Android 15 中正式淘汰。

TRIM_MEMORY_UI_HIDDEN:這個信號表示應用程式的 UI 已從使用者檢視畫面中移出。這時您可釋出與介面嚴格綁定的實質記憶體配置,例如點陣圖、影片播放緩衝區或複雜的動畫資源。

TRIM_MEMORY_BACKGROUND:在此層級,您的程序會駐留在背景,並成為終止候選程序,以滿足系統的整體記憶體需求。如要延長程序停留在快取狀態的時間,並減少應用程式冷啟動的次數,您應在使用者繼續工作階段時,積極釋出可輕鬆重建的任何資源。

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

注意:onTrimMemory 整合功能可能需要 SDK 支援。舉例來說,某些遊戲會透過遊戲引擎啟用這項功能。請參閱遊戲記憶體最佳化文件

使用 ProfilingManager 進行進階記憶體觀測

如要找出並診斷無法在本機重現的記憶體問題,請善用 ProfilingManager API。這項進階可觀測性 API 已在 Android 15 中推出,可讓您以程式輔助方式收集實際使用者的 Perfetto 設定檔。

如果團隊沒有專屬基礎架構來管理及代管效能構件,Crashlytics 正在探索專用解決方案,以簡化這項工作流程。並邀請開發人員提供意見

Android 17 導入了新的事件驅動觸發條件,最值得注意的包括 TRIGGER_TYPE_OOMTRIGGER_TYPE_ANOMALY

  • OOM 觸發程序會在發生 OutOfMemoryError 異常終止時,自動收集 Java 記憶體快照資料,提供精確的分配狀態。應用程式下次啟動並註冊 registerForAllProfilingResults 回呼時,系統就會提供收集到的 OOM 設定檔。
  • 「異常狀況觸發條件」會偵測嚴重效能問題,例如過多的繫結器垃圾內容或超出記憶體閾值。記憶體異常會傳送堆積傾印,時間點就在系統終止應用程式之前。
    val profilingManager = 
applicationContext.getSystemService(ProfilingManager::class.java)
    val triggers = ArrayList<ProfilingTrigger>()  


    triggers.add(ProfilingTrigger.Builder(
                 ProfilingTrigger.TRIGGER_TYPE_ANOMALY))
    val mainExecutor: Executor = Executors.newSingleThreadExecutor()
    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (profilingResult.errorCode != ProfilingResult.ERROR_NONE) {
            // upload profile result to server for further analysis          
            setupProfileUploadWorker(profilingResult.resultFilePath)
        } 

    profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback)
    profilingManager.addProfilingTriggers(triggers)

收集記憶體快照資料後,您可以從伺服器下載設定檔,也可以透過 ADB pull 在本機下載,然後將檔案拖曳到 Perfetto 使用者介面。如要簡化記憶體偵錯工作流程,請使用 記憶體快照探索器,這是 Perfetto UI 中記憶體快照的新預設檢視畫面。這項工具提供直覺式介面,可檢查 Java 記憶體快照資料,讓您以視覺化方式呈現物件配置階層、計算保留的記憶體大小,以及找出垃圾回收根目錄的最短路徑。善用記憶體快照資料探索器,您就能快速找出記憶體洩漏、過度保留的物件 (例如過多的點陣圖配置),並在同一處分析堆積物件配置。

pic5-perfettoheapdump-analyzer.png
使用記憶體快照探索工具內嵌的火焰圖,以視覺化方式檢查及瀏覽記憶體配置量最高的物件。

結論

使用 R8 最佳化位元碼、採用圖片載入最佳做法,以及解決記憶體洩漏問題,都是在資源壓力下有效管理資源,同時提供優質使用者體驗的關鍵步驟。採取這些主動式措施有助於維持應用程式穩定性和效能,防止非預期終止,同時保護使用者環境。如要進一步瞭解效能,請參閱我們修訂的記憶體指南

撰寫者:
繼續閱讀