概述
一直聽說 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