Android 上的應用程式程序並非獨立運作,應用程式通常會依賴其他應用程式或系統本身提供的服務。當一個程序透過服務繫結連線至另一個程序時,會建立依附元件,對 Android 架構管理記憶體的方式造成深遠影響。
程序狀態和 OOM 分數
Android 架構會使用「程序狀態」追蹤每個執行中程序的優先順序。OomAdjuster 接著會使用這些狀態指派 OOM 分數調整項 (oom_score_adj) 值,範圍介於 -1000 到 1000 之間。
oom_score_adj 值越低,表示該程序越重要,越不可能遭到記憶體不足終止工具 (LMK) 終止。
常見程序狀態
下表列出一些最常見的程序狀態,以及這些狀態的典型 oom_score_adj 值。如需完整且最新的清單,請參閱 Android 原始碼中的 android.app.ActivityManager 和 com.android.server.am.psc.Constants。
| 程序狀態 (縮寫) | 說明 | 一般oom_score_adj |
|---|---|---|
| PER (永久) | 必須一律執行的系統程序 (例如電話)。 | -800 |
| TOP | 使用者目前正在互動的程序。 | 0 |
| VIS (可視) | 程序有可見的活動 (例如半透明對話方塊後方的活動)。 | 100 |
| PERC (可感知) | 使用者知道的背景程序 (例如播放音樂)。 | 200 |
| FGS | 代管前景服務的程序。 | 0 到 200 (視情況而定) |
| BTOP (Bound Top) | 受 TOP 應用程式限制的程序。 | 100 |
| BFGS | 繫結前景服務 (通常是系統繫結)。 | 0 |
| PREV (上一頁) | 使用者目前所在程序的前一個程序。 | 700 |
| 已快取 | 可安全終止的背景應用程式。 | 900 至 999 |
Service binding 的影響
當用戶端程序 (例如處於 TOP 狀態的應用程式) 繫結至伺服器程序中的服務時,伺服器程序通常會沿用較高的優先順序。確保服務在用戶端需要時仍可使用。

