管理 WebView 对象

Android 提供了几个 API 来帮助您管理在应用中显示网页内容的 WebView 对象。

本文将介绍如何使用这些 API 更有效地处理 WebView 对象,从而提高应用的稳定性和安全性。

Version API

从 Android 7.0(API 级别 24)开始,要在 WebView 对象中显示网页内容,用户可以从几个不同的软件包中选择。Jetpack Webkit 库包含 getCurrentWebViewPackage() 方法,用于提取与在应用中显示网页内容的软件包相关的信息。当应用尝试使用特定软件包的 WebView 实现来显示网页内容时,如果仅在这种情况下发生错误,此方法会非常有用。

要使用此方法,请添加以下代码段中显示的逻辑:

Kotlin

val webViewPackageInfo = WebViewCompat.getCurrentWebViewPackage(appContext)
Log.d("MY_APP_TAG", "WebView version: ${webViewPackageInfo.versionName}")

Java

PackageInfo webViewPackageInfo = WebViewCompat.getCurrentWebViewPackage(appContext);
Log.d("MY_APP_TAG", "WebView version: " + webViewPackageInfo.versionName);

Google 安全浏览服务

为向用户提供更安全的浏览体验,WebView 对象会使用 Google 安全浏览(可让应用在用户尝试访问可能不安全的网站时向用户显示警告)验证网址。

虽然 EnableSafeBrowsing 的默认值为 true,但在某些情况下,您可能希望仅根据条件启用安全浏览功能或停用此功能。Android 8.0(API 级别 26)及更高版本支持使用 setSafeBrowsingEnabled() 来针对单个 WebView 对象启用或停用安全浏览。

如果您希望所有 WebView 对象都选择停用安全浏览检查,请向应用的清单文件添加以下 <meta-data> 元素:

<manifest>
    <application>
        <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
                   android:value="false" />
        ...
    </application>
</manifest>

定义程序化操作

WebView 实例尝试加载被 Google 归类为已知威胁的页面时,WebView 默认会显示一个插页,以便警告用户存在已知威胁。通过该界面,用户可以选择仍然加载网址或返回到上一个安全的页面。

如果您以 Android 8.1(API 级别 27)或更高版本为目标平台,则可以通过编程的方式定义您的应用响应已知威胁的方式,具体如下:

  • 您可以控制应用是否向安全浏览功能报告已知威胁。
  • 您可以让应用每次遇到被归类为已知威胁的网址时都自动执行特定操作(例如返回安全的页面)。

以下代码段演示了如何指示应用的 WebView 实例在遇到已知威胁后始终返回安全页面:

MyWebActivity.java

Kotlin

private lateinit var superSafeWebView: WebView
private var safeBrowsingIsInitialized: Boolean = false

// ...

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    superSafeWebView = WebView(this)
    superSafeWebView.webViewClient = MyWebViewClient()
    safeBrowsingIsInitialized = false

    if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
        WebViewCompat.startSafeBrowsing(this, ValueCallback<Boolean> { success ->
            safeBrowsingIsInitialized = true
            if (!success) {
                Log.e("MY_APP_TAG", "Unable to initialize Safe Browsing!")
            }
        })
    }
}

Java

private WebView superSafeWebView;
private boolean safeBrowsingIsInitialized;

// ...

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    superSafeWebView = new WebView(this);
    superSafeWebView.setWebViewClient(new MyWebViewClient());
    safeBrowsingIsInitialized = false;

    if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
        WebViewCompat.startSafeBrowsing(this, new ValueCallback<Boolean>() {
            @Override
            public void onReceiveValue(Boolean success) {
                safeBrowsingIsInitialized = true;
                if (!success) {
                    Log.e("MY_APP_TAG", "Unable to initialize Safe Browsing!");
                }
            }
        });
    }
}

MyWebViewClient.java

Kotlin

class MyWebViewClient : WebViewClientCompat() {
    // Automatically go "back to safety" when attempting to load a website that
    // Google identifies as a known threat. An instance of WebView calls this
    // method only after Safe Browsing is initialized, so there's no conditional
    // logic needed here.
    override fun onSafeBrowsingHit(
            view: WebView,
            request: WebResourceRequest,
            threatType: Int,
            callback: SafeBrowsingResponseCompat
    ) {
        // The "true" argument indicates that your app reports incidents like
        // this one to Safe Browsing.
        if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY)) {
            callback.backToSafety(true)
            Toast.makeText(view.context, "Unsafe web page blocked.", Toast.LENGTH_LONG).show()
        }
    }
}

Java

public class MyWebViewClient extends WebViewClientCompat {
    // Automatically go "back to safety" when attempting to load a website that
    // Google identifies as a known threat. An instance of WebView calls this
    // method only after Safe Browsing is initialized, so there's no conditional
    // logic needed here.
    @Override
    public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
            int threatType, SafeBrowsingResponseCompat callback) {
        // The "true" argument indicates that your app reports incidents like
        // this one to Safe Browsing.
        if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY)) {
            callback.backToSafety(true);
            Toast.makeText(view.getContext(), "Unsafe web page blocked.",
                    Toast.LENGTH_LONG).show();
        }
    }
}

HTML5 Geolocation API

