關於WebView的內存泄露,由於WebView在加載網頁後會長期佔用內存而不能被釋放,所以咱們在Activity銷燬後要調用它的destory()
方法來銷燬它以釋放內存。java
另外在查閱WebView
內存泄露相關資料時看到這種狀況:android
Webview
下面的Callback
持有Activity
引用,形成Webview
內存沒法釋放,即便是調用了Webview.destory()
等方法都沒法解決問題(Android5.1以後)。web
最終的解決方案是:在銷燬WebView
以前須要先將WebView從
父容器中移除,而後在銷燬WebView
。詳細分析過程請參考這篇文章:WebView內存泄漏解決方法。app
@Override protected void onDestroy() { super.onDestroy(); // 先從父控件中移除WebView mWebViewContainer.removeView(mWebView); mWebView.stopLoading(); mWebView.getSettings().setJavaScriptEnabled(false); mWebView.clearHistory(); mWebView.removeAllViews(); mWebView.destroy(); }
在 Android 5.1 系統上,在項目中遇到一個WebView引發的問題,每打開一個帶webview的界面,退出後,這個activity都不會被釋放,activity的實例會被持有,因爲咱們項目中常常會用到瀏覽web頁面的地方,可能引發內存積壓,致使內存溢出的現象,因此這個問題仍是比較嚴重的。ide
問題分析post
使用Android Studio的內存monitor,獲得瞭如下的內存分析,我打開了三個BookDetailActivity界面(都有webview),檢查結果顯示有3個activity泄漏,以下圖所示:this
這個問題仍是比較嚴重的,那麼進一步看詳細的信息,找出究竟是哪裏引發的內存泄漏,詳情的reference tree以下圖所示:google
從上圖中能夠看出,在第1層中的 TBReaderApplication 中的 mComponentCallbacks 成員變量,它是一個array list,它裏面會持有住activity,引導關係是 mComponentCallbacks->AwContents->BaseWebView->BookDetailActivity, 代碼在 Application 類裏面,代碼以下所示:spa
public void registerComponentCallbacks(ComponentCallbacks callback) { synchronized (mComponentCallbacks) { mComponentCallbacks.add(callback); } } public void unregisterComponentCallbacks(ComponentCallbacks callback) { synchronized (mComponentCallbacks) { mComponentCallbacks.remove(callback); } }
上面兩個方法,會在 Context 基類中被調用,代碼以下:.net
/** * Add a new {@link ComponentCallbacks} to the base application of the * Context, which will be called at the same times as the ComponentCallbacks * methods of activities and other components are called. Note that you * <em>must</em> be sure to use {@link #unregisterComponentCallbacks} when * appropriate in the future; this will not be removed for you. * * @param callback The interface to call. This can be either a * {@link ComponentCallbacks} or {@link ComponentCallbacks2} interface. */ public void registerComponentCallbacks(ComponentCallbacks callback) { getApplicationContext().registerComponentCallbacks(callback); } /** * Remove a {@link ComponentCallbacks} object that was previously registered * with {@link #registerComponentCallbacks(ComponentCallbacks)}. */ public void unregisterComponentCallbacks(ComponentCallbacks callback) { getApplicationContext().unregisterComponentCallbacks(callback); }
從第二張圖咱們已經知道,是webview引發的內存泄漏,並且能看到是在 org.chromium.android_webview.AwContents 類中,難道是這個類註冊了component callbacks,可是未反註冊?通常按系統設計,都會反註冊的,最有可能的緣由就是某些狀況下致使不能正常反註冊,很少說,read the fucking source。基於這個思路,我把chromium的源碼下載下來,代碼在這裏 chromium_org(https://android.googlesource.com/platform/external/chromium_org/?spm=5176.100239.blogcont61612.7.j9EPtE)
而後找到 org.chromium.android_webview.AwContents 類,看看這兩個方法 onAttachedToWindow 和 onDetachedFromWindow:
@Override public void onAttachedToWindow() { if (isDestroyed()) return; if (mIsAttachedToWindow) { Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring"); return; } mIsAttachedToWindow = true; mContentViewCore.onAttachedToWindow(); nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(), mContainerView.getHeight()); updateHardwareAcceleratedFeaturesToggle(); if (mComponentCallbacks != null) return; mComponentCallbacks = new AwComponentCallbacks(); mContext.registerComponentCallbacks(mComponentCallbacks); } @Override public void onDetachedFromWindow() { if (isDestroyed()) return; if (!mIsAttachedToWindow) { Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring"); return; } mIsAttachedToWindow = false; hideAutofillPopup(); nativeOnDetachedFromWindow(mNativeAwContents); mContentViewCore.onDetachedFromWindow(); updateHardwareAcceleratedFeaturesToggle(); if (mComponentCallbacks != null) { mContext.unregisterComponentCallbacks(mComponentCallbacks); mComponentCallbacks = null; } mScrollAccessibilityHelper.removePostedCallbacks(); }
系統會在attach處detach進行註冊和反註冊component callback,注意到 onDetachedFromWindow() 方法的第一行,if (isDestroyed()) return;, 若是 isDestroyed() 返回 true 的話,那麼後續的邏輯就不能正常走到,因此就不會執行unregister的操做,經過看代碼,能夠獲得,調用主動調用 destroy()方法,會致使 isDestroyed() 返回 true。
/** * Destroys this object and deletes its native counterpart. */ public void destroy() { if (isDestroyed()) return; // If we are attached, we have to call native detach to clean up // hardware resources. if (mIsAttachedToWindow) { nativeOnDetachedFromWindow(mNativeAwContents); } mIsDestroyed = true; new Handler().post(new Runnable() { @Override public void run() { destroyNatives(); } }); }
通常狀況下,咱們的activity退出的時候,都會主動調用 WebView.destroy() 方法,通過分析,destroy()的執行時間在onDetachedFromWindow以前,因此就會致使不能正常進行unregister()。
解決方案
找到了緣由後,解決方案也比較簡單,核心思路就是讓onDetachedFromWindow先走,那麼在主動調用以前destroy(),把webview從它的parent上面移除掉。
ViewParent parent = mWebView.getParent(); if (parent != null) { ((ViewGroup) parent).removeView(mWebView); } mWebView.destroy();
完整的代碼以下:
public void destroy() { if (mWebView != null) { // 若是先調用destroy()方法,則會命中if (isDestroyed()) return;這一行代碼,須要先onDetachedFromWindow(),再 // destory() ViewParent parent = mWebView.getParent(); if (parent != null) { ((ViewGroup) parent).removeView(mWebView); } mWebView.stopLoading(); // 退出時調用此方法,移除綁定的服務,不然某些特定系統會報錯 mWebView.getSettings().setJavaScriptEnabled(false); mWebView.clearHistory(); mWebView.clearView(); mWebView.removeAllViews(); try { mWebView.destroy(); } catch (Throwable ex) { } } }
Android 5.1以前的代碼
對比了5.1以前的代碼,它是不會存在這樣的問題的,如下是kitkat的代碼,它少了一行 if (isDestroyed()) return;,有點不明白,爲何google在高版本把這一行代碼加上。
/** * @see android.view.View#onDetachedFromWindow() */ public void onDetachedFromWindow() { mIsAttachedToWindow = false; hideAutofillPopup(); if (mNativeAwContents != 0) { nativeOnDetachedFromWindow(mNativeAwContents); } mContentViewCore.onDetachedFromWindow(); if (mComponentCallbacks != null) { mContainerView.getContext().unregisterComponentCallbacks(mComponentCallbacks); mComponentCallbacks = null; } if (mPendingDetachCleanupReferences != null) { for (int i = 0; i < mPendingDetachCleanupReferences.size(); ++i) { mPendingDetachCleanupReferences.get(i).cleanupNow(); } mPendingDetachCleanupReferences = null; } }
結束
在開發過程當中,還發現一個支付寶SDK的內存問題,也是由於這個緣由,具體的類是 com.alipay.sdk.app.H5PayActivity,咱們沒辦法,也想了一個不是辦法的辦法,在每一個activity destroy時,去主動把 H5PayActivity 中的webview從它的parent中移除,但這個問題限制太多,不是特別好,但的確也能解決問題,方案以下:
/** * 解決支付寶的 com.alipay.sdk.app.H5PayActivity 類引發的內存泄漏。 * * <p> * 說明:<br> * 這個方法是經過監聽H5PayActivity生命週期,得到實例後,經過反射將webview拿出來,從 * 它的parent中移除。若是後續支付寶SDK官方修復了該問題,則咱們不須要再作什麼了,無論怎麼 * 說,這個方案都是很是噁心的解決方案,很是不推薦。同時,若是更新了支付寶SDK後,那麼內部被混淆 * 的字段名可能更改,因此該方案也無效了。 * </p> * * @param activity */ public static void resolveMemoryLeak(Activity activity) { if (activity == null) { return; } String className = activity.getClass().getCanonicalName(); if (TextUtils.equals(className, "com.alipay.sdk.app.H5PayActivity")) { Object object = Reflect.on(activity).get("a"); if (DEBUG) { LogUtils.e(TAG, "AlipayMemoryLeak.resolveMemoryLeak activity = " + className + ", field = " + object); } if (object instanceof WebView) { WebView webView = (WebView) object; ViewParent parent = webView.getParent(); if (parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(webView); } } } }