6 月 3 日の「#Android11: The Beta Launch Show」にぜひご参加ください。

WebView オブジェクトの管理

Android には、アプリでウェブ コンテンツを表示する WebView オブジェクトの管理に役立つ API がいくつか用意されています。

このページでは、これらの 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 デバイスなど)、または Android 4.4(API レベル 19)以前のようにアップデート可能な WebView の実装がない場合、getCurrentWebViewPackage() メソッドは null を返すことがあります。

Google セーフ ブラウジング サービス

ユーザーが安全にブラウジングできるように、WebView オブジェクトは Google セーフ ブラウジングを使用して URL を検証します。これにより、ユーザーが安全でない可能性のあるウェブサイトに移動しようした場合に、アプリで警告を表示できます。

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 はその既知の脅威についてユーザーに警告するインタースティシャルをデフォルトで表示します。ユーザーはこの画面で、とにかくその URL を読み込むか、安全な前のページに戻るかを選択できます。

Android 8.1(API レベル 27)以降を対象とするアプリの場合、次のように既知の脅威への対処方法をプログラムで定義できます。

  • アプリで既知の脅威をセーフ ブラウジングに報告するかどうかを制御できます。
  • アプリに特定のアクションを自動で実行させることもできます(既知の脅威と判断された URL へのアクセスを試みたときに安全なページに戻るなど)。

注: 既知の脅威から最大限保護するために、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)以降を対象とするアプリの場合、Geolocation API は HTTPS などを使用するセキュアなオリジンサイトでのみサポートされます。セキュアでないオリジンサイトの Geolocation API へのリクエストは、対応する onGeolocationPermissionsShowPrompt() メソッドが呼び出されることなく、自動的に拒否されます。

指標収集のオプトアウト

WebView には、ユーザーが同意した場合に匿名の診断データを Google にアップロードする機能があります。データは、WebView をインスタンス化したアプリ単位で収集されます。この機能をオプトアウトするには、マニフェストの <application> 要素で次のタグを作成します。

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

ユーザーの同意があり、かつアプリでこの機能をオプトアウトしていない場合にのみ、アプリからデータがアップロードされます。

Termination Handling API

この 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 を指定すると、システムがレンダラ プロセスを維持し続けているかどうかを、アプリ側では考慮しなくなります。実際、優先度がこのレベルまで下がると、メモリ不足に陥った際に、レンダラ プロセスが強制終了される可能性が高くなります。

警告: アプリの安定性を維持するために、WebView オブジェクトに対するレンダラの優先度ポリシーは変更しないでください。ただし、Termination Handle API を併用して、関連するレンダラが消滅した際の WebView の動作を指定している場合は除きます。

メモリ不足の状況におけるシステムの対処方法について詳しくは、プロセスとアプリケーションのライフサイクルをご覧ください。