系統記憶體預算

在 Android 17 以上版本中,平台會使用兩個互補的 Linux cgroup v2 機制,與一般應用程式記憶體限制器搭配運作,控管系統程序記憶體:

  • 系統 Daemon 保護下限 (SystemMemoryBudgetManager 搭配 memory.min):自我調整 cgroup v2 memory.min 保留 (設為每個 Daemon 穩定常駐工作集的 90%,全域上限為實體 RAM 總量的 10%),可在記憶體壓力下保護重要的互動式 Daemon 和殼層程序 (system_server、surfaceflinger、圖形 composer HAL、audioserver、cameraserver、netd、SystemUI 和主畫面啟動器),取代靜態 PinnerService mlock 檔案固定。
  • 系統應用程式記憶體上限 (<memory-budget> 搭配 memory.high): 在全域記憶體壓力發生前,針對永久和背景系統應用程式 (Bluetooth、Nfc、CalendarProvider、Traceur、MediaProvider、:PhotoPicker、NetworkStack、AppSearch 和 TeleService),在資訊清單檔案中宣告的工作負載校正 cgroup v2 memory.high 軟限制 (通常為 30 MB 至 190 MB),會主動回收冷檔案頁面快取和突發後匿名堆積。

下表比較這兩種機制:

維度 系統 Daemon 保護下限 系統應用程式記憶體上限
平台服務 SystemMemoryBudgetManager MemoryLimiter
Cgroup v2 控制項 memory.min 硬性保護底價 memory.high 軟回收上限
目標程序 對延遲時間要求嚴格的互動式 Daemon 和殼層 UI (system_server、surfaceflinger、composer HAL、audioserver、cameraserver、netd、SystemUI 和 Google Home 應用程式) 永久和無頭系統應用程式、內容供應器和背景服務 (例如 Bluetooth、Nfc、MediaProvider、NetworkStack、AppSearch 和 TeleService)
規模調整模型 執行階段自我調整 (90% 的固定工作集,全球上限為總 RAM 的 10%),不需為每部裝置設定調整表 根據 RAM 級別、程序狀態和顯示像素數量校正的宣告式 <memory-budget> 資訊清單 XML 屬性
主要優點 在記憶體壓力下,防止互動式 UI 資源浪費和喚醒停滯,同時取代靜態 PinnerService mlock 檔案清單 持續清除冷一次性檔案快取和突發後堆積,為前景和快取應用程式保留更多實體 RAM

如要進一步瞭解 <memory-budget> 語法、XML 屬性、執行階段 API 和 OS 記憶體計算,請參閱「設定應用程式記憶體預算」和「瞭解 Android 記憶體及排解相關問題」。

使用 SystemMemoryBudgetManager 保護重要的互動式 Daemon

SystemMemoryBudgetManager 會在記憶體壓力期間,將直接位於使用者互動路徑上的程序保留在實體 RAM 中。SystemMemoryBudgetManager 不會使用 mlock 將 APK、ODEX 和 VDEX 檔案的靜態清單固定到記憶體 (如 PinnerService 對 config_defaultPinnerServiceFiles 和 config_pinnerHomePinBytes 的做法),而是會將動態 cgroup v2 memory.min 下限指派給下列程序:

  • 核心系統切片 (/sys/fs/cgroup/system): system_server、surfaceflinger、圖形 composer HAL、audioserver、cameraserver 和 netd,以及 config_systemMemoryBudgetAdditionalSystemProcesses 中列出的任何板型規格專用原生系統或供應商 Daemon (例如 XR 裝置上的 android.hardware.perceptionhost 和 auracamerahal)。
  • 互動式應用程式切片 (/sys/fs/cgroup/apps):已設定的 SystemUI 套件 (config_systemUi) 的主要程序,以及目前使用者主畫面角色的主要程序。

除了 config_systemUi 之外,也保護 Home 角色持有者,確保所有板型規格都受到保護。在 config_systemUi 為輕量相容性套件的外型規格上 (例如 Wear OS,錶面、通知和資訊方塊在 Google Home 應用程式中執行),面向使用者的殼層仍會受到完整工作集保護。如果 Home 意圖解析為 config_systemUi 套件,程序會受到一次保護。只有每個套件的主程序 (processName == packageName) 會獲得保護底限,因此輔助子程序 (例如 :screenshot) 不會消耗保護預算。

自我調整政策和三層 cgroup 委派

