Toast是Android系统中最常用的提示,但是在开发中碰到过几次棘手的问题。
1. 莫名其妙的闪退
1 | android.view.WindowManager$BadTokenException |
在Android 7版本都有这个问题,当我看到这个问题的时候,我想好解决,是不是Activity销毁了导致的,所以在Toast show的时候判断Activity是否销毁,发现线上还会有闪退,too young!! 然后继续想办法,将Application当做Context 传入,想着应该解决了吧,然而发现问题继续存在。
只能查看源码了,为什么在Android 7版本上有问题,对比了不同版本的源码发现Android 8版本上加上了try catch判断,那怎么解决这个问题呢?
方案一:
hook context getWindowManager
1 | final class SafeToastContext extends ContextWrapper { |
改完后发布线上,发现完美,没有闪退。可以开心睡觉了! 高兴的太早,线上用户反馈部分机型Toast无法弹出,最终发现在一些机型上的确无法弹出,部分Rom修改了一些逻辑,加入了Context的一些判断逻辑。
继续战斗,将只有Android 7上才Hook Context,减少影响范围,线上用户并没有反馈Android 7 Toast弹不出来的问题,问题终于解决。但作为一个完美的程序员,考虑虽然减少范围只hook Android 7 ,万一部分Android 7机型弹不出Toast怎么办。
方案二:
继续阅读源码,找找可以解决问题的方法,发现可以hook toast Handler handlerShow方法,这样就不会影响Context,最终测试,发现完美,可以加个鸡腿了。
1 | class SafelyHandlerWrapper extends Handler { |
虽然解决了这个问题,但是具体什么导致的一直没找到原因,也一直未能复现,后来参考一些资料解释:
这个问题由于targetSDKVersion升到26之后,在7.1.1机型上概率性出现。稳定复现的步骤是,在Toast.show()之后,UI线程做了耗时的操作阻塞了Handler message的处理,如使用Thread.sleep(5000),然后这个崩溃就出现了。原因是7.1.1系统对TYPE_TOAST的Window类型做了超时限制,绑定了Window Token,最长超时时间是3.5s,如果UI在这段时间内没有执行完,Toast.show()内部的handler message得不到执行,NotificationManageService那端会把这个Toast取消掉,同时把Toast对于的window token置为无效。等App端真正需要显示Toast时,因为window token已经失效,ViewRootImpl就抛出了上面的异常。
2. 当通知栏权限被禁掉Toast无法弹出
发现在Android 6版本及以上,通知栏权限被禁止了Toast无法弹出,这个逻辑我真不值得google是怎么想的,那我们App中的提示该怎么办呢?
可参考Toast源码,自定义一个提示,通过window manger addview显示, 首先Params.type=Toast类型,发现通知权限被禁掉以后,使用Toast类型会出异常。发现Params.type不设置的时候,可以弹出自定义View。 后面有部分用户反馈Toast还是无法弹出,检查发现部分机型Context不是Activity的时候,不设置Params.type是无法显示的。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 private void addWindowToast(Context context, Toast toast) {
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.format = PixelFormat.TRANSLUCENT;
mParams.windowAnimations = R.style.ToastAnimation;
// 为什么不能加 TYPE_TOAST,因为通知权限在关闭后设置显示的类型为Toast会报错
// android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
// mParams.type = WindowManager.LayoutParams.TYPE_TOAST;
// 判断是否为 Android 6.0 及以上系统并且有悬浮窗权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Settings.canDrawOverlays(context)) {
mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(context)){
mParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
mParams.setTitle("Toast");
mParams.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
final int gravity = toast.getGravity();
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = toast.getXOffset();
mParams.y = toast.getYOffset();
mParams.verticalMargin = toast.getVerticalMargin();
mParams.horizontalMargin = toast.getHorizontalMargin();
mParams.packageName = context.getPackageName();
try {
mWM.addView(toast.getView(), mParams);
} catch (WindowManager.BadTokenException e) {
/* ignore */
Log.d(TAG, "addWindowToast: " + e.getMessage());
} catch (Exception e1) {
//ignore
Log.d(TAG, "addWindowToast: " + e1.getMessage());
} catch (Error error) {
//ignore
}
mHandler.postDelayed(this, toast.getDuration() == Toast.LENGTH_LONG ? 4000 : 2000);
}
继续思考解决方法,主动初始化调用application.registerActivityLifecycleCallbacks,缓存Activity, 使用顶部Activity弹出自定义Toast,在Activity销毁移除Toast,通过这个方案可以完美解决上述问题。
解决这个问题后还有问题,当App没有Activity的时候,在Service中还是存在无法弹出问题,所以可以添加悬浮窗权限,在通知权限禁掉的时候主动申请悬浮窗权限,使用Params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY来解决这个问题, 但是用户有的不一定同意悬浮窗权限,所以还是存在可能Toast无法弹出的问题。
最终也只做到这一步,只是尽量做的更好,没有完美的解决这个问题。