本頁面說明如何主動降低應用程式的記憶體用量。如要瞭解 Android 作業系統管理記憶體的方式,請參閱「記憶體管理總覽」。
隨機存取記憶體 (RAM) 在任何軟體開發環境都是重要資源,在行動裝置作業系統上尤其如此,因為行動裝置的實體記憶體通常有限。雖然 Android 執行階段 (ART) 和 Dalvik 虛擬機器都會執行例行垃圾回收作業,但不代表可以因此忽略應用程式配置和釋放記憶體的時機和位置。您依然需要避免記憶體流失,這種情形通常是因在靜態成員變數中保留物件參照所造成。您也需要在生命週期回呼定義的適當時機釋放所有 Reference 物件。
減少應用程式的程式碼和資源足跡
程式碼中的部分資源和程式庫,可能會在您不注意的情況下消耗大量記憶體。應用程式的整體大小會計入第三方程式庫或內嵌資源,並影響應用程式的記憶體消耗量。只要從程式碼中移除多餘、不必要或過大的元件、資源和程式庫,就能改善應用程式的記憶體消耗情形。
啟用 R8 縮減整體應用程式大小
編譯後的應用程式程式碼是執行階段記憶體用量的重要部分。執行時,每個類別、方法、程式庫依附元件和字串常數都必須載入 RAM。編譯後的程式碼集越大,應用程式所需的實體 RAM 就越多。
您可以使用 R8 減少應用程式的記憶體用量。R8 傳統上以縮減 APK 大小著稱,但它對執行階段記憶體 (RAM) 也有直接的正面影響。R8 會分析應用程式的位元碼,移除無效程式碼、合併多餘的類別、內嵌方法,以及縮短 ID。從 APK 載入 RAM 的已編譯位元組碼減少,應用程式的整體基準記憶體用量也會隨之減少。此外,將類別、方法和欄位名稱縮短為識別項,可直接減少 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 插入依附元件
依附元件插入架構可簡化您撰寫的程式碼,並提供可自動調節的環境,對測試和其他設定變更作業相當實用。
如果想在應用程式內使用依附元件插入架構,建議您使用 Hilt 或 Dagger。Hilt 是 Android 的依附元件插入程式庫,可在 Dagger 上執行。Dagger 掃描應用程式程式碼時,不會使用反射方法。您可以在 Android 應用程式中使用 Dagger 的靜態編譯時間實作項目,無須消耗不必要的執行階段成本或記憶體用量。
其他使用反射方法的依附元件插入架構,經常會掃描程式碼來找出註解,藉此初始化程序。這個程序需要使用更大量的 CPU 週期和 RAM,且可能在應用程式啟動時造成明顯延遲。
使用依附元件注入時,請務必確保物件範圍適當,以免發生記憶體流失。如果將物件繫結至錯誤的生命週期,導致物件保留時間超過必要時間,可能會造成記憶體流失。詳情請參閱「使用範圍物件避免記憶體流失」指南。
慎重考量圖片載入方式
圖像點陣圖通常是應用程式記憶體中最大的常見物件。即使您處理的是 JPEG 等壓縮檔,檔案也必須膨脹成未壓縮的點陣圖,才能顯示在螢幕上。壓縮後的小型圖片檔可能會擴展為非常大的點陣圖。
舉例來說,大多數點陣圖都使用 ARGB_8888 設定,也就是說,每個像素需要 4 個位元組的記憶體,分別是紅色、綠色、藍色和 Alpha (透明度) 各 1 個位元組。假設您有 100 KB 的 JPEG,並在 1000×1000 像素的檢視區塊中顯示,點陣圖會為每個像素需要 4 個位元組,加總起來就是 4 MB 的記憶體。
您可以採取幾種做法,盡量減少圖片用量。舉例來說,使用圖片載入程式庫有助於在不需要時釋放記憶體。如要瞭解如何有效處理圖片,請參閱「最佳化點陣圖圖片」。
監控可用記憶體和記憶體用量
您必須先找出應用程式的記憶體用量問題才能修正。Android Studio 的記憶體分析器提供以下幾種方法,可幫助您找出及診斷記憶體問題:
- 瞭解應用程式配置記憶體的變化趨勢。記憶體分析器會顯示即時圖表,呈現應用程式目前的記憶體用量、已配置的 Java 物件數量,以及進行垃圾回收的時間點。
- 在應用程式執行期間,啟動垃圾回收事件並擷取 Java 堆積的快照。
- 記錄應用程式的記憶體配置,然後檢查所有配置的物件、檢視每個配置的堆疊追蹤,並在 Android Studio 編輯器中跳到對應的程式碼位置。
記憶體分析器也與 LeakCanary 記憶體流失偵測程式庫整合。使用 LeakCanary,您可以將記憶體流失分析從測試裝置移至開發機器,大幅加快工作流程。詳情請參閱「Android Studio 版本資訊」。
您可以使用其他工具,根據執行正式版應用程式的使用者資料診斷記憶體問題:
- 使用 Android Vitals 追蹤記憶體不足終止 (LMK) 事件。
- 使用剖析管理工具追蹤記憶體不足錯誤,以及可能由記憶體流失導致的異常應用程式行為。
為回應事件釋放記憶體
Android 可收回應用程式的記憶體,在必須釋放記憶體供重要工作使用時,也可以完全終止應用程式。詳情請參閱「記憶體管理總覽」。為了進一步平衡系統記憶體,並避免系統必須停止應用程式程序,您可以在 Activity 類別內實作 ComponentCallbacks2 介面。系統提供的 onTrimMemory() 回呼方法會通知應用程式生命週期或記憶體相關事件,讓應用程式有機會主動減少記憶體用量。釋放記憶體可減少應用程式遭記憶體不足終止工具終止的次數。
onTrimMemory() 的實作方式應只著重於 TRIM_MEMORY_UI_HIDDEN 和 TRIM_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 Vitals 主要關注前台程序終止問題,因為這類問題是記憶體管理不當的高保真度指標。雖然 LMK 率高於 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 架構包含幾種經過最佳化的資料容器,包括 SparseArray、SparseBooleanArray 和 LongSparseArray。舉例來說,SparseArray 類別效率較高,因為有了這些類別,系統就不需要將鍵自動裝箱,有時也不需要將值自動裝箱。自動裝箱作業會額外建立一個物件,或為每個項目建立兩個物件。
如有必要,您隨時可以改為使用原始陣列,方便精簡資料結構。
謹慎運用程式碼抽象化
開發人員經常使用抽象化方法,因為這是良好的程式設計做法,可以提升程式碼的彈性和維護能力。不過,抽象化通常需要執行更多程式碼。如「縮減應用程式的程式碼和資源占用空間」一文所述,編譯後的程式碼集越大,應用程式所需的實體 RAM 就越多。如果抽象化無法帶來顯著效益,應避免使用這項做法。
使用精簡通訊協定緩衝區序列化資料
為了將結構化資料序列化,Google 設計出通訊協定緩衝區,這項可擴充機制適合各種語言及平台使用,雖然功能和 XML 十分類似,但是更小、更快,也更簡單。如果您使用通訊協定緩衝區處理資料,那麼用戶端程式碼也應一律使用精簡的通訊協定緩衝區。一般的通訊協定緩衝區會產生非常詳細的程式碼,導致應用程式在 RAM 中的程式碼占用空間增加 (請參閱「管理及最佳化應用程式的程式碼占用空間」),並造成 APK 大小增加。
詳情請參閱 protobuf README。
謹防記憶體流失
如果參照管理不當,可能會導致記憶體流失,物件的生命週期會超出實用期限,垃圾收集器也無法回收流失物件的記憶體。為避免記憶體流失,請實作生命週期感知設計。
詳情請參閱「記憶體洩漏」。
避免記憶體抖動
垃圾收集事件不會影響應用程式效能。不過,如果短時間內發生多次垃圾回收事件,就可能因垃圾回收器和應用程式執行緒之間必要的互動,導致電量快速消耗,並稍微增加設定影格的時間。系統花越多時間進行垃圾回收,電量就消耗越快。
「記憶體抖動」經常會導致發生大量垃圾回收事件。實際上,記憶體抖動描述的是在指定時間內配置的暫存物件數量。
舉例來說,您可以在 for 迴圈內配置多個暫存物件。也可以在檢視畫面的 onDraw() 函式內建立新的 Paint 或 Bitmap 物件。在這兩種情況下,應用程式都會快速建立大量物件。這些物件可能會在年輕代快速消耗所有可用記憶體,導致必須產生垃圾回收事件。
修復記憶體抖動問題之前,您需要先使用記憶體分析器,找出程式碼中問題較嚴重的部分。
從程式碼中找到問題區域後,請嘗試在會嚴重影響效能的區域內減少配置數量。請考慮移出內部迴圈的內容,或移到以工廠為基礎的配置結構。
您也可以評估物件集區是否對用途有幫助。如果使用物件集區,不再需要的物件例項就能釋放到集區,而不必遭到捨棄。下次需要使用該類型的物件例項時,您可以從集區中取得該例項,不必進行配置。
如要判斷特定情況是否適合使用物件集區,請對效能進行全面評估。在某些情況下,使用物件集區可能會對效能造成不良影響。雖然使用集區可避免進行配置,但是會產生其他負擔。舉例來說,維護集區通常需要進行同步處理作業,並產生不容忽視的負擔。另外,如果為了避免記憶體流失,而在釋放過程中清除集區物件例項,那麼例項在獲取過程中進行初始化時,就可能產生非零的負擔。
如果集區中保留的非必要物件例項越多,也會對垃圾回收作業造成負擔。雖然物件集區可減少垃圾收集叫用次數,但由於使用中 (可連線) 的位元組越多,工作量也越多,最終就會增加每次叫用需處理的工作量。