一次Dialog導致的內存泄漏

今天上午10:30來到公司后,一頭扎進了張鴻洋大神所寫的OkHttpUtils源碼中去,繼續昨晚未完成的任務,11:30后,終于對整個框架有了一個比較全局、清晰的了解,心里更是對大神充滿滿滿的崇拜和敬意;然后回到公司的工作,打開jira,發現距離我兩個工位的美女測試姐姐給我提了一個頁面刷新bug,臥槽,居然還有bug,趕緊拿起數據線,插上Mac電腦和華為榮耀6手機,進入bug頁面,執行相關操作,程序按正常邏輯自動退出進入上一層頁面,檢查應該刷新的兩個頁面,發現通過EventBus通知刷新的頁面都刷新了,沒問題啊,嗯嗯...?好像剛才執行點擊操作時,在頁面退出之前,手機屏幕好像出現了短暫的黑屏現象,確認應該沒看錯,趕緊打開Android Studio的log日志,發現如下:

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容