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

时间: 2017-05-08

Android 解决WebView无法上传文件的问题

Android 解决WebView无法上传文件的问题 Android原生的WebView并不支持上传文件,需要我们自己实现相应的方法.于是我把工作中的相关代码记录下来.下次直接拿来用就行了.一点一滴都是经验. 1.需要定义三个变量 private ValueCallback<Uri[]> uploadMessageAboveL; private final static int FILE_CHOOSER_RESULT_CODE = 10000; private ValueCallback<

Android WebView的使用方法总结

 Android WebView的使用方法 Android app打开H5页一般要实现如下需求: 1.打开指定url网页: 2.点击链接可以跳转到下一页,并更新标题: 3.按back键或左箭头可以返回上一页: 4.当webview显示的是第一级url时, 按返回键或左箭头关闭当前界面: 5.WebView如何传值给android, 例如使用H5登录成功后返回姓名.token等等字段. 6.支持JavaScript, 支持显示js对话框. 7.无网络时显示默认布局, 以提高用户体验. 8.避免We

Android WebView 不支持 H5 input type="file" 解决方法

最近因为赶项目进度,因此将本来要用原生控件实现的界面,自己做了H5并嵌入webview中.发现点击H5中 input type="file" 标签 不能打开android资源管理器. 通过网络搜索发现是因为 android webview 由于考虑安全原因屏蔽了 input type="file" 这个功能 . 经过不懈的努力,以及google 翻译的帮助 在 stackoverflow 中找到了解决的方法. 具体可以理解为 重写webview 的WebChrome

详解Android Webview加载网页时发送HTTP头信息

详解Android Webview加载网页时发送HTTP头信息 当你点击一个超链接进行跳转时,WebView会自动将当前地址作为Referer(引荐)发给服务器,因此很多服务器端程序通过是否包含referer来控制盗链,所以有些时候,直接输入一个网络地址,可能有问题,那么怎么解决盗链控制问题呢,其实在webview加载时加入一个referer就可以了,如何添加呢? 从Android 2.2 (也就是API 8)开始,WebView新增加了一个接口方法,就是为了便于我们加载网页时又想发送其他的HT

Android如何让WebView中的HTML5页面实现视频全屏播放

前言 本文主要是将最近工作中遇到的一个问题进行总结分享,主要介绍的是如何让WebView中H5页面全屏播放视频.关于这个问题,做一下简单分析,希望对大家有所帮助,下面话不多说了,来看看详细的介绍吧. 效果图 运行效果 其实很简单,就是配置问题.关键地方配好了,基本没什么问题了. 硬件加速 设置WebView 在清单需要配置的AndroidManifest.xml <application android:allowBackup="true" android:icon="

Android webview 内存泄露的解决方法