由於互動式 Daemon 的工作集會因板型規格和 RAM 容量而有很大差異 (例如,SystemUI 在智慧手錶上使用約 45 MB,在 6 GB 手機上使用 60 MB,在 16 GB 手機上使用 180 MB),因此靜態的每裝置對照表並不穩定。相反地,SystemMemoryBudgetManager 會使用三個常數,直接從每個程序的常駐記憶體用量衍生出下限:

政策參數 預設值 行為
個別程序底價 90% 的已結算memory.current,扣除未對應的檔案快取 保護匿名堆積、slab 和熱對應程式碼,同時保留最冷 10% 的尾端可回收,因此仍可剔除真正冷的頁面。系統會先扣除未對應的檔案快取 (memory.stat 中的 max(0, file - shmem - file_mapped)),再套用 90% 的比率,因此啟動套件掃描快取或一次性資產讀取不會提高下限。
全域 RAM 上限 RAM 總量的 10% (config_systemMemoryBudgetLowKb) 符合PinnerService config_pinnerMaxPinnedMemoryPercentage護欄。如果 90% 底價的總和超過 MemTotal 的 10%,則每個底價都會按相同比例縮減。
重新整理頻率 啟動後 1、4、16 和 64 秒,之後每 30 分鐘一次 幾何穩定序列會收斂至每個 Daemon 啟動後的工作集,不需硬式編碼每部裝置的暖機延遲時間,而 30 分鐘的穩定狀態重新整理作業,則會追蹤裝置生命週期內工作集的逐漸增長和縮減。

在 Linux cgroup v2 記憶體控制器 (mm/memcontrol.c:effective_protection) 中,葉 cgroup 的有效 memory.min 保護受限於其父項和祖先 cgroup 上設定的 memory.min。如果只在葉 pid_<pid> 目錄上設定 memory.min,而 uid_<uid>/memory.min 為 0,則不會產生任何有效保護。相反地,如果只在頂層 /sys/fs/cgroup/system 切片上設定固定的 memory.min,系統會將未宣告的保護機制按比例分配給所有子 UID 和程序 (memory_recursiveprot),讓共用 uid_1000 (android.uid.system) 的診斷或次要程序從 surfaceflinger 和 audioserver 轉移保護機制。

為提供精確的個別 Daemon 隔離,SystemMemoryBudgetManager 會在每次重新整理時,在 cgroup v2 階層的所有三個層級寫入保護,如圖 1 所示:

  1. 葉處理程序節點 (pid_<pid>/memory.min):設為處理程序的 (縮放) 90% 工作集下限。
  2. UID 節點 (uid_<uid>/memory.min):設為其受管理pid_*葉節點底限的確切總和,因此共用該 UID 的未受管理程序 (例如其他AID_SYSTEM daemon 或sharedUserId="android.uid.system"套件) 具有memory.min = 0,且無法要求保護。
  3. 頂層切片 (/sys/fs/cgroup/system/memory.min 和 /sys/fs/cgroup/apps/memory.min):設為其 uid_* 子項的確切總和。受管理節點上的任何過時 memory.low 值都會清除為 0,且在先前重新整理時管理的任何 uid_* 目錄 (例如使用者切換啟動器後的先前主畫面啟動器) 都會重設為 0。

/sys/fs/cgroup/system 和 /sys/fs/cgroup/apps 的三層 cgroup v2 memory.min 保護階層,顯示葉程序底限加總至 UID 和切片節點,而未受保護的同層程序仍為零

圖 1. cgroup 切片的三層保護底限委派。

為何在 MGLRU 核心上使用 memory.min 而非 memory.low

在多世代 LRU (MGLRU,Android 6.6 以上版本核心的預設頁面回收引擎) 下,memory.low 和 memory.min 在 mm/vmscan.c:shrink_one 中的行為不同:

  • memory.low (單次延遲):用量低於 memory.low 的 cgroup 會輪替至 memcg LRU (MEMCG_LRU_TAIL) 的尾端,並在單次回收中略過。在記憶體壓力持續的情況下,核心會在下次傳遞時完整回收 cgroup,並記錄 MEMCG_LOW 事件。在實體裝置壓力測試中,匿名記憶體佔用量增加至實體 RAM 的 85%,memory.low 允許智慧手錶上的 SystemUI 從約 45 MB 縮減至約 5 到 10 MB,而 16 GB 摺疊式裝置上的 audioserver 則從 15 MB 降至 2 MB,而下限約為 14 MB。
  • memory.min (硬性保護下限):全域回收會略過用量低於 memory.min 的 cgroup。如圖 2 所示,在相同的 85% RAM 壓力工作負載下,memory.min 只會捨棄每個 Daemon 最不常使用的 10% 尾端,並將 SystemUI、surfaceflinger 和 audioserver 保持在 90% 的下限,不會發生匿名重新錯誤,也不會增加 lmkd 程序終止。

