欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2023-12-03 11:06:34
问题背景 在排查项目内存泄漏过程中发现了一些由webview引起的内存泄漏,经过测试发现该部分泄漏只会出现在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);
}

上面这几个方法是在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内存泄漏问题及快速解决方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。