Android弹窗的实现及相关bug

弹窗是APP用来与用户交互提醒的一种表现形式,就本人知道的实现方式有以下几种:
1)activity
2)fragment
3)popupwindow
4)dialog
5)DialogFragment
下面我们进行展开说明:
1、也许有人会问,activity如何实现弹窗的呢?其实可以的,只要你将activity设置为透明主题即可
2、fragment就不用说了,本来碎片的优点就是灵活可复用,实现弹窗不是问题,更何况后面要讲的DialogFragment本身就是继承Fragment的
3、popupwindow也没什么好说的,它与dialog最大的区别在于它是阻塞线程的,而dialog是非阻塞线程的
4、说起dialog,我们说一下有关的两个bug:

dialog的第一个bug:

WindowManager: android.view.WindowLeaked: Activity xxx.xxx.xxx.xxxActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{4ac945c4 V.E..... R.....ID 0,0-900,90} that was originally added here

所谓的“has leaked window”,就是窗口泄漏,我们都知道dialog是依赖于activity存在的,故创建dialog时的context必须是activity的context。而之所以会出现窗口泄露,一般情况是dialog正显示着,但activity却被销毁了,用代码重现大概是这样子的:

dialog.show();
finish();

换句话说,在activity被销毁前,没有调用dialog的dismiss方法,就会出现上面的窗口泄露bug
知道原因后,解决方案也非常简单:
1.主动销毁:在finish之前把dialog给dismiss掉就行了。
2.被动销毁:在activity的onDestroy中,或者根据自己项目具体情况,在activity生命周期覆写函数中把dialog 给dismiss掉。

dialog的第二个bug

java.lang.IllegalArgumentException: View=com.android.internal.policy.impl.PhoneWindow$DecorView{4b155550 V.E..... R.....I. 0,0-900,90} not attached to window manager

所谓“not attached to window manager”,就是说dialog没有可附加的窗口,一般情况是当activity被销毁后,调用dialog的show或dismiss方法就会出现该bug,用代码重现大概是这样子的:

finish();
handler.post(new Runnable() {
            @Override
            public void run() {
               dialog.dismiss();
            }
        },1000)

如上代码,activity销毁,1秒后调用dialog的dismiss方法,一般的业务场景为:使用线程进行网络请求后要关掉对话框,但此时activity已被销毁(手速很快的返回或其它原因退出当前activity),就会出现该bug。
同样知道原因后,解决方案也非常简单:
我们只需在调用dialog的show和dismiss的方法前,判断activity是否已经被销毁。为了便于使用,建议在创建dialog时直接覆盖show和dismiss两个方法即可,代码如下:

    Dialog dlg = new Dialog(activity){
            @Override
            public void show() {
                if(!activity.isFinishing())
                    super.show();
            }
            @Override
            public void dismiss() {
                if(!activity.isFinishing())
                   super.dismiss();
            }
        };

5、DialogFragment是在android 3.0时被引入的,有些人会奇怪:为什么还要引入这么一个东东呢?
其实嘛,上面dialog的两个bug可不是白讲的!没错,如果使用DialogFragment的话,完全不存在着上面那样的窗口泄露问题。
因为就如上面所说的,DialogFragment是继承于Fragment的,所以,它拥有Fragment的生命周期,由FragmentManager进行管理,故此,其明显比dialog有更大的好处,举个最简单的例子:一个activity上正显示着一个dialog,如果此时旋转了屏幕方向,activity重建后,dialog却消失了,并且会出现上面dialog的第一个bug——窗口泄露,而如果使用DialogFragment的话,则完全不受影响,activity重建后,dialog依然能够正常显示,这得益于DialogFragment拥有完整的生命周期。
下面来说说本人使用DialogFragment遇到的问题:
如果你将DialogFragment进行复用的话,当你多次调用其show方法的话,会出现如下异常:

java.lang.IllegalStateException: Fragment already added: LoadDialogFragment{4ad202d4 #0 loading}

正常来说,如果show和dismiss配对调用的话,是不会出现该问题的,但是,有时确实很难保证配对调用,保不齐哪里的异步就出现多次调用show呢?所以,该问题还是得解决的!
其实,从这个bug字面上来说,不就是说Fragment已经被添加了吗?那么在调用show之前,我们进行一下判断不是OK了吗?将代码改为如下:

  public void showLoading() {
        if (loadingDialog == null) {
            loadingDialog=new LoadDialogFragment();
        }
        if(!loadingDialog.isAdded()){
            loadingDialog.show(getSupportFragmentManager(),"loading");
        }
    }

可惜结果还是不行,两次调用 showLoading(),程序还是崩溃!
打了断点,发现第一次调用show方法后,mAdded这个变量依然是false,这就奇了怪啦!难道isAdded()这个API是摆设的吗?不可能!本着“存在即是合理”的原则,我再次查看了show方法的源码:

 public void show(FragmentManager manager, String tag) {
        mDismissed = false;
        mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commit();
    }

如上源码,我们注意到最后一步事务提交: ft.commit(),其实这个commit并不是立即执行的, 它会被发送到主线程的任务队列当中去, 当主线程准备好执行它的时候执行。
也就是说,其实它是异步的,故此才导致mAdded变量刷新不及时,所以,解决方案为,使事务提交即时生效,只需要在commit之后加上executePendingTransactions(),这样就能将异步转为同步,故代码更改为:

  public void showLoading() {
        if (loadingDialog == null) {
            loadingDialog=new LoadDialogFragment();
        }
        if(!loadingDialog.isAdded()){
            loadingDialog.show(getSupportFragmentManager(),"loading");
            getSupportFragmentManager().executePendingTransactions();//即时生效
        }
    }

经试验,原因如上述所说,该方案有效!
后继续查看源码,无意中发现DialogFragment除了show之外,还有showNow这个API,源码如下:

  public void showNow(FragmentManager manager, String tag) {
        mDismissed = false;
        mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commitNow();
    }

其实区别也在最后一步:ft.commit();变成了 ft.commitNow();
经查资料得知:之前用executePendingTransactions()会将所有在队列中还有你当前提交的transaction都执行了, 而commitNow()将只会执行你当前要提交的transaction. 所以commitNow()可以避免你不小心执行了那些你可能并不想执行的transactions。
同时,考虑到当activity被销毁后,getSupportFragmentManager()会出现空指针异常,故最终将代码更改为如下:

  public void showLoading() {
        if (loadingDialog == null) { 
            loadingDialog=new LoadDialogFragment();
        }
        if(!loadingDialog.isAdded() && !isFinishing()){
            loadingDialog.showNow(getSupportFragmentManager(),"loading");
        }
    }

最后,如同官方所云:推荐使用DialogFragment来创建对话框,不推荐直接用Dialog创建对话框。
另外,如果弹窗是从底部出来的话,可考虑直接使用android.support.design.widget.BottomSheetDialogFragment哦,O(∩_∩)O

你可能感兴趣的:(Android弹窗的实现及相关bug)