IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Toast与Snackbar的那点事,惊喜 -> 正文阅读

[移动开发]Toast与Snackbar的那点事,惊喜

      floatToastShow(toast, context);
 }

}

private static void floatToastShow(Toast toast, Context context) {

new MToast(context)
       .setDuration(mDuration)
       .setView(mNextView)
       .setGravity(mGravity, mX, mY)
       .setMargin(mHorizontalMargin, mVerticalMargin)
       .show();

}
}


其中MToast是`TYPE_TOAST`类型的的Window,这样即使禁掉通知权限,业务代码也可以不作任何修改,继续弹出Toast。而底层已经被无感知的替换成自己的MToast了,以最小的成本达到了目标。

斗争二:`BadTokenException`
-----------------------

美团App在线上经常会上报`BadTokenException`Crash,而且集中在Android 5.0 - Android 7.1.2的机型上。具体Crash堆栈如下:

android.view.WindowManager$BadTokenException: Unable to add window – token android.os.BinderProxy@6caa743 is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:607)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:341)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:106)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3242)BadTokenException
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2544)
at android.app.ActivityThread.access 900 ( A c t i v i t y T h r e a d . j a v a : 168 ) a t a n d r o i d . a p p . A c t i v i t y T h r e a d 900(ActivityThread.java:168) at android.app.ActivityThread 900(ActivityThread.java:168)atandroid.app.ActivityThreadH.handleMessage(ActivityThread.java:1378)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5665)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:822)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:712)


### `BadTokenException`原因分析

我们知道在Android上,任何视图的显示都要依赖于一个视图窗口Window,同样Toast的显示也需要一个窗口,前文已经分析了这个窗口的类型就是TYPE\_TOAST,是一个系统窗口,这个窗口最终会被WindowManagerService(WMS)标记管理。但是我们的普通应用程序怎么能拥有添加系统窗口的权限呢?查看源码后发现需要以下几个步骤:

1.  当显示一个Toast时,NMS会生成一个token,而NMS本身就是一个系统级的服务,所以由它生成的token必然拥有权限添加系统窗口。
2.  NMS通过ITransientNotification也就是tn对象,将生成的token回传到我们自己的应用程序进程中。
3.  应用程序调用handleShow方法,去向WindowManager添加窗口。
4.  WindowManager检查当前窗口的token是否有效,如果有效,则添加窗口展示Toast;如果无效,则抛出上述异常,Crash发生。

详细的原理图如下:

![](https://user-gold-cdn.xitu.io/2018/3/30/162760d25348c3fa?imageView2/0/w/1280/h/960/ignore-error/1)

在Android 7.1.1的NMS源码中,关键代码如下:

void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
// 调用tn对象的show方法展示toast,并回传token
record.callback.show(record.token);
// 超时处理
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {

}
}
}

private void scheduleTimeoutLocked(ToastRecord r) {
mHandler.removeCallbacksAndMessages?;
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
// 根据toast显示的时长,延迟触发消息,最终调用下面的方法
mHandler.sendMessageDelayed(m, delay);
}

private void handleTimeout(ToastRecord record) {
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}

void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
// 调用tn对象的hide方法隐藏toast
record.callback.hide();
} catch (RemoteException e) {

}

ToastRecord lastToast = mToastQueue.remove(index);
// 移除当前的toast的token,token就此失效
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);

}


### 问题验证

通过以上分析`showNextToastLocked()`被调用后,如果此时主线程由于其它原因被阻塞导致`handleShow()`不能及时调用,从而触发超时逻辑导致token失效。主线程阻塞结束后,继续执行Toast的show方法时,发现token已经失效了,于是抛出`BadTokenException`异常从而导致上述Crash。

可以使用以下的代码验证此异常:

Toast.makeText(this, “测试Crash”, Toast.LENGTH_SHORT).show();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}


### 解决方案

那么如何解决这个异常呢?首先想到就是对Toast加上try-catch,但是发现不起作用,原因是这个异常并非在当前线程中立即被抛出的,而是添加到了消息队列中,等待消息真正执行时才会被抛出。Google在Android 8.0的代码提交中修复了这个问题,把8.0的源码和前一版本对比可以发现,如同我们的分析,Google在消息执行处将异常catch住了。那么针对8.0之前的版本发生的Crash怎么办呢?美团平台使用了一个类似代理反射的通用解决方案,结构如下图:

