WebView 객체 관리

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

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

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

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 인스턴스에 지시하는 방법을 보여줍니다.

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

사용자가 동의하는 동시에 앱이 선택 해제하지 않은 경우에만 앱에서 데이터가 업로드됩니다. 진단 데이터 보고 선택 해제에 관한 자세한 내용은 WebView 보고의 사용자 개인 정보 보호를 참고하세요.

종료 처리 API

종료 처리 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;
    }
}

렌더기 중요도 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 인수는 시스템에서 렌더기 프로세스를 유지하는지 여부를 앱이 신경 쓰지 않는다는 것을 나타냅니다. 실제로 우선순위 수준이 낮으면 메모리가 부족한 상황에서 렌더기 프로세스가 종료될 가능성이 높습니다.

시스템이 메모리 부족 상황을 처리하는 방법을 자세히 알아보려면 프로세스 및 앱 수명 주기를 참고하세요.