memory.min 提供硬體實體保留,與其取代的 PinnerService mlock 固定保護相符,其優點是核心 LRU (而非靜態檔案清單) 會選擇哪些熱程式碼、堆積和圖形頁面保持常駐。

系統記憶體壓力從實體 RAM 的 40% 增加到 95% 時,Daemon 常駐記憶體的比較,顯示 memory.min 保持在 90% 的下限,而 memory.low 在單一 MGLRU 延遲傳遞後崩潰

圖 2. 記憶體壓力上升時,Daemon 工作組保留。

開機預熱和開機後未對應檔案快取回收

如要取代 config_defaultPinnerServiceFiles 的 PinnerService 永久 mlock,必須協調兩種開機時行為:

  • 單次背景啟動預熱 (PinnerService):略過 PinnerService mlock 而不預熱頁面快取,會在 SystemUI 啟動時 (OnBootPhase_550),將約 95 MB 的 SystemUI APK、ODEX 和 VDEX 磁碟讀取作業轉移至同步啟動路徑,導致啟動完成時間增加 300 至 450 毫秒。呼叫 madvise(MADV_WILLNEED) 是不夠的,因為核心中的 force_page_cache_ra 會將單一 MADV_WILLNEED 呼叫限制在一個預讀視窗 (通常為 128 KB 至 2 MB)。相反地,當 SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning 處於啟用狀態時,PinnerService 會在 onStart 期間,於背景執行緒上對應 config_defaultPinnerServiceFiles 中的每個檔案,使用 Os.mlock 將檔案同步填入頁面快取,並立即呼叫 Os.munlock、munmap 和 close。這樣一來,系統就能維持快速啟動時間,同時不留下任何永久鎖定的位元組,以便日後移除冷啟動頁面。
  • 排除並回收 system_server 啟動套件掃描快取:啟動期間,PackageManagerService 會掃描已安裝的 APK,並將 1.5 到 2.5 GB 的冷未對應檔案頁面快取,計入 system_server cgroup。如果計入 system_server 的 memory.min 計算,冷啟動快取會達到 10% 的全域 RAM 上限,並按比例縮減 surfaceflinger、SystemUI 和 audioserver 的保護下限。為避免上限不足,SystemMemoryBudgetManager 會讀取 memory.stat 的 system_server,將未對應的檔案快取計算為 max(0, file - shmem - file_mapped),並在計算 90% 下限前,從 memory.current 減去未對應的快取,如圖 3 所示。此外,在 PHASE_BOOT_COMPLETED 之後 10 秒,管理員會對 Linux 6.2 和更新版本核心上的 system_server 的 memory.reclaim 節點執行 <unmapped_file> swappiness=0 的一次性寫入,主動清除冷套件掃描快取,而不交換匿名堆積或清除對應的 DEX 和程式碼頁面。在拒絕 swappiness=0 和 EINVAL 的舊核心 (例如 android14-6.1) 上,管理員會略過 memory.reclaim 寫入,而不是改用不合格的回收寫入,這會在啟動期間換出匿名堆積。

開機後 system_server 記憶體的時間軸,顯示幾何穩定重新整理時間為 1、4、16 和 64 秒,從 90% memory.min 下限排除未對應的開機套件掃描快取,以及 swappiness=0 的單次 memory.reclaim

圖 3. 啟動後,保護下限會收斂,並回收快取。

核心需求和裝置設定疊加層

SystemMemoryBudgetManager 依賴 Linux 核心中的 mem_cgroup_calculate_protection,在巢狀 cgroup 之間傳播 memory.min。Android 通用核心 android15-6.6 和 android16-6.12 內建完整的 MGLRU 巢狀保護支援。搭載 android14-6.1 且啟用 MGLRU 的裝置,需要在 mm/vmscan.c 和 mm/memcontrol.c 中進行三個上游向後移植,以便空白的中間切片和 UID cgroup 將保護機制傳播至分葉程序 cgroup:

  • 30d77b7eef01 (mm/mglru: fix ineffective protection calculation)
  • 8aa420617918 (mm/mglru: respect min_ttl_ms with memcgs)
  • adb8213014b2 (mm: memcg: fix stale protection of reclaim target memcg)

