WebView とメモリ

WebView は、Android アプリ内でウェブ コンテンツを表示できる強力なコンポーネントです。ただし、WebView は基本的にフル機能のブラウザ エンジン(Chromium)であるため、メモリ使用量が大きく、マルチプロセス アーキテクチャが複雑です。

技術的背景: マルチプロセス アーキテクチャ

最新の Android 搭載デバイスでは、WebView はマルチプロセス モデルを使用してセキュリティと安定性を向上させています。アプリで WebView を使用すると、メモリはさまざまなプロセスに分散されます。

  1. ブラウザ プロセス(アプリ プロセス): これはアプリのメイン プロセスです。Java の WebView オブジェクトと Chromium エンジンの「ブラウザ」部分が含まれています。このプロセスは、UI、ネットワーク リクエスト、GPU レンダリング(Android HWUI レンダリング パイプラインと直接統合)を管理します。 Chrome とは異なり、WebView には個別の GPU プロセスはありません。
  2. レンダラ プロセス: このプロセスは、HTML の解析、 JavaScript の実行、レイアウトを担当します。セキュリティ上の理由から、システムの他の部分から分離されています。現在、アプリはすべての WebView に対して 1 つのレンダラ プロセスを取得します(まれに特別なケースを除く)。Chrome では、多くの場合、サイトごとに個別のレンダラ プロセスが使用されます。

WebView のアーキテクチャ

メモリに関する重要性

dumpsys meminfo <your_package> を使用すると、 **ブラウザ プロセス** (アプリ プロセス)で使用されるメモリのみが表示されます。レンダラ プロセスで使用されるメモリは個別に計上されます。

ブラウザ プロセス内では、WebView メモリは次のように分散されます。

  • Java ヒープ: WebView Java ラッパーと関連オブジェクトが含まれます。
  • ネイティブ ヒープ: Chromium ブラウザ エンジンの内部データ 構造、キャッシュ、状態が含まれます。PartitionAlloc の使用により、WebView のネイティブ割り当ての一部が dumpsys meminfo の [Native Heap] にカウントされず、代わりに [Other] または [Unknown] に表示される場合があります。
  • 共有メモリ: グラフィック バッファやその他のデータの共有に使用されます。dumpsys meminfo で明確に分類されない場合があります。

トラブルシューティング ツール

Chrome DevTools

WebView の内部(レンダラ プロセス)のメモリを分析する最も強力なツールは、Chrome DevTools です。

  1. アプリで WebView のデバッグを有効にします。

    // NOTE: In production, this should be gated behind a developer setting
    // or only enabled for debuggable builds to prevent reverse engineering.
    WebView.setWebContentsDebuggingEnabled(true);
    
  2. デバイスを USB 経由で接続します。

  3. ホストマシンで Chrome を開き、chrome://inspect/#devices に移動します。

  4. アプリを見つけて [inspect] をクリックします。

  5. DevTools ウィンドウで、[Memory] タブに移動して、ヒープ スナップショットを取得するか、JavaScript ヒープの割り当てタイムラインを記録します。

dumpsys meminfo

adb shell dumpsys meminfo --all <package> を使用して、メモリの内訳を確認します。 出力で WebView カテゴリとオブジェクト数を確認します。

レンダラのプロファイリング

レンダラは別のプロセスで実行されるため、アプリをプロファイリングするだけではネイティブ ヒープをプロファイリングできません。レンダラ プロセスの PID を特定する必要があります。

複数の WebView がアクティブな場合に正しいレンダラ PID を特定するには:

  1. を使用する dumpsys activity:

    adb shell dumpsys activity processes <your_package_name>
    

    mConnections セクションを探します。アプリを SandboxedProcessService にリンクする ConnectionRecord が表示されます。そのプロセスの PID がレンダラです。例:

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. プロセス名を確認する: レンダラ プロセスは通常、 com.google.android.webview:sandboxed_processX などの名前が付けられます。WebView を使用しているアプリが 1 つしかない場合は、1 つだけになります。

PID がわかったら、heapprofd を使用してプロファイリングできます。

WebView メモリに関するベスト プラクティス

明示的な破棄

アプリは、インスタンスの処理が完了したときに WebView.destroy() を呼び出す必要があります。

