WebView内存泄漏解决方法


前言:在项目的开发过程当中,因为对内存要求较高,最近对应用的内存分析比较在乎,前段时间监控图片内存,对Bitmap形成的内存泄漏进行了分析,并解决了问题。可是在图片内存泄漏以后,发如今访问网页的时候,webview居然也会有内存泄漏,虽然内存占用很小,可是用户屡次访问仍是存在隐患。

因而,开始对webview进行内存分析,发现webview下面的callback持有activity引用,形成webview内存没法释放,在网上也找了不少方法,可是webview.destory()等方法大都没法解决问题。

最后看到一篇文章,才算明了出现这个bug的缘由,按照做者的作法,确实解决了问题,安卓5.1和6.0系统都不存在内存泄漏问题。

文章附下:



销毁webview的方式

mWebView.removeAllViews();
/**、
* 这里内存泄漏了,由于它的父容器在退出前没有被销毁,因此就会持有引用,内存泄漏
* */
// mWebView.destroy();


改成

在 Android 5.1 系统上,在项目中遇到一个WebView引发的问题,每打开一个带webview的界面,退出后,这个activity都不会被释放,activity的实例会被持有,因为咱们项目中常常会用到浏览web页面的地方,可能引发内存积压,致使内存溢出的现象,因此这个问题仍是比较严重的。android

问题分析web

使用Android Studio的内存monitor,获得了如下的内存分析,我打开了三个BookDetailActivity界面(都有webview),检查结果显示有3个activity泄漏,以下图所示:app

这个问题仍是比较严重的,那么进一步看详细的信息,找出究竟是哪里引发的内存泄漏,详情的reference tree以下图所示:ide

从上图中能够看出,在第1层中的 TBReaderApplication 中的 mComponentCallbacks 成员变量,它是一个array list,它里面会持有住activity,引导关系是 mComponentCallbacks->AwContents->BaseWebView->BookDetailActivity, 代码在 Application 类里面,代码以下所示:post

public void registerComponentCallbacks(ComponentCallbacks callback) {
    synchronized (mComponentCallbacks) {
        mComponentCallbacks.add(callback);
    }
}

public void unregisterComponentCallbacks(ComponentCallbacks callback) {
    synchronized (mComponentCallbacks) {
        mComponentCallbacks.remove(callback);
    }
}

上面两个方法,会在 Context 基类中被调用,代码以下:this

/**
 * 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);
}

从第二张图咱们已经知道,是webview引发的内存泄漏,并且能看到是在 org.chromium.android_webview.AwContents 类中,难道是这个类注册了component callbacks,可是未反注册?通常按系统设计,都会反注册的,最有可能的缘由就是某些状况下致使不能正常反注册,很少说,read the fucking source。基于这个思路,我把chromium的源码下载下来,代码在这里 chromium_org(https://android.googlesource.com/platform/external/chromium_org/?spm=5176.100239.blogcont61612.7.j9EPtEgoogle

而后找到 org.chromium.android_webview.AwContents 类,看看这两个方法 onAttachedToWindow 和 onDetachedFromWindow:spa

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

系统会在attach处detach进行注册和反注册component callback,注意到 onDetachedFromWindow() 方法的第一行,if (isDestroyed()) return;, 若是 isDestroyed() 返回 true 的话,那么后续的逻辑就不能正常走到,因此就不会执行unregister的操做,经过看代码,能够获得,调用主动调用 destroy()方法,会致使 isDestroyed() 返回 true。.net

/**
 * Destroys this object and deletes its native counterpart.
 */
public void destroy() {
    if (isDestroyed()) return;
    // If we are attached, we have to call native detach to clean up
    // hardware resources.
    if (mIsAttachedToWindow) {
        nativeOnDetachedFromWindow(mNativeAwContents);
    }
    mIsDestroyed = true;
    new Handler().post(new Runnable() {
        @Override
        public void run() {
            destroyNatives();
        }
    });
}

