無駄のないプロセスでバックグラウンド処理を維持する

多くのアプリは、UI が表示されなくなった後も、大規模なアセットパックのダウンロード、ローカル データベースの同期、オーディオのストリーミング、エクササイズ ワークアウトの記録、接続済みデバイスのセッションの維持など、正当な処理を実行します。このバックグラウンド処理が、フォアグラウンド UI およびグラフィック エンジンと同じ OS プロセス内で実行されると、プロセス全体が、UI によって割り当てられたすべてのメモリとともに、高い重要度レベル(0 と 250 の間の oom_score_adj)に維持されます。

このページでは、このことがアプリだけでなくデバイスの他の部分にも悪影響を及ぼす理由、android:process を使用してバックグラウンド処理を専用の軽量プロセスに移行する方法、adb shell と Perfetto を使用してメモリの節約を確認する方法について説明します。

単一プロセスの処理で UI メモリが保持される理由

Android は、個々のコンポーネント(Activity、Service、BroadcastReceiver)ではなく、OS プロセス(pid)の粒度でメモリとプロセスの終了を管理します。アクティビティ マネージャー(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 に固定したままにする単一プロセス コロケーションと、ワーカーのみを oom_score_adj 200 に維持し、UI プロセスをキャッシュにドロップする分割プロセス分離の比較。
図 1. バックグラウンド処理中の単一プロセスでのコロケーションと分割プロセスでの分離の比較。

負荷の高い 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 を制約することなく、厳格なメモリ予算を割り当てることができます。

2 番目のランタイムには小さなベースライン コストがあるため、2 番目のプロセスを実行すると、アプリの合計メモリ使用量が増加するように見えることがあります。この比較は、バックグラウンド処理に移行するたびに、単一のプロセスが 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)で実行され、ワーカー独自のヒープとコードのみを使用します。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 ツールキット、画像読み込みプール、アナリティクス 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 などのゲームエンジンを使用してビルドする場合は、UnityPlayer などのエンジンクラスを参照しない Java または Kotlin で記述された、プレーンな Android ライブラリまたはプラグインにバックグラウンド サービスを実装します。:bg プロセスは、エンジンのネイティブ ライブラリを読み込みません。確認するには、:bg プロセスの adb shell cat /proc/<pid>/maps に libunity.so または libil2cpp.so が表示されていないことを確認します。

UI プロセスを固定せずに通信する

:bg で AssetDownloadService が実行されている間にユーザーが MainActivity を開いている場合、UI にダウンロードの進行状況をリアルタイムで表示する必要があることがよくあります。プロセスの重要性は、クライアントから、クライアントが使用するサービスまたはコンテンツ プロバイダをホストするプロセスに流れるため、各依存関係の方向が重要になります。

  • ワーカーへの UI バインディングは安全です。MainActivity が AssetDownloadService にバインドされると、システムは :bg を、表示されている UI の重要度まで引き上げます。ユーザーが離れると、UI プロセスはキャッシュに保存され、バインディングは何も昇格しなくなります。
  • UI プロセスに依存するワーカーは安全ではありません。:bg のコードが、メインプロセスで実行されるサービスにバインドするか、コンテンツ プロバイダにクエリを実行すると、システムはメインプロセスの重要度をワーカーの重要度まで引き上げ、UI メモリは再び固定されたままになります。代わりに、Messenger や AIDL コールバック インターフェースなどのコールバックをワーカーに登録します。バインダー オブジェクトをコールバックとして渡しても、どちらのプロセスの重要度も変わりません。

バインドを解除する前にコールバックの登録を解除して、システムがワーカーをフリーズした後にワーカーが UI プロセスを呼び出さないようにします。次の例では、UI が表示されている間のみワーカーにバインドします。

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: write-ahead log 書き込み(ほとんどのデバイスの Room のデフォルトのジャーナル モード)を使用して、両方のプロセスがデータベースの読み取りと書き込みを同時に行えるようにします。:bg から enableMultiInstanceInvalidation を呼び出さないでください。Room はメインプロセスで無効化サービスをホストするため、ワーカーはメインプロセスにバインドされ、ワーカーの重要度が高い状態で UI メモリを保持します。代わりに、UI が登録するコールバックを通じてワーカーに変更を報告させ、UI の起動時にデータベースを再クエリさせます。
  • 設定: プロセス間で SharedPreferences を共有しないでください。マルチプロセス コードで DataStore を使用するか、IPC を介して明示的に状態を渡します。
  • アセット ファイル: ダウンロードしたアセットパックを :bg のステージング ファイルに書き込み、書き込み後に :bg で fdatasync を呼び出してから posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) を呼び出し(メモリページを OS に戻すを参照)、ファイルの名前をアトミックに変更します。フリーズしている可能性のある UI プロセスを呼び出すのではなく、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 秒間、アクティビティ マネージャーはフォアグラウンド サービスを実行するプロセスを 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

