Android 5.1 WebView記憶體洩漏分析
背景
在 Android 5.1 系統上,在專案中遇到一個WebView引起的問題,每開啟一個帶webview的介面,退出後,這個activity都不會被釋放,activity的例項會被持有,由於我們專案中經常會用到瀏覽web頁面的地方,可能引起記憶體積壓,導致記憶體溢位的現象,所以這個問題還是比較嚴重的。
問題分析
使用Android Studio的記憶體monitor,得到了以下的記憶體分析,我開啟了三個BookDetailActivity介面(都有webview),檢查結果顯示有3個activity洩漏,如下圖所示:
這個問題還是比較嚴重的,那麼進一步看詳細的資訊,找出到底是哪裡引起的記憶體洩漏,詳情的reference tree如下圖所示:
從上圖中可以看出,在第1層中的 TBReaderApplication 中的 mComponentCallbacks 成員變數,它是一個array list,它裡面會持有住activity,引導關係是 mComponentCallbacks->AwContents->BaseWebView->BookDetailActivity, 程式碼在 Application 類裡面,程式碼如下所示:
public void registerComponentCallbacks(ComponentCallbacks callback) {
synchronized (mComponentCallbacks) {
mComponentCallbacks.add(callback);
}
}
public void unregisterComponentCallbacks(ComponentCallbacks callback) {
synchronized (mComponentCallbacks) {
mComponentCallbacks.remove(callback);
}
}
上面兩個方法,會在 Context 基類中被呼叫,程式碼如下:
/**
* 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);
}
}
}
}
以上是對發現的WebView記憶體洩漏的一個簡單分析,權且記錄一下。
相關文章
- WebView引起的記憶體洩漏WebView記憶體
- Android記憶體洩漏Android記憶體
- Android 記憶體洩漏Android記憶體
- 分析記憶體洩漏和goroutine洩漏記憶體Go
- valgrind 記憶體洩漏分析記憶體
- Android記憶體洩漏場景Android記憶體
- PHP 記憶體洩漏分析定位PHP記憶體
- Android備忘錄《記憶體洩漏》Android記憶體
- Android中的記憶體洩漏模式Android記憶體模式
- Android中常見的記憶體洩漏Android記憶體
- 初步探究Android記憶體洩漏(1)Android記憶體
- linux程式之記憶體洩漏分析Linux記憶體
- Android 輕鬆解決記憶體洩漏Android記憶體
- Android Handler機制之記憶體洩漏Android記憶體
- Android常見記憶體洩漏總結Android記憶體
- 解決記憶體洩漏(1)-ApacheKylin InternalThreadLocalMap洩漏問題分析記憶體Apachethread
- js記憶體洩漏JS記憶體
- jvm 記憶體洩漏JVM記憶體
- Java記憶體洩漏Java記憶體
- Handler記憶體洩漏分析及解決記憶體
- 記憶體洩漏問題分析之非託管資源洩漏記憶體
- Android效能優化篇之記憶體優化--記憶體洩漏Android優化記憶體
- Android記憶體溢位、記憶體洩漏常見案例分析及最佳實踐總結Android記憶體溢位
- 記一次堆外記憶體洩漏分析記憶體
- 慧銷平臺ThreadPoolExecutor記憶體洩漏分析thread記憶體
- 記憶體洩漏的原因記憶體
- Android中使用Handler為何造成記憶體洩漏?Android記憶體
- Android Native 記憶體洩漏系統化解決方案Android記憶體
- Android記憶體洩漏檢測與修復技巧Android記憶體
- 【記憶體洩漏和記憶體溢位】JavaScript之深入淺出理解記憶體洩漏和記憶體溢位記憶體溢位JavaScript
- JVM——記憶體洩漏與記憶體溢位JVM記憶體溢位
- iOS檢測記憶體洩漏iOS記憶體
- ThreadLocal記憶體洩漏問題thread記憶體
- 記憶體洩漏除錯工具記憶體除錯
- ThreadLocal真會記憶體洩漏?thread記憶體
- Perfdog 玩轉記憶體洩漏記憶體
- JavaScript之記憶體洩漏【四】JavaScript記憶體
- .Net程式記憶體洩漏解析記憶體
- 記憶體的分配與釋放,記憶體洩漏記憶體