管理應用程式的記憶體

本頁面說明如何主動降低應用程式的記憶體用量。如要瞭解 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) {

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

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

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

節制使用服務

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

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

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

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

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

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

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

謹慎運用程式碼抽象化

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

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

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

詳情請參閱 protobuf readme

避免記憶體流失

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

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

舉例來說,您可以在 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 來使它正常運作。

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