管理應用程式的記憶體

本頁面說明如何主動降低應用程式的記憶體用量。如要瞭解 Android 作業系統管理記憶體的方式,請參閱「記憶體管理總覽」。

隨機存取記憶體 (RAM) 在任何軟體開發環境都是重要資源,在行動裝置作業系統上尤其如此,因為行動裝置的實體記憶體通常有限。雖然 Android 執行階段 (ART) 和 Dalvik 虛擬機器都會執行例行垃圾收集作業,但不代表可以因此忽略應用程式配置和釋放記憶體的時機和位置。您仍須避免導入記憶體流失 (通常是因保留靜態成員變數中的物件參照而造成),並在生命週期回呼定義的適當時間釋放任何 Reference 物件。

減少應用程式的程式碼和資源足跡

程式碼中的部分資源和程式庫,可能會在您不注意的情況下消耗大量記憶體。應用程式的整體大小會計入第三方程式庫或內嵌資源,並影響應用程式的記憶體消耗量。只要從程式碼中移除多餘、不必要或過大的元件、資源和程式庫,就能改善應用程式的記憶體消耗情形。

啟用 R8 縮減整體應用程式大小

編譯後的應用程式程式碼是執行階段記憶體用量的重要部分。執行時,每個類別、方法、程式庫依附元件和字串常數都必須載入 RAM。編譯後的程式碼集越大,應用程式所需的實體 RAM 就越多。

您可以使用 R8 減少應用程式的記憶體用量。R8 傳統上以縮減 APK 大小著稱,但它對執行階段記憶體 (RAM) 有直接的正面影響。R8 會分析應用程式的位元碼,去除無效程式碼、合併多餘的類別、內嵌方法,以及縮短 ID。從 APK 載入 RAM 的編譯位元碼越少,應用程式的整體基準記憶體用量就越少。此外,將類別、方法和欄位名稱縮短為 ID,可直接減少 RAM 負擔。類別合併和大量方法內嵌等最佳化作業,也會取代耗費資源的執行階段查閱和分配模式,進而最佳化堆積和堆疊記憶體。

瞭解保留規則

保留規則是設定指令,可告知 R8 在最佳化期間要保留哪些程式碼部分,避免移除或縮減應用程式所依附的程式碼。詳情請參閱「保留規則總覽」。

如果保留規則編寫不當,R8 就無法最佳化程式碼集的大部分內容。避免使用過於寬鬆的保留規則,並遵循下列最佳做法:

  • 應避免的全球規則:
    • -dontoptimize:完全停用整個應用程式的最佳化功能,導致可執行檔較大且速度較慢。
    • -dontshrink:防止移除未使用的程式碼和資源。
    • -dontobfuscate:防止名稱縮減,錯失寶貴的記憶體節省空間 (尤其是在大型應用程式中)。
  • 避免使用套件範圍的萬用字元:廣泛規則 (例如 -keep class com.example.package.** { *; }) 會強制 R8 保留該套件中的每個類別、欄位和方法。這會完全停止 R8 移除、最佳化或縮減該套件中程式碼的能力。

  • 使用預設 R8 設定檔:一律使用 proguard-android-optimize.txt

如要進一步瞭解如何撰寫保留規則,請參閱「保留規則總覽」。如要瞭解應使用和避免使用的特定模式,請參閱保留規則最佳做法

R8 設定分析器可深入瞭解 R8 設定,以及每項保留規則對應用程式的影響。如要進一步瞭解如何找出阻礙最佳化的規則,請參閱「R8 設定分析器」。

謹慎使用外部程式庫

外部程式庫的程式碼通常並非專為行動裝置環境編寫,因此在行動裝置用戶端上的工作效率可能不佳。使用外部程式庫時,您可能需要針對行動裝置進行程式庫最佳化。請預先規劃這項工作,並在使用外部程式庫之前分析它的程式碼大小和 RAM 用量。

就算是已針對行動裝置進行最佳化的程式庫,也可能因實作項目不同而產生問題。舉例來說,某程式庫可能使用精簡版通訊協定緩衝區,而另一個程式庫使用微型通訊協定緩衝區,導致應用程式實作兩種不同的通訊協定緩衝區。如有不同的記錄、分析、圖片載入架構、快取和其他非預期的實作項目,就可能發生這種情形。

