Toast源码深度分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Toast源码深度分析相关的知识,希望对你有一定的参考价值。

目录介绍

  • 1.最简单的创建方法
    • 1.1 Toast构造方法
    • 1.2 最简单的创建
    • 1.3 简单改造避免重复创建
    • 1.4 为何会出现内存泄漏
    • 1.5 吐司是系统级别的
  • 2.源码分析
    • 2.1 Toast(Context context)构造方法源码分析
    • 2.2 show()方法源码分析
    • 2.3 mParams.token = windowToken是干什么用的
    • 2.4 scheduleTimeoutLocked吐司如何自动销毁的
    • 2.5 TN类中的消息机制
    • 2.6 普通应用的Toast显示数量是有限制的
    • 2.7 为何Activity销毁后Toast仍会显示
  • 3.经典总结
    • 3.1 判断应用程序获取通知权限是否开启
    • 3.2 使用Toast注意事项
    • 3.3 Toast的显示和隐藏重点逻辑
    • 3.4 Snackbar和Toast比较
  • 4.Toast封装库介绍
    • 4.1 能够满足的需求
    • 4.2 具有的优势
  • 5.Toast遇到的问题
    • 5.1 Toast偶尔报错Unable to add window
    • 5.2 Toast运行在子线程问题
    • 5.3 Toast如何添加系统窗口的权限
    • 5.4 token null is not valid

好消息

  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
  • 链接地址:https://github.com/yangchong211/YCBlogs
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!
  • Toast封装库项目地址:https://github.com/yangchong211/YCDialog
  • 02.Toast源码深度分析
    • 最简单的创建,简单改造避免重复创建,show()方法源码分析,scheduleTimeoutLocked吐司如何自动销毁的,TN类中的消息机制是如何执行的,普通应用的Toast显示数量是有限制的,用代码解释为何Activity销毁后Toast仍会显示,Toast偶尔报错Unable to add window是如何产生的,Toast运行在子线程问题,Toast如何添加系统窗口的权限等等
  • 03.DialogFragment源码分析
    • 最简单的使用方法,onCreate(@Nullable Bundle savedInstanceState)源码分析,重点分析弹窗展示和销毁源码,使用中show()方法遇到的IllegalStateException分析
  • 05.PopupWindow源码分析
    • 显示PopupWindow,注意问题宽和高属性,showAsDropDown()源码,dismiss()源码分析,PopupWindow和Dialog有什么区别?为何弹窗点击一下就dismiss呢?
  • 06.Snackbar源码分析
    • 最简单的创建,Snackbar的make方法源码分析,Snackbar的show显示与点击消失源码分析,显示和隐藏中动画源码分析,Snackbar的设计思路,为什么Snackbar总是显示在最下面
  • 07.弹窗常见问题
    • DialogFragment使用中show()方法遇到的IllegalStateException,什么常见产生的?Toast偶尔报错Unable to add window,Toast运行在子线程导致崩溃如何解决?

1.最简单的创建方法

1.1 Toast构造方法

  • Toast只会弹出一段信息,告诉用户某某事情已经发生了,过一段时间后就会自动消失。它不会阻挡用户的任何操作。
  • Toast是没有焦点,而且Toast显示的时间有限,过一定的时间就会自动消失。
    • 通过new Toast(context)直接创建,除了将mContext = context,还有一步重要的操作,创建TN,下面会说到……
      public Toast(Context context) {
      mContext = context;
      mTN = new TN();
      mTN.mY = context.getResources().getDimensionPixelSize(
              com.android.internal.R.dimen.toast_y_offset);
      mTN.mGravity = context.getResources().getInteger(
              com.android.internal.R.integer.config_toastDefaultGravity);
      }

