許多應用程式在 UI 不再顯示時,仍會執行合法工作,例如下載大型資產包、同步處理本機資料庫、串流音訊、記錄運動訓練,或維護連結裝置工作階段。如果這項背景工作與前景 UI 和圖形引擎在同一個 OS 程序中執行,整個程序會與 UI 分配的所有記憶體一起保持在較高的重要性層級 (oom_score_adj 介於 0 和 250 之間)。
本頁面說明這類做法為何會對應用程式和裝置其他部分造成負面影響、如何使用 android:process 將背景工作移至專屬的輕量程序,以及如何使用 adb shell 和 Perfetto 驗證記憶體節省量。
為什麼單一程序工作會保留 UI 記憶體
Android 管理記憶體和終止程序時,是以 OS 程序 (pid) 為單位,而非個別元件 (Activity、Service 或 BroadcastReceiver)。Activity Manager (OomAdjuster) 會為每個程序指派單一 oom_score_adj 值,反映該程序中最重要的活動元件或傳入繫結:
- 當使用者與
Activity互動時,程序會在TOP狀態 (oom_score_adj = 0) 下執行。 - 當使用者離開,且應用程式沒有任何執行中的背景元件時,程序會移至
PREV(oom_score_adj = 700) 或CACHED(oom_score_adj >= 900) 值區。在快取值區中,平台會使用 cgroup v2 凍結器凍結程序,使用CachedAppOptimizer壓縮記憶體,並讓lmkd在記憶體壓力下優先終止程序。 - 不過,如果同一個程序也執行前景服務 (例如
oom_score_adj = 200的dataSync、mediaPlayback、connectedDevice或location類型),或託管系統或其他重要用戶端繫結的服務,則即使Activity停止,整個程序仍會維持較高的優先順序。
當繁重的 UI 程序停留在 oom_score_adj = 200 時,記憶體中會保留幾個大型類別:
- 已停止
Activity和檢視區塊階層:除非Activity明確完成,否則當使用者在其他應用程式中時,其Window、檢視區塊樹狀結構、版面配置結構和檢視區塊狀態仍可在 Java 堆積上存取。 - 原生圖形內容和 GPU 分配:標準
HardwareRenderer會保留其 GPU 內容和快取,而停止的Activity則會存在於未快取的程序中。建立自己的 Vulkan (VkDevice、VkSwapchainKHR、描述元集區、管道快取) 或 OpenGL ES (EGLContext、紋理、頂點緩衝區) 內容的應用程式和遊戲引擎,也會保留其驅動程式結構、著色器程式和圖形緩衝區,除非引擎在停止時明確拆除圖形裝置。 - 堆積片段和執行階段負擔:即使應用程式在
onTrimMemory中釋放快取,部分填入的原生配置器頁面、髒 Zygote 寫入時複製頁面、載入的共用程式庫 (.so) 和執行階段中繼資料仍會保留在程序的私人記憶體中。由於處理程序高於快取門檻 (oom_score_adj < 900),平台不會凍結處理程序,也不會對其執行快取應用程式壓縮。如要進一步瞭解繫結服務的這項效果,請參閱「將繫結服務與記憶體密集型 UI 放在同一位置」。
精簡的背景程序如何保護應用程式
在背景狀態下保留前景大小的記憶體,會使背景工作面臨風險:
- 背景記憶體限制比前景限制更嚴格。在 Android 17 以上版本中,如果應用程式沒有任何程序顯示可見 UI (包括前景服務執行時),記憶體限制器會對應用程式套用較小的不可見記憶體限制。這項限制涵蓋應用程式的所有程序。如果執行前景服務的程序仍保留 UI 和圖形記憶體,該記憶體會計入較小的限制。如果應用程式超出限制,核心會限制應用程式,同時回收其頁面,導致下載、同步或播放速度變慢。如果應用程式在交換空間用盡後仍繼續分配,系統就會終止該應用程式。如要確認應用程式是否受到影響,請參閱「應用程式記憶體限制」。
- 記憶體不足時,系統會先終止較大的程序。當
lmkd必須釋出記憶體,且設定為選擇重要性值區 (ro.lmk.kill_heaviest_task) 中最繁重的程序時,系統會先終止同樣保留數百 MB UI 記憶體的前景服務程序,再終止重要性相同但較小的程序,而背景工作也會隨之停止。請參閱「kswapd 和 lmkd 互動」。 - 快取的 UI 狀態可以凍結、壓縮,並寫回快閃記憶體。允許前景 UI 狀態移至快取程序,可讓 OS 凍結程序、將記憶體壓縮至 zRAM,並將冷頁面寫回快閃儲存空間。其常駐記憶體用量會縮減,而 UI 狀態則保持不變,讓背景工作和快取的 UI 都有最大機會留存到使用者返回。
- 每個程序的記憶體預算變得實用。只執行背景工作的程序具有可預測的小型工作集,因此您可以為其提供嚴格的記憶體預算,而不會限制 UI。
執行第二個程序可能會增加應用程式的總記憶體用量,因為第二個執行階段的基準成本較低。只有在單一程序轉換至背景工作時,能完全釋出所有 UI 和圖形狀態,這項比較才成立,但應用程式和 UI 工具包在實務上無法可靠地做到這一點。讓 OS 凍結、壓縮及寫回獨立的快取 UI 程序,通常會使用較少的實體記憶體,讓應用程式保持在不可見的限制範圍內,並減少應用程式和整個裝置的記憶體不足終止情形。
在專屬程序中隔離背景工作
如要在背景工作期間消除 UI 和圖形常駐,請在專屬的輕量程序中執行長時間執行的背景元件,同時將 UI 和圖形引擎保留在主程序中:
| 程序 | 資訊清單宣告 | 託管元件 | 使用者在背景工作期間離開 UI 時的狀態 |
|---|---|---|---|
| 主要 UI 程序 | 預設值 (com.example.app) |
Activity、UI 檢視畫面、圖片快取、Vulkan 或 OpenGL ES 轉譯器、音訊和場景引擎 |
降至 PREV,然後降至 CACHED (oom_score_adj >= 900)。凍結、壓縮並寫回快閃記憶體。 |
| 背景工作站程序 | android:process=":bg" |
前景 Service、背景資產下載工具、同步處理工作站或連結裝置管理工具 |
在 FGS (oom_score_adj = 200) 執行,只有 worker 自己的堆積和程式碼,沒有 UI 檢視畫面或 GPU 情境。 |
宣告程序和記憶體預算
在 AndroidManifest.xml 中設定 android:process=":bg",將背景 Service 指派給私人子程序。開頭的半形冒號 (:) 會建立一個私人程序,範圍限定為應用程式的 UID。
在 Android 17 QPR2 (API 級別 37.2) 以上版本中,請在 <processes> 區塊內,將程序分割與個別程序的應用程式記憶體預算結合,讓作業系統對背景工作站強制執行嚴格的工作集上限,而不限制前景 UI 程序:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".HeavyApplication"
android:label="@string/app_name">
<!-- App-wide budgets that cover every process, including :bg -->
<memory-budget android:maxMb="512" />
<memory-budget
android:maxMb="128"
android:state="background" />
<processes>
<!-- Tight budget for the headless background worker process -->
<process android:process=":bg">
<memory-budget android:maxMb="32" />
</process>
</processes>
<activity
android:name=".MainActivity"
android:exported="true" />
<service
android:name=".AssetDownloadService"
android:process=":bg"
android:foregroundServiceType="dataSync"
android:exported="false" />
</application>
</manifest>
:bg 中的記憶體用量會同時計入自身預算和應用程式層級預算。在搭載 Android 17 QPR2 以下版本的裝置上,系統會忽略 <memory-budget>,而 android:process=":bg" 則適用於所有 API 級別。
在 Application.onCreate 中初始化閘道
Android 會例項化 Application 類別,並在為套件產生的「每個」程序中呼叫 Application.onCreate。採用 android:process=":bg" 時,常見的錯誤是將繁重的 UI 工具包、圖片載入集區、Analytics SDK 或原生引擎初始化 (System.loadLibrary) 留在未受保護的 Application.onCreate 方法中。這麼做會使 :bg 程序膨脹,產生您想避免的相同基準足跡。
在 onCreate 中檢查 Application.getProcessName (適用於 Android 9 (API 級別 28) 以上版本),並僅初始化目前程序所需的依附元件:
Kotlin
class HeavyApplication : Application() {
override fun onCreate() {
super.onCreate()
val processName = Application.getProcessName()
if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
initBackgroundWorkerProcess()
return
}
initMainUiProcess()
}
private fun initBackgroundWorkerProcess() {
// Initialize only lightweight HTTP, storage, and sync primitives.
// Avoid loading native rendering libraries or allocating UI caches.
}
private fun initMainUiProcess() {
// Initialize UI frameworks, image caches, and native engines.
System.loadLibrary("heavy_engine_jni")
}
companion object {
private const val BG_PROCESS_SUFFIX = ":bg"
}
}
Java
public final class HeavyApplication extends Application {
private static final String BG_PROCESS_SUFFIX = ":bg";
@Override
public void onCreate() {
super.onCreate();
String processName = Application.getProcessName();
if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
initBackgroundWorkerProcess();
return;
}
initMainUiProcess();
}
private void initBackgroundWorkerProcess() {
// Initialize only lightweight HTTP, storage, and sync primitives.
// Avoid loading native rendering libraries or allocating UI caches.
}
private void initMainUiProcess() {
// Initialize UI frameworks, image caches, and native engines.
System.loadLibrary("heavy_engine_jni");
}
}
透過內容供應器初始化的程式庫 (例如 Jetpack App Startup),只會在主要程序中初始化,因為未宣告 android:process 的內容供應器會在主要程序中執行。如果 :bg 中的程式碼依附於這類程式庫,請在 initBackgroundWorkerProcess 中明確初始化。
如果您使用 Unity 等遊戲引擎建構,請在以 Java 或 Kotlin 編寫、不參照 UnityPlayer 等引擎類別的一般 Android 程式庫或外掛程式中實作背景服務。:bg 程序不會載入引擎原生資料庫。如要確認,請檢查 :bg 程序的 adb shell cat /proc/<pid>/maps 是否未列出 libunity.so 或 libil2cpp.so。
不固定 UI 程序,直接進行通訊
當使用者開啟 MainActivity,而 AssetDownloadService 在 :bg 中執行時,UI 通常需要顯示即時下載進度。程序重要性會從用戶端流向託管服務或內容供應器的程序,因此每個依附元件的方向都很重要:
- 與 worker 的 UI 繫結是安全的。當
MainActivity繫結至AssetDownloadService時,系統會將:bg提升至可見 UI 的重要性。使用者離開後,UI 程序會快取,繫結也不會再提升任何內容。 - 依賴 UI 程序的工作站並不安全。如果
:bg中的程式碼繫結至服務,或查詢在主程序中執行的內容供應器,系統會將主程序提升至工作站的提升重要性,且 UI 記憶體會再次固定。請改為讓 UI 向 worker 註冊回呼,例如Messenger或 AIDL 回呼介面。將繫結器物件做為回呼傳遞,不會改變任一程序的重要性。
請先取消註冊回呼,再取消繫結,這樣系統凍結 worker 後,worker 就不會呼叫 UI 程序。以下範例只會在 UI 顯示時繫結至 worker:
Kotlin
class MainActivity : Activity() {
private var downloadService: IBinder? = null
private val downloadConnection = object : ServiceConnection {
override fun onServiceConnected(
name: ComponentName,
service: IBinder
) {
downloadService = service
// Register a Messenger or AIDL callback for progress updates.
}
override fun onServiceDisconnected(name: ComponentName) {
// The binding stays active and reconnects if the worker restarts.
downloadService = null
}
}
override fun onStart() {
super.onStart()
val intent = Intent(this, AssetDownloadService::class.java)
bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE)
}
override fun onStop() {
super.onStop()
if (downloadService != null) {
// Unregister the progress callback here, before unbinding.
downloadService = null
}
// Unbind the ServiceConnection even if onServiceConnected hasn't run
// yet.
unbindService(downloadConnection)
}
}
Java
public final class MainActivity extends Activity {
private IBinder downloadService;
private final ServiceConnection downloadConnection =
new ServiceConnection() {
@Override
public void onServiceConnected(
ComponentName name, IBinder service) {
downloadService = service;
// Register a Messenger or AIDL callback for progress
// updates.
}
@Override
public void onServiceDisconnected(ComponentName name) {
// The binding stays active and reconnects if the worker
// restarts.
downloadService = null;
}
};
@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, AssetDownloadService.class);
bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
if (downloadService != null) {
// Unregister the progress callback here, before unbinding.
downloadService = null;
}
// Unbind the ServiceConnection even if onServiceConnected hasn't run
// yet.
unbindService(downloadConnection);
}
}
如要進一步瞭解繫結旗標如何影響跨程序的 oom_score_adj,請參閱「服務繫結和程序狀態」。
安全地協調共用的磁碟狀態
由於主要程序和 :bg 在不同的位址空間中執行,且具有不同的執行階段和檔案描述元:
- SQLite 和 Room:使用預寫記錄 (大多數裝置上 Room 的預設日誌模式),讓兩個程序可以同時讀取及寫入資料庫。請勿從
:bg呼叫enableMultiInstanceInvalidation。Room 會在主要程序中託管其撤銷服務,因此 worker 會繫結至主要程序,並將 UI 記憶體保留在 worker 的重要性提升。而是讓 worker 透過 UI 註冊的回呼回報變更,並讓 UI 在啟動時重新查詢資料庫。 - 偏好設定:請勿跨程序共用
SharedPreferences。請在多程序程式碼中使用 DataStore,或透過 IPC 明確傳遞狀態。 - 資產檔案:將下載的資產包寫入
:bg中的暫存檔案,在寫入後呼叫:bg中的fdatasync,然後呼叫posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)(如「將記憶體頁面傳回 OS」所示),並以不可分割的方式將檔案重新命名。讓 UI 在下次啟動並繫結至 worker 時擷取結果,而不是呼叫可能凍結的 UI 程序。
在 ADB 殼層中觀察效果
您可以前往主畫面,然後檢查 Activity Manager 程序狀態和 dumpsys meminfo,確認分割前景和背景程序是否能隔離升高的記憶體用量。本節的範例輸出內容來自 MemoryLab 範例應用程式,該應用程式在兩種設定的 UI 程序中執行相同的 64 MB 原生分配,並在 :remote 程序中託管分割的前景服務。
1. 比較程序狀態和 OOM 分數
啟動背景工作負載,按下主畫面按鈕,讓 Activity 停止,然後查詢 Activity Manager:
adb shell dumpsys activity processes com.example.app | grep -E "Proc #|oom:|state:"
在單一程序架構中,即使 Activity 停止,主要程序仍會維持提升狀態。使用者離開後約 15 秒,Activity Manager 會保留在 oom_score_adj = 50 (fg-service-act) 執行前景服務的程序,然後將其降至 200 (fg-service)。按下主畫面按鈕 20 秒後,系統會擷取下列輸出內容:
Proc #1: prcp F/S/FGS ---NFUATI t: 0 13794:com.android.memorylab/u0a284 (fg-service)
oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
state: cur=FGS set=FGS lastRss=0.00 lastCachedRss=0.00
在分割程序架構中,只有 worker 程序會停留在 oom_score_adj = 200,而主要 UI 程序會降至 PREV (oom_score_adj = 700),之後再降至快取值區 (oom_score_adj >= 900):
Proc #3: prcp F/S/FGS ---NFUATI t: 0 13544:com.android.memorylab:remote/u0a284 (fg-service)
oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
state: cur=FGS set=FGS lastRss=0.00 lastCachedRss=0.00
Proc #1: prev b/ /LAST -------TI t: 0 13312:com.android.memorylab/u0a284 (previous)
oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
state: cur=LAST set=LAST lastRss=0.00 lastCachedRss=0.00
前一個應用程式逾時後,主要程序會移至快取值區 (cch、oom_score_adj = 900、previous-expired),並失去所有程序功能 (---------),包括隱含 CPU 作業時間 (I)。此時,系統會凍結該程序,並可隨時壓縮或回收,而不會中斷工作站程序。
2. 比較不同程序的 dumpsys meminfo
比較權限提升工作站的 dumpsys meminfo 摘要與 UI 程序:
adb shell dumpsys meminfo com.example.app:bg
adb shell dumpsys meminfo com.example.app
oom_score_adj = 200 處 MemoryLab worker 程序 App Summary 輸出內容範例:
App Summary
Pss(KB) Rss(KB)
------ ------
Java Heap: 380 13840
Native Heap: 1548 2832
Code: 132 55816
Stack: 200 208
Graphics: 0 0
Private Other: 496
System: 1926
Unknown: 4168
TOTAL PSS: 4682 TOTAL RSS: 76864 TOTAL SWAP PSS: 441
oom_score_adj = 700 上的 MemoryLab UI 程序範例 App Summary 輸出,其中保留 Private Other 中的 64 MB 原生分配:
App Summary
Pss(KB) Rss(KB)
------ ------
Java Heap: 1316 19816
Native Heap: 30408 31596
Code: 3148 115796
Stack: 656 664
Graphics: 0 0
Private Other: 67044
System: 13687
Unknown: 71092
TOTAL PSS: 116259 TOTAL RSS: 238964 TOTAL SWAP PSS: 259
工作站程序需要小於 5 MB 的 PSS,因為它與 Zygote 共用大部分的程式碼和執行階段頁面。在以 Vulkan 或 OpenGL ES 算繪的應用程式中,UI 程序的 Graphics 列也會包含 GPU 記憶體。這個範例是在 Cuttlefish 虛擬裝置上擷取,並使用軟體算繪,因此兩個程序中的 Graphics 都是 0。
3. 檢查個別程序 cgroup 帳戶
在 UID 下,每個程序都有自己的 cgroup v2 目錄,位於應用程式的父項 cgroup 內。下列指令會列出應用程式每個程序的 oom_score_adj、memory.current 和應用程式層級 memory.high:
adb shell '
for PID in $(pidof com.example.app com.example.app:bg); do
CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
echo "pid=$PID ($(tr -d "\0" < /proc/$PID/cmdline))" \
"adj=$(cat /proc/$PID/oom_score_adj)" \
"memory.current=$(cat $CG/memory.current)" \
"app.memory.high=$(cat ${CG%/*}/memory.high)"
done
'
按下「主畫面」後,MemoryLab 的輸出內容範例:
pid=15301 (com.android.memorylab) adj=700 memory.current=103260160 app.memory.high=966787072
pid=16518 (com.android.memorylab:remote) adj=200 memory.current=3608576 app.memory.high=966787072
工作站程序收費不到 4 MB,而 UI 程序收費約 98 MB。每個程序都有自己的 memory.current,但記憶體限制器會對包含應用程式所有程序的父項 cgroup 設定限制。兩行都顯示相同的 app.memory.high (966,787,072 位元組),也就是下列輸出內容中未顯示的 922 MB 限制。
如要查看記憶體限制器在裝置上套用的可見和不可見限制,請執行下列指令:
adb shell am memory-limiter status
以下是 RAM 為 4 GB 的裝置輸出範例,其中不可見限制為可見限制的一半:
Memory limiter
enabled limits=true killing=true ignored=none
visible: memHigh=1843MB memMax=2048MB swapMax=1024MB
not-visible: memHigh=922MB memMax=1024MB swapMax=1024MB
在 Perfetto 中觀察效果
Perfetto 可讓您以視覺化方式呈現 oom_score_adj 和 RSS 在兩個程序中的時間軸,當使用者從前景 UI 移至主畫面時,背景工作仍會繼續。
1. 錄製追蹤記錄
在應用程式中啟動背景工作並前往主畫面時,記錄 15 秒的追蹤記錄,擷取程序記憶體計數器、GPU 記憶體和 oom_score_adj 變更:
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/process-splitting.perfetto-trace <<'EOF'
buffers: {
size_kb: 32768
fill_policy: DISCARD
}
data_sources: {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
proc_stats_poll_ms: 250
}
}
}
data_sources: {
config {
name: "android.gpu.memory"
}
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "oom/oom_score_adj_update"
ftrace_events: "gpu_mem/gpu_mem_total"
}
}
}
duration_ms: 15000
EOF
adb pull /data/misc/perfetto-traces/process-splitting.perfetto-trace
2. 在 Perfetto UI 中檢查程序追蹤
在 Perfetto UI 中開啟追蹤記錄,並展開 com.example.app 和 com.example.app:bg 的程序群組:
- 固定這兩個程序的
oom_score_adj和mem.rss.anon追蹤記錄,讓兩者並排顯示在時間軸頂端。 - 找出使用者按下主畫面按鈕的時間戳記:
- 在 worker 程序中,
oom_score_adj保持在200,mem.rss.anon則保持平穩且較小。 - 在 UI 程序中,
oom_score_adj從0升至200,在Activity停止時停留約一秒,然後升至700(PREV)。在先前應用程式逾時後,它會移至快取範圍,系統可以在不影響工作站的情況下壓縮並回收其記憶體。
- 在 worker 程序中,
在圖 2 中,UI 程序會短暫停留在 200,而 Activity 則會停止,然後上升至 700,而 worker 則會停留在 200,並有約 19 MB 的匿名 RSS。相較之下,圖 3 顯示共置設定的 35 秒追蹤記錄,其中單一程序會停留在 50,然後停留在 200,並保留所有 UI 記憶體。

