Android 5.1 WebView內(nèi)存泄漏問題及快速解決方法
問題背景
在排查項(xiàng)目內(nèi)存泄漏過程中發(fā)現(xiàn)了一些由WebView引起的內(nèi)存泄漏,經(jīng)過測試發(fā)現(xiàn)該部分泄漏只會出現(xiàn)在android 5.1及以上的機(jī)型。雖然項(xiàng)目使用WebView的場景并不多,但秉承著一個泄漏都不放過的精神,我們肯定要把它給解決了。
遇到的問題
項(xiàng)目中使用WebView的頁面主要在FAQ頁面,問題也出現(xiàn)在多次進(jìn)入退出時,發(fā)現(xiàn)內(nèi)存占用大,GC頻繁。使用LeakCanary觀察發(fā)現(xiàn)有兩個內(nèi)存泄漏很頻繁:


我們分析一下這兩個泄漏:
從圖一我們可以發(fā)現(xiàn)是WebView的ContentViewCore中的成員變量mContainerView引用著AccessibilityManager的mAccessibilityStateChangeListeners導(dǎo)致activity不能被回收造成了泄漏。
引用關(guān)系:mAccessibilityStateChangeListeners->ContentViewCore->WebView->SettingHelpActivity
從圖二可以發(fā)現(xiàn)引用關(guān)系是: 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中定義的,根據(jù)方法調(diào)用可以發(fā)現(xiàn)在ViewRootImpl初始化會調(diào)用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中定義的,根據(jù)方法調(diào)用可以發(fā)現(xiàn)是在Context 基類中被調(diào)用
/**
* 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);
}
根據(jù)泄漏路徑,難道是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();
}
在以上兩個方法中我們發(fā)現(xiàn)了mComponentCallbacks的蹤影,
在onAttachedToWindow的時候調(diào)用mContext.registerComponentCallbacks(mComponentCallbacks)進(jìn)行注冊,
在onDetachedFromWindow中反注冊。
我們仔細(xì)看看onDetachedFromWindow中的代碼會發(fā)現(xiàn)
如果在onDetachedFromWindow的時候isDestroyed條件成立會直接return,這有可能導(dǎo)致無法執(zhí)行mContext.unregisterComponentCallbacks(mComponentCallbacks);
也就會導(dǎo)致我們第一個泄漏,因?yàn)閛nDetachedFromWindow無法正常流程執(zhí)行完也就不會調(diào)用ViewRootImp的dispatchDetachedFromWindow方法,那我們找下這個條件什么時候會為true
/**
* Destroys this object and deletes its native counterpart.
*/
public void destroy() {
mIsDestroyed = true;
destroyNatives();
}
發(fā)現(xiàn)是在destroy中設(shè)置為true的,也就是說執(zhí)行了destroy()就會導(dǎo)致無法反注冊。我們一般在activity中使用webview時會在onDestroy方法中調(diào)用mWebView.destroy();來釋放webview。根據(jù)源碼可以知道如果在onDetachedFromWindow之前調(diào)用了destroy那就肯定會無法正常反注冊了,也就會導(dǎo)致內(nèi)存泄漏。
問題的解決
我們知道了原因后,解決就比較容易了,就是在銷毀webview前一定要onDetachedFromWindow,我們先將webview從它的父view中移除再調(diào)用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以下的機(jī)型不會內(nèi)存泄漏呢,我們看下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這個判斷條件的,這也證明了就是這個原因造成的內(nèi)存泄漏。
問題的總結(jié)
使用webview容易造成內(nèi)存泄漏,如果使用沒有正確的去釋放銷毀很容易造成oom。webview使用也有很多的坑,需多多測試。
以上這篇Android 5.1 WebView內(nèi)存泄漏問題及快速解決方法就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
簡單掌握Android開發(fā)中彩信的發(fā)送接收及其附件的處理
這篇文章主要介紹了簡單掌握Android開發(fā)中彩信的發(fā)送接收及其附件的處理,由于微信的流行,使用彩信的用戶已經(jīng)很少了,簡單了解即可,需要的朋友可以參考下2016-02-02
Android實(shí)現(xiàn)頁面跳轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)頁面跳轉(zhuǎn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
Android11及以上文件讀寫權(quán)限申請?jiān)敿?xì)介紹
安卓11改變了此前安卓系統(tǒng)對于文件管理的規(guī)則,在安卓 11 上,文件讀寫變成了特殊權(quán)限,下面這篇文章主要給大家介紹了關(guān)于Android11及以上文件讀寫權(quán)限申請的相關(guān)資料,需要的朋友可以參考下2022-08-08
Android的ListView多選刪除操作實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android的ListView多選刪除操作實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05
android開發(fā)中獲取手機(jī)分辨率大小的方法
不管是在我們的布局還是在實(shí)現(xiàn)代碼中進(jìn)行操控,我們的靈活性都不是局限于一個固定的數(shù)值,而是面對不同的手機(jī)對象都有一個適應(yīng)的數(shù)值。2013-04-04
Android中截取當(dāng)前屏幕圖片的實(shí)例代碼
該篇文章是說明在Android手機(jī)或平板電腦中如何實(shí)現(xiàn)截取當(dāng)前屏幕的功能,并把截取的屏幕保存到SDCard中的某個目錄文件夾下面。實(shí)現(xiàn)的代碼如下:2013-08-08
RecyclerView實(shí)現(xiàn)插入和刪除
這篇文章主要為大家詳細(xì)介紹了RecyclerView實(shí)現(xiàn)插入和刪除,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
Android NestedScrolling嵌套滾動的示例代碼
這篇文章主要介紹了Android NestedScrolling嵌套滾動的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05
Android網(wǎng)絡(luò)編程之UDP通信模型實(shí)例
這篇文章主要介紹了Android網(wǎng)絡(luò)編程之UDP通信模型實(shí)例,本文給出了服務(wù)端代碼和客戶端代碼,需要的朋友可以參考下2014-10-10
Android Studio 自定義Debug變量視圖的方法
這篇文章主要介紹了Android Studio 自定義Debug變量視圖的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07

