服務繫結和程序狀態

Android 上的應用程式程序並非獨立運作,應用程式通常會依賴其他應用程式或系統本身提供的服務。當一個程序透過服務繫結連線至另一個程序時,會建立依附元件,對 Android 架構管理記憶體的方式造成深遠影響。

程序狀態和 OOM 分數

Android 架構會使用「程序狀態」追蹤每個執行中程序的優先順序。OomAdjuster 接著會使用這些狀態指派 OOM 分數調整項 (oom_score_adj) 值,範圍介於 -1000 至 1000。

oom_score_adj 值越低,表示該程序越重要,越不可能遭到記憶體不足終止工具 (LMK) 終止。

常見程序狀態

下表列出一些最常見的程序狀態及其典型 oom_score_adj 值。如需完整且最新的清單,請參閱 Android 原始碼中的 android.app.ActivityManagercom.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
CACHED 可安全終止的背景應用程式。 900 至 999

Service binding 的影響

當用戶端程序 (例如處於 TOP 狀態的應用程式) 繫結至伺服器程序中的服務時,伺服器程序通常會沿用較高的優先順序。這可確保服務在用戶端需要時保持可用。

圖表:程序 A (TOP) 透過 system_server 呼叫 bindService(),將程序 B 提升至 BTOP

使用 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

您會看到處於 TOP 狀態的主要程序 com.android.memorylab: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_adj100。這比一般背景服務 (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_statsam 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. 查詢記憶體不足分數轉換

使用 PerfettoSQL,您可以查看遠端程序的 OOM 分數相對於 UI 程序有何變化:

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 會永久繫結至使用者選取的語音互動服務

圖表:system_server 繫結至 Google 應用程式的互動器程序

如果您檢查程序狀態 (例如使用 dumpsys activity processes),可能會看到類似 com.google.android.googlequicksearchbox:interactor 的程序處於「已繫結的前景服務」BFGS狀態,並由 system_server (UID 1000) 的繫結保持運作。

NotificationListenerService

對於某些系統到應用程式的繫結,目標不是延遲,而是避免頻繁的冷啟動。NotificationListenerService 是個絕佳的例子,這個服務會在系統發布或移除新通知時接收系統的呼叫。一般智慧型手機使用者一天可能會收到數百則通知。如果系統從通知監聽器取消繫結,該應用程式的程序可能會進入快取狀態,並可能遭到 LMK 終止。

當下一個通知送達時 (可能幾秒後),系統會被迫再次冷啟動應用程式程序,只為傳送事件。如果不斷終止及冷啟動程序,消耗的 CPU 和電量會遠遠超過讓程序在背景繫結及保持運作。

啟動器的「-1 畫面」(新聞動態消息)

新式啟動器應用程式通常會結合核心導覽功能 (主畫面圖示和 Widget) 與新聞動態消息,並整合至啟動器使用者體驗,方便使用者在啟動器畫面存取。新聞動態消息可能由其他應用程式提供。舉例來說,在 Google Pixel 上,啟動器會整合 Google 應用程式提供的動態消息。

在主畫面向左滑動來查看新聞動態消息時,轉場效果必須流暢。啟動器會繫結至提供新聞動態消息的應用程式中的服務介面,並在啟動器存留期間維持繫結狀態,藉此達成上述目標。即使你沒有查看動態消息,系統仍會將動態消息內容顯示在記憶體中。

其他常見範例

  • 啟動器 (HOME_APP_ADJ):啟動器應用程式 (Home) 在優先順序清單中具有專屬的特殊位置。雖然不一定受服務限制,但會指派 HOME_APP_ADJ (通常為 600)。系統會優先保持啟動器運作,因為使用者經常會返回啟動器。事實上,系統會優先終止先前使用的應用程式 (PREV_APP_ADJ = 700),而不是啟動器,因為終止啟動器會導致使用者在結束任何應用程式時,必須等待啟動器冷啟動,造成使用者體驗不佳。
  • 輸入法編輯器 (IME):當您輸入內容時,系統會繫結至您選擇的鍵盤應用程式 (例如 Gboard)。即使鍵盤暫時隱藏,鍵盤程序仍會保持在提升狀態。這樣一來,輕觸其他文字欄位時,鍵盤就能立即重新顯示。
  • NFC 付款:輕觸手機付款時,系統會連結至 NFC 付款服務 (例如 Google 錢包)。這類交易通常對商家刷卡機有嚴格的即時要求。如果付款應用程式必須冷啟動,交易可能會逾時並失敗。

取捨和效能懸崖

雖然繫結是效能和正確性的必要條件,但會對系統的記憶體健康狀態造成影響。

  • 彈性降低:每個繫結程序都是 LMK 無法輕易終止的程序。這樣會減少系統可用的快取程序「緩衝區」,導致系統在記憶體不足時無法釋放記憶體。
  • 加劇效能下降:如果繫結的程序過多,系統可能會發現幾乎沒有可終止的背景程序。記憶體壓力增加時,系統會更快「從效能懸崖上墜落」,因為系統被迫終止更重要的程序,或將頁面快取清除。

← 縣市 | ↑ 向上 | 全系統 →