管理 WebView 对象

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

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

Version API

从 Android 7.0(API 级别 24)开始,要在 WebView 对象中显示网页内容,用户可以从几个不同的软件包中选择。AndroidX 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);
    

注意:如果设备设置有误,不支持使用 WebView(例如 Wear OS 设备)或缺少可更新的 WebView 实现,则 getCurrentWebViewPackage() 方法会返回 null,在 Android 4.4(API 级别 19)及更早版本中也是如此。

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 对象的 loadUrl() 方法。

以下代码段演示了如何指示应用的 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 has identified 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 has identified 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>
    

只有用户同意未退出应用时,才能从应用上传数据。

Termination Handling

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

注意:如果您的应用在渲染器进程消失后继续执行,便无法重复使用关联的 WebView 实例,无论渲染器进程已终止还是出现崩溃。您的应用必须从视图层次结构中移除然后销毁该实例才能继续执行。然后,该应用必须创建一个全新的 WebView 实例才能继续渲染网页。

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

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

Kotlin

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

        override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
            if (!detail.didCrash()) {
                // Renderer was 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 crashed 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 choose to handle the crash more gracefully
            // and allow your app to continue executing, you should 1) destroy the
            // current WebView instance, 2) specify logic for how the app can
            // continue executing, and 3) 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 was 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 crashed 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 choose to handle the crash more gracefully
            // and allow your app to continue executing, you should 1) destroy the
            // current WebView instance, 2) specify logic for how the app can
            // continue executing, and 3) 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);
    

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

警告:为保持应用的稳定性,除非您还使用 Termination Handle API 来指定 WebView 在其关联的渲染器消失时作何反应,否则不应更改 WebView 对象的渲染器优先级政策。

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