Android webview 内存泄露的解决方法 最近在activity嵌套webview显示大量图文发现APP内存一直在涨,没法释放内存,查了很多资料,大概是webview的一个BUG,引用了activity导致内存泄漏,所以就尝试传递getApplicationContext. 1.避免在xml直接写webview控件,这样会引用activity,所以在xml写一个LinearLayout,然后 linearLayout.addView(new MyWebview(getApplicati

Android实现webview实例代码

webview是一个很简单的功能,代码没有什么逻辑上的难度,只是需要注意权限上的问题.其实在安卓编程的过程当中,权限问题可以算是出现的比较多的BUG. 1.MainAct package com.lxq.webview01; import android.app.Activity; import android.os.Bundle; import android.view.KeyEvent; import android.view.View; import android.view.View.O

Android中WebView实现点击超链接启动QQ的方法

前言 之前有次在面试的时候,面试官问了一个如何在WebView点击超链接启动类型QQ类似第三方应用,我当时的回答是用WebView与js交互可以做到.面试官听了没再说什么,应该是答案不是他期望的.今天发现原来可以这样实现,记录一下. 实现思路 在Web开发中,启动QQ来临时会话,可以通过一个URL链接 <a target="_blank" href="http://wpa.qq.com/msgrd?v=3&uin=748895431&site=qq&am

Android中WebView用法实例分析

本文实例讲述了Android中WebView用法.分享给大家供大家参考,具体如下: WebView相当于一个迷你浏览器,采用WebKit内核,因此完美支持html,javascript,css等. 在开发过程中应该注意几点: 1.AndroidManifest.xml中必须使用许可"android.permission.INTERNET",否则会出Web page not available错误. 2.如果访问的页面中有Javascript,则webview必须设置支持Javascri

Android中WebView的一些简单用法

Android中WebView的一些简单用法 一直想写一个关于 WebView 控件的 一些简单运用,都没什么时间,这次也是挤出时间写的,里面的一些基础知识就等有时间再更新讲解一下,今天就先把项目出来做一些简单介绍,过多的内容可以看我的源码,都传到github上了. 下面是项目的效果图: 应用用到的是 MVP 设计模式,对这种模式还不太了解的可以先自行google一下,不然项目估计会看的晕,虽然我的代码都很简洁的. 对于MVP 可以带着一个思路看源码,那就是 activity(或其他组件)通过

Android中WebView与Js交互的实现方法

获取WebView对象 调用WebView对象的getSettings()方法,获取WebSettings对象 调用WebSettings对象的setJavaScriptEnabled()方法,设置js可用,参数:布尔值 在判断是否支持js的时候,不要用alert(),默认不起作用,可以先用document.write()测试 调用WebView对象的addJavascriptInterface(obj, interfaceName)方法,添加js接口,参数:Object对象,String接口名

android中webview定位问题示例详解

前言 现在很多App里都内置了Web网页(Hyprid App),比如说很多电商平台,淘宝.京东.聚划算等等 京东首页 那么这种该如何实现呢?其实这是Android里一个叫WebView的组件实现的. 最近在做安卓的网页开发.有一个页面需要用到定位,但是一直定位获取失败.很难过.网上教程也很多,但是无一例外全部失败.最后老夫花了3天时间,呕心沥血,终于研制出了解决方案. 三步走战略: 一.获取权限 android 6.0 以后,需要动态的获取位置或者存储权限,按照各自的爱好放置位置.我是应用开启

Android中WebView控件支持地理位置定位方法

Android WebView从assets中加载html5页面,实现地理位置定位,有需要的朋友可以参考下. 今天调研一个html5页面的定位问题,发现在手机浏览器上html5是可以实现定位的,但是在webview中就无法定位了.而我居然以为html5的地理定位在webview中不可行. html5页面内容如下: <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.o

Android中WebView图片实现自适应的方法

本文实例讲述了Android中WebView图片实现自适应的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: WebSettings ws = tv.getSettings(); 加上这个属性后,html的图片就会以单列显示就不会变形占了别的位置 ws.setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN); //让缩放显示的最小值为起始 webView.setInitialScale(5); // 设置支持缩放 webSettin

Android中WebView加载网页设置进度条

本文实例为大家分享了Android中WebView加载网页设置进度条的具体代码,供大家参考,具体内容如下 效果: xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" a

Android 中ListView setOnItemClickListener点击无效原因分析

前言 最近在做项目的过程中,在使用listview的时候遇到了设置item监听事件的时候在没有回调onItemClick 方法的问题.我的情况是在item中有一个Button按钮.所以不会回调.上百度找到了解决办法有两种,如下: 1.在checkbox.button对应的view处加android:focusable="false" 复制代码 代码如下: android:clickable="false" android:focusableInTouchMode=&

Android 中使用EditText 点击全选再次点击取消全选功能

最近在开发浏览器碰到这么一个需求:点击地址栏的时候,需要全选并调出键盘,再次点击就取消全选显示光标.点击屏幕除地址栏其他位置时,键盘隐藏,隐藏光标. 大部分浏览器都是这样的逻辑,这样可以提高用户体验,减少操作. 代码很简单,这里我简化了逻辑,页面只有一个EditText. 布局文件如下:里面有两个属性需要注意 android:focusable="true" android:selectAllOnFocus="true" 完整布局文件 <?xml versio