WebView は、インスタンスをガベージ コレクションしてすべてのリソースを自動的に解放できるようにしますが、100% のケースで保証することは困難です。自動ガベージ コレクションが機能する場合でも、大幅に遅延する可能性があり、アプリが想定よりもはるかに長くリソースを保持する可能性があります。

アプリが適切なタイミング( Activity.onDestroy() など)で WebView.destroy() を呼び出す場合、WebView オブジェクト自体への参照を保持しても、 ネイティブ リソースがリークすることはありません。Activity フィールドの WebView オブジェクトへの参照を破棄後に null にする必要はありません。Activity 自体がガベージ コレクションされるときにクリーンアップされます。

演習: WebView メモリのハンズオン

演習 1: マルチプロセス フットプリントの観察

  1. MemoryLab を起動し、アプリのメモリのベースライン測定を行います。

    adb shell dumpsys meminfo com.android.memorylab
    

    ベースラインの例(rango): TOTAL PSS: 18915 KB

  2. [Launch WebView (Normal)] をタップします。

  3. WebView で、[Allocate JS Memory (1000 DIVs)] を数回タップします。

  4. アプリのメモリをもう一度確認します。

    adb shell dumpsys meminfo com.android.memorylab
    
  5. アプリ プロセスのメモリがベースラインと比較して大幅に増加していないことを確認します。 これは、DOM 要素がレンダラ プロセス にあるためです。

  6. レンダラ プロセスを見つけます。

    adb shell ps -A | grep webview | grep sandboxed
    

    出力例:

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. レンダラ プロセスのメモリ(PID を使用)を確認します。

    adb shell dumpsys meminfo 14227
    
  8. レンダラ プロセスの TOTAL PSS が高いことを確認します。サンプル実行では、数回の割り当て後に ~55 MB に増加しました。JavaScript の割り当て(V8 エンジンによって処理される)は通常、Dalvik ヒープではなく、dumpsys meminfo の [Private Other] または [Unknown](mmap)セクションに寄与します。

演習 2: Java 側の WebView リーク

よくある間違いは、静的フィールドまたはリークする存続期間の長いオブジェクトで WebView インスタンスを保持することです。WebView オブジェクトは、ネイティブ リソースとレンダラ プロセス全体を保持する重い「アンカー」であるため、リークするとコストが非常に高くなります。

WebView のリークの影響

  1. MemoryLab で、[Launch WebView (Java Leak)] をタップします。
  2. ページが読み込まれると、アクティビティは自動的に閉じます(繰り返しのナビゲーションとリークの蓄積をシミュレートします)。
  3. ボタンを 4 回タップします。
  4. アプリ内の WebView インスタンスの数を確認します。

    adb shell dumpsys meminfo com.android.memorylab
    

    一番下の [Objects] セクションを探します。WebViews の数が 4 に増えています。

    rango での出力例(4 つのインスタンスがリーク):

     Objects
               Views:       51         ViewRootImpl:        5
         AppContexts:       14           Activities:        5
              Assets:       38        AssetManagers:        0
       Local Binders:       55        Proxy Binders:       77
       Parcel memory:       41         Parcel count:       68
    Death Recipients:        3             WebViews:        4
    
  5. ヒープダンプをキャプチャし、AHAT を使用してリークを見つけます。パスに ahat がない場合は、Android ツリーからビルドできます。

    # Dump heap from device
    adb shell am dumpheap com.android.memorylab /data/local/tmp/heap.hprof
    adb pull /data/local/tmp/heap.hprof
    # Run ahat using the built JAR (found in out/host/linux-x86/framework/)
    java -jar out/host/linux-x86/framework/ahat.jar -p 8888 heap.hprof
    
  6. AHAT ウェブ インターフェース(localhost:8888)で、上部のメニューの [allocations] リンク(または [sites])をクリックして、メモリ使用量全体を確認します。

    AHAT の割り当て

  7. android.webkit.WebView クラスを検索します。[instance count] をクリックして、すべてのライブ インスタンスを表示します。リストに複数のインスタンスが表示されます。

    AHAT WebView インスタンス

  8. リークした WebView インスタンスの 1 つをクリックします。[Sample Path from GC Root] セクションまでスクロールします。com.android.memorylab.WebViewActivitysLeakedWebViews リストによって保持されていることがわかります。

    AHAT Path to GC Root


← ネイティブ | ↑ 上へ | アプリコード →