分割プロセス アーキテクチャでは、ワーカー プロセスのみが 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 ワーカー プロセスの 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

ワーカー プロセスは、コードとランタイム ページのほとんどを Zygote と共有するため、必要な PSS は 5 MB 未満です。Vulkan または OpenGL ES でレンダリングするアプリでは、UI プロセスの Graphics 行に GPU メモリも含まれます。このサンプルは、ソフトウェア レンダリングを使用する Cuttlefish 仮想デバイスでキャプチャされたため、Graphics は両方のプロセスで 0 です。

3. プロセスごとの cgroup アカウンティングを検査する

UID の下の各プロセスには、アプリの親 cgroup 内に独自の cgroup v2 ディレクトリがあります。次のコマンドは、アプリのすべてのプロセスについて、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 がありますが、Memory Limiter は、アプリのすべてのプロセスを含む親 cgroup に上限を設定します。どちらの行も同じ app.memory.high(966,787,072 バイト)を示しています。これは、次の出力では表示されない 922 MB の上限です。

Memory Limiter がデバイスに適用する表示可能な上限と表示不可能な上限を確認するには、次のコマンドを実行します。

adb shell am memory-limiter status

4 GB の RAM を搭載したデバイスからの出力例。不可視の制限は可視の制限の半分です。

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 を使用すると、ユーザーがフォアグラウンド UI からホーム画面に移動し、バックグラウンド処理が続行されるときに、両方のプロセスで oom_score_adj と RSS のタイムラインを可視化できます。

1. トレースを記録する

アプリでバックグラウンド ジョブを開始してホーム画面に移動する際に、プロセス メモリ カウンタ、GPU メモリ、oom_score_adj の変化をキャプチャする 15 秒間のトレースを記録します。

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 のプロセス グループを開きます。

  1. 両方のプロセスの oom_score_adj トラックと mem.rss.anon トラックを固定して、タイムラインの上部に並べて表示します。
  2. ユーザーがホームボタンを押したタイムスタンプを見つけます。
    • ワーカー プロセスでは、oom_score_adj は 200 のままで、mem.rss.anon はフラットで小さいままです。
    • UI プロセスでは、oom_score_adj が 0 から 200 に上昇し、Activity が停止している間、約 1 秒間その状態を維持した後、700(PREV)に上昇します。以前のアプリのタイムアウト後、キャッシュに保存された範囲に移動します。この範囲では、システムはワーカーに影響を与えることなくメモリを圧縮して再利用できます。
UI プロセスとワーカー プロセスの Perfetto トラック。UI プロセスの oom_score_adj は、ホームボタンを押した後に 0 から 200、さらに 700 に上昇しますが、ワーカー プロセスは 200 のままで、約 19 MB の匿名 RSS を使用しています。
図 2. ユーザーがホームボタンを押した後にプロセスを分割します。

図 2 では、UI プロセスは Activity が停止している間、200 に短時間留まり、その後 700 に上昇します。一方、ワーカーは 200 に留まり、約 19 MB の匿名 RSS を使用しています。比較のため、図 3 に、同じ場所に配置された構成の 35 秒間のトレースを示します。この構成では、単一のプロセスが 50 に留まり、その後、すべての UI メモリとともに 200 に留まります。

単一のコロケーション プロセスの Perfetto トラック。oom_score_adj は、Home の後に 0 から 50 に上昇し、約 15 秒後に 200 に上昇しますが、匿名 RSS は高いままです。
図 3. ユーザーがホームボタンを押した後の、同じ場所に配置されたプロセス。

これらのビューを調べるには、Perfetto UI で分割トレースとコロケーション トレースを開きます。

3. PerfettoSQL を使用してメモリ使用量の増加をクエリする

[Query (SQL)] タブで次の PerfettoSQL クエリを実行して、各 oom_score_adj レベルでアプリの各プロセスが保持する匿名の RSS とファイルの 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

分割構成では、最初の 1 秒後に oom_score_adj = 200 に残る匿名メモリは約 19 MB のみです。同じ場所に配置された構成では、フォアグラウンド サービスが実行されている限り、約 117 MB が 50 に残り、その後 200 に残ります。


← メモリページを OS に戻す | ↑ 上へ