Android 5.1 WebView内存泄漏问题及快速解决方法

问题背景

在排查项目内存泄漏过程中发现了一些由WebView引起的内存泄漏,经过测试发现该部分泄漏只会出现在android 5.1及以上的机型。虽然项目使用WebView的场景并不多,但秉承着一个泄漏都不放过的精神,我们肯定要把它给解决了。

遇到的问题

项目中使用WebView的页面主要在FAQ页面,问题也出现在多次进入退出时,发现内存占用大,GC频繁。使用LeakCanary观察发现有两个内存泄漏很频繁: 

我们分析一下这两个泄漏:

从图一我们可以发现是WebView的ContentViewCore中的成员变量mContainerView引用着AccessibilityManager的mAccessibilityStateChangeListeners导致activity不能被回收造成了泄漏。

引用关系:mAccessibilityStateChangeListeners->ContentViewCore->WebView->SettingHelpActivity

从图二可以发现引用关系是: mComponentCallbacks->AwContents->WebView->SettingHelpActivity

问题分析

我们找找mAccessibilityStateChangeListeners 与 mComponentCallbacks是在什么时候注册的,我们先看看mAccessibilityStateChangeListeners

AccessibilityManager.java

private final CopyOnWriteArrayList<AccessibilityStateChangeListener>

mAccessibilityStateChangeListeners = new CopyOnWriteArrayList<>();

/**

* Registers an {@link AccessibilityStateChangeListener} for changes in

* the global accessibility state of the system.

*

* @param listener The listener.

* @return True if successfully registered.

*/

public boolean addAccessibilityStateChangeListener(

@NonNull AccessibilityStateChangeListener listener) {

// Final CopyOnWriteArrayList - no lock needed.

return mAccessibilityStateChangeListeners.add(listener);

}

/**

* Unregisters an {@link AccessibilityStateChangeListener}.

*

* @param listener The listener.

* @return True if successfully unregistered.

*/

public boolean removeAccessibilityStateChangeListener(

@NonNull AccessibilityStateChangeListener listener) {

// Final CopyOnWriteArrayList - no lock needed.

return mAccessibilityStateChangeListeners.remove(listener);

}

上面这几个方法是在AccessibilityManager.class中定义的,根据方法调用可以发现在ViewRootImpl初始化会调用addAccessibilityStateChangeListener 添加一个listener,然后会在dispatchDetachedFromWindow的时候remove这个listener。

既然是有remove的,那为什么会一直引用着呢?我们稍后再分析。

我们再看看mComponentCallbacks是在什么时候注册的

Application.java

public void registerComponentCallbacks(ComponentCallbacks callback) {

synchronized (mComponentCallbacks) {

mComponentCallbacks.add(callback);

}

}

public void unregisterComponentCallbacks(ComponentCallbacks callback) {

synchronized (mComponentCallbacks) {

mComponentCallbacks.remove(callback);

}

}

上面这两个方法是在Application中定义的,根据方法调用可以发现是在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);

}

根据泄漏路径,难道是AwContents中注册了mComponentCallbacks未反注册么?

只有看chromium源码才能知道真正的原因了,好在chromium是开源的,我们在android 5.1 Chromium源码中找到我们需要的AwContents(自备梯子),看下在什么时候注册了

AwContents.java

@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();

mNativeGLDelegate.detachGLFunctor();

}

在以上两个方法中我们发现了mComponentCallbacks的踪影,

在onAttachedToWindow的时候调用mContext.registerComponentCallbacks(mComponentCallbacks)进行注册,

在onDetachedFromWindow中反注册。

我们仔细看看onDetachedFromWindow中的代码会发现

如果在onDetachedFromWindow的时候isDestroyed条件成立会直接return,这有可能导致无法执行mContext.unregisterComponentCallbacks(mComponentCallbacks);

也就会导致我们第一个泄漏,因为onDetachedFromWindow无法正常流程执行完也就不会调用ViewRootImp的dispatchDetachedFromWindow方法,那我们找下这个条件什么时候会为true

/**

* Destroys this object and deletes its native counterpart.

*/

public void destroy() {

mIsDestroyed = true;

destroyNatives();

}

发现是在destroy中设置为true的,也就是说执行了destroy()就会导致无法反注册。我们一般在activity中使用webview时会在onDestroy方法中调用mWebView.destroy();来释放webview。根据源码可以知道如果在onDetachedFromWindow之前调用了destroy那就肯定会无法正常反注册了,也就会导致内存泄漏。

问题的解决

我们知道了原因后,解决就比较容易了,就是在销毁webview前一定要onDetachedFromWindow,我们先将webview从它的父view中移除再调用destroy方法,代码如下:

@Override

protected void onDestroy() {

super.onDestroy();

if (mWebView != null) {

ViewParent parent = mWebView.getParent();

if (parent != null) {

((ViewGroup) parent).removeView(mWebView);

}

mWebView.removeAllViews();

mWebView.destroy();

mWebView = null;

}

}

还有个问题,就是为什么在5.1以下的机型不会内存泄漏呢,我们看下4.4的源码AwContents

/**

* @see android.view.View#onAttachedToWindow()

*

* Note that this is also called from receivePopupContents.

*/

public void onAttachedToWindow() {

if (mNativeAwContents == 0) return;

mIsAttachedToWindow = true;

mContentViewCore.onAttachedToWindow();

nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(),

mContainerView.getHeight());

updateHardwareAcceleratedFeaturesToggle();

if (mComponentCallbacks != null) return;

mComponentCallbacks = new AwComponentCallbacks();

mContainerView.getContext().registerComponentCallbacks(mComponentCallbacks);

}

/**

* @see android.view.View#onDetachedFromWindow()

*/

public void onDetachedFromWindow() {

mIsAttachedToWindow = false;

hideAutofillPopup();

if (mNativeAwContents != 0) {

nativeOnDetachedFromWindow(mNativeAwContents);

}

mContentViewCore.onDetachedFromWindow();

updateHardwareAcceleratedFeaturesToggle();

if (mComponentCallbacks != null) {

mContainerView.getContext().unregisterComponentCallbacks(mComponentCallbacks);

mComponentCallbacks = null;

}

mScrollAccessibilityHelper.removePostedCallbacks();

if (mPendingDetachCleanupReferences != null) {

for (int i = 0; i < mPendingDetachCleanupReferences.size(); ++i) {

mPendingDetachCleanupReferences.get(i).cleanupNow();

}

mPendingDetachCleanupReferences = null;

}

}

我们可以看到在onDetachedFromWindow方法上是没有isDestroyed这个判断条件的,这也证明了就是这个原因造成的内存泄漏。

问题的总结

使用webview容易造成内存泄漏,如果使用没有正确的去释放销毁很容易造成oom。webview使用也有很多的坑,需多多测试。

以上这篇Android 5.1 WebView内存泄漏问题及快速解决方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。

以上是 Android 5.1 WebView内存泄漏问题及快速解决方法 的全部内容, 来源链接: utcz.com/z/324905.html

回到顶部