如要探索這些檢視畫面,請在 Perfetto UI 中開啟分割追蹤記錄和共置追蹤記錄。
3. 使用 PerfettoSQL 查詢記憶體用量偏高的問題
在「Query (SQL)」分頁中執行下列 PerfettoSQL 查詢,測量應用程式各程序在每個 oom_score_adj 層級保留的平均匿名和檔案 RSS:
INCLUDE PERFETTO MODULE android.memory.process;
SELECT
process_name,
score AS oom_score_adj,
ROUND(SUM(dur) / 1e9, 2) AS duration_sec,
ROUND(SUM(anon_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_anon_mb,
ROUND(SUM(file_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_file_mb
FROM memory_oom_score_with_rss_and_swap_per_process
WHERE process_name GLOB 'com.example.app*'
GROUP BY process_name, score
ORDER BY process_name, score;
圖 2 分割追蹤記錄的輸出內容範例:
process_name oom_score_adj duration_sec avg_anon_mb avg_file_mb
com.android.memorylab 0 4.11 93.3 110.4
com.android.memorylab 200 1.06 126.8 110.5
com.android.memorylab 700 9.72 115.1 110.5
com.android.memorylab:remote 0 0.01 NULL NULL
com.android.memorylab:remote 200 12.87 18.9 53.7
圖 3 顯示共置追蹤記錄的輸出內容範例:
process_name oom_score_adj duration_sec avg_anon_mb avg_file_mb
com.android.memorylab 0 4.10 96.0 110.4
com.android.memorylab 50 16.04 117.6 110.5
com.android.memorylab 200 14.68 116.5 110.6
在分割設定中,第一秒後只有約 19 MB 的匿名記憶體保留在 oom_score_adj = 200。在共置設定中,只要前景服務執行,約 117 MB 的記憶體就會保留在 50,然後保留在 200。