美文网首页
Android弹窗的实现及相关bug

Android弹窗的实现及相关bug

作者: 俗人浮生 | 来源:发表于2019-03-04 21:24 被阅读0次

    弹窗是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

          本文链接:https://www.haomeiwen.com/subject/kjdkuqtx.html