管理應用程式的記憶體

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

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

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

您必須先找出應用程式裡的記憶體用量問題,然後才能進行修復。Android Studio 的記憶體分析器可以藉由以下幾種方法幫助您找出並診斷記憶體問題:

  1. 瞭解應用程式配置記憶體的過程。記憶體分析器可以顯示即時圖表,展示應用程式記憶體用量、配置的 Java 物件數目,以及進行垃圾收集的時間點。
  2. 在執行應用程式時,啟動垃圾收集事件並擷取 Java 堆積的快照。
  3. 記錄應用程式的記憶體配置,然後檢查所有配置的物件、檢視每個配置的堆疊追蹤,並在 Android Studio 編輯器中跳到對應的程式碼處。

為回應事件釋放記憶體

如同「Android 記憶體管理總覽」所說的一樣,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 was raised.
     */
    override fun onTrimMemory(level: Int) {

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

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

                   The user interface has moved to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory that 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 will
                   begin killing 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 will be one of
                   the first to be terminated.
                */
            }

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

                  The app received 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 was raised.
     */
    public void onTrimMemory(int level) {

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

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved 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 that 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 will
                   begin killing 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 will be one of
                   the first to be terminated.
                */

                break;

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

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

Android 4.0 (API 級別 14) 新增了 onTrimMemory() 回呼。若是較舊的版本,則可以使用 onLowMemory(),這和 TRIM_MEMORY_COMPLETE 事件大致相同。

檢查您應使用多少記憶體

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

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

以下程式碼片段範例展示了如何在應用程式內使用 getMemoryInfo() 方法。

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see 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 to see 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 快取保留的快取程序數量,導致切換應用程式更耗費效能。如果記憶體用量吃緊,而系統無法維持足夠的程序去容納所有執行中的服務時,還可能導致系統發生輾轉現象。

持續使用服務會持續要求取得可用記憶體,因此在一般情況下您應該避免這樣做。我們建議您改為其他實作,例如 JobScheduler。如果想進一步瞭解如何使用 JobScheduler 排程背景程序,請參閱「背景最佳化」。

如果您必須使用服務,那麼限制服務生命週期最好的方式就是使用 IntentService,當它處理完畢啟動此項目的意圖之後就會自動結束。詳情請參閱「執行背景服務」。

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

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

Android 架構內含幾種經過最佳化的資料容器,包括 SparseArraySparseBooleanArrayLongSparseArray。舉例來說,SparseArray 類別在使用上更有效率,因為這些類別可以避免出現程式 autobox 鍵,有時甚至需要 autobox 值 (這樣會額外建立一個物件,或每個項目產生兩個物件) 的需求。

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

謹慎運用程式碼抽象化

開發人員經常使用抽象化的原因非常簡單,因為這是良好的程式設計做法,可以加強程式碼的彈性和維護能力。不過,使用抽象化並不容易:一般來說,抽象化需要執行更多程式碼,而這些程式碼要對應到記憶體也需要消耗更多時間和 RAM。因此,如果抽象化無法帶來顯著的效益,應避免使用抽象化。

使用精簡 Protobufs 序列化資料

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

詳情請參閱 protobuf README 的「精簡版本」章節。

避免記憶體流失

正如我們之前所說的,垃圾收集事件並不會影響應用程式效能。不過,如果在短時間內發生多次垃圾收集事件,也可能會快速消耗電力,而且由於垃圾收集程式和應用程式執行緒之間的必要互動,也可能會稍微增加設定影格所需的時間。系統花越多時間進行垃圾收集,電力就消耗得越快。

「記憶體流失」經常會導致發生非常多個垃圾收集事件。實際上,記憶體流失描述的就是在一定時間內配置的暫存物件數量。

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

在修復記憶體流失問題之前,當然需要先在程式碼裡找到記憶體流失較嚴重的部分。如果要達成此效果,您應該使用 Android Studio 的記憶體分析器

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

您也可以評估物件集區對這個用途有沒有助益。藉由使用物件集區,您就不必拋棄物件執行個體,而可以在不再需要物件執行個體後把它釋放到集區內。下次需要使用該類型的物件執行個體時,系統可以從集區內獲取,不必進行配置。

如果想判斷是否適合使用物件集區,就必須對效能進行全面性的評估。在某些情況下,使用物件集區可能會對效能造成不良影響。雖然使用集區可以避免進行配置,但是會額外產生負載。舉例來說,維護集區通常需要進行同步處理,並會產生無法忽視的負載。另外,如果在釋放時清除集區內的物件執行個體 (以便避免發生記憶體流失),那麼集區在獲取時進行初始化時就可能會產生非零的負載。最後,如果在集區裡保留必要數量以上的物件執行個體,也會對 GC 造成負擔。雖然物件集區可以減少 GC 叫用的數量,但由於工作量和活躍中 (可達的) 位元組成正比,因此最終會增加每次叫用時必須處理的工作量。

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

程式碼裡使用的部分資源和程式庫可能會在您沒發現的情況下佔用大量記憶體。應用程式的整體大小,包括第三方程式庫或內嵌資源在內,可能會影響應用程式消耗的記憶體多寡。藉由移除程式碼中多餘、非必要或過於龐大的元件、資源或程式庫,就能改善應用程式的記憶體消耗情形。

縮減整體 APK 大小

您可以藉由減少應用程式的整體大小達到大幅降低應用程式記憶體用量的效果。點陣圖大小、資源、動畫影格、第三方程式庫都可能會影響應用程式的大小。Android Studio 和 Android SDK 提供多種有助於減少資源和外部依附元件大小的工具。這些工具都支援現代化的程式碼縮減方法,例如 R8 編譯。(Android Studio 3.3 以下版本並未使用 R8 編譯,而是 ProGuard)。

若想進一步瞭解如何減少應用程式整體大小,請參閱減少應用程式大小指南

使用 Dagger 2 插入依附元件

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

如果您想在應用程式內使用插入依附元件架構,請考慮使用 Dagger 2。Dagger 掃描應用程式的程式碼時,不會使用反射。Dagger 採用靜態的編譯時間實作,不需要消耗不必要的執行階段成本或記憶體用量就能在 Android 應用程式裡使用。

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

謹慎使用外部程式庫

外部程式庫的程式碼通常並非專為行動裝置環境撰寫,在行動用戶端上的使用效率可能較為不佳。如果您決定使用外部程式庫,則可能需要針對行動裝置最佳化程式庫。請提早做好準備,並分析程式庫的程式碼大小和 RAM 足跡,然後再決定是否要使用這個程式庫。

就算是某些已經針對行動裝置最佳化的程式庫,也可能因為實作方式不同而發生問題。舉例來說,某程式庫可能使用的是精簡 protobufs,而另一個程式庫使用微型 protobufs,導致應用程式實作兩種不同的 protobufs。當記錄、分析、圖片載入架構、快取和其他各種您預想不到的內容有不同實作時,就可能會發生這種情形。

雖然 ProGuard 可以藉由合適的標記幫助您移除 API 和資源,但是卻無法移除程式庫的大型內部依附元件。您想藉由這些程式庫使用的功能可能需要使用低階的依附元件。在使用某程式庫的 Activity 子類別 (通常會有較長的依附元件),以及程式庫使用反射 (這種情形非常常見,也表示您需要花費更多時間手動調整 ProGuard 才能正常運作) 等情況下,問題就更明顯了。

另外,也請您避免只因為想使用數十種功能當中的一到兩種功能,就使用共用程式庫。我們不建議您為了根本不會用到的功能而納入大量的程式碼和負載。在考慮是否要使用程式庫時,請找出最能迎合需求的實作。不然,您也可以考量是否要自行建立實作。