一次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点了,明天再弄清原因。
明天打算把一个内存泄漏引发的血案给阅读一下,彻底理清这里面的缘由。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容