Toast的View界面是如何绘制出来的--Toast的Window(view)创建过程

xiaoxiao2021-02-28  22

前面我们已经讲述了Activity的Window创建过程、Dialog的Window创建过程, 本文将继续探索Window相关的知识:Toast的创建过程 及 其 View界面的展示。

#####代码示例

Toast的一般使用非常简单, 一行代码就可以搞定:

Toast.makeText(this, "Toast测试", Toast.LENGTH_SHORT).show();

通过makeText创建一个Toast, 然后调用show方法,去真正的显示出来这个Toast, 这一点和前文的Dialog很相似。

我们去看看这个makeText源码:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) { Toast result = new Toast(context); LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message); tv.setText(text); result.mNextView = v; result.mDuration = duration; return result; }

代码也很简单, 就是 new 一个Toast, 然后填充一个默认视图,最后把这个新建的Toast对象返回。从这里也可以看出, 其实我们可以自己new一个Toast,然后填充我们的自定义布局, 就可以定义我们自己想要的Toast了。这里Toast为我们提供了setView(View v)方法实现我们的自定义布局。

#####Toast的Window创建过程

Toast和前文讲到的Dialog有所不同,Toast的工作过程要更复杂一些。Toast也是基于Window的, 这是毋庸置疑的,但是由于Toast有具有定时取消的特点,即我们通过设置Toast.LENGTH_SHORT或者Toast.LENGTH~LONG,Toast具有固定显示实现, 分别是2.5s和3.5s,显示完后就要消失, 所以这里系统采用了handler的延时机制去完成。

在Toast内部, 有两个不同的IPC过程, 第一个是Toast访问NotificationManagerService, 第二个是NotificationManagerService回调给Toast里的TN接口(这里的TN就是一个类,它的类名就是TN), 这里说是一个回调其实是为了方便我们理解, 它的本质其实是NotificationManagerService通过IPC来访问我们的Toast,同时把数据传过来。关于Binder通信机制可以自行搜索一下, 或者参考我的博客**Android的IPC机制–实现AIDL的最简单例子(上)(下)**, AIDL本质就是一个Binder通信。

除了这两个IPC过程, 最后Window的添加也是一个IPC过程, 所以Toast的IPC过程总共有三个,其中创建过程两个, 添加过程一个。

Toast是一个系统级Window(这个后面会说明), show和cancel两个方法用于显示和隐藏Toast。 刚刚看过了makeText()方法,里面很简单,接着我们看看show方法:

public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }

注意这一行代码:

INotificationManager service = getService();

这里就是去获取一个NotificationManagerService的本地代理 service对象, 然后通过service.enqueueToast(pkg, tn, mDuration) 向NotificationManagerService发送了消息:我要创建一个Toast啦。 这里就是我们刚刚所提到的第一个IPC过程,其中enqueueToast的三个参数别是包名,TN对象以及 toast的时长。 我们去看一下这个getService()方法:

static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; }

如果了解AIDL的通信, 是不是觉得这里很眼熟, 这其实就是一个AIDL啊,还不了解AIDL的同学,推荐看完本文后,再移步看一下我的另两篇博客**Android的IPC机制–实现AIDL的最简单例子(上)(下)**。跨进程通信不是本篇的重点, 这里只是提一下。

回到刚刚的位置, 我们通过service.enqueueToast()方法向NotificationManagerService发送消息后, 将会执行NotificationManagerService的enqueueToas方法, 继续进去查看源码:

