本篇文章已授權(quán)微信公眾號(hào)guolin_blog(郭霖)獨(dú)家發(fā)布
Android Profiler的使用可以看這里:https://blog.csdn.net/Double2hao/article/details/78784758
發(fā)現(xiàn)這個(gè)問題的原由是測(cè)試提出的一個(gè)bug,是某個(gè)地圖頁(yè)面多次操作以后會(huì)出現(xiàn)卡頓甚至?xí)嗀NR,很明顯肯定是內(nèi)存的問題,我就用Android Profiler查看了一下內(nèi)存,發(fā)現(xiàn)出現(xiàn)某個(gè)圖層操作的時(shí)候短時(shí)間內(nèi)會(huì)很頻繁的觸發(fā)GC操作,然后無(wú)意中發(fā)現(xiàn)退出這個(gè)地圖頁(yè)面的時(shí)候LeakCanary會(huì)說(shuō)此頁(yè)面泄露了10M+的內(nèi)存,雖然這個(gè)LeakCanary不是每次都很準(zhǔn)確,不過(guò)它報(bào)了就得去查看一下,然后在Android Profiler里發(fā)現(xiàn)此頁(yè)面(以后該頁(yè)面稱為MapActivity)在退出后仍然占用了大量?jī)?nèi)存,頻繁觸發(fā)GC先不管,先把這個(gè)問題解決掉,圖片如下:

這個(gè)MapActivity已經(jīng)退出了,看來(lái)是有實(shí)例在引用著它導(dǎo)致沒辦法釋放內(nèi)存,然后看一下都有誰(shuí)引用了此類

機(jī)智的我一眼就看到這個(gè)ConfigBean,這是個(gè)啥玩意?看來(lái)問題應(yīng)該出在了這里,然后我就搜索了一下這個(gè)類,發(fā)現(xiàn)是第三方Dialog庫(kù)com.hss01248.dialog里的,而context是此類里的一個(gè)屬性
/**
* Created by Administrator on 2016/10/9 0009.
*/
public class ConfigBean extends MyDialogBuilder implements Styleable {
public int type;
public Context context;
//省略其他代碼
}
好了,再查一下context是在哪里設(shè)置的,不過(guò)這個(gè)字段最好用弱引用WeakReference去包一下,而且是public的我覺得不太好吧。。。不過(guò)作者可能有他的考慮。。。如果是我的話我會(huì)用WeakReference去包一下,不然太容易內(nèi)存泄漏了,然后我找到了context設(shè)置的地方
public ConfigBean setActivity(Activity activity) {
this.context = activity;
return this;
}
在MapActivity類里我是這么調(diào)用的
override fun showLoading() {
StyledDialog.buildLoading()
.setCancelable(false, false)
.setActivity(this)
.show()
}
所以這個(gè)context是MapActivity,而內(nèi)存泄露的也是這個(gè)MapActivity,然后我們點(diǎn)擊前面的箭頭展開context,看誰(shuí)引用了ConfigBean

