优化 WebView 启动

当应用首次使用 WebView 时,系统会执行特定的启动任务。此启动过程非常繁重。默认情况下,当应用首次在 android.webkitandroidx.webkit 软件包中调用多个 API,或扩充包含 WebView 标记的布局时,系统会在界面线程上隐式执行此操作。

重要性

由于此隐式启动完全在主线程上进行,因此会阻止应用处理用户输入,并大幅增加发生“应用无响应”(ANR) 错误的风险。如需详细了解 Android 如何处理单线程执行模型,请参阅进程和线程概览

隐式启动的触发条件

可以通过以下方式触发隐式启动:

  • 以编程方式:调用 WebSettings.getUserAgentString() 等 API。
  • 使用布局:对包含 <WebView> 的 XML 资源调用 setContentView()layoutInflater.inflate()

隐式启动还会对您的业务指标(例如应用启动时间和首次显示时间)产生负面影响。如果隐式初始化不适合您的应用,请改用 startUpWebView

本页讨论了如何使用 startUpWebView API 优化 WebView 启动性能。

掌控 WebView 启动

如需提高性能并最大限度地减少 ANR,请使用 Jetpack Webkit 库中提供的 startUpWebView API。此 API 可让您明确控制 WebView 的启动时间。它将大量启动工作负载转移到后台线程,并允许必须在界面线程上完成的任何工作以块的形式完成,而不是以一个大的单体块的形式完成。这样一来,界面线程就可以并行处理其他关键应用任务,从而减少阻塞用户体验的可能性。

该 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 组件,系统会在等待初始化完成时阻塞界面线程。您的应用可能会从已完成的后台工作中获得一些性能优势,但无法获得最大优势。

当 WebView 位于关键路径上时

如果应用的核心用户历程需要立即使用 WebView,您可能无法等待 WebView 启动完成。在这种情况下,您仍应在应用生命周期内尽早调用 startUpWebView(例如在 Application.onCreate 中),但不要等待回调触发。而是直接使用 WebView API(如果需要)。

为了最大限度地利用异步启动,请务必延迟实例化 WebView 或调用 WebView API,直到没有其他关键路径界面线程操作需要运行(例如膨胀布局层次结构、初始化其他 SDK 或绘制初始帧)。

如果您调用 startUpWebView,然后立即在主线程上调用 WebView API,界面线程会阻塞,等待初始化赶上进度。在这种情况下,性能不会有任何提升。

如果 WebView 使用情况可能会成为关键路径,但您不想完全启动 WebView,可以选择有选择地运行能够在后台线程上运行的 WebView 启动任务,从而释放界面线程以用于其他应用关键任务。为此,您可以使用 shouldRunUiThreadStartUpTasks(false)

在应用生命周期的后期,您可以再次调用 startUpWebView 并使用 shouldRunUiThreadStartUpTasks(true) 在界面线程上完成剩余的启动任务。您是否需要在该时间点等待回调,取决于 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。

  • 注入到 APK 中,可在应用启动期间触发 WebView API。ContentProviders

  • 意外过早发生的布局扩充或程序化调用(例如,提取用户代理字符串)。

为了帮助您诊断这些意外的初始化发生的位置和原因,WebViewStartUpResult 对象提供了内置的审核功能:

  • getUiThreadBlockingStartUpLocations():返回 StartUpLocation 对象的列表,这些对象表示 WebView 启动任务阻塞主界面线程的位置。

  • 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 可保持与手动解决方法相当的性能。这样,您就可以在新设备上采用现代启动优势,而不会导致旧设备上的性能下降。

虽然优化 WebView 启动可以降低应用启动期间发生 ANR 错误的风险,但您还应保护应用免受运行时渲染器崩溃和系统内存回收的影响。如需在 WebView 运行后保持全面的应用稳定性,请参阅处理 WebView 终止。此外,通过使用 WebView.saveState() 并遵循高效管理 WebView 状态中的内存安全实践,在后台进程终止后保留用户状态。

如果您遇到问题或对 startUpWebView API 有反馈,请在公开问题跟踪器上提交 bug。