管理應用程式的記憶體

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

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

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

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

  • 瞭解應用程式配置記憶體的變化趨勢。記憶體分析器可以顯示即時圖表,呈現應用程式目前的記憶體用量、已配置的 Java 物件數量,以及進行垃圾收集的時間點。
  • 在應用程式執行期間,啟動垃圾收集事件並提供 Java 堆積的數據匯報。
  • 記錄應用程式的記憶體配置、檢查所有配置的物件、查看每個配置的堆疊追蹤,並在 Android Studio 編輯器中跳到對應的程式碼位置。

為回應事件釋放記憶體

Android 可收回應用程式的記憶體,在必須釋放記憶體供重要工作使用時,也可以完全終止應用程式。詳情請參閱「記憶體管理總覽」。為了進一步平衡系統記憶體,並避免系統必須停止應用程式程序,您可以在 Activity 類別內實作 ComponentCallbacks2 介面。所提供的 onTrimMemory() 回呼方法可讓應用程式在位於前景或背景時監聽記憶體相關事件,並在應用程式生命週期或系統事件指出系統需要收回記憶體時,釋出物件做為回應。

您可以實作 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) {

        // Determine which lifecycle or system event is raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

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) {

        // Determine which lifecycle or system event is raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

檢查需要的記憶體用量

為了能夠同時執行多個程序,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;
}

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

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

節制使用服務

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

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

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

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

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

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

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

謹慎運用程式碼抽象化

開發人員經常使用抽象化方法,因為這是良好的程式設計做法,可以提升程式碼的彈性和維護能力。不過,抽象化耗用的資源非常高,因為它需要執行更多程式碼,因而需要更多時間和 RAM 將程式碼對應至記憶體。如果抽象化無法帶來顯著效益,應避免使用這項做法。

使用精簡 Protobufs 序列化資料

為了將結構化資料序列化,Google 設計出通訊協定緩衝區,這項可擴充機制適合各種語言及平台使用,雖然功能和 XML 十分類似,但是更小、更快,也更簡單。如果您使用通訊協定緩衝區處理資料,那麼用戶端程式碼也應一律使用精簡的通訊協定緩衝區。一般的通訊協定緩衝區會產生非常詳細的程式碼,可能會在應用程式內造成多種問題,例如 RAM 用量提高、APK 大小大幅增加、執行速度減慢等等。

詳情請參閱 protobuf 讀取圖

避免記憶體流失

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

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

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

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

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

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

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

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

移除會佔用大量記憶體的資源和程式庫

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

縮減整體 APK 大小

減少應用程式的整體大小後,即可大幅降低應用程式的記憶體用量。影響應用程式大小的因素,包括點陣圖大小、資源、動畫影格和第三方程式庫。Android Studio 和 Android SDK 提供多種工具來縮減資源和外部依附元件的大小。這些工具都支援新型程式碼縮減方法,例如 R8 編譯

如要進一步瞭解如何縮減應用程式整體大小,請參閱「縮減應用程式大小」。

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

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

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

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

謹慎使用外部程式庫

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

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

雖然 ProGuard 可運用合適的標記協助移除 API 和資源,但無法移除程式庫的大型內部依附元件。在這類程式庫中,您想要的功能可能需使用低階依附元件。以下情況會特別容易發生問題:您使用某程式庫的 Activity 子類別 (依附元件通常較多),而程式庫使用反射方法。程式庫使用反射方法的情形相當常見,也表示您需要更多時間手動調整 ProGuard 來使它正常運作。

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