1. 程式人生 > >Android 5.1 WebView記憶體洩漏問題及解決

Android 5.1 WebView記憶體洩漏問題及解決

問題背景

在排查專案記憶體洩漏過程中發現了一些由WebView引起的記憶體洩漏,經過測試發現該部分洩漏只會出現在android 5.1及以上的機型。雖然專案使用WebView的場景並不多,但秉承著一個洩漏都不放過的精神,我們肯定要把它給解決了。

遇到的問題

專案中使用WebView的頁面主要在FAQ頁面,問題也出現在多次進入退出時,發現記憶體佔用大,GC頻繁。使用LeakCanary觀察發現有兩個記憶體洩漏很頻繁:
圖1
圖2

我們分析一下這兩個洩漏:
從圖一我們可以發現是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之Android WebView常見問題及解決方案彙總 http://www.cnblogs.com/lee0oo0/p/4026774.html