Android 弹窗常见问题

1.DialogFragment使用中show()方法遇到的IllegalStateException

  • 报错日志如下:

    1
    lang.IllegalStateException: Can not perform this action after onSaveInstanceState at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1493)
  • 出现该问题的原因

    • Activity 调用了onSaveInstanceState()以后有触发了dialog的显示,dialog.show()方法里边用的是commit()而不是commitAllowingStateLoss()
  • 追踪报错日志的来源

    • 于是,我挺好奇,show方法中只有两个参数,决定从getSupportFragmentManager()方法分析.FragmentManager是抽象类,我这里主要是看FragmentManagerImpl实现类代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    //第一步:
    public FragmentManager getSupportFragmentManager() {
    return mFragments.getSupportFragmentManager();
    }

    //第二步:
    public FragmentManager getSupportFragmentManager() {
    return mHost.getFragmentManagerImpl();
    }

    //第三步:
    FragmentManagerImpl getFragmentManagerImpl() {
    return mFragmentManager;
    }

    //第四步:看beginTransaction()方法
    @Override
    public FragmentTransaction beginTransaction() {
    return new BackStackRecord(this);
    }

    //第五步:看BackStackRecord类中看commit方法
    @Override
    public int commit() {
    return commitInternal(false);
    }

    @Override
    public int commitAllowingStateLoss() {
    return commitInternal(true);
    }

    //第六步:可以看到这俩函数的区别就是commitInternal()方法中参数一个为true,一个为false
    int commitInternal(boolean allowStateLoss) {
    if (mCommitted) throw new IllegalStateException("commit already called");
    if (FragmentManagerImpl.DEBUG) {
    Log.v(TAG, "Commit: " + this);
    LogWriter logw = new LogWriter(TAG);
    PrintWriter pw = new PrintWriter(logw);
    dump(" ", null, pw, null);
    pw.close();
    }
    mCommitted = true;
    if (mAddToBackStack) {
    mIndex = mManager.allocBackStackIndex(this);
    } else {
    mIndex = -1;
    }
    mManager.enqueueAction(this, allowStateLoss);
    return mIndex;
    }

    //第七步:再追踪到enqueueAction(this,allowStateLoss)
    public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
    if (!allowStateLoss) {
    checkStateLoss();
    }
    synchronized (this) {
    if (mDestroyed || mHost == null) {
    throw new IllegalStateException("Activity has been destroyed");
    }
    if (mPendingActions == null) {
    mPendingActions = new ArrayList<>();
    }
    mPendingActions.add(action);
    scheduleCommit();
    }
    }

    //第八步:checkStateLoss()方法,这里可以看到抛出的错误日志呢
    private void checkStateLoss() {
    if (mStateSaved) {
    throw new IllegalStateException(
    "Can not perform this action after onSaveInstanceState");
    }
    if (mNoTransactionsBecause != null) {
    throw new IllegalStateException(
    "Can not perform this action inside of " + mNoTransactionsBecause);
    }
    }

2.Toast偶尔报错Unable to add window

1
2
android.view.WindowManager$BadTokenException
Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
  • 查询报错日志是从哪里来的

    • image
  • 发生该异常的原因

    • 这个异常发生在Toast显示的时候,原因是因为token失效。通常情况下,一般是不会出现这种异常。但是由于在某些情况下, Android进程某个UI线程的某个消息阻塞。导致 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息之后,迟迟没有执行。这时候,NotificationManager 的超时检测结束,删除了 WMS 服务中的 token 记录。删除 token 发生在 Android 进程 show 方法之前。这就导致了上面的异常。
    • 测试代码。模拟一下异常的发生场景,其实很容易,只需要这样做就可以出现上面这个问题
    1
    2
    3
    4
    5
    6
    Toast.makeText(this,"潇湘剑雨-yc",Toast.LENGTH_SHORT).show();
    try {
    Thread.sleep(20000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
  • 解决办法,目前见过好几种,思考一下那种比较好……

    • 第一种,既然是报is your activity running,那可以不可以在吐司之前先判断一下activity是否running呢?
    • 第二种,抛出异常增加try-catch,代码如下所示,最后仍然无法解决问题
      • 按照源码分析,异常是发生在下一个UI线程消息中,因此在上一个ui线程消息中加入try-catch是没有意义的。而且用到吐司地方这么多,这样做也不方便啦!
    • 第三种,那就是自定义类似吐司Toast的view控件。个人建议除非要求非常高,不然不要这样做。毕竟发生这种异常还是比较少见的
  • 哪些情况会发生该问题?

    • UI 线程执行了一条非常耗时的操作,比如加载图片等等,就类似上面用 sleep 模拟情况
    • 进程退后台或者息屏了,系统为了减少电量或者某种原因,分配给进程的cpu时间减少,导致进程内的指令并不能被及时执行,这样一样会导致进程看起来”卡顿”的现象
    • 当TN抛出消息的时候,前面有大量的 UI 线程消息等待执行,而每个 UI 线程消息虽然并不卡顿,但是总和如果超过了 NotificationManager 的超时时间,还是会出现问题

3.Toast运行在子线程问题

  • 先来看看问题代码,会出现什么问题呢?

    1
    2
    3
    4
    5
    6
    new Thread(new Runnable() {
    @Override
    public void run() {
    ToastUtils.showRoundRectToast("潇湘剑雨-杨充");
    }
    }).start();
    • 报错日志如下所示:
    • image
  • 然后找找报错日志从哪里来的

    • image
  • 子线程中吐司的正确做法,代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    new Thread(new Runnable() {
    @Override
    public void run() {
    Looper.prepare();
    ToastUtils.showRoundRectToast("潇湘剑雨-杨充");
    Looper.loop();
    }
    }).start();
  • 得出的结论

    • Toast也可以在子线程执行,不过需要手动提供Looper环境的。
    • Toast在调用show方法显示的时候,内部实现是通过Handler执行的,因此自然是不阻塞Binder线程,另外,如果addView的线程不是Loop线程,执行完就结束了,当然就没机会执行后续的请求,这个是由Hanlder的构造函数保证的。可以看看handler的构造函数,如果Looper==null就会报错,而Toast对象在实例化的时候,也会为自己实例化一个Hanlder,这就是为什么说“一定要在主线程”,其实准确的说应该是 “一定要在Looper非空的线程”。
    • Handler的构造函数如下所示:
    • image
    • image