阅读 945

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万字],转载请注明出处,谢谢!
  • 链接地址:github.com/yangchong21…
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!
  • Toast封装库项目地址:github.com/yangchong21…

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);
    }
    复制代码
    • image
  • 在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();
    }
    
    /**
     * schedule handleHide into the right thread
     */
    @Override
    public void hide() {
        if (localLOGV) Log.v(TAG, "HIDE: " + this);
        mHandler.post(mHide);
    }
    复制代码
  • 接着看下这个ITransientNotification.aidl文件
    /** @hide */
    oneway interface ITransientNotification {
        void show();
        void hide();
    }
    复制代码

2.2 show()方法源码分析

  • 通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口,然后把TN对象和一些参数传递到远程NotificationManagerService中去
    • 当 Toast在show的时候,然后把这个请求放在 NotificationManager 所管理的队列中,并且为了保证 NotificationManager 能跟进程交互,会传递一个TN类型的 Binder对象给NotificationManager系统服务,接着看下面getService方法做了什么?
    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }
    
        //通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口,当前Toast类相当于上面例子的客户端!!!相当重要!!!
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;
    
        try {
            //把TN对象和一些参数传递到远程NotificationManagerService中去
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }
    复制代码
  • 接着看看getService方法
    • 通过单利模式获取sService对象。
    //远程NotificationManagerService的服务访问接口
    private static INotificationManager sService;
    static private INotificationManager getService() {
        //单例模式
        if (sService != null) {
            return sService;
        }
        //通过AIDL(Binder)通信拿到NotificationManagerService的服务访问接口
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }
    复制代码
  • 接下来看看service.enqueueToast(pkg, tn, mDuration)这段代码,相信有的小伙伴会质疑,这段代码报红色,如何查看呢?
    • image
    • 于是,我直接在studio中全局搜索NotificationManagerService,终于给找到了,如下所示:
    • image
    • 下面就到重点呢……注意:record是将Toast封装成ToastRecord对象,放入mToastQueue中。通过下面代码可以得知:通过isSystemToast判断是否为系统Toast。如果当前Toast所属的进程的包名为“android”,则为系统Toast。如果是系统Toast一定可以进入到系统Toast队列中,不会被黑名单阻止。
    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        long callingId = Binder.clearCallingIdentity();
        try {
            ToastRecord record;
            int index;
            //判断是否是系统级别的吐司
            if (!isSystemToast) {
                index = indexOfToastPackageLocked(pkg);
            } else {
                index = indexOfToastLocked(pkg, callback);
            }
            if (index >= 0) {
                record = mToastQueue.get(index);
                record.update(duration);
                record.update(callback);
            } else {
                //创建一个Binder类型的token对象
                Binder token = new Binder();
                //生成一个Toast窗口,并且传递token等参数
                mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                record = new ToastRecord(callingPid, pkg, callback, duration, token);
                //添加到吐司队列之中
                mToastQueue.add(record);
                //对当前索引重新进行赋值
                index = mToastQueue.size() - 1;
            }
            //将当前Toast所在的进程设置为前台进程
            keepProcessAliveIfNeededLocked(callingPid);
            if (index == 0) {
                //如果index为0,说明当前入队的Toast在队头,需要调用showNextToastLocked方法直接显示
                showNextToastLocked();
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
    复制代码
  • 接下来看一下showNextToastLocked()方法中的源代码,看看这个方法中做了什么……
    • 首先获取吐司消息队列中第一个ToastRecord对象,然后判断该对象如果不为null的话,就开始通过callback进行show,且传递了token参数,注意这个show是通知进程显示。然后再调用scheduleTimeoutLocked(record)方法执行超时后自动取消的逻辑【下面详细分析】。同时需要注意的时,如果出现了异常,则会从吐司消息队列中移除该record……
    • 那么callback是干嘛的呢,一般印象中callback是处理回调的?从ITransientNotification callback得知,这个callback哥们竟然是是一个 ITransientNotification 类型的对象,也就是前面说到的TN的Binder代理对象,那么他传递的这个token参数是干什么用的呢?这里我们程序员小伙伴可以接着往下看哈!
    • image

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。
    • image
    • image
  • 这个显示窗口的方法比较简单,就是将所传递过来的窗口 token 赋值给窗口属性对象 mParams, 然后通过调用 WindowManager.addView 方法,将 Toast中的mView对象纳入WindowManager中,而WindowManager看源码可知是一个接口,具体是放在WindowManagerService中处理。

2.4 scheduleTimeoutLocked吐司如何自动销毁的

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

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

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

3.经典总结

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

  • 一行代码调用即可:DialogUtils.requestMsgPermission(this);
  • 大部分手机通知权限是开启的。如果关闭了,则吐司是无法显示的,但是仍有部分手机,比如某型号小米手机,锤子手机等就权限需要手动开启。
  • Toast的展示是由NMS服务控制的,NMS服务会做一些权限、token等的校验,当通知权限一旦关闭,Toast将不再弹出。
  • 具体可以参考我的弹窗封装库:github.com/yangchong21…
    • 自定义对话框,其中包括:自定义Toast,采用builder模式,支持设置吐司多个属性;自定义dialog控件,仿IOS底部弹窗;自定义DialogFragment弹窗,支持自定义布局,也支持填充recyclerView布局;自定义PopupWindow弹窗,轻量级,还有自定义Snackbar等等;还有自定义loading加载窗,简单便用。
    //判断是否有权限
    NotificationManagerCompat.from(context).areNotificationsEnabled()
    
    //如果没有通知权限,则直接跳转设置中心设置
    @SuppressLint("ObsoleteSdkInt")
    private static void toSetting(Context context) {
        Intent localIntent = new Intent();
        localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (Build.VERSION.SDK_INT >= 9) {
            localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
            localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
        } else if (Build.VERSION.SDK_INT <= 8) {
            localIntent.setAction(Intent.ACTION_VIEW);
            localIntent.setClassName("com.android.settings",
                    "com.android.setting.InstalledAppDetails");
            localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName());
        }
        context.startActivity(localIntent);
    }
    复制代码

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 能够满足的需求

  • 可以设置吐司的位置,偏移,吐司文字颜色,吐司背景颜色等等。简单的代码就可以实现你需要的多种场景。也可以设置定义布局的吐司。项目地址:github.com/yangchong21…

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

  • 报错日志,是不是有点眼熟呀?更多可以看我的开源项目:github.com/yangchong21…
    android.view.WindowManager$BadTokenException
        Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
    复制代码
  • 查询报错日志是从哪里来的
    • image
  • 发生该异常的原因
    • 这个异常发生在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
  • 然后找找报错日志从哪里来的
    • 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的构造函数如下所示:
    • image
    • image

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。
    • image
    • image

5.4 token null is not valid

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

关于其他内容介绍

01.关于博客汇总链接

02.关于我的博客

关注下面的标签,发现更多相似文章
评论