雖然使用 R8 最佳化應用程式可從依附元件中移除未使用的程式碼,但其效用通常會受到程式庫內部設定的限制。舉例來說,廣泛的保留規則或程式庫內使用反射,可能會導致 R8 無法縮減程式碼,進而增加記憶體用量。如需選取有效率程式庫的策略,請參閱「明智地選擇程式庫」。

請避免只為了一或兩種功能,就使用包含數十項功能的共用程式庫,不要為了不會用到的功能,增加大量程式碼和負擔。在考慮是否要使用程式庫時,請找出確實符合需求的實作項目,否則建議您考慮自行建立實作項目。

使用 Hilt 或 Dagger 2 插入依附元件

依附元件插入架構可簡化您撰寫的程式碼,並提供可自動調節的環境,對測試和其他設定變更作業相當實用。

如果想在應用程式內使用依附元件插入架構,建議您使用 HiltDagger。Hilt 是適用於 Android 的依附元件插入程式庫,以 Dagger 為基礎。Dagger 掃描應用程式程式碼時,不會使用反射方法。您可以在 Android 應用程式中使用 Dagger 的靜態編譯時間實作項目,無須消耗不必要的執行階段成本或記憶體用量。

其他使用反射方法的依附元件插入架構,經常會掃描程式碼來找出註解,藉此初始化程序。這個程序需要使用更大量的 CPU 週期和 RAM,且可能在應用程式啟動時造成明顯延遲。

使用依附元件注入時,請務必適當設定物件範圍,以免發生記憶體流失。如果將物件繫結至錯誤的生命週期,導致物件保留時間過長,可能會造成記憶體流失。詳情請參閱這篇文章,瞭解如何使用範圍物件避免記憶體流失。

慎重考量圖片載入方式

圖像點陣圖通常是應用程式記憶體中最大的常見物件。即使您處理的是 JPEG 等壓縮檔,檔案也必須膨脹成未壓縮的點陣圖,才能顯示在螢幕上。壓縮後的小型圖片檔可能會擴展為非常大的點陣圖。

舉例來說,大多數點陣圖都使用 ARGB_8888 設定,這表示每個像素需要 4 個位元組的記憶體,分別是紅色、綠色、藍色和 Alpha (透明度) 各一個位元組。假設您有 100 KB 的 JPEG,並在 1000×1000 像素的檢視區塊中顯示,點陣圖會為每個像素需要 4 個位元組,加總起來就是 4 MB 的記憶體。

您可以採取幾項措施,盡量發揮圖片的效益。舉例來說,使用圖片載入程式庫有助於在不需要時釋放記憶體。如要瞭解如何有效處理圖片,請參閱「最佳化點陣圖」。

監控可用記憶體和記憶體用量

您必須先找出應用程式的記憶體用量問題才能修正。Android Studio 的記憶體分析器提供以下幾種方法,可幫助您找出及診斷記憶體問題:

記憶體分析器也整合了 LeakCanary 記憶體流失偵測程式庫。使用 LeakCanary,您可以將記憶體流失分析從測試裝置移至開發機器,大幅加快工作流程。詳情請參閱 Android Studio 版本資訊

您可以使用其他工具,根據執行正式版應用程式的使用者資料診斷記憶體問題:

為回應事件釋放記憶體

Android 可收回應用程式的記憶體,在必須釋放記憶體供重要工作使用時,也可以完全終止應用程式。詳情請參閱「記憶體管理總覽」。為了進一步平衡系統記憶體,並避免系統必須終止應用程式程序,您可以在 Activity 類別內實作 ComponentCallbacks2 介面。所提供的 onTrimMemory() 回呼方法會通知應用程式生命週期或記憶體相關事件,讓應用程式有機會主動減少記憶體用量。釋放記憶體可減少應用程式遭低記憶體終止程序終止的頻率。

onTrimMemory() 的實作項目應只著重於 TRIM_MEMORY_UI_HIDDENTRIM_MEMORY_BACKGROUND 事件。(從 Android 14 開始,系統不再傳送其他舊版常數的通知。這些常數已在 Android 15 中正式淘汰。

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

  • TRIM_MEMORY_BACKGROUND:這個信號表示您的程序位於背景,現在是終止候選程序,可滿足系統的整體記憶體需求。如要延長程序停留在快取狀態的時間,並減少應用程式冷啟動次數,您應積極釋出任何可輕鬆重建的資源,讓使用者恢復工作階段。

這個程式碼範例說明如何實作 onTrimMemory() 回呼,以回應多種記憶體相關事件:

Kotlin

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

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * 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.
        }
    }
}

Java

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

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

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

        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.
        }
    }
}