1.2 最简单的创建

  • 一行代码调用,十分方便,但是这样存在一种弊端。
    • 使用中遇到的问题:例如,当点击有些按钮,需要吐司进行提示时;快速连续点击了多次按钮,Toast就触发了多次。系统会将这些Toast信息提示框放到队列中,等前一个Toast信息提示框关闭后才会显示下一个Toast信息提示框。可能导致Toast就长时间关闭不掉了。又或者我们其实已在进行其他操作了,应该弹出新的Toast提示,而上一个Toast却还没显示结束
      Toast.makeText(this,"吐司",Toast.LENGTH_SHORT).show();

1.3 简单改造避免重复创建

  • 为了解决1.2中的重复创建问题,则可以这样解决
    • 如下所示,简易型代码,需要注意问题,这里传递的上下文context需要是activity.getApplicationContext()全局上下文,避免静态toast对象内存泄漏
      /**
      * 吐司工具类    避免点击多次导致吐司多次,最后导致Toast就长时间关闭不掉了
      * 注意:这里如果传入context会报内存泄漏;传递activity..getApplicationContext()
      * @param content       吐司内容
      */
      private static Toast toast;
      @SuppressLint("ShowToast")
      public static void showToast(String content) {
      checkContext();
      if (toast == null) {
          toast = Toast.makeText(mApp, content, Toast.LENGTH_SHORT);
      } else {
          toast.setText(content);
      }
      toast.show();
      }
  • 这样用的原理
    • 先判断Toast对象是否为空,如果是空的情况下才会调用makeText()方法来去生成一个Toast对象,否则就直接调用setText()方法来设置显示的内容,最后再调用show()方法将Toast显示出来。由于不会每次调用的时候都生成新的Toast对象,因此刚才我们遇到的问题在这里就不会出现

1.4 为何会出现内存泄漏

  • 原因在于:如果在 Toast 消失之前,Toast 持有了当前 Activity,而此时,用户点击了返回键,导致 Activity 无法被 GC 销毁, 这个 Activity 就引起了内存泄露。

1.5 吐司是系统级别的

  • 经常看到的一个场景就是你在你的应用出调用了多次 Toast.show函数,然后退回到桌面,结果发现桌面也会弹出 Toast,就是因为系统的 Toast 使用了系统窗口,具有高的层级

2.源码分析

2.1 Toast(Context context)构造方法源码分析

  • 在构造方法中,创建了NT对象,那么有人便会问,NT是什么东西呢?于是带着好奇心便去看看NT的源码,可以发现NT实现了ITransientNotification.Stub,提到这个感觉是不是很熟悉,没错,在aidl中就会用到这个。
    • 针对aidl,如果有人不明白,可以参考我的这边文章Aidl进程间通信详细介绍主要是Aidl相关属性介绍,实际开发中案例操作,部分源码解析,客户端绑定服务端service原理
      public Toast(Context context) {
      mContext = context;
      mTN = new TN();
      mTN.mY = context.getResources().getDimensionPixelSize(
              com.android.internal.R.dimen.toast_y_offset);
      mTN.mGravity = context.getResources().getInteger(
              com.android.internal.R.integer.config_toastDefaultGravity);
      }
    • 技术分享图片
  • 在TN类中,可以看到,实现了AIDL的show与hide方法

    • TN是Toast内部的一个私有静态类,继承自ITransientNotification.Stub,ITransientNotification.Stub是出现在服务端实现的Service中,就是一个Binder对象,也就是对一个aidl文件的实现而已
      
      /**
      * schedule handleShow into the right thread
      */
      @Override
      public void show(IBinder windowToken) {
      if (localLOGV) Log.v(TAG, "SHOW: " + this);
      mHandler.obtainMessage(0, windowToken).sendToTarget();
      }

    /**

  • 接着看下这个ITransientNotification.aidl文件
    /** @hide */
    oneway interface ITransientNotification {
        void show();
        void hide();
    }

2.2 show()方法源码分析

