WebView の状態を効率的に管理する

Android アプリのライフサイクルを管理するうえで、バックグラウンドでのリソース再利用時にユーザーの状態を保持することは、シームレスなユーザー エクスペリエンスを実現するための重要な要素です。ウェブ ワークフローを組み込んだアプリの場合、WebView.saveState(Bundle) WebView のナビゲーション履歴と状態を Bundle にシリアル化できます。 このデータは、WebView.restoreState(Bundle) を使用して復元できます。

ただし、標準的な実装では、ブラウジング セッションが重い場合にトランザクション サイズの上限に達することがあります。このページでは、このようなアーキテクチャ上の制限について説明し、ナビゲーション履歴を維持しながらメモリ関連の例外を防ぐための戦略を紹介します。

1 MB のトランザクション上限と状態の消去

Android では、savedInstanceState に保存できるデータの合計容量に 1 MB という厳しい上限が設けられています。この 1 MB の予算は、アプリプロセス全体で共有されます。アプリに複数の WebView インスタンスが組み込まれている場合、それらのナビゲーション状態と履歴の合計が、この単一の共有割り当て内に収まる必要があります。この上限を超えると TransactionTooLargeException が発生し、アプリがクラッシュします。

一般的な対応策として、WebView の状態バンドルのサイズをモニタリングし、任意の安全しきい値(300 KB など)を超えた場合は WebView の履歴を完全にクリアするという方法があります。この方法でクラッシュを防ぐことはできますが、ユーザー エクスペリエンスに重大な回帰が発生します。

  • 戻るナビゲーションの喪失: Android は、他のタスクのためにメモリを再利用するために、バックグラウンドのアプリ プロセスを終了することがよくあります。onSaveInstanceState() ライフサイクル コールバック内で saveState(Bundle) を使用すると、ナビゲーション履歴を保持できます。1 MB の取引限度額を回避するためにこの履歴を消去すると、ナビゲーション スタック全体が失われます。プロセスが再起動したかどうかに関係なく、ユーザーがアプリに戻ると、戻るナビゲーションをサポートする履歴コンテキストが残っていないため、システムの戻るボタンを押すとすぐにコンポーネントまたはアプリが終了します。

  • BFCache の無効化: 履歴をクリアすると、アプリで Back-Forward Cache(BFCache)を使用できなくなり、 以前にアクセスしたページを瞬時にレンダリングできなくなります。

  • レイテンシの増加: ユーザーは WebView 内の現在の状態を失うため、 完全な再ナビゲーションと再初期化が必要になります。このプロセスにより、ネットワーク オーバーヘッドとトランザクション レイテンシが大幅に増加します。

アーキテクチャ上の緩和策

履歴を完全に削除して ユーザー エクスペリエンスを低下させることなく TransactionTooLargeException クラッシュを防ぐには、状態の保持とメモリ効率の厳格な バランスを維持する必要があります。次の最適化戦略を実装することで、重要なナビゲーション履歴とセッションの整合性を維持しながら、1 MB のトランザクション予算を安全に管理できます。

状態のシリアル化にサイズ制限を適用する

ナビゲーション スタックが大きくなりすぎた場合に完全に消去するのではなく、履歴データを切り捨てる方が効果的です。

  • ターゲット ドロップ ポリシー: WebViewCompat.saveState() を使用して、特定のバイト制限を適用しながら状態をシリアル化します(例: WebViewCompat.saveState(webView, outState, maxSizeBytes))。この API は、合計ペイロードが定義した割り当て内に収まるまで、古いナビゲーション エントリを順番に削除します。重要なのは、アクティブな WebView のライブ履歴を変更またはクリアせずに、シリアル化された Bundle のみを切り捨てるため、すぐに戻るナビゲーションを完全に維持できることです。

  • 前方エントリの削除: アプリケーション インターフェースに戻るボタンはあるが、専用の次に進むボタンがない場合は、すべての進むナビゲーション エントリを破棄できます。 saveState API の includeForwardState パラメータを false に設定します。これにより、ユーザーが利用できるナビゲーション パスに影響を与えることなく、ペイロード サイズを大幅に削減できます。

HTTP Cache Quota API でリソース レイテンシを管理する

saveState は一時的なナビゲーション履歴の 1 MB の Bundle 上限を管理しますが、HTTP Cache Quota API を使用すると、プロファイルごとに永続化されたウェブリソース(ディスク キャッシュ)を手動で制御できます。これにより、短期的なナビゲーション コンテキストと長期的なキャッシュ アセットを明確に区別できます。

適切な割り当てを選択するには、パフォーマンスとのトレードオフが必要です。

  • 割り当てを増やす と、より多くのアセットがディスクに保持されるため、オフラインでの可用性とリソースの読み込みレイテンシが向上します。
  • 割り当てを減らす と、アプリのディスク使用量を最小限に抑え、OS による他の重要なアプリデータのキャッシュ削除を防ぐことができます。

これらの設定はアプリの再起動後も保持され、メインスレッドから構成する必要があります。

次の実装は、デフォルト プロファイルのディスク キャッシュ割り当てを構成する方法を示しています。

Kotlin

if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) &&
    WebViewFeature.isFeatureSupported(WebViewFeature.HTTP_CACHE)) {
    val defaultProfile = ProfileStore.getInstance()
        .getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME)
    val httpCache = defaultProfile.httpCache

    // Set explicit cache size to 50MB (50 * 1024 * 1024 bytes)
    httpCache.setQuotaBytes(50L * 1024 * 1024)
}

Java

if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) &&
    WebViewFeature.isFeatureSupported(WebViewFeature.HTTP_CACHE)) {
    Profile defaultProfile = ProfileStore.getInstance()
        .getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME);
    HttpCache httpCache = defaultProfile.getHttpCache();

    // Set explicit cache size to 50MB (50 * 1024 * 1024 bytes)
    httpCache.setQuotaBytes(50L * 1024 * 1024);
}

割り当てサイズの決定戦略、ライフサイクル管理、および プロファイルの境界の詳細については、WebView で HTTP キャッシュ割り当てを管理するをご覧ください。

パフォーマンスに関する主な考慮事項

次の点は、WebView の状態の動作を制御する技術的な制限と内部データの動作を示しています。

  • 不透明な PageState BLOB: saveState によって保存されるデータの約 70% は、レンダリング エンジンからの内部 PageState BLOB で構成されています。 このデータは、フォーム入力や iframe のスクロール位置など、詳細なセッション状態をキャプチャします。これらの BLOB から個々のセグメントを手動で解析または削除しようとすると、重大なセキュリティ リスクが発生し、セッション復元の整合性が損なわれるため、避けてください。

  • 詳細な履歴管理: 標準の WebBackForwardList API は 個々の履歴要素の任意の削除をネイティブでサポートしていません。厳格な状態管理を行うには、WebViewCompat.saveState() 内の maxSizeBytes パラメータと includeForwardState パラメータを使用して切り捨て戦略を実装し、アーキテクチャの安全性を確保する必要があります。