原始設備製造商 (OEM) 可以使用下列架構資源疊加層,自訂或停用 SystemMemoryBudgetManager:

  • config_systemMemoryBudgetLowKb (整數,KB):0 (預設) 將所有 memory.min 下限的總和限制為實體 RAM 總量的 10%。正值會以 KB 為單位設定明確上限,負值則會停用裝置上的 SystemMemoryBudgetManager (保留舊版 PinnerService 靜態 mlock 固定)。
  • config_systemMemoryBudgetHighKb (整數,KB):0 (預設) 會將 /sys/fs/cgroup/system/memory.high 保持在 max。正值會設定頂層切片 memory.high 上限 (必須大於或等於下限)。
  • config_systemMemoryBudgetAdditionalSystemProcesses (字串陣列):列出 /sys/fs/cgroup/system 中其他對延遲時間敏感的原生系統或供應商 daemon 程序名稱 (與 /proc/<pid>/comm 或 /proc/<pid>/cmdline 中 argv[0] 的基本名稱比對,適用於 10000 下的系統和供應商 UID),以 90% memory.min 下限保護,以及預設的六個系統 daemon。請使用這個疊加層,而不是在 init.rc 中設定 memory.min,因為 SystemMemoryBudgetManager 會將 uid_<uid>/memory.min 重新計算為其代管分葉程序的總和,並在每次重新整理時清除未代管的 uid_* 節點。舉例來說,XR 裝置可以使用下列疊加層,保護空間追蹤和相機 HAL 守護程序:

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi (字串):平台 SystemUI 應用程式的套件名稱,使用 MATCH_SYSTEM_ONLY 解析。

如要檢查裝置上有效的 SystemMemoryBudgetManager 樓層和 cgroup 節點,請執行:

# Inspect policy state, resolved PIDs, and applied floors in dumpsys meminfo
adb shell dumpsys meminfo | sed -n '/System Memory Budgets/,$p'
# Inspect raw cgroup v2 memory.min nodes across the system and apps slices
adb shell head -n 1 \
  /sys/fs/cgroup/system/memory.min \
  /sys/fs/cgroup/system/uid_*/memory.min \
  /sys/fs/cgroup/system/uid_*/pid_*/memory.min \
  /sys/fs/cgroup/apps/memory.min \
  /sys/fs/cgroup/apps/uid_*/memory.min \
  /sys/fs/cgroup/apps/uid_*/pid_*/memory.min

使用記憶體預算限制永久和背景系統應用程式

雖然 SystemMemoryBudgetManager 會使用 memory.min 下限保護互動式 Daemon 免於回收,但許多其他系統應用程式和特殊權限平台服務會長時間在背景執行,例如 PERSISTENT 程序、繫結前景服務或經常查詢的內容供應器。如果沒有明確的記憶體上限,這些程序就不會受到限制,直到達到數 GB 的記憶體限制器上限 (例如可見應用程式為 4 GB 至 10 GB,背景應用程式為 2 GB 至 5 GB),或全域記憶體壓力觸發全系統 kswapd 和低記憶體終止程式 (lmkd) 活動。

如果不加以限制,背景系統程序通常會累積大量冷記憶體,這些記憶體不屬於其活動工作集:

  • 無限制檔案頁面快取:為媒體建立索引、搜尋資料庫或同步處理日曆和供應商資料表的服務 (例如 AppSearch、MediaProvider 和 CalendarProvider),會透過磁碟上的大型檔案進行對應或串流。如果沒有 cgroup memory.high 上限,一次性讀取作業產生的乾淨檔案備份頁面會無限期地向程序 cgroup 收費,消耗數百 MB 的實體 RAM。
  • 突發後匿名堆積保留:如果服務會出現短暫的活動高峰,例如 Bluetooth 音訊和 BLE 探索突發、NetworkStack 網頁認證入口 TLS 驗證,或 UI 輔助程式轉換至背景,則可能會在突發完成後,長時間保留冷匿名堆積頁面。