檢查需要的記憶體用量

為了能夠同時執行多個程序,Android 會強制限制每個應用程式能配置的堆積大小,實際限制取決於裝置能夠提供的整體 RAM。如果應用程式達到堆積容量上限,並嘗試配置更多記憶體,系統便會擲回 OutOfMemoryError

為避免記憶體不足,您可以查詢系統,判斷目前裝置有多少可用的堆積空間。您可以呼叫 getMemoryInfo(),向系統查詢這項數值。這會回傳 ActivityManager.MemoryInfo 物件,該物件會說明裝置目前的記憶體狀態,包括可用記憶體、總記憶體和記憶體門檻。記憶體門檻是系統會開始終止程序的記憶體層級。ActivityManager.MemoryInfo 物件也會公開簡易的布林值 lowMemory,可用來判斷裝置是否即將用完記憶體。

以下程式碼片段範例說明如何在應用程式中使用 getMemoryInfo() 方法。

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

監控記憶體不足終止作業

當系統記憶體嚴重不足時,就會發生使用者感知記憶體耗盡 (LMK) 事件。記憶體不足時,lmkd (記憶體不足終止工具常駐程式) 會根據程序的 oom_adj_score 終止程序。如果應用程式已快取,或正在執行沒有相關聯 UI 的服務 (例如工作),則會獲得最高分數,並優先終止。如果記憶體仍嚴重不足,精靈會強制從 oom_adj_score 為 0 的程序回收記憶體。由於該分數是保留給可見應用程式,因此終止這些應用程式會導致立即且不正常的程序結束。對使用者而言,這就像是應用程式當機,通常會略過標準生命週期狀態儲存機制,導致使用者進度遺失。

Android 系統會優先終止前景程序,因為這類程序是記憶體管理不當的高保真度指標。雖然任何高於 1% 的 LMK 發生率都表示需要立即採取行動,但低發生率不一定代表健康狀態良好。使用者感知 LMK 發生率偏低可能表示 LMK 精靈經常在程序處於背景時終止程序,這會導致「暖啟動」效能和多工處理流暢度下降。因此,無論目前的 LMK 分數為何,我們都建議您遵循記憶體最佳做法,確保長期穩定性和裝置健康狀態。

使用 ProfilingManager 追蹤記憶體問題

Android 平台提供ProfilingManager,這項進階可觀測性 API 可讓您根據設定的觸發條件,在正式環境中擷取使用者資料,有助於找出難以重現的記憶體問題。

Android 17 推出的兩項新觸發條件,特別有助於找出記憶體問題:

  • TRIGGER_TYPE_OOM 表示應用程式已擲回 OutOfMemoryError。應用程式在當機下次啟動時,如果註冊了剖析觸發條件,就會觸發此事件。
  • TRIGGER_TYPE_ANOMALY 會在系統偵測到應用程式的異常行為時觸發,例如記憶體用量過高。觸發時機是應用程式記憶體用量過高之後,以及系統採取任何行動停止違規程序之前。舉例來說,如果應用程式超出 Android 17 中導入的記憶體限制,系統會在終止應用程式前觸發 TRIGGER_TYPE_ANOMALY

如要進一步瞭解如何使用 ProfilingManager 以程式輔助方式註冊及擷取觸發條件,請參閱以觸發條件為基礎的剖析說明文件。

您也可以使用應用程式驅動的剖析功能,手動定義追蹤記錄的開始和結束點。建議您這麼做,以便在懷疑可能發生記憶體流失或記憶體用量過高的區域,手動擷取堆積傾印或堆積設定檔。

使用節省記憶體的程式碼結構

部分 Android 功能、Java 類別和程式碼結構使用的記憶體通常多於其他項目。您可以在程式碼中選擇更節省記憶體的替代項目,藉此降低應用程式的記憶體用量。

節制使用服務

我們強烈建議您不要在非必要時繼續執行服務。在非必要時繼續執行服務,是 Android 應用程式最嚴重的記憶體管理問題之一。如果應用程式需要使用服務在背景執行工作,請勿在服務不需要執行工作時讓它繼續執行。服務完成工作後,請停止服務,否則可能會導致記憶體流失。

啟動服務後,系統偏好為繼續執行該服務的程序。這項行為會導致服務程序耗用大量資源,因為服務使用的 RAM 無法供其他程序使用。這樣會減少系統可在 LRU 快取機制保留的快取程序數量,導致應用程式切換效率下降。如果記憶體用量吃緊,且系統無法維持足夠的程序來提供所有執行中的服務,還可能導致系統發生輾轉現象。