2.3 mParams.token = windowToken是干什么用的

  • 如果你仔细一点,你可以看到在handleShow(IBinder windowToken)这个方法中,将windowToken赋值给mParams.token,那么就会思考这个token是干什么用的呢?它是哪里传递过来的呢?
    • 这个所需要的这个系统窗口 token ,是由我们的 NotificationManager 系统服务所生成,由于系统服务具有高权限,果真是厉害呀。
    • 上文2.3中我已经分析了showNextToastLocked()方法部分源码record.callback.show(record.token),可以知道callback对象的show方法中需要传递的参数 record.token实际上就是上面所说的NotificationManager服务所生成的窗口的 token。
    • 技术分享图片
    • 技术分享图片
  • 这个显示窗口的方法比较简单,就是将所传递过来的窗口 token 赋值给窗口属性对象 mParams, 然后通过调用 WindowManager.addView 方法,将 Toast中的mView对象纳入WindowManager中,而WindowManager看源码可知是一个接口,具体是放在WindowManagerService中处理。

2.4 scheduleTimeoutLocked吐司如何自动销毁的

  • 接下来再来看看scheduleTimeoutLocked(record)这部分代码,这个主要是超时监听消息逻辑
    • 通过看这段代码知道,handler延迟delay时间后发送消息,并且这个delay时间只有原生自带的两种时间类型,无法开发者自己定义。
    • 技术分享图片
  • 既然发送了消息,那肯定有地方接收消息并且处理消息呀。接着看下面代码,重点看cancelToastLocked源码
    • 可以看到当接收到消息时,先判断是否吐司,如果是有的话,也就是索引index>=0,那么就去cancel,在cancelToastLocked(int index)这段源码里面,我们终于可以看到record.callback.hide()这个方法了,前面我们知道callback是前面提到TN的binder代理对象,所以这个方法是调用了TN类中的hide()方法,下面2.5中将详细讲解TN中的消息机制。
    • 同时结束吐司之后,移除消息队列中对象,同时判断吐司消息队列中是否还有剩下的消息,如果是有的话,则会接着调用showNextToastLocked()继续弹吐司,关于showNextToastLocked()可以看2.3中的源码分析。
    • 技术分享图片
    • 技术分享图片
    • 技术分享图片
    • 技术分享图片
  • cancelToastLocked源码逻辑主要是
    • 调用 ITransientNotification.hide 方法,通知客户端隐藏窗口,并且移除队列中对象
    • 将给Toast 生成的窗口Token从WMS 服务中删除
    • 判断吐司消息队列中是否存在消息,如果存在消息,则继续开始show吐司……

2.5 TN类中的消息机制

  • 看源码可知,TN中的消息机制也是通过handler消息机制实现的。如果对handler 消息机制还不太熟悉,可以查看我的这篇博客:Handler消息机制
  • 当创建TN对象的时候,就创建了handler和runnable对象。
    • 然后看看show与hide方法,在show方法中发送消息,当mHandler接受到消息之后,就调用handleShow(token)处理逻辑,通过WindowManager将view添加进来,同时在该方法中也设置了大量的布局属性。
    • 在把Toast的View添加之前发现Toast的View已经被添加过(有partent)则删掉;把Toast的View添加到窗口,其中mParams.type在构造函数中赋值为TYPE_TOAST!
    • 技术分享图片
    • 技术分享图片
  • 同时,当toast执行show之后,过了一会儿会自动销毁,那么这又是为啥呢?那么是哪里调用了hide方法呢?

    • 回调了Toast的TN的show,当timeout可能就是hide呢。从上面我分析NotificationManagerService源码中的showNextToastLocked()的scheduleTimeoutLocked(record)源码,可以知道在NotificationManagerService通过handler延迟delay时间发送消息,然后通过callback调用hide,由于callback是TN中Binder的代理对象, 所以便可以调用到TN中的hide方法达到销毁吐司的目的。handleHide()源码如下所示

      public void handleHide() {
      if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
      if (mView != null) {
          // note: checking parent() just to make sure the view has
          // been added...  i have seen cases where we get here when
          // the view isn‘t yet added, so let‘s try not to crash.
          if (mView.getParent() != null) {
              if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
              mWM.removeViewImmediate(mView);
          }
      
          mView = null;
      }
      }