使用 BIND 旗標控制繼承
使用 Context.BIND_AUTO_CREATE 時,系統預設會採用沿用機制。
不過,開發人員可以使用 bindService() 中的各種標記,控制繫結對目標程序重要性的影響。
OOM 分數的重要 BIND 旗標
管理全系統記憶體壓力時,下列標記最為重要:
BIND_AUTO_CREATE:最常見的旗標。只要繫結存在,這項機制就能確保服務程序啟動並保持運作。根據預設,這也會提升伺服器程序的優先順序,與用戶端相符。BIND_NOT_FOREGROUND:防止目標服務的程序提升至前景排程優先順序 (CPU 優先順序)。不過,這仍允許提升記憶體優先順序 (oom_score_adj)。這項功能適用於不應與 UI 爭奪 CPU 週期,但仍應避免遭到終止的背景工作。BIND_WAIVE_PRIORITY:非常強烈的旗標,可指示系統不要影響目標程序的排程或記憶體管理優先順序。系統會將服務程序視為 LRU 清單中的一般背景程序進行管理,因此即使服務已繫結,仍可能遭到 OOM 終止。BIND_ABOVE_CLIENT:指出服務比用戶端應用程式本身更重要。當系統需要回收記憶體時,會優先終止用戶端應用程式,再終止繫結服務。這比BIND_AUTO_CREATE「更強大」,因為它會犧牲用戶端效能,為服務提供額外一層保護。BIND_NOT_PERCEPTIBLE:將目標服務的重要性降至PERCEPTIBLE級別以下,讓系統回收其記憶體,為更重要的使用者可感知程序騰出空間。
實作:觀察繫結效果
我們會使用 MemoryLab 應用程式,示範
TOP應用程式的繫結如何影響獨立程序的狀態。
1. 啟動 MemoryLab
下列指令會啟動應用程式。開啟應用程式後,請確保應用程式保持在前台 (請勿按下「首頁」或切換應用程式)。
adb shell am start -n com.android.memorylab/.MainActivity
2. 找出程序
繫結前請先檢查程序狀態。MemoryLab 會在一個程序中執行主要 UI,並在 :remote 程序中執行 RemoteService。
adb shell dumpsys activity processes com.android.memorylab
輸出內容片段範例:
Process OOM control (154 total, non-act at 7, non-svc at 7):
Proc #0: fg T/A/TOP LCMNFUATI t: 0 13470:com.android.memorylab/u0a417 (top-activity)
oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0
state: cur=TOP set=TOP lastRss=0.00 lastCachedRss=0.00
您會看到主要程序 com.android.memorylab 處於 TOP 狀態。「:remote」程序尚未開始。
3. 觸發條件繫結
將廣播傳送至應用程式,觸發服務繫結:
adb shell am broadcast -a com.android.memorylab.LEAK_BINDER
4. 觀察提升的狀態
再次檢查程序狀態:
adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"
輸出內容片段範例:
Proc # 1: vis F/ /BTOP ---NFUATI t: 0 13560:com.android.memorylab:remote/u0a417 (service)
com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
oom: max=1001 curRaw=100 setRaw=100 cur=100 set=100
state: cur=BTOP set=BTOP lastRss=0.00 lastCachedRss=0.00
:remote 程序現在正在執行,且處於 BTOP (繫結 TOP) 狀態,oom_score_adj 為 100。這比一般背景服務 (500 以上) 的保護力高出許多。<=Proc{...} 符號表示哪個程序負責提升優先順序。
5. 傳送至背景
按下裝置上的「HOME」按鈕。再次檢查狀態:
adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"
輸出內容片段範例:
Proc # 2: prev b/ /LAST --------I t: 0 13560:com.android.memorylab:remote/u0a417 (service)
com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
state: cur=LAST set=LAST lastRss=0.00 lastCachedRss=0.00
Proc # 1: prev b/ /LAST --------I t: 0 13470:com.android.memorylab/u0a417 (previous)
oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
state: cur=LAST set=LAST lastRss=209MB lastCachedRss=0.00
現在這兩個程序都已轉移至較低的優先順序狀態 (PREV /
oom_score_adj 700),因為用戶端程序不再是 TOP。(注意:狀態傾印中的 LAST 是指 LAST_ACTIVITY 內部狀態,對應於高階摘要中的 PREV)。
使用 procstats 進行分析
procstats 工具會顯示這些狀態的歷史記錄。
# View stats for MemoryLab over the last hour
adb shell dumpsys procstats --hours 1 com.android.memorylab
輸出內容片段範例:
* com.android.memorylab / u0a417 / v37:
* Prc com.android.memorylab / u0a417 / v37:
TOTAL: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
Top: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
* Prc com.android.memorylab:remote / u0a417 / v37:
TOTAL: 0.19%
Bnd Top: 0.19%
其中 Bnd Top 表示遠端程序在 TOP 狀態下,受應用程式繫結的時間百分比。
使用 Perfetto 擷取及分析繫結
dumpsys 可提供快照,Perfetto 則可讓您查看發生繫結的確切時間,以及 OOM 分數的即時變化。
1. 錄製追蹤記錄
使用包含 linux.process_stats 和 am atrace 類別的設定:
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/service_bindings.perfetto-trace <<EOF
buffers: { size_kb: 65536 }
data_sources: {
config {
name: "linux.process_stats"
process_stats_config { proc_stats_poll_ms: 100 }
}
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config { ftrace_events: "am/am_proc_bound" }
}
}
duration_ms: 15000
EOF
2. 查詢 OOM 分數轉換
使用 PerfettoSQL,您可以查看遠端程序相對於 UI 程序的 OOM 分數變化:
SELECT ts, p.name, value AS oom_score_adj
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.memorylab%'
AND t.name = 'oom_score_adj'
ORDER BY ts;
3. 找出繫結事件
如要查看繫結依附元件的確切建立時間,以及啟動該依附元件的程序,請使用下列查詢:
SELECT
s.ts,
p.name AS process_name,
t.name AS thread_name,
s.name AS slice_name
FROM slice s
JOIN thread_track tt ON s.track_id = tt.id
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE s.name LIKE 'bindService:{com.android.memorylab%';
系統與應用程式的繫結
Android 系統本身通常會繫結至第三方應用程式中的服務,以提供核心功能。這些繫結的目標通常是減少延遲。 讓程序保持運作並留在記憶體中,可避免在發生重要使用者互動時,系統因「冷啟動」(載入 APK、初始化執行階段,以及建立 Application 物件) 而產生高昂的負擔。其他繫結可防止應用程式頻繁冷啟動,這類應用程式需要處理背景事件串流。
以下列舉幾個實際案例,您可以在一般裝置上觀察到這些情況:
VoiceInteractor
使用者希望手機作業系統內建數位助理,只要說出啟動字詞或快速輸入手勢,就能立即喚醒助理,並與助理流暢互動。
當助理觸發字詞出現時 (例如 Google Pixel 手機上的「Ok Google」熱字詞),數位助理必須立即回應。為確保這點,system_server 會永久繫結至使用者選取的語音互動服務。