一般來說,請避免使用持續性服務,因為它會不斷要求取得可用記憶體。建議您改用其他實作項目,例如 WorkManager。如要進一步瞭解如何使用 WorkManager 為背景程序排程,請參閱「最佳化背景程序」。

使用經過最佳化的資料容器

程式設計語言所提供的類別中,有些類別並未針對行動裝置用途進行最佳化。舉例來說,一般 HashMap 實作項目的記憶體使用效率並不佳,因為每次對應都需要獨立項目物件。

Android 架構包含幾種經過最佳化的資料容器,包括 SparseArraySparseBooleanArrayLongSparseArray。舉例來說,SparseArray 類別效率較高,因為有了這些類別,系統就不需要將鍵自動裝箱,有時也不需要將值自動裝箱。自動裝箱作業會額外建立一個物件,或為每個項目建立兩個物件。

如有必要,您隨時可以改為使用原始陣列,方便精簡資料結構。

謹慎運用程式碼抽象化

開發人員經常使用抽象化方法,因為這是良好的程式設計做法,可以提升程式碼的彈性和維護能力。不過,抽象化通常需要執行更多程式碼。如「縮減應用程式的程式碼和資源占用空間」一文所述,編譯程式碼集越大,應用程式所需的實體 RAM 就越多。如果抽象化無法帶來顯著效益,應避免使用這項做法。

使用精簡通訊協定緩衝區序列化資料

通訊協定緩衝區 (protobuf) 是 Google 設計的可擴充機制,適合各種語言及平台使用,可將結構化資料序列化,雖然功能和 XML 十分類似,但是更小、更快,也更簡單。如果您使用通訊協定緩衝區處理資料,那麼用戶端程式碼也應一律使用精簡的通訊協定緩衝區。一般的通訊協定緩衝區會產生非常詳細的程式碼,導致應用程式在 RAM 中的程式碼占用空間增加 (請參閱「管理及最佳化應用程式的程式碼占用空間」),並造成 APK 大小增加。

詳情請參閱 protobuf README

謹防記憶體流失

如果參照管理不當,可能會導致記憶體流失,物件的生命週期會超出實用期限,垃圾收集器也無法回收流失物件的記憶體。為避免記憶體流失,請實作生命週期感知設計。

避免記憶體抖動

垃圾收集事件不會影響應用程式效能。不過,如果短時間內發生多次垃圾收集事件,就可能因垃圾收集器和應用程式執行緒之間必要的互動,導致電量快速消耗,並稍微增加設定影格的時間。系統花越多時間進行垃圾回收,電量就消耗越快。

「記憶體抖動」經常會導致發生大量垃圾回收事件。實際上,記憶體抖動描述的是在指定時間內配置的暫存物件數量。

舉例來說,您可以在 for 迴圈內配置多個暫存物件,也可以在檢視區塊的 onDraw() 函式內建立新的 PaintBitmap 物件。在這兩種情況下,應用程式都會快速建立大量物件。這些物件可能會在年輕代快速消耗所有可用記憶體,導致必須產生垃圾收集事件。

修復記憶體抖動問題之前,您需要先使用記憶體分析器,找出程式碼中問題較嚴重的部分。

從程式碼中找到問題區域後,請嘗試在會嚴重影響效能的區域內減少配置數量。請考慮移出內部迴圈的內容,或移到以工廠為基礎的配置結構。

您也可以評估物件集區是否對用途有幫助。如果使用物件集區,不再需要的物件例項就能釋放到集區,而不必遭到捨棄。下次需要使用該類型的物件例項時,您可以從集區中取得該例項,不必進行配置。

如要判斷特定情況是否適合使用物件集區,請對效能進行全面評估。在某些情況下,使用物件集區可能會對效能造成不良影響。雖然使用集區可避免進行配置,但是會產生其他負擔。舉例來說,維護集區通常需要進行同步處理作業,並產生不容忽視的負擔。另外,如果為了避免記憶體流失,而在釋放過程中清除集區物件例項,那麼例項在獲取過程中進行初始化時,就可能產生非零的負擔。

如果集區中保留的非必要物件例項越多,也會對垃圾回收作業造成負擔。雖然物件集區可減少垃圾收集叫用次數,但由於使用中 (可連線) 的位元組越多,工作量也越多,最終就會增加每次叫用需處理的工作量。