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


我們分析一下這兩個(gè)泄漏:
從圖一我們可以發(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
問(wèn)題分析
我們找找mAccessibilityStateChangeListeners 與 mComponentCallbacks是在什么時(shí)候注冊(cè)的,我們先看看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);
}
上面這幾個(gè)方法是在AccessibilityManager.class中定義的,根據(jù)方法調(diào)用可以發(fā)現(xiàn)在ViewRootImpl初始化會(huì)調(diào)用addAccessibilityStateChangeListener 添加一個(gè)listener,然后會(huì)在dispatchDetachedFromWindow的時(shí)候remove這個(gè)listener。
既然是有remove的,那為什么會(huì)一直引用著呢?我們稍后再分析。
我們?cè)倏纯磎ComponentCallbacks是在什么時(shí)候注冊(cè)的
Application.java
public void registerComponentCallbacks(ComponentCallbacks callback) {
synchronized (mComponentCallbacks) {
mComponentCallbacks.add(callback);
}
}
public void unregisterComponentCallbacks(ComponentCallbacks callback) {
synchronized (mComponentCallbacks) {
mComponentCallbacks.remove(callback);
}
}
上面這兩個(gè)方法是在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中注冊(cè)了mComponentCallbacks未反注冊(cè)么?
只有看chromium源碼才能知道真正的原因了,好在chromium是開(kāi)源的,我們?cè)赼ndroid 5.1 Chromium源碼中找到我們需要的AwContents(自備梯子),看下在什么時(shí)候注冊(cè)了
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();
}
在以上兩個(gè)方法中我們發(fā)現(xiàn)了mComponentCallbacks的蹤影,
在onAttachedToWindow的時(shí)候調(diào)用mContext.registerComponentCallbacks(mComponentCallbacks)進(jìn)行注冊(cè),
在onDetachedFromWindow中反注冊(cè)。
我們仔細(xì)看看onDetachedFromWindow中的代碼會(huì)發(fā)現(xiàn)
如果在onDetachedFromWindow的時(shí)候isDestroyed條件成立會(huì)直接return,這有可能導(dǎo)致無(wú)法執(zhí)行mContext.unregisterComponentCallbacks(mComponentCallbacks);
也就會(huì)導(dǎo)致我們第一個(gè)泄漏,因?yàn)閛nDetachedFromWindow無(wú)法正常流程執(zhí)行完也就不會(huì)調(diào)用ViewRootImp的dispatchDetachedFromWindow方法,那我們找下這個(gè)條件什么時(shí)候會(huì)為true
/**
* Destroys this object and deletes its native counterpart.
*/
public void destroy() {
mIsDestroyed = true;
destroyNatives();
}
發(fā)現(xiàn)是在destroy中設(shè)置為true的,也就是說(shuō)執(zhí)行了destroy()就會(huì)導(dǎo)致無(wú)法反注冊(cè)。我們一般在activity中使用webview時(shí)會(huì)在onDestroy方法中調(diào)用mWebView.destroy();來(lái)釋放webview。根據(jù)源碼可以知道如果在onDetachedFromWindow之前調(diào)用了destroy那就肯定會(huì)無(wú)法正常反注冊(cè)了,也就會(huì)導(dǎo)致內(nèi)存泄漏。
問(wèn)題的解決
我們知道了原因后,解決就比較容易了,就是在銷(xiāo)毀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;
}
}
還有個(gè)問(wèn)題,就是為什么在5.1以下的機(jī)型不會(huì)內(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方法上是沒(méi)有isDestroyed這個(gè)判斷條件的,這也證明了就是這個(gè)原因造成的內(nèi)存泄漏。
問(wèn)題的總結(jié)
使用webview容易造成內(nèi)存泄漏,如果使用沒(méi)有正確的去釋放銷(xiāo)毀很容易造成oom。webview使用也有很多的坑,需多多測(cè)試。
以上這篇Android 5.1 WebView內(nèi)存泄漏問(wèn)題及快速解決方法就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Android setTag方法的key問(wèn)題解決辦法
這篇文章主要介紹了Android setTag方法的key問(wèn)題解決辦法的相關(guān)資料,需要的朋友可以參考下2016-09-09
Android ProgressDialog進(jìn)度條使用詳解
這篇文章主要對(duì)Android開(kāi)發(fā)之ProgressDialog讀取文件進(jìn)度進(jìn)行解析,感興趣的朋友可以參考一下2016-02-02
Android中ViewPager你所不知道的優(yōu)化技巧分享
提到ViewPager想必各位同學(xué)一點(diǎn)都不陌生,它是Android中最常用的組件之一,這篇文章小編就帶大家一起來(lái)看看ViewPager一些新的優(yōu)化方式吧2024-04-04
RxJava和Retrofit2的統(tǒng)一處理單個(gè)請(qǐng)求示例詳解
這篇文章主要給大家介紹了關(guān)于RxJava和Retrofit2的統(tǒng)一處理單個(gè)請(qǐng)求的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11
Kotlin實(shí)現(xiàn)半圓形進(jìn)度條的方法示例
這篇文章主要給大家介紹了關(guān)于Kotlin實(shí)現(xiàn)半圓形進(jìn)度條的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-03-03
Android帶圓形數(shù)字進(jìn)度的自定義進(jìn)度條示例
本篇文章主要介紹了Android帶圓形數(shù)字進(jìn)度的自定義進(jìn)度條示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02
Android實(shí)現(xiàn)圖片疊加效果的兩種方法
這篇文章主要介紹了Android實(shí)現(xiàn)圖片疊加效果的兩種方法,結(jié)合實(shí)例形式分析了Android實(shí)現(xiàn)圖片疊加效果的兩種操作方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-08-08
Android 判斷SIM卡是中國(guó)移動(dòng)\中國(guó)聯(lián)通\中國(guó)電信(移動(dòng)運(yùn)營(yíng)商)
本文給帶來(lái)兩種方法來(lái)判斷sim卡是屬于哪個(gè)運(yùn)營(yíng)商的,要實(shí)現(xiàn)此功能我們需要先獲取手機(jī)的imsi碼然后在判斷,對(duì)此功能感興趣的朋友一起通過(guò)本文學(xué)習(xí)吧2016-09-09
Android實(shí)現(xiàn)未讀消息小紅點(diǎn)顯示實(shí)例
大家好,本篇文章主要講的是Android實(shí)現(xiàn)未讀消息小紅點(diǎn)顯示實(shí)例,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2022-02-02