如果您檢查程序狀態 (例如使用 dumpsys activity processes),可能會看到類似 com.google.android.googlequicksearchbox:interactor 的程序處於 BFGS (繫結前景服務) 狀態,並由 system_server (UID 1000) 的繫結保持運作。
NotificationListenerService
對於某些系統到應用程式的繫結,目標不是延遲,而是避免頻繁的冷啟動。NotificationListenerService 服務就是絕佳範例,當系統發布或移除新通知時,會呼叫這項服務。一般智慧型手機使用者一天可能會收到數百則通知。如果系統從通知監聽器取消繫結,該應用程式的程序可能會進入快取狀態,並可能遭到 LMK 終止。
當下一個通知送達時 (可能幾秒後),系統會被迫再次冷啟動應用程式程序,以便傳送事件。這種不斷終止和冷啟動的循環,會比單純將程序繫結並保持在背景運作,消耗更多 CPU 和電池電量。
啟動器的「-1 畫面」(新聞動態消息)
現代啟動器應用程式通常會結合核心導覽功能 (主畫面圖示和小工具) 和新聞動態消息,並在其中一個啟動器畫面上提供,且與啟動器 UX 無縫整合。新聞動態消息可能由其他應用程式提供。舉例來說,在 Google Pixel 上,啟動器會整合 Google 應用程式提供的動態消息。
在主畫面向左滑動來查看新聞動態消息時,轉場效果必須流暢。啟動器會繫結至提供新聞動態消息的應用程式服務介面,並在啟動器運作期間維持繫結狀態,藉此達成上述目標。即使你沒有查看動態消息,系統仍會將動態消息內容轉譯並儲存在記憶體中。
其他常見範例
- 啟動器 (HOME_APP_ADJ):啟動器應用程式 (主畫面) 在優先順序清單中具有專屬的特殊位置。雖然不一定受服務限制,但會指派
HOME_APP_ADJ(通常為 600)。系統會優先保持啟動器運作,因為使用者經常會返回啟動器。事實上,系統會優先終止先前使用的應用程式 (PREV_APP_ADJ = 700),而非啟動器,因為終止啟動器會導致使用者在結束任何應用程式時,必須等待啟動器冷啟動,造成使用者體驗不佳。 - 輸入法編輯器 (IME):輸入文字時,系統會繫結至你選擇的鍵盤應用程式 (例如 Gboard)。即使鍵盤暫時隱藏,鍵盤程序仍會保持在提升狀態。確保輕觸其他文字欄位時,鍵盤能立即重新顯示。
- NFC 付款:輕觸手機付款時,系統會連結至 NFC 付款服務 (例如 Google 錢包)。這類交易通常對商家終端機有嚴格的即時要求。如果付款應用程式必須冷啟動,交易可能會逾時並失敗。
權衡取捨和效能懸崖
雖然繫結對於效能和正確性來說是必要的,但會對系統的記憶體健康狀態造成影響。
- 彈性降低:每個繫結程序都是 LMK 無法輕易終止的程序。這樣會減少系統可用的快取程序「緩衝區」,導致系統在記憶體不足時無法釋放記憶體。
- 加劇效能驟降:如果繫結的程序過多,系統可能幾乎找不到可終止的背景程序。記憶體壓力增加時,系統會更快「從效能懸崖上墜落」,因為系統被迫終止更重要的程序,或將頁面快取清除。
常見的服務繫結反模式
由於服務繫結會直接提升 oom_score_adj,因此在取得或建構繫結時,如果生命週期有細微錯誤,可能會在數小時內,將大量記憶體固定在具備權限的狀態 (BTOP、BFGS 或 PERC)。設計或稽核繫結服務時,請留意這些常見的反模式。
長期用戶端中已忘記 unbindService()
從 Application 單例項、背景管理員或在 onStart() 中呼叫 bindService() 的 Activity (沒有 onStop() 中的相符 unbindService()) 繫結至服務,會導致 ServiceConnection 洩漏。
只要繫結保持有效,目標程序就會沿用用戶端提升的優先順序。如果用戶端是持續性系統元件或前景應用程式,繫結服務程序會無限期固定在 BFGS 或 BTOP 中 (在 procstats 中顯示接近 100%),即使服務完全閒置,LMK 也無法回收其記憶體。
補救措施:將範圍繫結嚴格限制在需要繫結的元件生命週期內,或實作閒置逾時,在閒置一段時間後呼叫 unbindService()。避免在每個個別 RPC 呼叫中取消繫結並重新繫結,這會導致程序輾轉現象和重複的繫結機制設定負擔;請改為在短暫的閒置計時器 (例如 5 到 30 秒) 後,合併工作爆發。
將繫結服務與耗用大量記憶體的 UI 共同放置
根據預設,APK 中的所有元件都會在同一個程序中執行。如果應用程式在與主要 Activity 相同的程序中,公開輕量型繫結服務 (例如 NotificationListenerService、小工具供應器,或系統或啟動器繫結的外掛程式服務),整個程序會沿用服務的提升狀態 (BFGS 或 PERC,通常是 200 以下的 oom_score_adj)。
使用者開啟應用程式的 UI 時,程序會分配大型檢視區塊階層、解碼的點陣圖和圖像緩衝區。使用者離開時,程序不會降至 CACHED (oom_score_adj 為 900 以上),因為使用中 Service binding 會讓程序保持在較高的優先順序。這會造成兩項複合問題:
- 不會在背景壓縮記憶體:系統只會在程序進入 狀態後壓縮程序。
CachedAppOptimizerCACHED - 不會修剪快取 LRU 記憶體或回收 LMK:服務繫結將程序維持在較高狀態時,系統不會傳送與快取 LRU 清單相關聯的背景修剪回呼 (
TRIM_MEMORY_BACKGROUND以上版本)。如果應用程式未在TRIM_MEMORY_UI_HIDDEN或Activity.onStop()上明確釋出 UI 資源,UI 分配的尖峰值會固定在 RAM 中,且 OOM 分數較高,LMK 無法輕易回收這些資源。
解決方法:當 UI 停止顯示時,請使用 Activity.onStop()、ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_UI_HIDDEN)、Application.ActivityLifecycleCallbacks 或 ProcessLifecycleOwner,明確清除 UI 快取、解碼點陣圖和檢視參照。或者,使用 android:process 資訊清單屬性,將一律繫結的服務移至獨立的輕量程序,這樣系統就能將主要 UI 程序移至 CACHED 狀態,獨立壓縮或回收記憶體。
省略優先順序豁免旗標
單獨呼叫 bindService() (使用 BIND_AUTO_CREATE) 會將來電者的完整排程和記憶體優先順序轉移至目標服務。如果前景應用程式僅使用 BIND_AUTO_CREATE 繫結至背景分析、記錄或預先擷取服務,就會無意間將該背景工作者升級為 BTOP。
解決方法:繫結至不需要與前景 UI 相同保護層級的輔助或盡力服務時,請將 BIND_AUTO_CREATE 與 BIND_WAIVE_PRIORITY、BIND_NOT_FOREGROUND 或 BIND_NOT_PERCEPTIBLE 結合,這樣系統仍可管理快取 LRU 清單中的目標程序。