日常开发中经常使用到Toast开发,用于显示一个提示用户的弹出消息,简单易用。
Toast.makeText(this, "Hello world", Toast.LENGTH_SHORT).show();
这样一句代码即可实现通用的,系统默认样式的Toast。当然也可以自定义。关于如何自定义在分析完Toast的代码后,即可知道。
makeText 创建Toast
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
Toast result = new Toast(context); //使用Toast的默认构造函数构造一个Toast 代码如下
LayoutInflater inflate = (LayoutInflater) //获取LayoutInflater
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); //inflate默认的样式
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message); //设置消息
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
show 显示Toast
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService(); //获取系统的INotificationManager 其实就是NotificationManagerService
String pkg = mContext.getOpPackageName(); //获取包名
TN tn = mTN; // TN 后面再分析 比较麻烦
tn.mNextView = mNextView; //设置了文本TextView的索引
try {
service.enqueueToast(pkg, tn, mDuration); //将要显示的内容打包进TN后压入service的Toast队列中
} catch (RemoteException e) {
// Empty
}
}
Toast
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. Toast show是通过将显示申请打包进TN,然后通过INotificationManager远程接口,实现进程间通信,添加TN到NotificationManagerService的ToastQuueue中。可以猜测,在服务程序中肯定是通过循环获取队列中的TN进行处理。
2. 有show函数,肯定还有cancle函数。cancle函数简单的设置了mTN的显示属性后,就调用getService.cancleToast
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
下面分析Toast构造和INotificationManager
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);
}
可以看到,核心内容还是TN,通过对TN进行了一些显示相关的参数配置。关于TN的详细内容,可以在代码Toast.java中查看。在后面再进行分析。
在show函数可以知道,显示一个Toast只是单纯的加入一个队列中。
public void show() {
...
INotificationManager service = getService(); //获取系统的INotificationManager 其实就是NotificationManagerService
...
service.enqueueToast(pkg, tn, mDuration); //将要显示的内容打包进TN后压入service的Toast队列中
}
其中getService如下:
static private INotificationManager getService() {
...
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
可以知道改函数获取的是系统的 NotificationManagerService,其实现类为NotificationManagerService.java 需要知道的是,上面的大量操作都在进行IPC操作。:(
下面直接分析NotificationManagerService 看看加入队列函数 enqueueToast 到底在做什么。
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
... 检测pkg和callback参数是否有效 以及判断是不是系统Toast 以及一些合法性检测
synchronized (mToastQueue) { //这里进行添加操作 加锁是为了防止多个应用程序同时提交出差
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback); //在现有的队列中依据pkg和callback查看是不是Toast已经提交过 防止多次提交
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration); //如果提交过 只是单纯的跟新一下duration 。。。
} else {
// 看注释就知道 下面的代码就是为了看看一个应用程序是不是提交的Toast显示已经超过了最大的许可 50 次 防止DOS攻击
// 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= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
record = new ToastRecord(callingPid, pkg, callback, duration); //如果一切检测合法 而且没有添加过还可以添加的情况下 将请求的Toast信息构造成一个ToastRecord 然后加入mToastQueue
mToastQueue.add(record); //mToastQueue是一个全局变量哦 整个系统只有一个
index = mToastQueue.size() - 1;
keepProcessAliveLocked(callingPid);
}
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated. Call back and tell it to show itself.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
if (index == 0) { //这句话是重点 从这里启动了NotificationManagerService对Toast进行显示
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
关于为何是showNextToastLocked开始了mToastQueue的提取以及Toast显示这里进行说明,因为mToastQueue函数里面 将一个新的Toast事件添加进队列的时候,是进行了加锁的。所以,当index == 0 就说明,当前队列之前是没有数据的,现在添加一个后刚好是第一个开始工作。也就是showNextToastLocked
在showNextToastLocked函数里面:
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) { //虽然是个while但是 只是为了错误处理 真正的循环不是这里
try {
record.callback.show(); //调用callback进行show 下面分析
scheduleTimeoutLocked(record); //函数中进行了延时操作 同时这里使得下一个Toast能够继续显示
return;
} catch (RemoteException e) {
... do not care
}
}
}
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); //这这里 一个添加流程已经ok mToastQueue 锁已经释放 可以进行下一次添加
}
scheduleTimeoutLocked 没有什么特殊的,单纯的构造了一个Message 然后使用record里面的duration选择了一个delay值 然后使用了handler的sendMessageDealyed。至于handler的使用,再别处再论。这里直接找到mHandler的定义看看,消息是如何处理的。
在源代码中可以很方面的找到mHandler的定义:
private final class WorkerHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord)msg.obj);
break;
... don't care
}
}
只看handlerTimeout:一看即明白
private void handleTimeout(ToastRecord record)
{
if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback); //找到toast然后
if (index >= 0) {
cancelToastLocked(index); //cancel掉
}
}
}
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide(); //回调hide函数 后面分析
} ...
mToastQueue.remove(index); //从列表中移除
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) { //如果列表不为空 就接着显示 同样是加了mToastQueue锁的
showNextToastLocked();
}
}
至此,Toast的添加显示过程就分析明白了。下面看看show和hide函数都是什么。要分析show和hide还是要看Toast里面的TN的源码,因为TN继承了ITransientNotification.Stub实现了ITransientNotification接口
private static class TN extends ITransientNotification.Stub {
final Runnable mShow = new Runnable() { //异步显示 必须要在UI线程进行
@Override
public void run() {
handleShow(); //进行显示
}
};
final Runnable mHide = new Runnable() { //异步消失
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
final Handler mHandler = new Handler();
...
TN() {
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
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;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON //这些配置使得Toast不能响应触摸点击事件
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
}
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow); //用过handler post一个Runable 然后就在主线程执行了
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide); //同上
}
public void handleShow() {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
。。。
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
。。。
mWM.addView(mView, mParams); //然后就通过到windowManager add到线上屏上
}
}
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView); //remove了
}
mView = null;
}
}
}
至此 Toast 的添加显示销毁就分析完成了。大量细节使用了IPC实现。调用NotificationManagerService和WindowManager本质上都是IPC
1. 系统所有的Toast都是由NotificationManagerService的mToastQueue进行统一维护
2. 每次show操作本质上是向mToastQueue队列进行一个添加入队列的操作。
3. 每次对mToast的操作都会进行加锁
4. 显示持续时间效果是由 Handler 的 postDelay进行的实现
5. Toast的显示本质是使用WindowManager进行显示和销毁
6. Toast不能响应触摸点击事件 是系统进行了参数设置
7. Toast的显示样式可以进行定制 具体定义细节参考Toast的makeToast静态方法。