当应用首次使用 WebView 时,系统会执行特定的启动任务。
此启动过程非常繁重。默认情况下,当应用首次在 android.webkit 或 androidx.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 启动完成。在这种情况下,您
仍应在应用生命周期的早期
(例如在 Application.onCreate 中)调用 startUpWebView,但不要等待回调
触发。而是直接在需要时使用 WebView API。
如需从异步启动中获得最大优势,请务必延迟实例化 WebView 或调用 WebView API,直到没有其他关键路径界面线程操作需要运行(例如扩充布局层次结构、初始化其他 SDK 或绘制初始帧)。
如果您调用 startUpWebView,然后在主线程上立即调用 WebView API,则界面线程会阻塞,等待初始化赶上。在这种情况下,没有任何性能优势。
如果 WebView 使用可能会进入关键路径,但您不想完全启动 WebView,则可以选择性地运行能够在后台线程上运行的 WebView 启动任务,从而为其他应用关键任务释放界面线程。为此,您可以使用 shouldRunUiThreadStartUpTasks(false)。
在应用生命周期的后期,您可以再次使用 shouldRunUiThreadStartUpTasks(true) 调用 startUpWebView,以完成界面线程上的其余启动任务。您是否在该时间点等待回调取决于 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 中的
ContentProviders会在应用启动期间触发 WebView API。布局扩充 或以编程方式进行的调用(例如提取用户代理字符串)过早发生。
为了帮助您诊断这些意外初始化发生的位置和原因,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 可与手动变通方案保持性能对等。这样,您就可以在新设备上采用现代启动优势,而不会在旧设备上产生性能损失。
如果您遇到问题或对 startUpWebView API 有反馈,请在 公开问题跟踪器上提交
bug。