將校正後的 <memory-budget> 宣告 (memory.high 軟限制,通常為 30 MB 至 190 MB) 套用至系統應用程式,可帶來兩大好處:

  • 主動、局部頁面回收:當系統程序超過設定的 memory.high 預算時,Linux 核心會直接針對該 cgroup 進行頁面回收,優先移除冷檔案支援頁面,並將不活躍的匿名頁面壓縮至 ZRAM,而不會等待全域裝置記憶體壓力。
  • 前景應用程式和快取應用程式集區有更多可用 RAM:在全天候運作系統服務中繫結冷檔案快取,可回收裝置上數十到數百 MB 的實體 RAM,減少 UI 卡頓、降低相機啟動壓力,並讓更多使用者應用程式保持在快取狀態。

設定系統應用程式記憶體預算

系統應用程式與一般應用程式使用相同的 <memory-budget> 資訊清單語法、程序狀態串聯、螢幕密度縮放和最後比對子句解析規則。如需完整的屬性清單,請參閱 XML 屬性參考資料。

原始設備製造商 (OEM) 可以直接在系統應用程式的 AndroidManifest.xml 中宣告 <memory-budget> 元素,或使用 Android.bp 中的 additional_manifests 屬性,從獨立 XML 檔案合併這些元素。按照慣例,AOSP 會將參考系統應用程式預算儲存在獨立的 *-budget.xml 檔案中 (位於核心平台套件的 frameworks/base/core/memory_budgets/ 中,或與個別模組 (例如 packages/modules/NetworkStack/memory_budgets/、packages/modules/AppSearch/apk/ 和 packages/services/Telephony/) 一起儲存),並使用 additional_manifests 附加這些檔案:

android_app {
    name: "TeleService",
    manifest: "AndroidManifest.xml",
    additional_manifests: ["Telephony-budget.xml"],
    // ...
}

套件層級 (UID) 與程序層級 (PID) 範圍

您宣告的是套件層級基準預算 (<application>) 還是多程序預算 (<processes><process>),會決定核心是在 UID cgroup 層級 (/sys/fs/cgroup/uid_<uid>/memory.high) 還是個別 PID cgroup 層級 (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high) 強制執行 memory.high,如圖 4 所示:

  • 套件層級 (<application>) 預算:如果 APK 在專屬 UID 下執行單一程序,請使用此預算 (例如 Bluetooth、Nfc、NetworkStack、CalendarProvider、Traceur 或 AppSearch)。
  • 程序層級 (<processes><process>) 預算:如果 APK 與其他系統套件共用 android:sharedUserId (例如 UID 為 1001 的 android.uid.phone 或 UID 為 1000 的 android.uid.system),請使用此預算,這樣共用該 UID 的其他程序就不會一起受到限制;或者,如果單一 APK 執行多個具有不同記憶體角色的程序 (例如背景 MediaProvider 守護程序和 :PhotoPicker UI 子程序),也請使用此預算。

Cgroup v2 階層比較專用 UID 的 UID 層級 memory.high 強制執行,以及共用系統 UID 和多程序 APK 的每個 PID memory.high 強制執行

圖 4. UID 層級與 PID 層級的記憶體上限。

以下範例顯示 MediaProvider-budget.xml,將 RAM 分級預算範圍限定為預設 MediaProvider 程序 (<process>),並將顯示比例預算限定為 :PhotoPicker UI 程序。每個 android:maxDeviceRamMb 屬性都會比對實體 RAM 總量 (MemTotal) 等於或低於指定 MB 門檻的裝置 (如省略,則適用於任何 RAM 容量)。子句的排序方式是從不受限制的預設值,到最小的 maxDeviceRamMb 門檻,因此最後比對子句解析規則會選取最嚴格的適用層級:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.android.providers.media.module">
    <uses-sdk android:targetSdkVersion="10000" />
    <application>
        <processes>
            <process>
                <memory-budget
                    android:maxMb="85" />
                <memory-budget
                    android:maxMb="75"
                    android:maxDeviceRamMb="12288" />
                <memory-budget
                    android:maxMb="60"
                    android:maxDeviceRamMb="8192" />
            </process>
            <process android:process=":PhotoPicker">
                <memory-budget
                    android:maxMb="85"
                    android:additionalBytesPerDisplayPixel="25" />
            </process>
        </processes>
    </application>
</manifest>

如何在 Google Pixel 裝置上校正 AOSP 參考預算

AOSP 中的參考系統應用程式記憶體預算,是根據 Google Pixel 裝置校準,涵蓋 6 GB 至 16 GB 的實體 RAM (包括 6 GB、8 GB、12 GB 和 16 GB 的手機、折疊式裝置和平板電腦),以及約 1.2 MP (720×1600) 至約 4.5 MP (2076×2152) 的螢幕解析度。

