今天上午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點了,明天再弄清原因。
明天打算把一個內存泄漏引發的血案給閱讀一下,徹底理清這里面的緣由。