Toast与Snackbar的那点事
作者:网友投稿 时间:2018-04-07 09:20
Toast是Android平台上的常用技术。从用户角度来看,Toast是用户与App交互最基本的提示控件;从开发者角度来看,Toast是开发过程中常用的调试手段之一。此外,Toast语法也非常简单,仅需一行代码。基于简单易用的优点,Toast在Android开发过程中被广泛使用。

但是,Toast是系统层面提供的,不依赖于前台页面,存在滥用的风险。为了规避这些风险,Google在Android系统版本的迭代过程中,不断进行了优化和限制。这些限制不可避免的影响到了正常的业务逻辑,在迭代过程中,我们遇到过以下几个问题:
设置中关闭某个App的【显示通知】开关,Toast不再弹出,极大的影响了用户体验。
Toast在Android 7.1.2(API25)以下会发生BadTokenException异常,导致App崩溃。
自定义TYPE_TOAST类型的Window,在Android 7.1.1、7.1.2发生token null is not valid异常,导致App崩溃。
与Toast斗争
在美团平台的业务中,Toast被用作主流程交互的提示控件,比如在完成下单、评价、分享后进行各种提示。Toast被限制之后会给用户带来误解。为了解决正常的业务Toast被系统限制误伤的问题,我们与Toast展开了一系列的斗争。
斗争一:Toast不弹出
举个案例:某个用户投诉美团App在分享朋友圈后没有任何提示,不知道是否分享成功。具体原因是用户在设置里关闭了美团App的【显示通知】开关,导致通知权限无法获取,这极大的影响了用户体验。然而,在Android 4.4(API19)以下系统中,这个开关的打开状态,也就是通知权限是否开启的状态我们是无法判断的,因此我们也无法感知Toast弹出与否,为了解决这个问题,需要从Toast的源码入手,最后源码总结步骤如下:
在Toast#show()源码中,Toast的展示并非自己控制,而是通过AIDL使用INotificationManager获取到NotificationManagerService(NMS)这个远程服务。
调用service.enqueueToast(pkg, tn, mDuration)将当前Toast的显示加入到通知队列,并传递了一个tn对象,这个对象就是NMS用作回传Toast的显示状态。
在tn的回调方法中,使用WindowManager将构造的Toast添加到当前的window中,需要注意的是这个window的type类型是TYPE_TOAST。

Toast不弹出原因分析
那么为什么禁掉通知权限会导致Toast不再弹出呢?
通过以上分析,Toast的展示是由NMS服务控制的,NMS服务会做一些权限、token等的校验,当通知权限一旦关闭,Toast将不再弹出。
可行性方案调研
如果能够绕过NMS服务的校验那么就可以达到我们的诉求,绕过的方法是按照Toast的源码,实现我们自己的MToast,并将NMS替换成自己的ToastManager,如下图:

方案定了后,需要做的事情就是代码替换。作为平台型App,美团App大量使用了Toast,人工替换肯定会出现遗漏的地方,为了能用更少的人力来解决这个问题,我们采用了如下方案。
解决方案
美团App在早期就因业务需要接入了AspectJ,AspectJ是Java中做AOP编程的利器,基本原理就是在代码编译期对切面的代码进行修改,插入我们预先写好的逻辑或者直接替换当前方法的实现。美团App的做法就是借用AspectJ,从源头拦截并替换Toast的调用实现。
关键代码如下:
@Aspect
public class ToastAspect {
@Pointcut("call(* android.widget.Toast+.show(..))")
public void toastShow() {
}
@Around("toastShow()")
public void toastShow(ProceedingJoinPoint point) {
Toast toast = (Toast) point.getTarget();
Context context = (Context) ReflectUtils.getValue(toast, "mContext");
if (Build.VERSION.SDK_INT >= 19 && NotificationManagerCompat.from(context).areNotificationsEnabled()) {
point.proceed(point.getArgs());
} else {
floatToastShow(toast, context);
}
}
private static void floatToastShow(Toast toast, Context context) {
...
new MToast(context)
.setDuration(mDuration)
.setView(mNextView)
.setMargin(mHorizontalMargin, mVerticalMargin)
.show();
}
}