2.6 普通应用的Toast显示数量是有限制的

  • 如何判断是否是系统吐司呢?如果当前Toast所属的进程的包名为“android”,则为系统Toast,或者调用isCallerSystem()方法
    final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
  • 接着看看isCallerSystem()方法源码,isCallerSystem的源码也比较简单,就是判断当前Toast所属进程的uid是否为SYSTEM_UID、0、PHONE_UID中的一个,如果是,则为系统Toast;如果不是,则不为系统Toast。

    private static boolean isUidSystem(int uid) {
        final int appid = UserHandle.getAppId(uid);
        return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
    }
    
    private static boolean isCallerSystem() {
        return isUidSystem(Binder.getCallingUid());
    }
  • 为什么要这样判断是否是系统吐司呢?从源码可知:首先系统Toast一定可以进入到系统Toast队列中,不会被黑名单阻止。然后系统Toast在系统Toast队列中没有数量限制,而普通pkg所发送的Toast在系统Toast队列中有数量限制。
    • 那么关于数量限制这个结果从何而来,大概是多少呢?查看将要入队的Toast是否已经在系统Toast队列中。这是通过比对pkg和callback来实现的。通过下面源码分析可知:只要Toast的pkg名称和tn对象是一致的,则系统把这些Toast认为是同一个Toast。
    • 然后再看看下面这个源码截图,可知,非系统Toast,每个pkg在当前mToastQueue中Toast有总数限制,不能超过MAX_PACKAGE_NOTIFICATIONS,也就是50
    • 技术分享图片
    • 技术分享图片

2.7 为何Activity销毁后Toast仍会显示

  • 记得以前昊哥问我,为何toast在activity销毁后仍然会弹出呢,我毫不思索地说,因为toast是系统级别的呀。那么是如何实现的呢,我就无言以对呢……今天终于可以回答呢!
    • 还是回到NotificationManagerService类中的enqueueToast方法中,直接查看keepProcessAliveIfNeededLocked(callingPid)方法。这段代码的意思是将当前Toast所在进程设置为前台进程,这里的mAm = ActivityManager.getService(),调用了setProcessImportant方法将当前pid的进程置为前台进程,保证不会系统杀死。这也就解释了为什么当我们finish当前Activity时,Toast还可以显示,因为当前进程还在执行。
    • 技术分享图片

3.经典总结

3.1 判断应用程序获取通知权限是否开启

3.2 使用Toast注意事项

  • 通过分析TN类的handler可以发现,如果想在非UI线程使用Toast需要自行声明Looper,否则运行会抛出Looper相关的异常;UI线程不需要,因为系统已经帮忙声明。
  • 在使用Toast时context参数尽量使用getApplicationContext(),可以有效的防止静态引用导致的内存泄漏。
  • 有时候我们会发现Toast弹出过多就会延迟显示,因为上面源码分析可以看见Toast.makeText是一个静态工厂方法,每次调用这个方法都会产生一个新的Toast对象,当我们在这个新new的对象上调用show方法就会使这个对象加入到NotificationManagerService管理的mToastQueue消息显示队列里排队等候显示;所以如果我们不每次都产生一个新的Toast对象(使用单例来处理)就不需要排队,也就能及时更新呢。

3.3 Toast的显示和隐藏重点逻辑

  • Toast调用show方法 ,其实就是是将自己纳入到NotificationManager的Toast管理中去,期间传递了一个本地的TN类型或者是 ITransientNotification.Stub的Binder对象
  • NotificationManager 收到 Toast 的显示请求后,将生成一个 Binder 对象,将它作为一个窗口的 token 添加到 WMS 对象,并且类型是 TOAST
  • NotificationManager 将这个窗口token通过ITransientNotification的show方法传递给远程的TN对象,并且抛出一个超时监听消息 scheduleTimeoutLocked
  • TN 对象收到消息以后将往 Handler 对象中 post 显示消息,然后调用显示处理函数将 Toast 中的 View 添加到了 WMS 管理中,Toast窗口显示
  • NotificationManager的WorkerHandler收到MESSAGE_TIMEOUT消息, NotificationManager远程调用hide方法进程隐藏Toast 窗口,然后将窗口token从WMS中删除,并且判断吐司消息队列中是否还有消息,如果有,则继续吐司!

