合规国际互联网加速 OSASE为企业客户提供高速稳定SD-WAN国际加速解决方案。 广告
#### 8.3.3 Toast的Window创建过程 Toast和Dialog不同,它的工作过程就稍显复杂。首先Toast也是基于Window来实现的,但是由于Toast具有定时取消这一功能,所以系统采用了Handler。在Toast的内部有两类IPC过程,第一类是Toast访问NotificationManagerService,第二类是Notification-ManagerService回调Toast里的TN接口。关于IPC的一些知识,请读者参考第2章的相关内容。为了便于描述,下面将NotificationManagerService简称为NMS。 Toast属于系统Window,它内部的视图由两种方式指定,一种是系统默认的样式,另一种是通过setView方法来指定一个自定义View,不管如何,它们都对应Toast的一个View类型的内部成员mNextView。Toast提供了show和cancel分别用于显示和隐藏Toast,它们的内部是一个IPC过程,show方法和cancel方法的实现如下: 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 } } public void cancel() { mTN.hide(); try { getService().cancelToast(mContext.getPackageName(), mTN); } catch (RemoteException e) { // Empty } } 从上面的代码可以看到,显示和隐藏Toast都需要通过NMS来实现,由于NMS运行在系统的进程中,所以只能通过远程调用的方式来显示和隐藏Toast。需要注意的是TN这个类,它是一个Binder类,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示或隐藏请求时会跨进程回调TN中的方法,这个时候由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程中。这里的当前线程是指发送Toast请求所在的线程。注意,由于这里使用了Handler,所以这意味着Toast无法在没有Looper的线程中弹出,这是因为Handler需要使用Looper才能完成切换线程的功能,关于Handler和Looper的具体介绍请参看第10章。 首先看Toast的显示过程,它调用了NMS中的enqueueToast方法,如下所示。 INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } NMS的enqueueToast方法的第一个参数表示当前应用的包名,第二个参数tn表示远程回调,第三个参数表示Toast的时长。enqueueToast首先将Toast请求封装为ToastRecord对象并将其添加到一个名为mToastQueue的队列中。mToastQueue其实是一个ArrayList。对于非系统应用来说,mToastQueue中最多能同时存在50个ToastRecord,这样做是为了防止DOS(Denial of Service)。如果不这么做,试想一下,如果我们通过大量的循环去连续弹出Toast,这将会导致其他应用没有机会弹出Toast,那么对于其他应用的Toast请求,系统的行为就是拒绝服务,这就是拒绝服务攻击的含义,这种手段常用于网络攻击中。 // Limit the number of toasts that any given package except the android // package can enqueue. Prevents DOS attacks and deals with leaks. if (! isSystemToast) { int count = 0; final int N = mToastQueue.size(); for (int i=0; i<N; i++) { final ToastRecord r = mToastQueue.get(i); if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return; } } } } 正常情况下,一个应用不可能达到上限,当ToastRecord被添加到mToastQueue中后,NMS就会通过showNextToastLocked方法来显示当前的Toast。下面的代码很好理解,需要注意的是,Toast的显示是由ToastRecord的callback来完成的,这个callback实际上就是Toast中的TN对象的远程Binder,通过callback来访问TN中的方法是需要跨进程来完成的,最终被调用的TN中的方法会运行在发起Toast请求的应用的Binder线程池中。 void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record ! = null) { if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record. callback); try { record.callback.show(); scheduleTimeoutLocked(record); return; } catch (RemoteException e) { Slog.w(TAG, "Object died trying to show notification " + record. callback + " in package " + record.pkg); // remove it from the list and let the process die 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; } } } } Toast显示以后,NMS还会通过scheduleTimeoutLocked方法来发送一个延时消息,具体的延时取决于Toast的时长,如下所示。 private void scheduleTimeoutLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); } 在上面的代码中,LONG_DELAY是3.5s,而SHORT_DELAY是2s。延迟相应的时间后,NMS会通过cancelToastLocked方法来隐藏Toast并将其从mToastQueue中移除,这个时候如果mToastQueue中还有其他Toast,那么NMS就继续显示其他Toast。 Toast的隐藏也是通过ToastRecord的callback来完成的,这同样也是一次IPC过程,它的工作方式和Toast的显示过程是类似的,如下所示。 try { record.callback.hide(); } catch (RemoteException e) { Slog.w(TAG, "Object died trying to hide notification " + record.callback + " in package " + record.pkg); // don't worry about this, we're about to remove it from // the list anyway } 通过上面的分析,大家知道Toast的显示和影响过程实际上是通过Toast中的TN这个类来实现的,它有两个方法show和hide,分别对应Toast的显示和隐藏。由于这两个方法是被NMS以跨进程的方式调用的,因此它们运行在Binder线程池中。为了将执行环境切换到Toast请求所在的线程,在它们的内部使用了Handler,如下所示。 /** * schedule handleShow into the right thread */ @Override public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.post(mShow); } /** * schedule handleHide into the right thread */ @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.post(mHide); } 上述代码中,mShow和mHide是两个Runnable,它们内部分别调用了handleShow和handleHide方法。由此可见,handleShow和handleHide才是真正完成显示和隐藏Toast的地方。TN的handleShow中会将Toast的视图添加到Window中,如下所示。 mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); mWM.addView(mView, mParams) 而NT的handleHide中会将Toast的视图从Window中移除,如下所示。 if (mView.getParent() ! = null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); } 到这里Toast的Window的创建过程已经分析完了,相信读者对Toast的工作过程有了一个更加全面的理解了。除了上面已经提到的Activity、Dialog和Toast以外,PopupWindow、菜单以及状态栏等都是通过Window来实现的,这里就不一一介绍了,读者可以找自己感兴趣的内容来分析。 本章的意义在于让读者对Window有一个更加清晰的认识,同时能够深刻理解Window和View的依赖关系,这有助于理解其他更深层次的概念,比如SurfaceFlinger。通过本章读者应该知道,任何View都是附属在一个Window上面的,那么这里问一个问题:一个应用中到底有多少个Window呢?相信读者都已经清楚了。