WebView 객체 관리

Android는 앱에 웹 콘텐츠를 표시하는 WebView 객체를 관리하는 데 도움이 되는 여러 개의 API를 제공합니다.

이 페이지에서는 이러한 API를 사용하여 WebView 객체를 더 효과적으로 사용하고 앱의 안정성과 보안을 개선하는 방법을 설명합니다.

버전 API

Android 7.0(API 레벨 24)부터 사용자는 WebView 객체에 웹 콘텐츠를 표시할 때 서로 다른 여러 패키지 중에서 선택할 수 있습니다. AndroidX 웹킷 라이브러리에는 앱에 웹 콘텐츠를 표시하는 패키지와 관련된 정보를 가져오는 getCurrentWebViewPackage() 메서드가 들어 있습니다. 이 메서드는 앱에서 WebView의 특정 패키지 구현을 사용하여 웹 콘텐츠를 표시하려고 할 때만 발생하는 오류를 분석하는 데 특히 유용합니다.

이 메서드를 사용하려면 다음 코드 스니펫에 표시된 로직을 추가합니다.

Kotlin

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

자바

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

참고: Android 4.4(API 레벨 19) 이하 버전에서처럼 기기가 잘못 설정되었거나 WebView 사용을 지원하지 않거나(예: Wear OS 기기) 업데이트 가능한 WebView 구현이 없는 경우 getCurrentWebViewPackage() 메서드는 null을 반환할 수 있습니다.

Google 세이프 브라우징 서비스

사용자에게 더 안전한 브라우징 환경을 제공하기 위해 WebView 객체는 Google 세이프 브라우징을 사용하여 URL을 확인합니다. 그러면 사용자가 안전하지 않을 수 있는 웹사이트로 이동하려고 할 때 앱에서 사용자에게 경고를 표시합니다.

EnableSafeBrowsing의 기본값은 true이지만 경우에 따라 세이프 브라우징을 조건부로만 사용하거나 사용 중지하고자 할 수 있습니다. Android 8.0(API 레벨 26) 이상에서는 setSafeBrowsingEnabled() 사용을 지원하기 때문에 WebView 객체마다 세이프 브라우징을 전환할 수 있습니다.

모든 WebView 객체에서 세이프 브라우징 검사를 선택 해제하기를 원하는 경우 다음 <meta-data> 요소를 앱의 manifest 파일에 추가하면 됩니다.

    <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!")
                }
            })
        }
    }
    

자바

    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()
            }
        }
    }
    

자바

    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 위치정보 API

Android 6.0(API 레벨 23) 이상을 타겟팅하는 앱의 경우 HTTPS와 같은 보안 출처에서만 위치정보 API가 지원됩니다. 보안 출처가 아닌 다른 출처에서 위치정보 API를 요청하는 경우 대응하는 onGeolocationPermissionsShowPrompt() 메서드를 호출하지 않고 자동으로 거부됩니다.

측정항목 수집 선택 해제

WebView은 사용자가 동의한 경우 Google에 익명의 진단 데이터를 업로드할 수 있습니다. 데이터는 WebView를 인스턴스화한 각 앱과 관련해 앱별로 수집됩니다. manifest의 <application> 요소에 다음 태그를 만들어 이 기능을 선택 해제할 수 있습니다.

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

사용자가 동의하고 동시에 앱이 선택 해제하지 않은 경우에만 앱에서 데이터가 업로드됩니다.

종료 처리 API

크게 필요한 메모리를 확보하기 위해 시스템이 렌더기를 종료하거나 렌더기 프로세스 자체가 다운되어 WebView 객체의 렌더기 프로세스가 사라지는 경우가 발생하면 종료 처리 API가 이를 처리합니다. 이 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
        }
    }
    

자바

    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;
        }
    }
    

렌더기 중요도 API

이제 WebView 객체가 멀티 프로세스 모드에서 작동한다는 점을 감안하면 메모리가 부족한 상황이 발생할 경우 앱이 이를 유연하게 처리하도록 할 수 있습니다. Android 8.0에 도입된 렌더기 중요도 API를 사용하여, 특정 WebView 객체에 할당된 렌더기와 관련된 우선순위 정책을 설정할 수 있습니다. 특히 앱의 WebView 객체를 표시하는 렌더기가 종료되더라도 앱의 주요 부분은 계속 실행할 수 있습니다. 예를 들어 렌더기가 사용했던 메모리를 시스템에서 다시 확보할 수 있도록 WebView 객체를 오랫동안 표시하지 않으려는 경우 이렇게 할 수 있습니다.

다음 코드 스니펫은 앱의 WebView 객체에 연결된 렌더기 프로세스에 우선순위를 할당하는 방법을 보여줍니다.

Kotlin

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

자바

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

이 특정 스니펫에서 렌더기의 우선순위는 앱의 기본 우선순위와 동일하거나 이 우선순위에 '결합'됩니다. 연결된 WebView 객체가 더 이상 표시되지 않으면 true 인수는 렌더기의 우선순위를 RENDERER_PRIORITY_WAIVED로 떨어뜨립니다. 다시 말해서 true 인수는 시스템에서의 렌더링 프로세스 활성 여부를 앱이 관리하지 않음을 나타냅니다. 사실 이 같은 낮은 우선순위 수준에서는 메모리가 부족한 상황에서 렌더기 프로세스가 종료될 가능성이 높습니다.

경고: 연결된 렌더기가 사라질 때의 WebView 반응을 지정하기 위해 종료 처리 API도 사용하지 않는다면, 앱 안정성을 유지하기 위해 WebView 객체와 관련된 렌더기 우선순위 정책을 변경해서는 안 됩니다.

시스템에서 메모리 부족 상황을 처리하는 방법에 관한 자세한 내용은 프로세스 및 애플리케이션 수명 주기를 참조하세요.