サービス バインディングとプロセス状態

Android のアプリプロセスは単独で存在するものではありません。アプリケーションは、他のアプリケーションやシステム自体が提供するサービスに依存することがよくあります。1 つのプロセスがサービス バインディングを介して別のプロセスに接続すると、Android フレームワークがメモリを管理する方法に大きな影響を与える依存関係が作成されます。

プロセスの状態と OOM スコア

Android フレームワークは、プロセス状態を使用して、実行中の各プロセスの重要度を追跡します。これらの状態は OomAdjuster によって使用され、-1,000 ~ 1,000 の範囲の OOM スコア調整oom_score_adj)値が割り当てられます。

oom_score_adj の値が小さいほど、プロセスが重要であり、Low Memory Killer(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

サービス バインディングの影響

クライアント プロセス(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)を引き上げることは引き続き可能です。これは、CPU サイクルで UI と競合しないが、強制終了されないように保護する必要があるバックグラウンド作業に役立ちます。
  • 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 は 1 つのプロセスでメイン 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.memorylabTOP 状態になっていることがわかります。: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(Bound TOP)状態になり、oom_score_adj100 になります。これは、一般的なバックグラウンド サービス(500 以上)よりも大幅に保護されています。表記 <=Proc{...} は、この優先度の引き上げを担当するプロセスを示します。

5. バックグラウンドに送信

デバイスのホームボタンを押します。状態をもう一度確認します。

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

クライアント プロセスが TOP でなくなったため、両方のプロセスが優先度の低い状態(PREV / oom_score_adj 700)に移行しました。(注: 状態ダンプの LASTLAST_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. クエリの OOM スコアの推移

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

ユーザーは、デジタル アシスタントがスマートフォンの OS に組み込まれており、音声の起動語句や簡単な入力ジェスチャーで瞬時に呼び出すことができ、やり取りがスムーズかつシームレスであることを期待しています。

アシスタントのトリガー(Google Pixel スマートフォンの「OK Google」などの起動ワード)が発生すると、デジタル アシスタントは即座に応答する必要があります。これを保証するため、system_server はユーザーが選択した音声操作サービスへの永続的なバインディングを維持します。

Google アプリのインタラクター プロセスにバインドする system_server を示す図

プロセス状態を確認すると(dumpsys activity processes を使用するなど)、BFGS(バインドされたフォアグラウンド サービス)状態の com.google.android.googlequicksearchbox:interactor のようなプロセスが system_server(UID 1000)からのバインディングによって維持されていることがわかります。

NotificationListenerService

システムとアプリのバインディングの中には、レイテンシではなく、コールド スタートの頻度を減らすことを目的とするものもあります。NotificationListenerService は、新しい通知が投稿または削除されたときにシステムから呼び出しを受け取るサービスであり、その代表的な例です。一般的なスマートフォン ユーザーは、1 日に数百件の通知を受け取ることがあります。システムが通知リスナーからバインド解除された場合、そのアプリのプロセスはキャッシュ状態になり、LMK によって強制終了される可能性があります。

次の通知が届くと(数秒後になる可能性もあります)、システムはイベントを配信するためだけにアプリのプロセスをコールド スタートし直すことを余儀なくされます。この終了とコールド スタートの絶え間ないサイクルは、プロセスをバックグラウンドでバインドして存続させるよりも、はるかに多くの CPU とバッテリーを消費します。

ランチャーの「-1 画面」(ニュース フィード)

最新のランチャー アプリは通常、コア ナビゲーション機能(ホームアイコンとウィジェット)と、ランチャー画面の 1 つで利用可能で、ランチャー UX とシームレスに統合されたニュース フィードを組み合わせています。ニュース フィードは別のアプリによって提供されることもあります。たとえば、Google Pixel では、ランチャーが Google アプリによって提供されるフィードと統合されています。

ホーム画面を左にスワイプしてニュース フィードを表示する際の切り替えはスムーズでなければなりません。ランチャーは、ニュース フィードを提供するアプリのサービス インターフェースにバインドし、ランチャーが動作している間、そのバインドを維持することで、これを実現します。これにより、フィード コンテンツは表示されていないときでもメモリにレンダリングされて準備完了状態になります。

その他の一般的な例

  • ランチャー(HOME_APP_ADJ): ランチャー アプリ(ホーム)は、優先度リストに独自の特別なスロットを持っています。サービスによって常にバインドされるわけではありませんが、HOME_APP_ADJ(通常は 600)が割り当てられます。ユーザーが頻繁に戻るため、システムはランチャーを存続させることを優先します。実際、ランチャーを強制終了すると、ユーザーがアプリを終了するたびにランチャーのコールド スタートを待つ必要が生じ、ユーザー エクスペリエンスが低下するため、システムはランチャーを強制終了するよりも、以前に使用したアプリを強制終了する(PREV_APP_ADJ = 700)ことを優先します。
  • インプット メソッド エディタ(IME): 入力時に、システムは選択したキーボード アプリ(Gboard など)にバインドされます。これにより、キーボードが一時的に非表示になっても、キーボード プロセスは昇格状態を維持します。これにより、別のテキスト フィールドをタップしたときにキーボードがすぐに再表示されるようになります。
  • NFC 決済: スマートフォンをタップして支払うと、システムは NFC 決済サービス(Google ウォレットなど)にバインドされます。これらのトランザクションでは、販売店の端末から厳しいリアルタイム要件が課されることがよくあります。支払いアプリをコールド スタートする必要がある場合、トランザクションがタイムアウトして失敗する可能性があります。

トレードオフとパフォーマンスの急落

バインディングはパフォーマンスと正確性には必要ですが、システムのメモリの健全性を損なう可能性があります。

  • 柔軟性の低下: バインドされたプロセスは、LMK が簡単に強制終了できないプロセスです。これにより、システムがメモリ不足時にメモリを解放するために使用できるキャッシュ プロセスの「クッション」が減少します。
  • パフォーマンスの急激な低下の悪化: バインドされるプロセスが多すぎると、システムは強制終了可能なバックグラウンド プロセスがほとんどない状態になる可能性があります。メモリ プレッシャーが増加すると、システムはより重要なプロセスを強制終了するか、ページキャッシュをスラッシュせざるを得なくなるため、「パフォーマンスの崖」からより速く落ちることになります。

← 地域区分 | ↑ 上 | システム全体 →