Android   发布时间:2022-04-28  发布网站:大佬教程  code.js-code.com
大佬教程收集整理的这篇文章主要介绍了Android 5.1 WebView内存泄漏问题及快速解决方法大佬教程大佬觉得挺不错的,现在分享给大家,也给大家做个参考。

问题背景

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

遇到的问题

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

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

Android 5.1 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);
}

上面这几个@L_618_21@是在Accessibilitymanager.class中定义的,根据@L_618_21@调用可以发现在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);
  }
}

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

在以上两个@L_618_21@中我们发现了R_228_11845@ComponentCallBACks的踪影,

在onAttachedToWindow的时候@L_727_23@mContext.registerComponentCallBACks(mComponentCallBACks)进行注册

在onDetachedFromWindow中反注册

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

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

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

/**

   * Destroys this object and deletes its native counterpart.

   */

  public void destroy() {

    mIsDestroyed = true;

    destroyNatives();

  }

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

问题的解决

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

@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.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@L_618_21@上是没有isDestroyed这个判断条件的,这也证明了就是这个原因造成的内存泄漏。

问题的总结

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

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

大佬总结

以上是大佬教程为你收集整理的Android 5.1 WebView内存泄漏问题及快速解决方法全部内容,希望文章能够帮你解决Android 5.1 WebView内存泄漏问题及快速解决方法所遇到的程序开发问题。

如果觉得大佬教程网站内容还不错,欢迎将大佬教程推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。