Android-WebView還會存在內存泄漏嗎? 概述 WebView銷燬時做了什麼 AwContents中的內存泄漏 總結

概述

一直聽說 WebView 使用不當容易造成內存泄漏,網上有很多針對內存泄漏的解決方案,比較多的是在 Activity.onDestroy 的時候將 WebView 從 View 樹中移除,然後再調用 WebView.destroy 方法:

override fun onDestroy() {
    val parent = webView?.parent
    if (parent is ViewGroup) {
        parent.removeView(webView)
    }
    webView?.destroy()
    super.onDestroy()
}

於是我寫了一個簡單的包含一個 WebView 的 Activity,然後在 Activity.onDestroy 中分別嘗試 啥也不幹只調用 WebView.destroy 方法,接着項目裏面集成了 leakcanary 用來檢測內存泄漏,啓動 App 後,反覆橫屏豎屏,發現 Activity.onDestroy 有被正常調用,但是 leakcanary 並沒有提示有內存泄漏,因此猜想 WebView 高版本應該把這個問題修復了。我用的測試機是 Android 9 版本的,於是想着換個低版本的機型試試,就弄了個 Android 6 的手機一跑,發現還是沒有發生內存泄漏,看了下網上這些講 WebView 內存泄漏的文章,有的還是 2019 年的,既然都 2019 年了還在談 WebView 會造成內存泄漏,那感覺 Android 6 的機型不應該表現正常呀,一臉懵逼。。。秉着不弄明白不罷休的原則,遇到這種問題好辦,Read The Fucking Source Code 就完事了。

WebView銷燬時做了什麼

既然網上的解決方案說先調用 removeView 移除 WebView,然後再調用 WebView.destroy 方法,那想着內存泄漏應該可以從 onDetachedFromWindow(從 Window 中 detach) 和 destroy(銷燬) 這兩個邏輯裏找原因,看一下 WebView 中的這兩個方法:

public void destroy() {
    checkThread();
    mProvider.destroy();
}

protected void onDetachedFromWindowInternal() {
    mProvider.getViewDelegate().onDetachedFromWindow();
    super.onDetachedFromWindowInternal();
}

一般而言 destroy 方法應該在 Activity.onDestroy 時手動調用,而 onDetachedFromWindowInternal 方法在 View detach 的時候會由系統回調。注意 onDestroy 的調用時機早於 onDetachedFromWindow,相關的源碼可以參考 Android圖形系統綜述 中 View 系列的文章自行跟蹤。

上面這兩個方法都出現了一個叫 mProvider 的對象,這個對象是啥呢?在 WebView.java 中搜索了一下 mProvider = 發現只有一處賦值:

private WebViewProvider mProvider;

mProvider = getFactory().createWebView(this, new PrivateAccess());

它是一個 WebViewProvider 類型的實例,接着看它是怎麼被賦值的,首先看一看 getFactory 返回的工廠對象是什麼:

private static WebViewFactoryProvider getFactory() {
    return WebViewFactory.getProvider();
}

// WebViewFactory
static WebViewFactoryProvider getProvider() {
    if (sProviderInstance != null) return sProviderInstance;
    Class<WebViewFactoryProvider> providerClass = getProviderClass();
    // CHROMIUM_WEBVIEW_FACTORY_METHOD = "create"
    staticFactory = providerClass.getMethod(CHROMIUM_WEBVIEW_FACTORY_METHOD, WebViewDelegate.class);
    sProviderInstance = (WebViewFactoryProvider) staticFactory.invoke(null, new WebViewDelegate());
    return sProviderInstance;
}

上面的 WebViewFactory.getProvider() 方法看上去是通過調用 providerClass 中的 create 方法拿到了 sProviderInstance 實例,於是得繼續看 getProviderClass 方法到底是返回了一個什麼類型的類:

private static Class<WebViewFactoryProvider> getProviderClass() {
    // ...
    return getWebViewProviderClass(clazzLoader);
}

public static Class<WebViewFactoryProvider> getWebViewProviderClass(ClassLoader clazzLoader) throws ClassNotFoundException {
    return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY, true, clazzLoader);
}

查看源碼,可以發現 CHROMIUM_WEBVIEW_FACTORY 取值爲 com.android.webview.chromium.WebViewChromiumFactoryProviderForP,我查看的源碼版本是 Android P 的,所以這裏是 WebViewChromiumFactoryProviderForP,看了一下其它 Android 版本的源碼,發現都有一個對應的 WebViewChromiumFactoryProviderForX 值。這個 WebViewChromiumFactoryProviderForP 類在 AOSP 中是沒有的,那應該去哪裏找呢?