不知道為什么,其中ConfigBean$3這個(gè)類我并沒有找到,但是Tool的3個(gè)匿名類我在Tool的字節(jié)碼文件里找到了
public static void setListener(android.app.Dialog, com.hss01248.dialog.config.ConfigBean);
Code:
0: aload_0
1: ifnonnull 5
4: return
5: aload_0
6: new #27 // class com/hss01248/dialog/Tool$2
9: dup
10: aload_1
11: aload_0
12: invokespecial #28 // Method com/hss01248/dialog/Tool$2."<init>":(Lcom/hss01248/dialog/config/ConfigBean;Landroid/app/Dialog;)V
15: invokevirtual #29 // Method android/app/Dialog.setOnShowListener:(Landroid/content/DialogInterface$OnShowListener;)V
18: aload_0
19: new #30 // class com/hss01248/dialog/Tool$3
22: dup
23: aload_1
24: invokespecial #31 // Method com/hss01248/dialog/Tool$3."<init>":(Lcom/hss01248/dialog/config/ConfigBean;)V
27: invokevirtual #32 // Method android/app/Dialog.setOnCancelListener:(Landroid/content/DialogInterface$OnCancelListener;)V
30: aload_0
31: new #33 // class com/hss01248/dialog/Tool$4
34: dup
35: aload_1
36: aload_0
37: invokespecial #34 // Method com/hss01248/dialog/Tool$4."<init>":(Lcom/hss01248/dialog/config/ConfigBean;Landroid/app/Dialog;)V
40: invokevirtual #35 // Method android/app/Dialog.setOnDismissListener:(Landroid/content/DialogInterface$OnDismissListener;)V
43: return
可以看到,是Tool類的setListener方法里的代碼,然后我們看源碼里的這個(gè)方法
public static void setListener(final Dialog dialog, final ConfigBean bean) {
if(dialog ==null){
return;
}
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog0) {
if (bean.alertDialog!= null){
setMdBtnStytle(bean);
setTitleMessageStyle(bean.alertDialog,bean);
}
bean.listener.onShow();
DialogsMaintainer.addWhenShow(bean.context,dialog);
if (bean.type == DefaultConfig.TYPE_IOS_LOADING || bean.type == DefaultConfig.TYPE_MD_LOADING) {
DialogsMaintainer.addLoadingDialog(bean.context,dialog);
}
/*dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
Tool.showSoftKeyBoardDelayed(bean.needSoftKeyboard,bean.viewHolder);
Tool.showSoftKeyBoardDelayed(bean.needSoftKeyboard,bean.customContentHolder);*/
}
});
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog0) {
if(bean.type == DefaultConfig.TYPE_IOS_INPUT){
IosAlertDialogHolder iosAlertDialogHolder = (IosAlertDialogHolder) bean.viewHolder;
if(iosAlertDialogHolder!=null){
iosAlertDialogHolder.hideKeyBoard();
}
}
if(bean.listener!=null) {
bean.listener.onCancle();
}
/*DialogsMaintainer.removeWhenDismiss(dialog);
if (bean.type == DefaultConfig.TYPE_IOS_LOADING || bean.type == DefaultConfig.TYPE_MD_LOADING) {
DialogsMaintainer.dismissLoading(dialog);
}*/
}
});
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog0) {
// bean.context = null;
if(bean.listener !=null){
bean.listener.onDismiss();
}
DialogsMaintainer.removeWhenDismiss(dialog);
if (bean.type == DefaultConfig.TYPE_IOS_LOADING || bean.type == DefaultConfig.TYPE_MD_LOADING) {
DialogsMaintainer.dismissLoading(dialog);
}
}
});
}
可以看到,這3個(gè)類正是dialog設(shè)置的3個(gè)監(jiān)聽,是3個(gè)匿名類,而這3個(gè)匿名類都引用了外部的Dialog和ConfigBean,所以這3個(gè)匿名類持有參數(shù)傳遞過(guò)來(lái)的Dialog和ConfigBean兩個(gè)實(shí)例的強(qiáng)引用,我們先看其中的一個(gè)方法dialog.setOnShowListener的源碼
/**
* Sets a listener to be invoked when the dialog is shown.
* @param listener The {@link DialogInterface.OnShowListener} to use.
*/
public void setOnShowListener(@Nullable OnShowListener listener) {
if (listener != null) {
mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
} else {
mShowMessage = null;
}
}
而這個(gè)mShowMessage也對(duì)應(yīng)了我們上圖中的mShowMessage引用,我再發(fā)一次

其他兩個(gè)監(jiān)聽的設(shè)置也是一樣的,分別將3個(gè)匿名類作為Message的obj屬性存到了Message里,現(xiàn)在的情況是Message持有匿名類的實(shí)例,而匿名類持有Dialog和ConfigBean的實(shí)例
然后在我們隱藏Dialog的時(shí)候,調(diào)用了這個(gè)第三方庫(kù)的此方法
override fun hideLoading() {
StyledDialog.dismissLoading(this)
}
然后我們看一下這個(gè)方法的源碼
/**
* 一鍵讓loading消失.
*/
public static void dismissLoading(Activity activity) {
DialogsMaintainer.dismissLoading(activity);
}
然后StyledDialog類的這個(gè)方法又調(diào)用了DialogsMaintainer.dismissLoading(activity);我們繼續(xù)查看DialogsMaintainer類的此方法
...
private static HashMap<Activity, Set<Dialog>> dialogsOfActivity = new HashMap<>();
private static HashMap<Activity, Set<Dialog>> loadingDialogs = new HashMap<>();
...
public static void dismissLoading(Activity activity) {
if (activity == null) {
return;
}
if (!loadingDialogs.containsKey(activity)) {
return;
}
Set<Dialog> dialogSet = loadingDialogs.get(activity);
for (Dialog dialog : dialogSet) {
dialog.dismiss();
//在callback內(nèi)部自動(dòng)會(huì)去移除在dialogsOfActivity的引用
}
loadingDialogs.remove(activity);
}
我覺得
dialogsOfActivity和loadingDialogs這兩個(gè)Map也是用弱引用比較好
這個(gè)方法我們會(huì)找到該Activity里所有的Dialog然后調(diào)用dialog的dismiss()方法
而Dialog的dismiss方法做了什么呢,看代碼
/**
* Dismiss this dialog, removing it from the screen. This method can be
* invoked safely from any thread. Note that you should not override this
* method to do cleanup when the dialog is dismissed, instead implement
* that in {@link #onStop}.
*/
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
當(dāng)不在創(chuàng)建Dialog的線程的時(shí)候,會(huì)調(diào)用Dialog線程的mHandler發(fā)送mDismissAction這個(gè)Runnable,否則就直接在創(chuàng)建Dialog的線程執(zhí)行dismissDialog()方法,mDismissAction這個(gè)Runnable的run方法會(huì)執(zhí)行dismissDialog()方法(這個(gè)Runnable只是執(zhí)行run方法,它并沒有新起一個(gè)線程去start),然后看dismissDialog()方法
void dismissDialog() {
if (mDecor == null || !mShowing) {
return;
}
if (mWindow.isDestroyed()) {
Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
return;
}
try {
mWindowManager.removeViewImmediate(mDecor);
} finally {
if (mActionMode != null) {
mActionMode.finish();
}
mDecor = null;
mWindow.closeAllPanels();
onStop();
mShowing = false;
sendDismissMessage();
}
}
繼續(xù)看sendDismissMessage()方法
private void sendDismissMessage() {
if (mDismissMessage != null) {
// Obtain a new message so this dialog can be re-used
Message.obtain(mDismissMessage).sendToTarget();
}
}
可以看到之前將匿名類設(shè)置給自己obj屬性的Message將自己發(fā)送到了它的targerHandler所在Looper中的MessageQueue中
到現(xiàn)在了我們根據(jù)下圖總結(jié)一下

