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