3.4 Snackbar和Toast比较

  • 可以使用snackBar替代Toast,即使用户禁掉了通知权限,也可以显示出来。SnackBar,其实就是使用View系统去模拟一个窗口行为,而且还能更加快速的实现动画效果,是不是很棒。
  • Snackbar是Android自5.0系统推出MaterialDesign后官方推荐的控件,在交互友好性方面比Toast要好

4.Toast封装库介绍

4.1 能够满足的需求

4.2 具有的优势

  • 采用builder构造者模式,链式编程,一行代码调用即可设置吐司Toast。
  • 为了避免静态toast对象内存泄漏,固可以使用应用级别的上下文context。所以这里我就直接采用了应用级别Application上下文,需要在application进行初始化一下。即可调用……

    //初始化
    ToastUtils.init(this);
    
    //可以自由设置吐司的背景颜色,默认是纯黑色
    ToastUtils.setToastBackColor(this.getResources().getColor(R.color.color_7f000000));
    
    //直接设置最简单吐司,只有吐司内容
    ToastUtils.showRoundRectToast("自定义吐司");
    
    //设置吐司标题和内容
    ToastUtils.showRoundRectToast("吐司一下","他发的撒经济法的解放军");
    
    //第三种直接设置自定义布局的吐司
    ToastUtils.showRoundRectToast(R.layout.view_layout_toast_delete);
    
    //或者直接采用bulider模式创建
    ToastUtils.Builder builder = new ToastUtils.Builder(this.getApplication());
    builder
            .setDuration(Toast.LENGTH_SHORT)
            .setFill(false)
            .setGravity(Gravity.CENTER)
            .setOffset(0)
            .setDesc("内容内容")
            .setTitle("标题")
            .setTextColor(Color.WHITE)
            .setBackgroundColor(this.getResources().getColor(R.color.blackText))
            .build()
            .show();
  • 因为看到网上有许多toast的封装,需要传递上下文,后来感觉是不是不需要传递这个参数,直接统一初始化一下就好呢。所以才有了这个toast的改良版。
    • 如果没有调用ToastUtils.init(this)初始化,则会提示报错ToastUtils context is not null,please first init",具体看下面代码。
      /**
      * 检查上下文不能为空,必须先进性初始化操作
      */
      private static void checkContext(){
      if(mApp==null){
          throw new NullPointerException("ToastUtils context is not null,please first init");
      }
      }

5.Toast遇到的异常问题

