アプリで WebView を初めて使用するとき、システムは特定の起動タスクを実行します。この起動プロセスは負荷が高いです。デフォルトでは、アプリケーションが android.webkit パッケージまたは androidx.webkit パッケージ内の多くの API を初めて呼び出すとき、または WebView タグを含むレイアウトを初めて拡張するときに、UI スレッドで暗黙的に発生します。
重要である理由
この暗黙的な起動はすべてメインスレッドで行われるため、アプリがユーザー入力を処理できなくなり、アプリケーション応答なし(ANR)エラーのリスクが大幅に高まります。Android がシングル スレッド実行モデルを処理する方法について詳しくは、プロセスとスレッドの概要をご覧ください。
暗黙的な起動のトリガー
暗黙的な起動は、次の方法でトリガーできます。
- プログラム:
WebSettings.getUserAgentString()などの API を呼び出します。 - レイアウトを使用する:
<WebView>を含む XML リソースでsetContentView()またはlayoutInflater.inflate()を呼び出す。
暗黙的な起動は、アプリの起動時間や初回表示までの時間など、ビジネス指標にも悪影響を及ぼす可能性があります。暗黙的な初期化がアプリに最適でない場合は、代わりに startUpWebView を使用します。
このページでは、startUpWebView API を使用して WebView の起動パフォーマンスを最適化する方法について説明します。
WebView の起動を制御する
パフォーマンスを改善し、ANR を最小限に抑えるには、Jetpack Webkit ライブラリで使用できる startUpWebView API を使用します。この API を使用すると、WebView の起動タイミングを明示的に制御できます。これにより、起動ワークロードの大部分がバックグラウンド スレッドに移行され、UI スレッドで実行する必要がある作業を、大きなモノリシック ブロックではなく、チャンク単位で実行できるようになります。これにより、UI スレッドが解放され、他の重要なアプリタスクを並行して処理できるようになるため、ユーザー エクスペリエンスがブロックされる可能性が低くなります。
API は androidx.webkit.WebViewOutcomeReceiver コールバックを使用するため、初期化の成功をトラッキングできます。
この API を使用するには、Jetpack Webkit ライブラリを build.gradle ファイルに追加します。バージョン 1.16.0 以降を使用していることを確認します。
dependencies {
implementation("androidx.webkit:webkit:1.16.0")
}
startUpWebView API を使用する
起動フローの最適化方法は、アプリが実際に WebView を表示する必要があるタイミングによって異なります。
WebView がクリティカル パスにない場合
アプリで WebView をすぐに読み込む必要がない場合は、初期化コストを完全に隠すことができます。アプリのライフサイクルの早い段階で startUpWebView を呼び出し、成功コールバックが起動するのを待ちます。
理想的には、他の WebView API を呼び出す前にコールバックを待つ必要があります。startUpWebView をトリガーしても、他の WebView コンポーネントに触れる前に完了を待機しない場合、システムは初期化の完了を待機している間、UI スレッドをブロックします。アプリは、すでに完了しているバックグラウンド作業からパフォーマンス上のメリットを得られる可能性がありますが、最大限のメリットは得られません。
WebView がクリティカル パスにある場合
アプリのコア ユーザー ジャーニーで WebView がすぐに必要な場合は、WebView の起動が完了するまで待つことはできません。このシナリオでは、アプリのライフサイクルの早い段階(Application.onCreate など)で startUpWebView を呼び出す必要がありますが、コールバックがトリガーされるのを待つ必要はありません。代わりに、必要なときに WebView API を直接使用します。
非同期スタートアップのメリットを最大限に活かすには、WebView のインスタンス化や WebView API の呼び出しを、実行すべきクリティカル パス UI スレッド オペレーション(レイアウト階層のインフレート、他の SDK の初期化、最初のフレームの描画など)がなくなるまで遅延させることが重要です。
startUpWebView を呼び出した後、メインスレッドで WebView API をすぐに呼び出すと、初期化が追いつくのを待つ間、UI スレッドがブロックされます。このシナリオでは、パフォーマンス上のメリットはありません。
WebView の使用がクリティカル パスになる可能性があるが、WebView を完全に起動したくない場合は、バックグラウンド スレッドで実行できる WebView 起動タスクを選択的に実行し、UI スレッドを他のアプリのクリティカル タスクのために解放できます。これには、shouldRunUiThreadStartUpTasks(false) を使用できます。
アプリのライフサイクルの後半で、shouldRunUiThreadStartUpTasks(true) を指定して startUpWebView を再度呼び出し、UI スレッドで残りの起動タスクを完了できます。その時点でコールバックを待つかどうかは、WebView の使用がクリティカル パス上にあるかどうかによって異なります。
実装例
この API は androidx.webkit.WebViewOutcomeReceiver コールバックを使用します。これにより、初期化の成功を追跡したり、診断の失敗を処理したりできます。
アプリのさまざまな部分から startUpWebView を複数回呼び出しても安全です。単純な再試行ループを実装しないことをおすすめします。
次のコードサンプルは、非同期初期化に WebViewCompat.startUpWebView API を使用する方法を示しています。
Kotlin
import android.content.Context
import android.util.Log
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewOutcomeReceiver
import androidx.webkit.WebViewStartUpConfig
import androidx.webkit.WebViewStartUpResult
import androidx.webkit.WebViewStartupException
import java.util.concurrent.Executors
fun initializeWebView(context: Context) {
// 1. Create a startup configuration specifying the background thread
// that WebView will use to run its initialization tasks.
val startUpConfig = WebViewStartUpConfig.Builder(
Executors.newSingleThreadExecutor()
).build()
// 2. Trigger WebView startup asynchronously
WebViewCompat.startUpWebView(
context,
startUpConfig,
object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {
override fun onResult(result: WebViewStartUpResult) {
// Success: The WebView has finished its background initialization.
// This callback is guaranteed to be invoked on the UI thread.
setupWebView()
}
override fun onError(error: WebViewStartupException) {
// Failure: The initialization encountered a startup exception.
Log.e("WebViewStartup", "Failed to initialize WebView", error)
}
}
)
}
Java
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.webkit.WebViewCompat;
import androidx.webkit.WebViewOutcomeReceiver;
import androidx.webkit.WebViewStartUpConfig;
import androidx.webkit.WebViewStartUpResult;
import androidx.webkit.WebViewStartupException;
import java.util.concurrent.Executors;
public void initializeWebView(Context context) {
// 1. Create the startup configuration specifying the background thread pool
// to handle internal non-UI initialization processes.
WebViewStartUpConfig startUpConfig = new WebViewStartUpConfig.Builder(
Executors.newSingleThreadExecutor()
).build();
// 2. Trigger WebView startup asynchronously
WebViewCompat.startUpWebView(
context,
startUpConfig,
new WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException>() {
@Override
public void onResult(@NonNull WebViewStartUpResult result) {
// Success: The WebView has finished its background initialization.
// This callback is invoked directly on the UI thread.
setupWebView();
}
@Override
public void onError(@NonNull WebViewStartupException error) {
// Failure: Handled using the concrete WebViewStartupException
Log.e("WebViewStartup", "Failed to initialize WebView", error);
}
}
);
}
非同期起動に関する問題をデバッグする
startUpWebView が期待どおりのパフォーマンス上のメリットをもたらさない場合、多くは、呼び出しの実行前にアプリの別の場所で WebView が暗黙的に初期化されていることが原因です。これは、次のいずれかの理由によるためです。
アプリのライフサイクルの早い段階で初期化されたサードパーティのライブラリまたは SDK。
アプリの起動時に WebView API をトリガーする APK に挿入される
ContentProviders。レイアウトのインフレーションや、予期せず早く発生するプログラムによる呼び出し(ユーザー エージェント文字列の取得など)。
予期しない初期化が発生している場所と理由を診断するために、WebViewStartUpResult オブジェクトには組み込みの監査機能が用意されています。
getUiThreadBlockingStartUpLocations(): WebView の起動タスクがメイン UI スレッドをブロックした場所を表すStartUpLocationオブジェクトのリストを返します。getNonUiThreadBlockingStartUpLocations(): 起動タスクの実行によってバックグラウンド スレッドがブロックされた特定の呼び出しサイトを返します。
各 StartUpLocation には、初期化をトリガーした正確なクラスとメソッドを見つけるためにログに記録したり検査したりできるスタック トレースが含まれています。
実装例
onResult コールバック内でこれらの場所を検査して、起動パスを監査できます。
override fun onResult(result: WebViewStartUpResult) {
// Check if WebView startup was blocked on the UI thread prior to or during initialization
val uiBlockingLocations = result.getUiThreadBlockingStartUpLocations()
if (!uiBlockingLocations.isNullOrEmpty()) {
for (location in uiBlockingLocations) {
// Log the stack trace of the call site that triggered the UI-blocking startup
Log.w("WebViewDebug", "WebView startup blocked the UI thread here:", location.getStack())
}
} else {
Log.i("WebViewDebug", "Excellent! No UI-blocking WebView startup detected.")
}
// Check where background initialization tasks were executed
val backgroundLocations = result.getNonUiThreadBlockingStartUpLocations()
backgroundLocations?.forEach { location ->
Log.d("WebViewDebug", "WebView background startup occurred at: ${location.getStack()}")
}
setupWebView()
}
監査でこのデータを使用する方法
アプリの WebView の起動を監査する際は、次の戦略を使用して診断データを分析し、パフォーマンスのボトルネックに対処します。
予期しないスタック トレースを探す:
getUiThreadBlockingStartUpLocations()が空でない場合は、出力されたスタック トレースを確認します。サードパーティ SDK に属するクラスや予期しないコンポーネントが見つかった場合は、暗黙的な初期化のボトルネックが見つかったことになります。呼び出し順序を確認する: ログ出力で、手動の
startUpWebView呼び出しの前に暗黙的な初期化が行われたことが示されている場合は、startUpWebView初期化をアプリの早い段階に移動するか、問題のある SDK が WebView 依存のタスクを遅延させるように構成する必要があります。
以前の回避策から移行する
以前は、ユーザー エージェント文字列の取得など、バックグラウンド スレッドで WebView の初期化を強制する明示的な回避策が使用されていた可能性があります。
これらの回避策はサポートされていない方法と見なされ、今後のリリースで基盤となる動作が変更される可能性があります。アプリが WebView の起動をトリガーまたは管理するために、明示的でドキュメント化されていない回避策に依存している場合は、代わりに startUpWebView API を使用することをおすすめします。startUpWebView API は、Jetpack Webkit ライブラリでサポートされているすべてのバージョンの Android と WebView で動作します。
Jetpack Webkit の実装を使用すると、Android エコシステム全体で一貫した動作を確保できます。この API の主な利点は、その復元力です。新しい最適化が利用できない古いデバイスでは、API は手動の回避策と同等のパフォーマンスを維持します。これにより、古いデバイスでパフォーマンスの低下を招くことなく、新しいデバイスで最新のスタートアップのメリットを活用できます。
startUpWebView API に関する問題が発生した場合やフィードバックがある場合は、公開バグトラッカーでバグを報告してください。