关于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); } } } }