校正作業結合了兩個互補的資料來源:整個機群的 cgroup v2 欄位遙測資料,以及在關鍵使用者歷程 (CUJ) 中受控的實體裝置 memory.high 掃描。

全機群 cgroup v2 遙測分析

系統會定期收集整個裝置群的 cgroup v2 快照,並依實體 RAM 級別 (≤8 GB、12 GB 和 ≥16 GB) 和程序狀態值區 (foreground、perceptible、background 和 cached) 進行切片。統計分析遵循六項原則:

  • 將 memory.current 分解為工作集和可回收檔案快取:由於 memory.high 控管常駐 cgroup 費用 (memory.current),因此每個快照都會使用 memory.stat 分解為匿名堆積 (anon)、交換匿名頁面 (swap) 和檔案支援頁面快取 (file),如圖 5 所示。不可壓縮的匿名需求總量 (P99 anon + swap) 會建立預算的最小工作集下限,因此程序不會有 ZRAM 耗盡的風險,而 memory.current P99 和 anon + swap P99 之間的巨大差距表示 memory.high 可回收的檔案頁面快取,前提是快取未對應且冷。
  • 考量 memory.high 無法回收的記憶體:並非所有檔案和共用記憶體都是冷快取。鎖定的頁面 (memory.stat 中的 unevictable,例如使用 mlock 鎖定的共用記憶體,或虛擬機器鎖定的客體 RAM,其主機程序會向應用程式的 cgroup 收費) 會在預算下設定硬性下限,而對應頁面 (file_mapped) 通常是有效工作集的一部分。請先檢查 unevictable、shmem 和 file_mapped,再將檔案記憶體視為可回收。舉例來說,約 100 MB 的 AppSearch 記憶體會鎖定且無法移除,因此其預算會涵蓋該下限加上其熱主機工作集,並為鎖定區域保留成長空間。
  • 篩除閒置狀態稀釋:內容供應器 (CalendarProvider) 和隨選開發人員工具 (Traceur) 在 70% 到 99.9% 的現場快照中,閒置時的記憶體用量約為 1 MB 到 3 MB。如果計算所有快照的原始百分位數,會嚴重稀釋 P95 和 P99 預估值。而是根據未稀釋的活躍快照 (篩除 CACHED 快照和閒置基準快照),計算百分位數和信賴區間。
  • 使用裝置分群信賴區間:由於單一裝置會隨著時間提供多個定期快照,因此將快照視為獨立樣本會低估尾端不確定性。所有 P95 和 P99 工作集預估值,都是使用保守的裝置分群 95% 信賴區間 (以不重複裝置數量 N_dev 做為有效樣本大小) 評估。
  • 使用 memory.peak 處理暫時性爆量:定期取樣可能會遺漏短暫、高分配的爆量 (例如 NetworkStack 網頁認證入口 TLS 驗證和並行 DNS 探測,或 Bluetooth A2DP 和 BLE 初始化)。對於突發性服務,預算大小應涵蓋 RAM 級別中核心 memory.peak 存留期高水位標記的第 90 到 95 個百分位數。
  • 確認 RAM 級別反映的是 RAM,而非裝置世代:較新的裝置往往有更多 RAM 和功能,因此隨著 RAM 級別增加的工作集,實際上可能會隨著功能增加。新增 maxDeviceRamMb 級別前,請先依裝置型號劃分資料。舉例來說,在同世代的 12 GB 和 16 GB 手機上,com.android.phone 達到相同的 P99 anon + swap (約 85 至 90 MB),而舊款 12 GB 摺疊式手機則使用 40 MB。因此,其預算使用單一值,沒有 RAM 級別,因此搭載較新電話功能的低 RAM 裝置不會預算不足。

cgroup 記憶體隨時間分解為不可壓縮的匿名工作集、有效檔案快取視窗,以及超過 memory.high 時回收的冷一次性檔案快取

圖 5. 工作集與可回收檔案快取。

實體裝置 CUJ 和顯示比例掃描

