本篇文章已授权微信公众号guolin_blog(郭霖)独家发布
Android Profiler的使用可以看这里:https://blog.csdn.net/Double2hao/article/details/78784758
发现这个问题的原由是测试提出的一个bug,是某个地图页面多次操作以后会出现卡顿甚至会ANR,很明显肯定是内存的问题,我就用Android Profiler查看了一下内存,发现出现某个图层操作的时候短时间内会很频繁的触发GC操作,然后无意中发现退出这个地图页面的时候LeakCanary会说此页面泄露了10M+的内存,虽然这个LeakCanary不是每次都很准确,不过它报了就得去查看一下,然后在Android Profiler里发现此页面(以后该页面称为MapActivity)在退出后仍然占用了大量内存,频繁触发GC先不管,先把这个问题解决掉,图片如下:
这个MapActivity已经退出了,看来是有实例在引用着它导致没办法释放内存,然后看一下都有谁引用了此类
机智的我一眼就看到这个ConfigBean,这是个啥玩意?看来问题应该出在了这里,然后我就搜索了一下这个类,发现是第三方Dialog库com.hss01248.dialog里的,而context是此类里的一个属性
/**
* Created by Administrator on 2016/10/9 0009.
*/
public class ConfigBean extends MyDialogBuilder implements Styleable {
public int type;
public Context context;
//省略其他代码
}
好了,再查一下context是在哪里设置的,不过这个字段最好用弱引用WeakReference去包一下,而且是public的我觉得不太好吧。。。不过作者可能有他的考虑。。。如果是我的话我会用WeakReference去包一下,不然太容易内存泄漏了,然后我找到了context设置的地方
public ConfigBean setActivity(Activity activity) {
this.context = activity;
return this;
}
在MapActivity类里我是这么调用的
override fun showLoading() {
StyledDialog.buildLoading()
.setCancelable(false, false)
.setActivity(this)
.show()
}
所以这个context是MapActivity,而内存泄露的也是这个MapActivity,然后我们点击前面的箭头展开context,看谁引用了ConfigBean
不知道为什么,其中ConfigBean$3这个类我并没有找到,但是Tool的3个匿名类我在Tool的字节码文件里找到了
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方法里的代码,然后我们看源码里的这个方法
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个类正是dialog设置的3个监听,是3个匿名类,而这3个匿名类都引用了外部的Dialog和ConfigBean,所以这3个匿名类持有参数传递过来的Dialog和ConfigBean两个实例的强引用,我们先看其中的一个方法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;
}
}
而这个mShowMessage也对应了我们上图中的mShowMessage引用,我再发一次
其他两个监听的设置也是一样的,分别将3个匿名类作为Message的obj属性存到了Message里,现在的情况是Message持有匿名类的实例,而匿名类持有Dialog和ConfigBean的实例
然后在我们隐藏Dialog的时候,调用了这个第三方库的此方法
override fun hideLoading() {
StyledDialog.dismissLoading(this)
}
然后我们看一下这个方法的源码
/**
* 一键让loading消失.
*/
public static void dismissLoading(Activity activity) {
DialogsMaintainer.dismissLoading(activity);
}
然后StyledDialog类的这个方法又调用了DialogsMaintainer.dismissLoading(activity);
我们继续查看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内部自动会去移除在dialogsOfActivity的引用
}
loadingDialogs.remove(activity);
}
我觉得
dialogsOfActivity
和loadingDialogs
这两个Map也是用弱引用比较好
这个方法我们会找到该Activity里所有的Dialog然后调用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);
}
}
当不在创建Dialog的线程的时候,会调用Dialog线程的mHandler发送mDismissAction这个Runnable,否则就直接在创建Dialog的线程执行dismissDialog()方法,mDismissAction这个Runnable的run方法会执行dismissDialog()方法(这个Runnable只是执行run方法,它并没有新起一个线程去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();
}
}
继续看sendDismissMessage()方法
private void sendDismissMessage() {
if (mDismissMessage != null) {
// Obtain a new message so this dialog can be re-used
Message.obtain(mDismissMessage).sendToTarget();
}
}
可以看到之前将匿名类设置给自己obj属性的Message将自己发送到了它的targerHandler所在Looper中的MessageQueue中
到现在了我们根据下图总结一下
这里有个套路,每行所在的类被下一行in前面指针所引用,所以下图就是:MapActivity被ConfigBean的context属性持有,ConfigBean作为参数bean被Tool$2、Tool$3、Tool$4这三个匿名类持有(val$bean代表方法参数bean),这三个匿名类又被Message的obj属性所持有,下面以此类推,不过下面就看不太清楚逻辑了,这时候我们需要Eclipse Memory Analyzer,也就是平时所说的MAT软件,下载过程不赘述,假设现在读者下载好了MAT,然后用Android Studio点击下图中红框里的按钮导出刚才我们分析的东东
导出文件假如命名为leak.hprof,然后打开终端用hprof-conv leak.hprof leak_mat.hprof生成可以给MAT分析的hprof文件,打开后我选择第一项
我刚发现直接点Cancel也可以打开文件并分析。。。
打开后点击如图所示的按钮
然后在向右的三个箭头那里输入我们泄露的MapActivity
然后右键选择空白的那个图标
选择Path to GC Roots和下面的没什么区别,这里我们选择Merge Shortest Paths to GC Roots,然后排除不需要关心的弱引用软引用之类的东东
然后结果出来了
看到这里,其实我们应该明白,Tool$4这个匿名类持有ConfigBean,而ConfigBean持有的context是我们的MapActivity,这个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);
}
}
});
方法参数new DialogInterface.OnDismissListener(){。。。}就是Tool$4,这个Tool$4被设置为了dialog的mDismissMessage类属性的obj属性,这个Message会在dialog执行dismiss的时候发送到Looper所持有的MessageQueue中,在Looper的loop方法中取到这个Message消费完以后会调用Message.recycleUnchecked方法去回收它所占用的内存,而这时候肯定因为某种原因无法释放,然后可以思考一下,这个Message是Dialog中的一个类属性,然后可以联想到这个Dialog因为某种原因被某类持有,然后查询一下这个第三方库会发现在DialogsMaintainer类中有2个静态集合,而在调用DialogsMaintainer.dismissLoading之后的流程中并没有把Dialog移除,好了这次内存泄漏分析之旅到此结束。
提示:如果把ConfigBean中的context设置为弱引用,那么需要把
DialogsMaintainer中的两个用到Activity的静态map的key也变为弱引用,因为这两个静态map的key和ConfigBean中的context类属性是同一个值,弱引用的特性是当一个对象仅仅被weak reference指向,而没有任何其他strong reference指向的时候,如果GC运行,那么这个对象就会被回收。所以如果只把ConfigBean中的context类属性改为弱引用,其他地方仍然有这个指针的强引用那么这样的改动没有任何效果。而在这个框架里如果要修复这个bug,应该像上面说明的那样改动。
其实我们公司的项目只是用到它显示了一个Dialog,后期我要去掉这个框架自己做一个Dialog来用,这个故事告诉我们用第三方框架要谨慎啊!!!