本頁面說明如何主動降低應用程式的記憶體用量。如要瞭解 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 架構包含幾種經過最佳化的資料容器,包括 SparseArray
、SparseBooleanArray
和 LongSparseArray
。舉例來說,SparseArray
類別效率較高,因為有了這些類別,系統就不需要將鍵自動裝箱,有時也不需要將值自動裝箱。自動裝箱作業會額外建立一個物件,或為每個項目建立兩個物件。
如有必要,您隨時可以改為使用原始陣列,方便精簡資料結構。
謹慎運用程式碼抽象化
開發人員經常使用抽象化方法,因為這是良好的程式設計做法,可以提升程式碼的彈性和維護能力。不過,抽象化耗用的資源非常高,因為它需要執行更多程式碼,因而需要更多時間和 RAM 將程式碼對應至記憶體。如果抽象化無法帶來顯著效益,應避免使用這項做法。
使用精簡通訊協定緩衝區序列化資料
為了將結構化資料序列化,Google 設計出通訊協定緩衝區,這項可擴充機制適合各種語言及平台使用,雖然功能和 XML 十分類似,但是更小、更快,也更簡單。如果您使用通訊協定緩衝區處理資料,那麼用戶端程式碼也應一律使用精簡的通訊協定緩衝區。一般的通訊協定緩衝區會產生非常詳細的程式碼,可能會在應用程式內造成多種問題,例如 RAM 用量提高、APK 大小大幅增加、執行速度減慢等等。
詳情請參閱 protobuf readme。
避免記憶體流失
垃圾收集事件不會影響應用程式效能。不過,如果短時間內發生多次垃圾收集事件,就可能因垃圾收集器和應用程式執行緒之間必要的互動,導致電量快速消耗,並稍微增加設定影格的時間。系統花越多時間進行垃圾收集,電量就消耗越快。
「記憶體抖動」經常會導致發生大量垃圾收集事件。實際上,記憶體抖動描述的是在指定時間內配置的暫存物件數量。
舉例來說,您可以在 for
迴圈內配置多個暫存物件,也可以在檢視畫面的 onDraw()
函式內建立新的 Paint
或 Bitmap
物件。在這兩種情況下,應用程式都會快速建立大量物件。這些物件可能會在新生代快速消耗所有可用記憶體,導致必須產生垃圾收集事件。
修復記憶體抖動問題之前,您需要先使用程式碼分析器,找出程式碼中問題較嚴重的部分。
從程式碼中找到問題區域後,請嘗試在會嚴重影響效能的區域內減少配置數量。請考慮移出內部迴圈的內容,或移到以工廠為基礎的配置結構。
您也可以評估物件集區是否對用途有幫助。如果使用物件集區,不再需要的物件例項就能釋放到集區,而不必遭到捨棄。下次需要使用該類型的物件例項時,您可以從集區中取得該例項,不必進行配置。
如要判斷特定情況是否適合使用物件集區,請對效能進行全面評估。在某些情況下,使用物件集區可能會對效能造成不良影響。雖然使用集區可避免進行配置,但是會產生其他負擔。舉例來說,維護集區通常需要進行同步處理作業,並產生不容忽視的負擔。另外,如果為了避免記憶體流失,而在釋放過程中清除集區物件例項,那麼例項在獲取過程中進行初始化時,就可能產生非零的負擔。
如果集區中保留的非必要物件例項越多,也會對垃圾收集作業造成負擔。雖然物件集區可減少垃圾收集叫用次數,但由於使用中 (可連線) 的位元組越多,工作量也越多,最終就會增加每次叫用需處理的工作量。
移除會占用大量記憶體的資源和程式庫
程式碼中的部分資源和程式庫,可能會在您不注意的情況下消耗大量記憶體。應用程式的整體大小會計入第三方程式庫或內嵌資源,並影響應用程式的記憶體消耗量。只要從程式碼中移除多餘、不必要或過大的元件、資源或程式庫,就能改善應用程式的記憶體消耗情形。
縮減整體 APK 大小
縮減應用程式的整體大小,即可大幅降低應用程式的記憶體用量。影響應用程式大小的因素,包括點陣圖大小、資源、動畫影格和第三方程式庫。您可以運用 Android Studio 和 Android SDK 提供的多種工具,縮減資源和外部依附元件的大小。這些工具都支援新型程式碼縮減方法,例如 R8 編譯。
如要進一步瞭解如何縮減應用程式整體大小,請參閱「縮減應用程式大小」。
使用 Hilt 或 Dagger 2 插入依附元件
依附元件插入架構可簡化您編寫的程式碼,並提供可自動調節的環境,對測試和其他設定變更作業相當實用。
如果想在應用程式內使用依附元件插入架構,建議您使用 Hilt 或 Dagger。Hilt 是 Android 的依附元件插入程式庫,可在 Dagger 上執行。Dagger 掃描應用程式程式碼時,不會使用反射方法。您可以在 Android 應用程式中使用 Dagger 的靜態編譯時間實作項目,無須消耗不必要的執行階段成本或記憶體用量。
其他使用反射方法的依附元件插入架構,經常會掃描程式碼來找出註解,藉此初始化程序。這麼做需要使用更大量的 CPU 週期和 RAM,且可能在應用程式啟動時造成明顯延遲。
謹慎使用外部程式庫
外部程式庫的程式碼通常並非專為行動裝置環境編寫,因此在行動裝置用戶端上的工作效率可能不佳。使用外部程式庫時,您可能需要對行動裝置進行程式庫最佳化。請預先規劃這項工作,並在使用外部程式庫之前分析它的程式碼大小和 RAM 用量。
就算是已針對行動裝置進行最佳化的程式庫,也可能因實作項目不同而發生問題。舉例來說,某程式庫可能使用精簡版通訊協定緩衝區,而另一個程式庫使用微型通訊協定緩衝區,導致應用程式實作兩種不同的通訊協定緩衝區。如有不同的記錄、分析、圖片載入架構、快取和其他非預期的實作項目,就可能發生這種情形。
雖然 ProGuard 可運用合適的標記,協助移除 API 和資源,但無法移除程式庫的大型內部依附元件。在這類程式庫中,您想要的功能可能需使用低階依附元件。以下情況會特別容易發生問題:您使用某程式庫的 Activity
子類別 (依附元件通常較多),而程式庫使用反射方法。程式庫使用反射方法的情形相當常見,也表示您需要更多時間手動調整 ProGuard 來使它正常運作。
請避免只為了一或兩種功能,就使用包含數十項功能的共用程式庫,不要為了不會用到的功能,增加大量程式碼和負擔。在考慮是否要使用程式庫時,請找出確實符合需求的實作項目,否則建議您考慮自行建立實作項目。