背景
原文鏈接: http://leehong2005.com/2016/08/19/webview-memory-leak/
參考文章: 零號路雜貨鋪 - Android 5.1 Webview 內(nèi)存泄漏新場景 , 我也是在這個基礎(chǔ)之上進(jìn)行一些學(xué)習(xí),感謝~~
在 Android 5.1
系統(tǒng)上捞镰,在項(xiàng)目中遇到一個WebView引起的問題束昵,每打開一個帶webview的界面奏窑,退出后灭必,這個activity都不會被釋放特占,activity的實(shí)例會被持有理肺,由于我們項(xiàng)目中經(jīng)常會用到瀏覽web頁面的地方摄闸,可能引起內(nèi)存積壓,導(dǎo)致內(nèi)存溢出的現(xiàn)象妹萨,所以這個問題還是比較嚴(yán)重的年枕。
問題分析
使用Android Studio的內(nèi)存monitor,得到了以下的內(nèi)存分析乎完,我打開了三個BookDetailActivity界面(都有webview)熏兄,檢查結(jié)果顯示有3個activity泄漏,如下圖所示:
這個問題還是比較嚴(yán)重的树姨,那么進(jìn)一步看詳細(xì)的信息摩桶,找出到底是哪里引起的內(nèi)存泄漏,詳情的reference tree如下圖所示:
從上圖中可以看出帽揪,在第1層中的 TBReaderApplication
中的 mComponentCallbacks 成員變量硝清,它是一個array list,它里面會持有住activity转晰,引導(dǎo)關(guān)系是 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
基類中被調(diào)用蔗崎,代碼如下:
/**
* 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);
}
從第二張圖我們已經(jīng)知道,是webview引起的內(nèi)存泄漏扰藕,而且能看到是在 org.chromium.android_webview.AwContents
類中缓苛,難道是這個類注冊了component callbacks,但是未反注冊邓深?一般按系統(tǒng)設(shè)計(jì)未桥,都會反注冊的番官,最有可能的原因就是某些情況下導(dǎo)致不能正常反注冊,不多說钢属,read the fucking source徘熔。基于這個思路淆党,我把chromium的源碼下載下來酷师,代碼在這里 chromium_org
然后找到 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();
}
系統(tǒng)會在attach處detach進(jìn)行注冊和反注冊component callback染乌,注意到 onDetachedFromWindow() 方法的第一行山孔,if (isDestroyed()) return;
, 如果 isDestroyed() 返回 true 的話荷憋,那么后續(xù)的邏輯就不能正常走到台颠,所以就不會執(zhí)行unregister的操作,通過看代碼勒庄,可以得到串前,調(diào)用主動調(diào)用 destroy()
方法侦香,會導(dǎo)致 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退出的時候刀疙,都會主動調(diào)用 WebView.destroy()
方法,經(jīng)過分析碳想,destroy()的執(zhí)行時間在onDetachedFromWindow之前寇损,所以就會導(dǎo)致不能正常進(jìn)行unregister()彤委。
解決方案
找到了原因后铐尚,解決方案也比較簡單拨脉,核心思路就是讓onDetachedFromWindow先走,那么在主動調(diào)用之前destroy()宣增,把webview從它的parent上面移除掉玫膀。
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.destroy();
完整的代碼如下:
public void destroy() {
if (mWebView != null) {
// 如果先調(diào)用destroy()方法,則會命中if (isDestroyed()) return;這一行代碼统舀,需要先onDetachedFromWindow()匆骗,再
// destory()
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
// 退出時調(diào)用此方法劳景,移除綁定的服務(wù)誉简,否則某些特定系統(tǒng)會報錯
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;
,有點(diǎn)不明白筋量,為什么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;
}
}
結(jié)束語
在開發(fā)過程中碉熄,還發(fā)現(xiàn)一個支付寶SDK的內(nèi)存問題,也是因?yàn)檫@個原因肋拔,具體的類是 com.alipay.sdk.app.H5PayActivity
锈津,我們沒辦法,也想了一個不是辦法的辦法凉蜂,在每個activity destroy時琼梆,去主動把 H5PayActivity 中的webview從它的parent中移除,但這個問題限制太多窿吩,不是特別好茎杂,但的確也能解決問題,方案如下:
/**
* 解決支付寶的 com.alipay.sdk.app.H5PayActivity 類引起的內(nèi)存泄漏纫雁。
*
* <p>
* 說明:<br>
* 這個方法是通過監(jiān)聽H5PayActivity生命周期煌往,獲得實(shí)例后,通過反射將webview拿出來轧邪,從
* 它的parent中移除刽脖。如果后續(xù)支付寶SDK官方修復(fù)了該問題,則我們不需要再做什么了忌愚,不管怎么
* 說曾棕,這個方案都是非常惡心的解決方案,非常不推薦菜循。同時翘地,如果更新了支付寶SDK后,那么內(nèi)部被混淆
* 的字段名可能更改癌幕,所以該方案也無效了衙耕。
* </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);
}
}
}
}
以上是對發(fā)現(xiàn)的WebView內(nèi)存泄漏的一個簡單分析,權(quán)且記錄一下勺远。
(完)