对于以 Android 6.0(API 级别 23)及更高版本为目标平台的应用,仅 HTTPS 等安全的起点支持 Geolocation API。如果不调用相应的 onGeolocationPermissionsShowPrompt() 方法,非安全起点的所有 Geolocation API 请求都会被自动拒绝。

选择停用指标收集

经用户同意,WebView 可将匿名诊断数据上传到 Google。系统会针对实例化 WebView 的每个应用收集数据。您可以在清单的 <application> 元素中创建以下标记,选择停用此功能:

<manifest>
    <application>
    ...
    <meta-data android:name="android.webkit.WebView.MetricsOptOut"
               android:value="true" />
    </application>
</manifest>

只有用户同意应用未退出时,才能从应用上传数据。如需详细了解如何选择不报告诊断数据,请参阅 WebView 报告中的用户隐私权

Termination Handling

终止处理 API 可处理 WebView 对象的渲染器进程消失(可能是因为系统为收回必要的内存终止了渲染器,或是因为渲染器进程崩溃)的情况。通过使用此 API,即使渲染器进程消失,您的应用也可以继续执行。

如果渲染器在加载特定网页时崩溃,则尝试再次加载同一网页可能会导致新的 WebView 对象表现出相同的渲染崩溃行为。

以下代码段演示了如何在 Activity 中使用此 API:

Kotlin

    
inner class MyRendererTrackingWebViewClient : WebViewClient() {
    private var mWebView: WebView? = null

    override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
        if (!detail.didCrash()) {
            // Renderer is killed because the system ran out of memory. The app
            // can recover gracefully by creating a new WebView instance in the
            // foreground.
            Log.e("MY_APP_TAG", ("System killed the WebView rendering process " +
                "to reclaim memory. Recreating..."))

            mWebView?.also { webView ->
                val webViewContainer: ViewGroup = findViewById(R.id.my_web_view_container)
                webViewContainer.removeView(webView)
                webView.destroy()
                mWebView = null
            }

            // By this point, the instance variable "mWebView" is guaranteed to
            // be null, so it's safe to reinitialize it.

            return true // The app continues executing.
        }

        // Renderer crashes because of an internal error, such as a memory
        // access violation.
        Log.e("MY_APP_TAG", "The WebView rendering process crashed!")

        // In this example, the app itself crashes after detecting that the
        // renderer crashed. If you handle the crash more gracefully and let
        // your app continue executing, you must destroy the current WebView
        // instance, specify logic for how the app continues executing, and
        // return "true" instead.
        return false
    }
}

Java

public class MyRendererTrackingWebViewClient extends WebViewClient {
    private WebView mWebView;

    @Override
    public boolean onRenderProcessGone(WebView view,
            RenderProcessGoneDetail detail) {
        if (!detail.didCrash()) {
            // Renderer is killed because the system ran out of memory. The app
            // can recover gracefully by creating a new WebView instance in the
            // foreground.
            Log.e("MY_APP_TAG", "System killed the WebView rendering process " +
                    "to reclaim memory. Recreating...");

            if (mWebView != null) {
                ViewGroup webViewContainer =
                        (ViewGroup) findViewById(R.id.my_web_view_container);
                webViewContainer.removeView(mWebView);
                mWebView.destroy();
                mWebView = null;
            }

            // By this point, the instance variable "mWebView" is guaranteed to
            // be null, so it's safe to reinitialize it.

            return true; // The app continues executing.
        }

        // Renderer crashes because of an internal error, such as a memory
        // access violation.
        Log.e("MY_APP_TAG", "The WebView rendering process crashed!");

        // In this example, the app itself crashes after detecting that the
        // renderer crashed. If you handle the crash more gracefully and let
        // your app continue executing, you must destroy the current WebView
        // instance, specify logic for how the app continues executing, and
        // return "true" instead.
        return false;
    }
}

Renderer Importance API

由于 WebView 对象现在以多进程模式运行,您可以灵活地控制应用处理内存不足情况的具体方式。您可以使用 Android 8.0 中引入的 Renderer Importance API 来为分配给特定 WebView 对象的渲染器设置优先级政策。具体而言,当显示应用的 WebView 对象的渲染器已终止时,您可能希望应用的主要部分继续执行。例如,如果您不希望长时间显示 WebView 对象,以便系统可以收回渲染器正在使用的内存,则可以这样做。

以下代码段展示了如何为与应用的 WebView 对象关联的渲染器进程分配优先级:

Kotlin

val myWebView: WebView = ...
myWebView.setRendererPriorityPolicy(RENDERER_PRIORITY_BOUND, true)

Java

WebView myWebView;
myWebView.setRendererPriorityPolicy(RENDERER_PRIORITY_BOUND, true);

在此代码段中,渲染器的优先级与应用的默认优先级相同(或绑定到应用的默认优先级)。当关联的 WebView 对象不再可见时,true 实参会将渲染器的优先级降低至 RENDERER_PRIORITY_WAIVED。换句话说,true 参数会指明您的应用并不关心系统是否会让渲染器进程保持活动状态。事实上,这个较低的优先级可能会使渲染器进程在内存不足的情况下被终止。

如需详细了解系统如何处理内存不足的情况,请参阅进程和应用生命周期