首先,所有 Android
进程的视图显示都需要依赖于一个窗口。而这个窗口对象,被记录在了我们的 WindowManagerService(后面简称 WMS) 核心服务中。WMS 是专门用来管理应用窗口的核心服务。当 Android
进程需要构建一个窗口的时候,必须指定这个窗口的类型。 Toast
的显示也同样要依赖于一个窗口, 而它被指定的类型是:
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//系统窗口
可以看出, Toast
是一个系统窗口,这就保证了 Toast
可以在 Activity
所在的窗口之上显示,并可以在其他的应用上层显示。那么,这就有一个疑问:
“如果是系统窗口,那么,普通的应用进程为什么会有权限去生成这么一个窗口呢?”
我们先来看下 Toast
从显示到隐藏的整个流程:
// code Toast.java
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();//调用系统的notification服务
String pkg = mContext.getOpPackageName();
TN tn = mTN;//本地binder
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
我们通过代码可以看出,当 Toast
在 show
的时候,将这个请求放在 NotificationManager
所管理的队列中,并且为了保证 NotificationManager
能跟进程交互, 会传递一个 TN
类型的 Binder
对象给 NotificationManager
系统服务。而在 NotificationManager
系统服务中:
//code NotificationManagerService
public void enqueueToast(...) {
....
synchronized (mToastQueue) {
...
{
// 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) {
//上限判断
return;
}
}
}
}
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token,
WindowManager.LayoutParams.TYPE_TOAST);//生成一个Toast窗口
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveIfNeededLocked(callingPid);
}
....
if (index == 0) {
showNextToastLocked();//如果当前没有toast,显示当前toast
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
们会得到以下的流程(在 NotificationManager
系统服务所在的进程中):
- 判断当前的进程所弹出的
Toast
数量是否已经超过上限MAX_PACKAGE_NOTIFICATIONS
,如果超过,直接返回 - 生成一个
TOAST
类型的系统窗口,并且添加到WMS
管理 - 将该
Toast
请求记录成为一个ToastRecord
对象
代码到这里,我们已经看出 Toast
是如何偷天换日的。实际上,这个所需要的这个系统窗口 token
,是由我们的 NotificationManager
系统服务所生成,由于系统服务具有高权限,当然不会有权限问题。不过,我们又会有第二个问题:
既然已经生成了这个窗口的 Token
对象,又是如何传递给 Android
进程并通知进程显示界面的呢?
我们知道, Toast
不仅有窗口,也有时序。有了时序,我们就可以让 Toast
按照我们调用的次序显示出来。而这个时序的控制,自然而然也是落在我们的NotificationManager
服务身上。我们通过上面的代码可以看出,当系统并没有 Toast
的时候,将通过调用 showNextToastLocked();
函数来显示下一个Toast
。
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
...
try {
record.callback.show(record.token);//通知进程显示
scheduleTimeoutLocked(record);//超时监听消息
return;
} catch (RemoteException e) {
...
}
}
}
这里,showNextToastLocked
函数将调用 ToastRecord
的 callback
成员的 show
方法通知进程显示,那么 callback
是什么呢?
final ITransientNotification callback;//TN的Binder代理对象
我们看到 callback
的声明,可以知道它是一个 ITransientNotification
类型的对象,而这个对象实际上就是我们刚才所说的 TN
类型对象的代理对象:
private static class TN extends ITransientNotification.Stub { ...}
那么 callback
对象的show
方法中需要传递的参数 record.token
呢?实际上就是我们刚才所说的NotificationManager
服务所生成的窗口的 token
。 相信大家已经对 Android
的 Binder
机制已经熟门熟路了,当我们调用 TN
代理对象的 show
方法的时候,相当于 RPC
调用了 TN
的 show
方法。来看下 TN
的代码:
// code TN.java
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);//处理界面显示
}
};
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
这时候 TN
收到了 show
方法通知,将通过 mHandler
对象去 post
出一条命令为 0 的消息。实际上,就是一条显示窗口的消息。最终,将会调用handleShow(Binder)
方法:
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
....
mParams.token = windowToken;
...
mWM.addView(mView, mParams);
...
}
}
而这个显示窗口的方法非常简单,就是将所传递过来的窗口 token
赋值给窗口属性对象 mParams
, 然后通过调用 WindowManager.addView
方法,将 Toast
中的mView
对象纳入 WMS
的管理。
上面我们解释了 NotificationManager
服务是如何将窗口 token
传递给 Android
进程,并且 Android
进程是如何显示的。我们刚才也说到,NotificationManager
不仅掌管着 Toast
的生成,也管理着 Toast
的时序控制。
因此,我们需要穿梭一下时空,回到 NotificationManager
的showNextToastLocked()
方法。大家可以看到:在调用 callback.show
方法之后又调用了个 scheduleTimeoutLocked
方法: