今天上午10:30来到公司后,一头扎进了张鸿洋大神所写的OkHttpUtils源码中去,继续昨晚未完成的任务,11:30后,终于对整个框架有了一个比较全局、清晰的了解,心里更是对大神充满满满的崇拜和敬意;然后回到公司的工作,打开jira,发现距离我两个工位的美女测试姐姐给我提了一个页面刷新bug,卧槽,居然还有bug,赶紧拿起数据线,插上Mac电脑和华为荣耀6手机,进入bug页面,执行相关操作,程序按正常逻辑自动退出进入上一层页面,检查应该刷新的两个页面,发现通过EventBus通知刷新的页面都刷新了,没问题啊,嗯嗯…?好像刚才执行点击操作时,在页面退出之前,手机屏幕好像出现了短暂的黑屏现象,确认应该没看错,赶紧打开Android Studio的log日志,发现如下:
我靠,居然发生了内存泄漏,按照日志调用栈的信息,应该是Activity在退出finish后,Dialog仍然持有Activity的引用,从而导致内存泄漏。
但是我明明已经调用了dialog.dismiss()方法了,这个Dialog与Activity应该没有关联引用了,怎么仍然持有引用?
下面是执行点击操作,弹出Dialog的代码
final TitleContentDialog dialog = new TitleContentDialog(ReceivableMoneyRecordActivity.this);
dialog.setContentView(getContentViewForDialog("确认删除此回款记录?"));
dialog.setTitle(null);
dialog.setCancelButton("取消", new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
dialog.setConfirmButton("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
deleteRecord();
}
});
dialog.show();
可以看出点击确定后,dialog先是dismiss(),然后执行deleteRecord()方法,deleteRecord()里面执行请求网络的删除操作。
打开浏览器,输入问题,都说是Activity finish时,Dialog仍然可见,要在Activity的onDestroy()方法中,确保已经关闭了Dialog,
OK,那我就在onDestroy()方法里面校验Dialog,我把Dialog提取出方法,成为Activity的一个成员变量mDeleteDialog;
同时重写Activity的onDestroy()方法:
@Override
protected void onDestroy() {
//防止内存泄漏
if (mDeleteDialog.isShowing()) {
mDeleteDialog.dismissImmediately();
}
mDeleteDialog = null;
super.onDestroy();
}
点击Android Studio的运行按钮,apk重新运行,再次进入问题页面,执行删除操作,黑屏事件再次发生,(^|_|^)
是谁?是谁?到底是谁窃取了我的Activity?
冷静了几秒,首先要肯定Activity在finish的时候,Dialog仍然持有Activity引用的真相。
为什么Dialog还持有Activity的引用?我明明在finish之前,就调用了Dialog的dismiss()方法。
mDeleteDialog.setConfirmButton("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
mDeleteDialog.dismiss();
deleteRecord();
}
});
进入Dialog的dismiss()方法
@Override
public void dismiss() {
if (mActivity.isFinishing()) {
return;
}
mDialogView.dismiss(new OnDialogDismissListener() {
@Override
public void onFinish() {
dismissImmediately();
}
});
}
mDialogView是一个自定义View,继承自RelativeLayout
/**
* 自定义标题、内容的Dialog容器
*/
public class TitleContentDialog extends Dialog {
/** 宿主 */
private Activity mActivity;
/** 实际显示的加载视图 */
private CustomTitleContentDialogView mDialogView;
...
private class CustomTitleContentDialogView extends RelativeLayout {
/*
* 显示配置及动画配置部分
*/
/** 对话框占比 */
private static final float RATIO = 0.75f;
/** 动画执行时间 */
private static final float ANIM_DUTAION = 200;
...
/** 动画执行器 */
private AnimRunnable mAnimRunnable;
...
/**
* 隐藏加载
*/
public void dismiss(OnDialogDismissListener listener) {
mDismissListener = listener;
mAnimRunnable.setAnimState(AnimState.DISMISSING);
}
...
/**
* 动画执行器
*
*
*/
private class AnimRunnable implements Runnable {
...
@Override
public void run() {
...
if (mAnimState != AnimState.NORMAL) {
if (mCurrentFrame < mTotalFrame) {
mCurrentFrame++;
} else {
if (mAnimState == AnimState.SHOWING) {
if (mNextState == AnimState.NONE) {
setAnimState(AnimState.NORMAL);
} else {
reset();
setAnimState(AnimState.DISMISSING);
}
} else if (mAnimState == AnimState.DISMISSING) {
reset();
if (mDismissListener != null) {
mDismissListener.onFinish();
}
}
}
}
...
}
}
}
/**
* 消失动画结束后的回调接口
*/
private interface OnDialogDismissListener {
/** 动画执行完毕 */
void onFinish();
}
}
从代码中可以看出,自定义的Dialog在执行重写的dismiss()方法时,先运行一段动画,动画执行完成后,再通过回调执行dismissImmediately()方法;
dismissImmediately()方法代码如下:
/**
* 立即关闭
*/
public void dismissImmediately() {
if (isShowing() && !mActivity.isFinishing()) {
Utils.hideInputMethod(mActivity);
TitleContentDialog.super.dismiss();
}
}
dismissImmediately()方法才会让Dialog立即消失,从而与Activity解除绑定;
在dismissImmediately()方法里面设置断点debug,发现程序没有执行if语句里面的代码;if条件里面的isShowing()是true,这是毫无疑问的,
那就是!mActivity.isFinishing()
不满足条件,也就是说此时的Activity已经执行完网络操作,运行了finish()方法。
整理一遍思绪,发现造成Dialog没有消失,而Activity已经finish的原因是Dialog消失时执行的动画,通过不断调用View.postDelayed(Runnable action,long delayMillis)方法,达到Dialog消失时渐变缩放的动画效果。
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
// Assume that post will succeed later
ViewRootImpl.getRunQueue().postDelayed(action, delayMillis);
return true;
}
原来调用了Handler.postDelayed()方法,熟悉Android开发应该了解Handler很容易造成Activity发生内存泄漏,不知道的同学可以看我的另一篇博客内存泄漏常见原因总结。
动画一共执行了200毫秒
/** 动画执行时间 */
private static final float ANIM_DUTAION = 200;
也就是说,在Dialog执行动画的200毫秒期间,Activity执行的网络操作已经结束,Activity运行finish()方法,但是Dialog在执行动画,还没消失,仍然持有Activity的引用,从而导致内存泄漏。
要解决这个问题,只要保证Activity运行finish()方法在Dialog执行完动画之后,由于网络请求的事件不确定,finish()方法只需要延迟200毫秒,就可以保证Activity的运行finish()方法在Dialog动画结束之后。
下面是网络请求执行完后的回调操作
@Override
public void OnRemoteApiFinish(BasicResponse response) {
if (response.status == BasicResponse.SUCCESS) {
Toast.makeText(ReceivableMoneyRecordActivity.this, "删除回款记录成功", Toast.LENGTH_SHORT).show();
EventBus.getDefault().post(new OnReceivableRecordListChangedEvent());
finish();
} else {
Toast.makeText(ReceivableMoneyRecordActivity.this, response.msg, Toast.LENGTH_SHORT).show();
}
}
finish()方法延时几百毫秒
@Override
public void OnRemoteApiFinish(BasicResponse response) {
if (response.status == BasicResponse.SUCCESS) {
Toast.makeText(ReceivableMoneyRecordActivity.this, "删除回款记录成功", Toast.LENGTH_SHORT).show();
_Application.getInstance().scheduleTask(new Runnable() {
@Override
public void run() {
EventBus.getDefault().post(new OnReceivableRecordListChangedEvent());
finish();
}
});
} else {
Toast.makeText(ReceivableMoneyRecordActivity.this, response.msg, Toast.LENGTH_SHORT).show();
}
}
调用了Application里面的scheduleTask(Runnable action)方法,达到延时500毫秒的目的。
启动Android studio,打开手机再次进入问题页面,执行删除操作,弹出确认Dialog,点击确认,退出,没有黑屏现象,问题终于解决了。
为了确保Dialog不会再导致内存泄漏,多测试几次,反复点击确认和取消按钮,我擦,第二次点击取消按钮,居然又出现黑屏现象,继续追踪,发现问题所在:
@Override
public void show() {
if (mActivity.isFinishing()) {
return;
}
super.show();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (!mActivity.isFinishing())
mDialogView.show();
}
}, 200);
}
定位到Handler.postDelayed()方法,这里发生了内存泄漏,这里为什么发生内存泄漏?暂时没找到确切原因,把Activity里面的全局变量Dialog还原,每次点击删除的时候,再new一个Dialog,这个问题又没了,现在猜测跟Message有关,但还是没想明白是哪个对象需要释放内存,但又被引用。
困…,已经到晚上11点了,明天再弄清原因。
明天打算把一个内存泄漏引发的血案给阅读一下,彻底理清这里面的缘由。