WebView 객체 관리

Android에서는 웹 콘텐츠를 앱에 표시하는 WebView 객체의 관리를 돕는 여러 API를 제공합니다.

이 페이지에서는 이들 API를 사용하여 WebView 객체를 보다 효율적으로 사용함으로써 앱의 안정성과 보안을 개선하는 방법에 대해 설명합니다.

버전 API

Android 7.0(API 레벨 24)부터는 웹 콘텐츠를 WebView 객체에 표시하기 위해 다양한 여러 패키지 중에 하나를 사용자가 선택할 수 있습니다. Android 8.0(API 레벨 26) 이상의 버전에는 웹 콘텐츠를 앱에 표시하는 패키지와 관련된 정보를 가져오기 위한 API가 포함됩니다. 특히 이 API는 앱이 특정 패키지의 WebView 구현을 사용하여 웹 콘텐츠를 표시하려고 시도할 때만 발생하는 오류를 분석할 때 유용합니다.

이 API를 사용하려면 다음 코드 스니펫에 나오는 논리를 추가하세요.

Kotlin

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

Java

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

참고: 기기가 올바로 설정되지 않은 경우 WebView.getCurrentWebViewPackage() 메서드가 null을 반환할 수 있습니다. 또한 WebView를 지원하지 않는 기기(예: Wear OS 기기)에서 앱을 실행 중인 경우에도 null을 반환할 수 있습니다.

Google 안전 브라우징 API

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

EnableSafeBrowsing의 기본값은 true(참)이지만, 경우에 따라 세이프 브라우징을 조건부로만 사용하거나 사용하지 않고자 할 수 있습니다. Android 8.0(API 레벨 26)이상은 setSafeBrowsingEnabled() 사용을 지원합니다. 낮은 API 레벨에서 컴파일하는 앱은 setSafeBrowsingEnabled()를 사용할 수 없으며 매니페스트에서 EnableSafeBrowsing의 값을 false(거짓)로 변경하여 모든 WebView 인스턴스 기능을 사용하지 않기로 설정해야 합니다.

앱의 대상이 Android 7.1(API 레벨 25) 이하인 경우, URL을 Google 세이프 브라우징의 안전하지 않은 웹사이트 목록에 비교하여 확인하지 않기로 WebView 객체를 선택 해제할 수 있습니다. 이렇게 하려면 앱의 매니페스트 파일에 다음과 같은 <meta-data> 요소를 추가하면 됩니다.

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

프로그래밍 방식의 작업 정의

Google에 의해 알려진 위협으로 분류된 페이지를 WebView 인스턴스가 로드하려고 시도할 경우, 기본적으로 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

    superSafeWebView.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;

    superSafeWebView.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 : WebViewClient() {
    // 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: SafeBrowsingResponse
    ) {
        // The "true" argument indicates that your app reports incidents like
        // this one to Safe Browsing.
        callback.backToSafety(true)
        Toast.makeText(view.context, "Unsafe web page blocked.", Toast.LENGTH_LONG).show()
    }
}

Java

public class MyWebViewClient extends WebViewClient {
    // 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, SafeBrowsingResponse callback) {
        // The "true" argument indicates that your app reports incidents like
        // this one to Safe Browsing.
        callback.backToSafety(true);
        Toast.makeText(view.getContext(), "Unsafe web page blocked.",
                Toast.LENGTH_LONG).show();
    }
}

HTML5 위치정보 API

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

통계 수집 옵트아웃

WebView는 Google에 익명 진단 데이터를 업로드할 수 있습니다(사용자가 동의한 경우). WebView를 인스턴스화한 각 앱에 대하여 앱별로 데이터가 수집됩니다. 이 기능을 선택 해제하려면 매니페스트의 <application> 요소에 다음과 같은 태그를 생성하면 됩니다.

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

사용자가 동의했고 앱이 선택 해제되지 않았을 때에만 앱에서 데이터가 업로드됩니다.

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

이 특정 스니펫에서 렌더러의 우선순위는 앱의 기본 우선순위와 동일합니다(또는 "바인딩됩니다"). 연결된 WebView 객체가 더 이상 보이지 않는 경우 true 인수는 렌더러의 우선순위를 RENDERER_PRIORITY_WAIVED로 줄입니다. 다시 말해, true 인수의 경우 앱은 시스템이 렌더러 프로세스를 활성 상태로 유지하는지 여부에 개의치 않습니다. 실제로, 이 낮은 우선순위 수준에서는 메모리 부족 상황에서 렌더러 프로세스가 종료될 가능성이 높습니다.

경고: 앱의 안정성을 유지하기 위해서는, 연결된 렌더러가 사라질 때의 WebView 반응 방식을 지정하기 위해 Termination Handle API를 함께 사용하지 않는 한, WebView 객체의 렌더러 우선순위 정책을 변경해서는 안 됩니다.

시스템이 메모리 부족 상황을 처리하는 방법에 대해 자세히 알아보려면 프로세스 및 애플리케이션 수명 주기를 참조하세요.