Toast源码深度分析

目录介绍

  • 1.最简单的建立方法php

    • 1.1 Toast构造方法
    • 1.2 最简单的建立
    • 1.3 简单改造避免重复建立
    • 1.4 为什么会出现内存泄漏
    • 1.5 吐司是系统级别的
  • 2.源码分析android

    • 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.经典总结git

    • 3.1 判断应用程序获取通知权限是否开启
    • 3.2 使用Toast注意事项
    • 3.3 Toast的显示和隐藏重点逻辑
    • 3.4 Snackbar和Toast比较
  • 4.Toast封装库介绍程序员

    • 4.1 可以知足的需求
    • 4.2 具备的优点
  • 5.Toast遇到的问题github

    • 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/yangchong2...
  • 若是以为好,能够star一下,谢谢!固然也欢迎提出建议,万事起于忽微,量变引发质变!
  • Toast封装库项目地址:https://github.com/yangchong2...
  • 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源码分析segmentfault

    • 显示PopupWindow,注意问题宽和高属性,showAsDropDown()源码,dismiss()源码分析,PopupWindow和Dialog有什么区别?为什么弹窗点击一下就dismiss呢?
  • 06.Snackbar源码分析markdown

    • 最简单的建立,Snackbar的make方法源码分析,Snackbar的show显示与点击消失源码分析,显示和隐藏中动画源码分析,Snackbar的设计思路,为何Snackbar老是显示在最下面
  • 07.弹窗常见问题app

    • 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);
    }
    • 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将再也不弹出。
  • 具体能够参考个人弹窗封装库:https://github.com/yangchong2...

    • 自定义对话框,其中包括:自定义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 可以知足的需求

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

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 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.关于个人博客

相关文章
相关标签/搜索