參考 Chrome developer 的文檔: WebView for Android,可以看到從 Android 4.4 開始,WebView 組件基於 Chromium open source project 項目,新的 Webview 與 Android 端的 Chrome 瀏覽器共享同樣的渲染引擎,因此 WebView 和 Chrome 之間的渲染應該會更加一致。而從 Android 5.0(Lollipop) 版本開始將 WebView 遷移到了一個獨立的 APK --- Android System WebView,因此可以單獨在 Android 平臺更新。這個 APP 可以在應用管理中看到,看到這裏我大概明白了之前爲啥用 Android 6 的機器也沒有測試出內存泄漏,猜想應該是它的 Android System WebView 應用版本已經把內存泄漏的問題解決了吧,看了一下其應用版本是 86.0.4240.198(可以在應用管理中查看 Android System WebView 應用的版本,另外也可以在瀏覽器中打開這個 網址 也會顯示版本)。於是我們驗證一下這個猜想。

關於 Chromium open source project 的源碼可以在這裏查看: Chromium open source project Ref,在這裏可以查看目標版本的源碼,我選擇 86.0.4240.198 版本的源碼進行解析。接着上面的 WebViewChromiumFactoryProviderForP 開始:

public class WebViewChromiumFactoryProviderForP extends WebViewChromiumFactoryProvider {
    public static WebViewChromiumFactoryProvider create(android.webkit.WebViewDelegate delegate) {
        return new WebViewChromiumFactoryProviderForP(delegate);
    }

    protected WebViewChromiumFactoryProviderForP(android.webkit.WebViewDelegate delegate) {
        super(delegate);
    }
}

可以看到返回了一個 WebViewChromiumFactoryProviderForP 實例,其 createWebView 方法在父類 WebViewChromiumFactoryProvider 中:

public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) {
    return new WebViewChromium(this, webView, privateAccess, mShouldDisableThreadChecking);
}

因此上面的 mProvider 是 WebViewChromium 實例,來看一下它的 onDetachedFromWindow 和 destroy 方法:

public WebViewProvider.ViewDelegate getViewDelegate() {
    return this;
}

public void onDetachedFromWindow() {
    // ...
    mAwContents.onDetachedFromWindow();
}

public void destroy() {
    // ...
    mAwContents.destroy();
}

這倆都會調用到 AwContents 中對應的方法,所以上面 WebView 銷燬的時候,其 destroy 和 onDetachedFromWindowInternal 方法最後會調用到 AwContents 中對應的方法,低版本的內存泄漏就發生在這裏。

AwContents中的內存泄漏

我們先看一下 mAwContents 的創建:

mAwContents = new AwContents(mFactory.getBrowserContextOnUiThread(), mWebView, mContext, ...);

86.0.4240.198版本

首先看看 86.0.4240.198 版本中的 AwContents 類中的幾個相關方法:

public void destroy() {
    if (isDestroyed(NO_WARN)) return;
    // ...
    // Remove pending messages
    mContentsClient.getCallbackHelper().removeCallbacksAndMessages();
    if (mIsAttachedToWindow) {
        // 如果此時沒有 detach 則先調用 onDetachedFromWindow 方法,然後纔將 mIsDestroyed 置爲 true
        Log.w(TAG, "WebView.destroy() called while WebView is still attached to window.");
        onDetachedFromWindow();
    }
    mIsDestroyed = true;
}

// onAttachedToWindow 時會調用
public void onAttachedToWindow() {
    if (isDestroyed(NO_WARN)) return;
    if (mIsAttachedToWindow) {
        Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
        return;
    }
    mIsAttachedToWindow = true;
    // ...
    if (mComponentCallbacks != null) return;
    mComponentCallbacks = new AwComponentCallbacks();
    // 註冊 ComponentCallbacks
    mContext.registerComponentCallbacks(mComponentCallbacks);
}

// onDetachedFromWindow 時會調用
public void onDetachedFromWindow() {
    if (isDestroyed(NO_WARN)) return;
    if (!mIsAttachedToWindow) {
        Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
        return;
    }
    mIsAttachedToWindow = false;
    // ...
    if (mComponentCallbacks != null) {
        // 將 ComponentCallbacks 解註冊
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null;
    }
}

在 View attach 到 Window 中的時候會調用上面的 onAttachedToWindow 方法,在 View detach 的時候會調用到 onDetachedFromWindow 方法,這兩個方法中調用了一個 registerComponentCallbacks 和 unregisterComponentCallbacks 函數分別註冊和解註冊了一個 Callback,低版本會發生內存泄漏的原因就在此!

所以我們再來看一下 ComponentCallbacks 相關的邏輯:

// Context
public void registerComponentCallbacks(ComponentCallbacks callback) {
    getApplicationContext().registerComponentCallbacks(callback);
}

// Application
public void registerComponentCallbacks(ComponentCallbacks callback) {
    synchronized (mComponentCallbacks) {
        mComponentCallbacks.add(callback);
    }
}

