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

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

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

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

oom_score_adj の値が小さいほど、プロセスが重要であり、Low Memory Killer(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
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.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 プロセスが実行され、oom_score_adj が 100 の BTOP(バウンド TOP)状態になっています。これは、一般的なバックグラウンド サービス(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)に移行しました。(注: 状態ダンプの 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

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

一般的なサービス バインディングのアンチパターン

サービス バインディングは oom_score_adj を直接昇格させるため、バインディングの取得や構造化の方法に微妙なライフサイクル上の誤りがあると、特権状態(BTOP、BFGS、PERC)で大量のメモリが数時間固定される可能性があります。バインドされたサービスを設計または監査する際は、次の一般的なアンチパターンに注意してください。

有効期間の長いクライアントで unbindService() が忘れられている

Application シングルトン、バックグラウンド マネージャー、または onStop() に一致する unbindService() がない onStart() で bindService() を呼び出す Activity からサービスにバインドすると、ServiceConnection がリークします。

このバインディングがアクティブである限り、ターゲット プロセスはクライアントの優先度を引き継ぎます。クライアントが永続的なシステム コンポーネントまたはフォアグラウンド アプリの場合、バインドされたサービス プロセスは BFGS または BTOP に無期限に固定され(procstats で 100% 近く表示される)、サービスが完全にアイドル状態であっても LMK がメモリを再利用できなくなります。

対策: スコープ バインディングを、それらを必要とするコンポーネントのライフサイクルに厳密に限定するか、非アクティブ状態が一定期間続いた後に unbindService() を呼び出すアイドル タイムアウトを実装します。個々のリモート プロシージャ コール呼び出しごとにバインド解除と再バインドを行うと、プロセスのスラッシングと Binder のセットアップのオーバーヘッドが繰り返されるため、避ける。代わりに、短いアイドル状態のタイマー(5 ~ 30 秒など)の背後で作業のバーストを統合する。

バインドされたサービスとメモリ使用量の多い UI を同じ場所に配置する

デフォルトでは、APK 内のすべてのコンポーネントは同じプロセスで実行されます。アプリがメインの Activity と同じプロセスで軽量なバインドされたサービス(NotificationListenerService、ウィジェット プロバイダ、システムまたはランチャーがバインドするプラグイン サービスなど)を公開する場合、プロセス全体がサービスの昇格状態(BFGS または PERC、通常は 200 以下の oom_score_adj)を継承します。

ユーザーがアプリの UI を開くと、プロセスは大きなビュー階層、デコードされたビットマップ、グラフィック バッファを割り当てます。ユーザーがアプリから離れても、アクティブなサービス バインディングによってプロセスが昇格された状態が維持されるため、プロセスは CACHED(oom_score_adj が 900 以上)に降格されません。これにより、次の 2 つの問題が複合的に発生します。

  • バックグラウンドでのメモリ圧縮なし: システムの CachedAppOptimizer は、プロセスが CACHED 状態になった後にのみ圧縮します。
  • キャッシュされた LRU メモリのトリムまたは LMK の再利用なし: サービス バインディングがプロセスを昇格状態に保持している間、システムはキャッシュされた LRU リスト(TRIM_MEMORY_BACKGROUND 以上)に関連付けられたバックグラウンド トリム コールバックを配信しません。アプリが TRIM_MEMORY_UI_HIDDEN または Activity.onStop() で UI リソースを明示的に解放しない場合、ピーク時の UI 割り当ては、LMK が簡単に再利用できない特権 OOM スコアで RAM に固定されたままになります。

解決策: UI が表示されなくなったときに、Activity.onStop()、ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_UI_HIDDEN)、Application.ActivityLifecycleCallbacks、または ProcessLifecycleOwner を使用して、UI キャッシュ、デコードされたビットマップ、ビュー参照を明示的に削除します。または、android:process マニフェスト属性を使用して、常にバインドされるサービスを別の軽量プロセスに移動します。これにより、システムはメインの UI プロセスを CACHED 状態に移動して、メモリを個別に圧縮または再利用できます。

優先度を無効にするフラグを省略する

BIND_AUTO_CREATE のみで bindService() を呼び出すと、呼び出し元のスケジューリングとメモリの優先度全体がターゲット サービスに転送されます。フォアグラウンド アプリが BIND_AUTO_CREATE のみを使用してバックグラウンドの分析、ロギング、プリフェッチ サービスにバインドすると、意図せずバックグラウンド ワーカーが BTOP に昇格します。

解決策: フォアグラウンド UI と同じ保護レベルを必要としない補助サービスまたはベストエフォート サービスにバインドする場合は、BIND_AUTO_CREATE を BIND_WAIVE_PRIORITY、BIND_NOT_FOREGROUND、または BIND_NOT_PERCEPTIBLE と組み合わせて、システムがキャッシュに保存された LRU リストのターゲット プロセスを管理できるようにします。


← ローカリティ | ↑ 上へ | システム全体 →