這里有個(gè)套路,每行所在的類被下一行in前面指針?biāo)茫韵聢D就是:MapActivity被ConfigBean的context屬性持有,ConfigBean作為參數(shù)bean被Tool$2、Tool$3、Tool$4這三個(gè)匿名類持有(val$bean代表方法參數(shù)bean),這三個(gè)匿名類又被Message的obj屬性所持有,下面以此類推,不過(guò)下面就看不太清楚邏輯了,這時(shí)候我們需要Eclipse Memory Analyzer,也就是平時(shí)所說(shuō)的MAT軟件,下載過(guò)程不贅述,假設(shè)現(xiàn)在讀者下載好了MAT,然后用Android Studio點(diǎn)擊下圖中紅框里的按鈕導(dǎo)出剛才我們分析的東東

導(dǎo)出文件假如命名為leak.hprof,然后打開終端用hprof-conv leak.hprof leak_mat.hprof生成可以給MAT分析的hprof文件,打開后我選擇第一項(xiàng)

我剛發(fā)現(xiàn)直接點(diǎn)Cancel也可以打開文件并分析。。。
打開后點(diǎn)擊如圖所示的按鈕

然后在向右的三個(gè)箭頭那里輸入我們泄露的MapActivity

然后右鍵選擇空白的那個(gè)圖標(biāo)

選擇Path to GC Roots和下面的沒什么區(qū)別,這里我們選擇Merge Shortest Paths to GC Roots,然后排除不需要關(guān)心的弱引用軟引用之類的東東
然后結(jié)果出來(lái)了

看到這里,其實(shí)我們應(yīng)該明白,Tool$4這個(gè)匿名類持有ConfigBean,而ConfigBean持有的context是我們的MapActivity,這個(gè)Tool$4匿名類是這樣的
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog0) {
if(bean.listener !=null){
bean.listener.onDismiss();
}
DialogsMaintainer.removeWhenDismiss(dialog);
if (bean.type == DefaultConfig.TYPE_IOS_LOADING || bean.type == DefaultConfig.TYPE_MD_LOADING) {
DialogsMaintainer.dismissLoading(dialog);
}
}
});
方法參數(shù)new DialogInterface.OnDismissListener(){。。。}就是Tool$4,這個(gè)Tool$4被設(shè)置為了dialog的mDismissMessage類屬性的obj屬性,這個(gè)Message會(huì)在dialog執(zhí)行dismiss的時(shí)候發(fā)送到Looper所持有的MessageQueue中,在Looper的loop方法中取到這個(gè)Message消費(fèi)完以后會(huì)調(diào)用Message.recycleUnchecked方法去回收它所占用的內(nèi)存,而這時(shí)候肯定因?yàn)槟撤N原因無(wú)法釋放,然后可以思考一下,這個(gè)Message是Dialog中的一個(gè)類屬性,然后可以聯(lián)想到這個(gè)Dialog因?yàn)槟撤N原因被某類持有,然后查詢一下這個(gè)第三方庫(kù)會(huì)發(fā)現(xiàn)在DialogsMaintainer類中有2個(gè)靜態(tài)集合,而在調(diào)用DialogsMaintainer.dismissLoading之后的流程中并沒有把Dialog移除,好了這次內(nèi)存泄漏分析之旅到此結(jié)束。
提示:如果把ConfigBean中的context設(shè)置為弱引用,那么需要把
DialogsMaintainer中的兩個(gè)用到Activity的靜態(tài)map的key也變?yōu)槿跻茫驗(yàn)檫@兩個(gè)靜態(tài)map的key和ConfigBean中的context類屬性是同一個(gè)值,弱引用的特性是當(dāng)一個(gè)對(duì)象僅僅被weak reference指向,而沒有任何其他strong reference指向的時(shí)候,如果GC運(yùn)行,那么這個(gè)對(duì)象就會(huì)被回收。所以如果只把ConfigBean中的context類屬性改為弱引用,其他地方仍然有這個(gè)指針的強(qiáng)引用那么這樣的改動(dòng)沒有任何效果。而在這個(gè)框架里如果要修復(fù)這個(gè)bug,應(yīng)該像上面說(shuō)明的那樣改動(dòng)。
其實(shí)我們公司的項(xiàng)目只是用到它顯示了一個(gè)Dialog,后期我要去掉這個(gè)框架自己做一個(gè)Dialog來(lái)用,這個(gè)故事告訴我們用第三方框架要謹(jǐn)慎?。。?!