Toast与Snackbar的那点事

作者:网友投稿 时间:2018-04-07 09:20

字号

Toast是Android平台上的常用技术。从用户角度来看,Toast是用户与App交互最基本的提示控件;从开发者角度来看,Toast是开发过程中常用的调试手段之一。此外,Toast语法也非常简单,仅需一行代码。基于简单易用的优点,Toast在Android开发过程中被广泛使用。

Toast与Snackbar的那点事

但是,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与Snackbar的那点事

Toast不弹出原因分析

那么为什么禁掉通知权限会导致Toast不再弹出呢?

通过以上分析,Toast的展示是由NMS服务控制的,NMS服务会做一些权限、token等的校验,当通知权限一旦关闭,Toast将不再弹出。

可行性方案调研

如果能够绕过NMS服务的校验那么就可以达到我们的诉求,绕过的方法是按照Toast的源码,实现我们自己的MToast,并将NMS替换成自己的ToastManager,如下图:

Toast与Snackbar的那点事

方案定了后,需要做的事情就是代码替换。作为平台型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) 

           .setGravity(mGravity, mX, mY) 

           .setMargin(mHorizontalMargin, mVerticalMargin) 

           .show(); 

  } 

责任编辑:CQITer新闻报料:400-888-8888   本站原创,未经授权不得转载
关键词 >>Toast Snackbar Android
继续阅读
热新闻
推荐
关于我们联系我们免责声明隐私政策 友情链接