5.1 Toast偶尔报错Unable to add window

  • 报错日志,是不是有点眼熟呀?更多可以看我的开源项目:https://github.com/yangchong211
    android.view.WindowManager$BadTokenException
        Unable to add window -- token [email protected] is not valid; is your activity running?
  • 查询报错日志是从哪里来的
    • 技术分享图片
  • 发生该异常的原因
    • 这个异常发生在Toast显示的时候,原因是因为token失效。通常情况下,一般是不会出现这种异常。但是由于在某些情况下, Android进程某个UI线程的某个消息阻塞。导致 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息之后,迟迟没有执行。这时候,NotificationManager 的超时检测结束,删除了 WMS 服务中的 token 记录。删除 token 发生在 Android 进程 show 方法之前。这就导致了上面的异常。
    • 测试代码。模拟一下异常的发生场景,其实很容易,只需要这样做就可以出现上面这个问题
      Toast.makeText(this,"潇湘剑雨-yc",Toast.LENGTH_SHORT).show();
      try {
          Thread.sleep(20000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  • 解决办法,目前见过好几种,思考一下那种比较好……
    • 第一种,既然是报is your activity running,那可以不可以在吐司之前先判断一下activity是否running呢?
    • 第二种,抛出异常增加try-catch,代码如下所示,最后仍然无法解决问题
      • 按照源码分析,异常是发生在下一个UI线程消息中,因此在上一个ui线程消息中加入try-catch是没有意义的。而且用到吐司地方这么多,这样做也不方便啦!
    • 第三种,那就是自定义类似吐司Toast的view控件。个人建议除非要求非常高,不然不要这样做。毕竟发生这种异常还是比较少见的
  • 哪些情况会发生该问题?
    • UI 线程执行了一条非常耗时的操作,比如加载图片等等,就类似上面用 sleep 模拟情况
    • 进程退后台或者息屏了,系统为了减少电量或者某种原因,分配给进程的cpu时间减少,导致进程内的指令并不能被及时执行,这样一样会导致进程看起来”卡顿”的现象
    • 当TN抛出消息的时候,前面有大量的 UI 线程消息等待执行,而每个 UI 线程消息虽然并不卡顿,但是总和如果超过了 NotificationManager 的超时时间,还是会出现问题

5.2 Toast运行在子线程问题

  • 先来看看问题代码,会出现什么问题呢?
    new Thread(new Runnable() {
        @Override
        public void run() {
            ToastUtils.showRoundRectToast("潇湘剑雨-杨充");
        }
    }).start();
    • 报错日志如下所示:
    • 技术分享图片
  • 然后找找报错日志从哪里来的
    • ![image]()
  • 子线程中吐司的正确做法,代码如下所示
    new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            ToastUtils.showRoundRectToast("潇湘剑雨-杨充");
            Looper.loop();
        }
    }).start();
  • 得出的结论
    • Toast也可以在子线程执行,不过需要手动提供Looper环境的。
    • Toast在调用show方法显示的时候,内部实现是通过Handler执行的,因此自然是不阻塞Binder线程,另外,如果addView的线程不是Loop线程,执行完就结束了,当然就没机会执行后续的请求,这个是由Hanlder的构造函数保证的。可以看看handler的构造函数,如果Looper==null就会报错,而Toast对象在实例化的时候,也会为自己实例化一个Hanlder,这就是为什么说“一定要在主线程”,其实准确的说应该是 “一定要在Looper非空的线程”。
    • Handler的构造函数如下所示:
    • 技术分享图片
    • 技术分享图片

5.3 Toast如何添加系统窗口的权限

  • 作为程序员,都知道任何视图的显示都要依赖于一个视图窗口Window,同样Toast的显示也需要一个窗口,而且它还是一个系统窗口,这个窗口最终会被WindowManagerService(WMS)标记管理。当显示一个Toast时,调用show方法后,会通过TN 类中的handleShow方法处理展示的逻辑,同时WMS会生成一个token,而我们知道WMS本身就是一个系统级的服务,所以由它生成的token必然拥有权限添加系统窗口,最后WMS调用addView方法将view和mParams参数带进来,这样就可以展示吐司呢。
  • 需要注意:WindowManager检查当前窗口的token是否有效,如果有效,则添加窗口展示Toast;如果无效,则抛出异常,会发生5.1这种类型的异常。
    • 在那个地方检查token呢?在mWM.addView(mView, mParams)这里检查token,点击去可以发现ViewManager是个接口,这时候可以去看WindowManagerImpl类,继承ViewManager。
    • 技术分享图片
    • 技术分享图片

5.4 token null is not valid

  • 看了美团的技术文档分享得知,这个异常其实并非是Toast的异常,而是Google对WindowManage的一些限制导致的。Android从7.1.1版本开始,对WindowManager做了一些限制和修改,特别是TYPE_TOAST类型的窗口,必须要传递一个token用于权限校验才允许添加。在stackoverflow上搜索,也较少得到这方面的解答,这块有点难以解决这个问题。

关于其他内容介绍

01.关于博客汇总链接

02.关于我的博客



以上是关于Toast源码深度分析的主要内容,如果未能解决你的问题,请参考以下文章

Toast源码分析(Android 11)

Toast源码分析与学习

Toast源码分析与学习

Toast的window创建过程以及源码分析

Toast的window创建过程以及源码分析

Android源码分析Window添加