private final IBinder mService = new INotificationManager.Stub() { @Override public void enqueueToast(String pkg, ITransientNotification callback, int duration) { ... synchronized (mToastQueue) { ... try { ToastRecord record; int index = indexOfToastLocked(pkg, callback); // 1. 如果这个toast已经存在于queue了,则只是更新它,但是并不会把它移动到队尾 if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { if (!isSystemToast) { int count = 0; final int N = mToastQueue.size(); for (int i=0; i<N; i++) { final ToastRecord r = mToastQueue.get(i); // 2. 判断如果是同一个包, 则最多只能存在50个toast, //否则不再允许添加进队列 if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { return; } } } } record = new ToastRecord(callingPid, pkg, callback, duration); //把传过来的toast加入进队列, 等待显示 mToastQueue.add(record); ... } // 3. 如果是第一个toast, 则直接显示 if (index == 0) { showNextToastLocked(); } ...

上面这一段源码,主要有三个重要代码:

判断这个Toast是否已存在,如果这个toast已经存在于queue了,则只是更新它,但是并不会把它移动到队列的队尾, 这里的mToastQueue其实是一个ArrayList; 判断如果是同一个包, 则最多只能存在50个toast,否则不再允许添加进队列, 这一点非常重要,试想一下, 如果我们通过循环去大量弹出toast, 那么这个toast队列里面的toast就会无穷无尽, 这个时候去打开其他APP, 它们都没机会弹出toast了。正常情况下, 一个应用的toast也打不到50个,完全够用了; 如果是第一个toast, 则直接调用showNextToastLocked方法弹出toast。

要注意这个enqueueToast的三个参数, 我们传递过来的分别是包名、 TN对象、 显示时间, 这里把TN对象赋值给了callback, TN实际上实现了ITransientNotification接口的。

#####Toast的Window显示过程

接着,我们去看看showNextToastLocked是如何显示toast的:

void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { try { //关键代码 1:回调show方法 record.callback.show(); // 关键代码2:执行toast的超时后的移除处理 scheduleTimeoutLocked(record); return; } catch (RemoteException e) { //这里处理由于某些意外导致 跨进程通信失败,即这个调用Toast的显示失败 //这个时候就从队列把这个Toast移除掉, 执行下一个Toast的显示 int index = mToastQueue.indexOf(record); if (index >= 0) { mToastQueue.remove(index); } keepProcessAliveLocked(record.pid); if (mToastQueue.size() > 0) { record = mToastQueue.get(0); } else { record = null; } } } }

这里的逻辑一样很简单, 主要看try代码块就行了, 二tyr代码块就两行代码,都是关键代码:

关键代码1: 回调是record.callback.show(): 这个callback对象其实就是我们的TN对象, 所以这里就回调给我们的toast中的TN了, 这里就是第二次跨进程通信;关键代码2 调用scheduleTimeoutLocked:从这个方法名上也可看出一二, 这是用来处理超时的方法, 主要是当显示超过我们的设定的Toast的显示时间后(即SHORT或者LONG), 就移除掉这个toast,然后继续调用下一个Toast。具体细节感兴趣的可以去跟一下

我们继续按照主路径走, 去看我们的关键代码, 这里回调给了TN的show方法, TN是Toast的一个内部类:

private static class TN extends ITransientNotification.Stub { final Runnable mShow = new Runnable() { @Override public void run() { handleShow(); } }; ... @Override public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.post(mShow); } ... }

我们看到 TN extends ITransientNotification.Stub, 了解AIDL的同学一看就知道这是一个跨进程通信的类, 它继承了ITransientNotification.Stub, 也就实现了ITransientNotification接口。 我们继续跟进show方法, 里面调用了 mHandler.post(mShow), 具体的执行在mShow里面,接着看, mShow里面又调用了handleShow(), 看一下这个方法:

public void handleShow() { if (mView != mNextView) { // 移除掉前一个Toast的view handleHide(); //把我们的Toast的View传进来 mView = mNextView; ... //关键代码, 获取WindowManagerb本地代理对象 mWM = (WindowManager)context.getSystemService ... //关键代码, 向WindowManager添加Window, 把我们的view传过去 mWM.addView(mView, mParams); } }

这个方法逻辑也比较清楚, 就是把上一个toast的view已移除掉,然后把要显示的Toast添加进入WindowManger, 这同样是一个IPC过程, Window的添加过程可以参考**从Window的添加过程理解Window和WindowManager**, 文中配了一张图片, 很清晰的展示了Window的添加过程。 这就是我们前面提到的到的Toast的 第三次IPC过程。

然后, Toast就真正的显示在我们的界面上啦~~

到这里, 整个Toast的view添加, Window添加,以及Toast的超时后自动消失的 整个流程就讲完了~~

#####Toast的一些不为人知的细节

######1、为什么我们通常只能在UI主线程使用Toast?

上面一段代码中, 我们看到,当NotificationManagerService回调给TN, 通知它可以显示Toast的时候, 回调给了TN的show方法, 我们再次看一些这个show方法:

@Override public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.post(mShow); }

注意到没有, 这里使用mHandler, 去提交的一个任务, 看一下这个mHandler的定义:

final Handler mHandler = new Handler();

就是一个普通的Handler, 而我们知道, 只有在主线程, 才能直接这么使用handler, 否则都会报错,因为主线程在初始化时, 就已经调用了Looper.prepare()方法。如果需要在子线程使用Handler的话, 必须调用Looper.prepare()方法。所以, 一般情况下, 我们在主线程使用Toast, 但是如果想要在子线程使用Toast, 也是可以的, 就在子线程的run方法最后一行, 调用Looper.prepare()即可。为什么是在最后一行调用呢, 因为这个Looper.prepare()内部是一个死循环, 在这个方法后面的代码就无法执行了。

######2、为什么说Toast是一个系统级的Window?

Window分为系统级Window, 应用级Window, 子Window, 其中子Window必须附属在其他两类Window上,这个在**从Window的添加过程理解Window和WindowManager** 一文中有所介绍, 那为什么说Toast是一个系统级Window呢, 我们只需要去看看它的层级即可。

我们再去看看Toast的Window的添加过程:

//关键代码, 向WindowManager添加Window, 把我们的view传过去 mWM.addView(mView, mParams);

层级是在WindowManager.LayoutParams.type中设置的, 我们从这里的mParams去看看, 找一下这个mParams在哪里赋值了, 找了一圈, 除了刚刚的handleShow方法, mparams在TN的构造方法里也有一些初始化:

TN() { final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = com.android.internal.R.style.Animation_Toast; //关键代码,给Window层级赋值 params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; }

我们看到, type的值为WindowManager.LayoutParams.TYPE_TOAST, 去看看这个值:

public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;

可以看到它的值是FIRST_SYSTEM_WINDOW+5, 这个FIRST_SYSTEM_WINDOW表示系统Window的第一个层级, 比它大的都是系统Window,FIRST_SYSTEM_WINDOW的值为2000。系统Window的层级为 2000~2999.

到这里,Toast的Window的创建到消失的整个流程就讲完了。

喜欢的朋友麻烦点一个赞吧~~

转载请注明原文地址: https://www.6miu.com/read-2800023.html

最新回复(0)