為了驗證遙測資料得出的預算,並校正與螢幕成比例的預算 (例如 :PhotoPicker) 和特定狀態的背景預算 (例如 Traceur),我們在 RAM 介於 6 GB 到 16 GB 的實體裝置上,執行了即時 memory.high 清除作業:

  • 找出零重新錯誤膝點:在執行端對端 CUJ (例如快速相片格線滑動、相簿瀏覽、全螢幕高解析度相片和影片預覽滑動,以及追蹤記錄和儲存) 時,memory.high 會向下步進,同時測量 cgroup memory.stat 計數器 (workingset_refault_anon、workingset_refault_file、pgscan 和 pgsteal) 和 memory.events (high)。如圖 6 所示,最佳預算剛好落在零重新錯誤膝點上方。零重新錯誤膝點是核心回收冷一次性檔案快取 (pgsteal > 0) 的門檻,不會觸發主動工作集抖動 (workingset_refault == 0)。
  • 驗證不同解析度的顯示像素縮放:在四種實體和模擬顯示解析度:PhotoPicker (從約 1.2 MP (720×1600) 到約 4.5 MP (2076×2152 展開內螢幕)) 進行測試,確認其作用中的 Glide 和 ViewPager2 點陣圖工作集會隨著顯示像素數量線性縮放。非顯示基準也會因裝置而異:在像素數量幾乎相同的情況下,16 GB 折疊式裝置外螢幕的零重新錯誤拐點約為 140 MB,而 6 GB 手機則為 110 至 125 MB。85 MB 基準加上每個顯示像素 25 位元組 (android:maxMb="85" 和 android:additionalBytesPerDisplayPixel="25"),在每個測試顯示器上都高於測得的膝點 (HD+ 約 110 MB,FHD+ 約 145 MB,展開顯示器約 190 MB)。
  • 驗證背景狀態下的 ART 垃圾收集:在 cgroup v2 中,Zygote 共用的 boot.art 和架構映像檔頁面仍會計入 Zygote 的 cgroup,並在 ART 並行標記壓縮 (CMC) 垃圾收集期間視為免疫空間。壓力測試證實,當系統 UI 應用程式轉換至 background 時,收緊 memory.high (例如在 Traceur 上設為 30 MB) 可明確限制私人堆積和頁面快取,而不會造成 GC 顛簸或交換活動。

CUJ 期間 memory.high 逐步下降時,冷快取回收 (pgsteal) 和工作集抖動 (workingset_refault) 的圖表,醒目顯示零重新錯誤膝點和目標預算

圖 6. 在 CUJ 期間找出零重新錯誤膝點。

AOSP 參考系統應用程式預算摘要

下表匯總 AOSP 中各 RAM 級別和程序狀態的校正參考預算:

系統套件或程序 範圍 ≤8 GB RAM 12 GB RAM ≥16 GB RAM 主要校正驅動程式
com.android.bluetooth UID (<application>) 80 MB 95 MB 110 MB 涵蓋 P90 存留期 memory.peak 爆發 (約 75 至 110 MB) 和 >2x P99 活躍 memory.current。
com.android.nfc UID (<application>) 50 MB 60 MB 60 MB 涵蓋有效主機卡片模擬 P99 (最多約 50 MB) 和 P90 至 P95 memory.peak,同時修剪冷檔案快取。
com.android.providers.calendar UID (<application>) 預設 70 MB (前景)
50 MB perceptible 和 background
有效工作集在各個 RAM 等級中皆不變。背景預算會保留匿名餘裕,用於提醒鬧鐘,同時在同步處理期間修剪 SQLite 頁面快取。
com.android.traceur UID (<application>) 預設 85 MB (前景和可感知)
30 MBbackground
在主動追蹤記錄和匯出期間 (TraceService),保留完整的 85 MB 預留空間,同時將閒置和廣播狀態限制在 30 MB。
com.android.providers.media.module (主要) PID (<process>) 60 MB 75 MB 85 MB 提供約 10 到 30 MB 的 P99 anon + swap 預留空間,同時回收冷 SQLite 和縮圖檔案快取。
com.android.providers.media.module:PhotoPicker PID (<process>) 85 MB + 25 位元組/像素
(HD+ 約 110 MB,FHD+ 約 145 MB,~4.5 MP 摺疊式裝置約 190 MB)
與螢幕像素數量 (additionalBytesPerDisplayPixel="25") 成比例縮放,以符合 Glide 點陣圖工作集膝點。
com.android.networkstack UID (<application>) 65 MB 70 MB 85 MB 主要為匿名記憶體,大小為 P95 存留時間 memory.peak,可涵蓋網頁認證入口 TLS、DNS 和 DHCP 突發流量。
com.android.appsearch.apk UID (<application>) 130 MB 涵蓋約 100 MB 的鎖定、不可移除記憶體,以及約 5 MB 的熱主機工作集,並預留鎖定區域成長空間。在冷頁快取 (P99 時最多約 330 MB) 回收時,測得的零重新錯誤值 ≥125 MB。
com.android.phone (TeleService) PID (<process>) 120 MB 共用 UID 1001 的每個 PID 範圍。工作集會追蹤裝置世代,而非 RAM,因此單一預算會比每個模型最高的 P99 anon + swap 高出約 30 MB。

