使用系统Toast的问题

记录下在使用系统Toast存在的问题:

1. 当通知权限被关闭时华为等手机Toast不显示;
2. Toast队列机制上在不同手机上可能不同;
3. Toast的BadTokenException问题;

当发现系统Toast存在问题时,不少同学使用自定义TYPE_TOAST弹框来实现相同效果.虽然情况下效果都是OK的,但TYPE_TOAST依然会存在问题:

4. Android8.0之后的token null is not valid问题(实测部分机型问题);
5. Android7.1之后,不允许同时展示两个TYPE_TOAST弹窗(实测部分机型问题);

那么解决方案是:

  1. 通知权限未被关闭时,使用SystemToast(修复了问题2和问题3的系统Toast);
  2. 通知权限被关闭时,使用自定义的TYPE_TOAST弹窗;
  3. 当使用自定义Toast出现token null is not valid时,尝试使用ActivityToast(自定义的TYPE_APPLICATION_ATTACHED_DIALOG弹窗,只有当传入Context为Activity时,才会启用ActivityToast).

相信不少同学旧项目中封装的ToastUtil都是直接使用的ApplicationContext作为上下文,然后在需要弹窗的时候直接就是ToastUtil.show(str),这样的使用方式对于我们来说是最方便的啦。

当然,使用YToast你也依然可以沿用这种封装方式,但这种方式在下面这个场景中可能会无法成功展示出弹窗(该场景下原生Toast也一样无法弹出),不过请放心不会导致应用崩溃,而且这个场景出现的概率较小,有以下三个必要条件:

1.通知栏权限被关闭(通知栏权限默认都是打开的)
2.非MIUI手机
3.Android8.0以上的部分手机(我最近测试中的几部8.0+设备都不存在该问题)。

不过,如果想要保证在所有场景下都能正常展示弹窗,还是建议在YToast.make(context)时传入Activity作为上下文,这样在该场景下YToast会启用ActivityToast展示出弹窗。

接下来再详细分析下上面提到的五个问题。

1 关闭通知权限时Toast不显示

看下方Toast源码中的show()方法,通过AIDL获取到INotificationManager,并将接下来的显示流程控制权交给NotificationManagerService。NMS中会对Toast进行权限校验,当通知权限校验不通过时,Toast将不做展示。

当然不同ROM中NMS可能会有不同,比如MIUI就对这部分内容进行了修改,所以小米手机关闭通知权限不会导致Toast不显示。

/**
 * Show the view for the specified duration.
 */
public void show() {
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}

如何解决这个问题?只要能够绕过NotificationManagerService即可。

YToast通过使用TYPE_TOAST实现全局弹窗功能,不使用系统Toast,也没有使用NMS服务,因此不受通知权限限制。

2 系统Toast的队列机制在不同手机上可能会不相同

我找了四台设备,创建两个Gravity不同的Toast并调用show()方法,结果出现了四种展示效果:

  1. 荣耀5C-android7.0(只看到展示第一个Toast)
  2. 小米8-MIUI10(只看到展示第二个Toast,即新的Toast.show会中止当前Toast的展示)
  3. 红米6pro-MIUI9(两个Toast同时展示)
  4. 荣耀5C-android6.0(第一个TOAST展示完成后,第二个才开始展示)

造成这个问题的原因应该是各大厂商ROM中NMS维护Toast队列的逻辑有差异。
同样的,YToast内部也维护着自己的队列逻辑,保证在所有手机上使用DToast的效果相同。

YToast中多个弹窗连续出现时:

相同优先级时,会终止上一个,直接展示后一个;
不同优先级时,如果后一个的优先级更高则会终止上一个,直接展示后一个。

3系统Toast的BadTokenException问题

什么情况下windowToken会失效?

UI线程发生阻塞,导致TN.show()没有及时执行,当NotificationManager的检测超时后便会删除WMS中的该token,即造成token失效。

如何解决?

Google在API26中修复了这个问题,即增加了try-catch:

// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
    mWM.addView(mView, mParams);
    trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
    /* ignore */
}

因此对于8.0之前的我们也需要做相同的处理。YToast是通过反射完成这个动作,具体看下方实现:

//捕获8.0之前Toast的BadTokenException,Google在Android 8.0的代码提交中修复了这个问题
 private void hook(Toast toast) {
     try {
         Field sField_TN = Toast.class.getDeclaredField("mTN");
         sField_TN.setAccessible(true);
         Field sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");
         sField_TN_Handler.setAccessible(true);

         Object tn = sField_TN.get(toast);
         Handler preHandler = (Handler) sField_TN_Handler.get(tn);
         sField_TN_Handler.set(tn, new SafelyHandlerWrapper(preHandler));
     } catch (Exception e) {
         e.printStackTrace();
     }
 }


 public class SafelyHandlerWrapper extends Handler {
     private Handler impl;

     public SafelyHandlerWrapper(Handler impl) {
         this.impl = impl;
     }

     @Override
     public void dispatchMessage(Message msg) {
         try {
             impl.dispatchMessage(msg);
         } catch (Exception e) {
         }
     }

     @Override
     public void handleMessage(Message msg) {
         impl.handleMessage(msg);//需要委托给原Handler执行
     }
 }

4 Android8.0之后的token null is not valid问题

Android8.0后对WindowManager做了限制和修改,特别是TYPE_TOAST类型的窗口,必须要传递一个token用于校验。
API25:(PhoneWindowManager.java源码)

public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
    int type = attrs.type;
    outAppOp[0] = AppOpsManager.OP_NONE;
    String permission = null;
    switch (type) {
        case TYPE_TOAST:
            // XXX right now the app process has complete control over
            // this...  should introduce a token to let the system
            // monitor/control what they are doing.
            outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW;
            break;
    }
    if (permission != null) {
        ...
    }
    return WindowManagerGlobal.ADD_OKAY;
}

API26:(PhoneWindowManager.java源码)

public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
    int type = attrs.type;

    outAppOp[0] = AppOpsManager.OP_NONE;

    if (!isSystemAlertWindowType(type)) {
        switch (type) {
            case TYPE_TOAST:

                outAppOp[0] = OP_TOAST_WINDOW;
                return ADD_OKAY;
        }
        return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
                == PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;
    }
}

为了解决问题一,DovaToast不得不选择绕过NotificationManagerService的控制,但由于windowToken是NMS生成的,绕过NMS就无法获取到有效的windowToken,于是作为TYPE_TOAST的DovaToast就可能陷入第四个问题。

因此,DToast选择在DovaToast出现该问题时引入ActivityToast,在DovaToast无法正常展示时创建一个依附于Activity的弹窗展示出来,不过ActivityToast只会展示在当前Activity,不具有跨页面功能。

如果说有更好的方案,那肯定是去获取悬浮窗权限然后改用TYPE_PHONE等类型,但悬浮窗权限往往不容易获取,目前来看恐怕除了微信其他APP都不能保证拿得到用户的悬浮窗权限。

5 Android7.1之后,不允许同时展示两个TYPE_TOAST弹窗

YToast的弹窗策略就是同一时间最多只展示一个弹窗,逻辑上就避免了此问题。因此仅捕获该异常。

其他建议

如果能够接受Toast不跨界面的话,建议使用SnackBar

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

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,968评论 3 119
  • 春有艳阳当空照, 家有小童喜乐欢。 童心洋溢关不住, 与儿共度好光年。 元宵佳节团圆日, 天下儿女喜相逢。 春风雨...
    陌上花开carrie阅读 298评论 0 2
  • 还没睁眼,内心真实的状态和想要达到的状态已经发生冲突,旁边加上期待的推波助澜,内心起伏变化着。 衡量和比较,拉出状...
    l筱红阅读 186评论 0 0
  • 据说青色的软件开发团队是软件团队进化的最高境界,它有下列几个特点: 团队不会超过12人,超过12人时会拆分。 没有...
    心水阅读 472评论 0 1