![](https://user-gold-cdn.xitu.io/2018/3/30/162760d7330532d1?imageView2/0/w/1280/h/960/ignore-error/1)

基本原理:使用我们自己实现的ToastHandler替换Toast内部的Handler,ToastHandler作用就是把异常catch住,这种修改思路和Android 8.0修复思路保持一致,只不过一个是在系统层面解决,一个是在用户层面解决。

斗争三:`token null is not valid`
-----------------------------

在Android 7.1.1、7.1.2和去年8月发布的Android 8.0系统中,我们的方案出现了另一个异常`token null is not valid`,这个异常堆栈如下:

android.view.WindowManager$BadTokenException: Unable to add window – token null is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:683)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)


### `token null is not valid`原因分析

这个异常其实并非是Toast的异常,而是Google对WindowManage的一些限制导致的。Android从7.1.1版本开始,对WindowManager做了一些限制和修改,特别是`TYPE_TOAST`类型的窗口,必须要传递一个token用于权限校验才允许添加。Toast源码在7.1.1及以上也有了变化,Toast的WindowManager.LayoutParams参数额外添加了一个token属性,这个属性的来源就已经在上文分析过了,它是在NMS中被初始化的,用于对添加的窗口类型进行校验。当用户禁掉通知权限时,由于AspectJ的存在,最终会调用我们封装的MToast,但是MToast没有经过NMS,因此无法获取到这个属性,另外就算我们按照NMS的方法自己生成一个token,这个token也是没有添加`TYPE_TOAST`权限的,最终还是无法避免这个异常的发生。

源码中关键代码如下:

// 方法签名多了一个IBinder类型的token,它是在NMS中创建的
public void handleShow(IBinder windowToken) {

if (mView != mNextView) {

mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;

 // 这里添加了token
 mParams.token = windowToken;
 
 if (mView.getParent() != null) {
     if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
     mWM.removeView(mView);
 }
 ...
 
 try {
     // 8.0版本的系统,将这里的异常catch住了
     mWM.addView(mView, mParams);
     trySendAccessibilityEvent();
 } catch (WindowManager.BadTokenException e) {
     /* ignore */
 }

}
}


### 解决方案

经过调研,发现Google对WindowManager的限制,让我们不得不放弃使用`TYPE_TOAST`类型的窗口替代Toast,也代表了我们上述使用WindowManager方案的终结。

斗争总结
----

我们的核心目标只是希望在用户关闭通知消息开关的情况下,能继续看到通知,所以我们使用了WindowManager添加自定义window的方式来替换Toast,但是在替换的过程中遇到了一些Toast的Crash异常,为了解决这些Crash,我们提出了使用自定义ToastHandler的方式来catch住异常,确保app正常运行。在方案推广上,为了能用更少的人力,更高的效率完成替换,我们使用了AspectJ的方案。最后,在Android 7.1.1版本开始,由于Google对WindowManager的限制,导致这种使用自定义window的替换Toast的方式不再可行,我们便开始寻找替换Toast的其它可行方案。

替换Toast的可行方案
============

为了继续能让用户在禁掉通知权限的情况下,也能看到通知以及屏蔽上述Toast带来的Crash,我们经过调研、分析并尝试了以下几种方案。

1.  在7.1.1以上系统中继续使用WindowManager方式,只不过需要把type改为TYPE\_PHONE等悬浮窗权限。
2.  使用Dialog、DialogFragment、PopupWindow等弹窗控件来实现一个通知。
3.  按照Snackbar的实现方式,找到一个可以添加布局的父布局,采用addView的方式添加通知。

以上几种方案的共同点是为了绕过通知权限的检查,即使用户禁掉了通知权限,我们自定义的通知依然可以不受影响的弹出来,但是也有很明显的缺陷,如下图:

![](https://user-gold-cdn.xitu.io/2018/3/30/162760db5f63899d?imageView2/0/w/1280/h/960/ignore-error/1)

经过对比,我们也采用了Snackbar替换Toast的方案,原因是Snackbar是Android自5.0系统推出MaterialDesign后官方推荐的控件,在交互友好性方面比Toast要好,例如:支持手势操作,支持与CoordinatorLayout联动等,Snackbar作为提示控件目前在市面上也被广泛使用,而其它方案有明显的缺陷如下:

首先,使用WindowManager添加悬浮窗的方式,虽然这种方式能和原生的Toast保持完美的一致性,但是需要的权限太高,坑也太多。`TYPE_PHONE`的权限要比`TYPE_TOAST`权限敏感太多,而且在Android 8.0系统上必须使用`TYPE_APPLICATION_OVERLAY`这个type,并且要申请以下两个权限,这两个权限不仅需要在清单文件中声明,而且绝大部分手机默认是关闭状态,需要我们引导用户开启,如果用户选择不开启,那么Toast还是不能弹出。同时还需要适配众多定制化ROM的国产机型。绕过了通知权限的坑,又跳入了悬浮窗权限的坑,这是不可取的。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-29 09:28:21  更:2021-08-29 09:28:53 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:38:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码