原始設備製造商 (OEM) 如何重新校準自家裝置的系統應用程式預算

如果原始設備製造商 (OEM) 自訂系統應用程式 (例如新增供應商藍牙設定檔、自訂電話通訊擴充功能或 OEM 媒體索引器),或提供額外的預先安裝系統服務,則可重新校準現有預算,或為其硬體產品組合編寫新的 *-budget.xml 檔案。

收集使用中的 cgroup v2 記憶體分析器

針對每個目標系統程序,在代表性工作負載和 RAM 級別中,讀取 /sys/fs/cgroup/uid_<uid>/pid_<pid>/ 下的 cgroup v2 記憶體節點:

  • memory.current:目前向 cgroup 收取的常駐記憶體 (不含 Zygote 共用頁面)
  • memory.peak:自程序啟動以來,生命週期內 memory.current 的最高用量
  • memory.swap.current:ZRAM 目前的壓縮交換空間用量
  • memory.stat:anon、file、pgscan、pgsteal、workingset_refault_anon 和 workingset_refault_file 的細目
  • memory.events:high 違規事件 (memory.high 節流和回收事件) 的累計次數

從 dogfood 或現場裝置匯總遙測資料時,請將作用中程序狀態 (TOP、FOREGROUND_SERVICE、作用中繫結和作用中同步) 與閒置 CACHED 或閒置 PERSISTENT 狀態分開,以免閒置快照導致 P95 和 P99 工作集計算結果偏低。

將不可壓縮的堆積與可回收的檔案快取分開

比較 anon + swap 的第 99 個百分位數與 memory.current 的第 99 個百分位數:

  • 檔案快取主導的服務 (file &gg; anon,例如資料庫供應商、搜尋索引器和記錄常駐程式):設定 android:maxMb,提供比 P99 anon + swap 更充裕的空間 (通常為 1.5 到 4 倍),以及熱門檔案頁面的工作視窗。這項設定可讓核心持續剔除冷未對應的檔案頁面,而不觸及匿名堆積。如果 unevictable 或 file_mapped 很大 (例如鎖定記憶體或記憶體對應資料庫),請將該足跡也計入工作集。
  • 匿名堆積主導或突發服務 (anon 占 memory.current 的大部分,例如網路或連線能力堆疊):調整 android:maxMb 的大小,涵蓋 memory.peak 的 P90 至 P95,這樣暫時性突發就不會在延遲敏感執行緒上觸發同步 ZRAM 交換。
  • 顯示密集型 UI 程序 (例如挑選器、圖片庫或系統 UI 輔助程式):測量最低和最高解析度螢幕的基準非 UI 占用空間和每百萬像素的增量點陣圖占用空間,並使用 android:maxMb 加上 android:additionalBytesPerDisplayPixel 表示預算。

在實體裝置上清除 memory.high

在建構作業中採用新的預算前,請使用 am memory-limiter manual (或在 userdebug 建構作業中直接寫入程序的 memory.high cgroup 節點),在執行自動化 CUJ 測試時測試候選限制:

# Override memory.high for a specific PID to 60 MB during a CUJ test
adb shell am memory-limiter manual <PID> 60MB

在 CUJ 前後監控 memory.stat 和 memory.events:

  • 預算過低: workingset_refault_anon 或 workingset_refault_file 在 CUJ 期間急遽上升、memory.swap.current 在前景工作期間持續增加,或 CUJ 影格或繫結器延遲時間倒退。將預算提高至超過預設轉折點。
  • 預算過高:即使在大量檔案串流或索引工作負載之後,memory.events:high 仍維持 0,且 memory.stat 中的 file 會隨著冷頁面無限成長。縮減預算,將資源集中在有效工作集。
  • 預算經過妥善校正:正常運作期間,穩態 memory.current 會維持在 memory.high 以下。在檔案或快取大量爆發期間,pgsteal 會回收冷檔案頁面,而 workingset_refault_anon 則維持在接近零,且 CUJ 延遲時間不變。