所以假設在 AwContents 中只調用了 registerComponentCallbacks 註冊方法而沒有調用 unregisterComponentCallbacks 方法來解註冊,那麼會出現什麼情況呢?我們看一下這個 AwComponentCallbacks 類的實現,發現它是 AwContents 中的一個非靜態內部類,因此它會持有外部 AwContents 實例的引用,而 AwContents 持有 WebView 的 Context 上下文,對於 xml 中的 WebView 佈局而言,這個上下文就是其所在的 Activity,因此如果在 Activity 生命週期結束後沒有調用 unregisterComponentCallbacks 方法解註冊的話,便可能會發生內存泄漏

86.0.4240.198 版本中,如果在 Activity.onDestroy 方法中啥也不幹,那麼在 View detach 的時候依舊會調用 unregisterComponentCallbacks 方法解註冊;而如果在 Activity.onDestroy 方法中隻手動調用了 WebView.destroy 方法,那麼還是會先通過調用 onDetachedFromWindow 來解註冊,此時的 if (isDestroyed(NO_WARN)) return; 判斷是 false,可以正常執行到解註冊的邏輯,然後纔會標記爲已銷燬。

54.0.2805.1版本

接着我們再看一箇舊版本 54.0.2805.1 中的 AwContents 這幾個方法:

public void destroy() {
    if (isDestroyed(NO_WARN)) return;
    // Remove pending messages
    mContentsClient.getCallbackHelper().removeCallbacksAndMessages();
    // ...
    if (mIsAttachedToWindow) {
        Log.w(TAG, "WebView.destroy() called while WebView is still attached to window.");
        nativeOnDetachedFromWindow(mNativeAwContents);
    }
    mIsDestroyed = true;
}

public void onAttachedToWindow() {
    if (isDestroyed(NO_WARN)) return;
    // ...
    if (mComponentCallbacks != null) return;
    mComponentCallbacks = new AwComponentCallbacks();
    mContext.registerComponentCallbacks(mComponentCallbacks);
}

public void onDetachedFromWindow() {
    if (isDestroyed(NO_WARN)) return;
    nativeOnDetachedFromWindow(mNativeAwContents);
    // ...
    if (mComponentCallbacks != null) {
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null;
    }
}

可以看到如果在 Activity.onDestroy 中只調用了 WebView.destroy 方法的話,那麼此時還沒有調用到 onDetachedFromWindow 方法去解註冊,卻已經將 mIsDestroyed 置爲了 true,於是當 detach 的時候,onDetachedFromWindow 判斷到 isDestroyed 爲 true 則不會走接下來解註冊的邏輯了,於是內存泄漏也隨之而來。

而如果在 Activity.onDestroy 中不手動調用 WebView.destroy 的話,理論上在 WebView detach 的時候能調用 onDetachedFromWindow 方法解註冊 Callback,那麼這個內存泄漏問題應該不會發生,但是沒有調用 WebView.destroy 方法的話,很可能會發生其它問題,比如說不會調用 mContentsClient.getCallbackHelper().removeCallbacksAndMessages() 去移除 pending 的消息,說不定又有新的內存泄漏之類的。。。

要測試低版本 Chromium 的內存泄漏,可以找一個低版本的 Android 手機,然後將其 Android System WebView 應用卸載到裝機版本,然後查看對應版本的 AwContents 類源碼,如果源碼中有內存泄漏的可能的話就可以測試了。另外如果手裏頭有 Root 的手機,可以嘗試將 Android System WebView 最新版卸載,然後在 apkmirror 中下載一個低版本的 Android System WebView APK 安裝到手機上;或者直接從源碼中編譯出一個指定版本的 Android System WebView 應用,源碼編譯時間有限我也沒試過,可以參考 build-instructions

總結

WebView 中的內存泄漏其實與 Chromium 內核版本有關,在新版本的 Chromium 內核中內存泄漏問題已經被解決了,而且從 Android 5.0(Lollipop) 版本開始將 Chromium WebView 遷移到了一個獨立的 APP -- Android System WebView,隨着 Android System WebView 的獨立發佈,低版本 Android 系統(Android 5以上)上搭載的 Chromium 內核一般來說也不會太舊,所以出現內存泄漏的概率應該是比較小的。如果仍需要兼容這很小的一部分機型,可以通過文章開頭的方式銷燬 WebView,即先移除 WebView 組件,確保先調用到 onDetachedFromWindow 方法解註冊,然後再通過 WebView.destroy 方法處理其它銷燬邏輯。

文中相關的內容如有錯誤或遺漏歡迎指出,共同進步!覺得不錯的留個贊再走哈~

在這裏我分享自己收錄整理的Android學習的PDF,裏面對WebView有詳細的講解,希望可以幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,可以分享給身邊好友一起學習

如果你有需要的話,可以順手點贊+評論,關注一波後點擊GitHub地址獲取:https://github.com/733gh/Android-T3/blob/master/JianShu.md

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章