通常状况下,咱们的activity退出的时候,都会主动调用 WebView.destroy() 方法,通过分析,destroy()的执行时间在onDetachedFromWindow以前,因此就会致使不能正常进行unregister()。设计

解决方案

找到了缘由后,解决方案也比较简单,核心思路就是让onDetachedFromWindow先走,那么在主动调用以前destroy(),把webview从它的parent上面移除掉。

ViewParent parent = mWebView.getParent();
if (parent != null) {
    ((ViewGroup) parent).removeView(mWebView);
}

mWebView.destroy();

完整的代码以下:

public void destroy() {
    if (mWebView != null) {
        // 若是先调用destroy()方法,则会命中if (isDestroyed()) return;这一行代码,须要先onDetachedFromWindow(),再
        // destory()
        ViewParent parent = mWebView.getParent();
        if (parent != null) {
            ((ViewGroup) parent).removeView(mWebView);
        }

        mWebView.stopLoading();
        // 退出时调用此方法,移除绑定的服务,不然某些特定系统会报错
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.clearView();
        mWebView.removeAllViews();

        try {
            mWebView.destroy();
        } catch (Throwable ex) {

        }
    }
}

Android 5.1以前的代码

对比了5.1以前的代码,它是不会存在这样的问题的,如下是kitkat的代码,它少了一行 if (isDestroyed()) return;,有点不明白,为何google在高版本把这一行代码加上。

/**
 * @see android.view.View#onDetachedFromWindow()
 */
public void onDetachedFromWindow() {
    mIsAttachedToWindow = false;
    hideAutofillPopup();
    if (mNativeAwContents != 0) {
        nativeOnDetachedFromWindow(mNativeAwContents);
    }

    mContentViewCore.onDetachedFromWindow();

    if (mComponentCallbacks != null) {
      mContainerView.getContext().unregisterComponentCallbacks(mComponentCallbacks);
      mComponentCallbacks = null;
    }

    if (mPendingDetachCleanupReferences != null) {
        for (int i = 0; i < mPendingDetachCleanupReferences.size(); ++i) {
            mPendingDetachCleanupReferences.get(i).cleanupNow();
        }
        mPendingDetachCleanupReferences = null;
    }
}

结束

在开发过程当中,还发现一个支付宝SDK的内存问题,也是由于这个缘由,具体的类是 com.alipay.sdk.app.H5PayActivity,咱们没办法,也想了一个不是办法的办法,在每一个activity destroy时,去主动把 H5PayActivity 中的webview从它的parent中移除,但这个问题限制太多,不是特别好,但的确也能解决问题,方案以下:

/**
 * 解决支付宝的 com.alipay.sdk.app.H5PayActivity 类引发的内存泄漏。
 *
 * <p>
 *     说明:<br>
 *         这个方法是经过监听H5PayActivity生命周期,得到实例后,经过反射将webview拿出来,从
 *         它的parent中移除。若是后续支付宝SDK官方修复了该问题,则咱们不须要再作什么了,无论怎么
 *         说,这个方案都是很是恶心的解决方案,很是不推荐。同时,若是更新了支付宝SDK后,那么内部被混淆
 *         的字段名可能更改,因此该方案也无效了。
 * </p>
 *
 * @param activity
 */
public static void resolveMemoryLeak(Activity activity) {
    if (activity == null) {
        return;
    }

    String className = activity.getClass().getCanonicalName();
    if (TextUtils.equals(className, "com.alipay.sdk.app.H5PayActivity")) {
        Object object = Reflect.on(activity).get("a");

        if (DEBUG) {
            LogUtils.e(TAG, "AlipayMemoryLeak.resolveMemoryLeak activity = " + className
                + ",  field = " + object);
        }

        if (object instanceof WebView) {
            WebView webView = (WebView) object;
            ViewParent parent = webView.getParent();
            if (parent instanceof ViewGroup) {
                ((ViewGroup) parent).removeView(webView);
            }
        }
    }
}