Android webview 内存泄漏源码分析及处理办法

问题背景

在排查项目内存泄漏过程中发现了一些由WebView引起的内存泄漏。

问题描述

项目中使用WebView的页面出现在多次进入退出时,发现内存占用大,GC频繁。使用LeakCanary观察发现有两个内存泄漏很频繁:
Android webview 内存泄漏源码分析及处理办法
Android webview 内存泄漏源码分析及处理办法

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

从图一我们可以发现是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;
  }
}
上一篇:SQL之三Listener动态监听静态监听注册实例(The listener supports no services解决)


下一篇:WPF 如何Debug数据绑定