您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關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出現內存泄漏如何解決有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注億速云行業資訊頻道,感謝大家的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。