最近在基于Toast做一个需求,其中一个功能是让Toast一直显示,做出来后,在6.0平台上,Toast能够正常显示;但是在8.1上就不行,所以专门研究了下Toast的运行流程.
手机上的所有Toast都由系统统一调度,更具体点说,就是NotificationManagerService这个类,这就涉及到跨进程通信,应用进程涉及到Toast的跨进程通信都由Toast的内部类TN来完成,TN是一个Binder实体,NotificationManagerService会记录每个TN的代理对象,NotificationManagerService有一个名为mToastQueue的ArrayList,这个容器存放了所有发往系统的Toast信息,Toast信息以ToastRecord的形式存在(跟ActivityRecord,ServiceRecord等一毛一样),用图形表示如下:
Toast的使用非常的简单,如下所示:
Toast.makeText(this,"显示toast",Toast.LENGTH_SHORT).show();
上面的代码一共分成两部分,第一部分是makeText这个方法,第二个是show这个方法,先来看下makeText方法:
/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used.
* @hide
*/
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
//首先创建一个toast对象
Toast result = new Toast(context, looper);
//设置toast的布局
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);
//记住mNextView,在显示Toast的时候会用到它,这个view就是我们看到的Toast的效果
result.mNextView = v;
result.mDuration = duration;
//返回toast
return result;
}
当然,上面的方法是一个重载的方法,我们直接调用的不是这个方法,但是我们自己调用makeText方法最后肯定会走到上面的方法中去的,所以直接看这个方法.makeText方法其实不难,最主要的是创建Toast对象,我们来看下他的构造方法:
/**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
//创建一个TN对象,重要
mTN = new TN(context.getPackageName(), looper);
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对象是一个非常重要的对象,Toast的显示和消失,都是由系统来控制,由TN来实现;可以看出,一个Toast就对应一个TN对象,来看看TN的实现:
private static class TN extends ITransientNotification.Stub {
//可以看到,TN是一个binder实体对象
TN(String packageName, @Nullable Looper looper){
//Toast窗口的属性
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
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
//显示Toast的包名
mPackageName = packageName;
//初始化Looper对象
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
//hanlder对象,控制了toast的显示和消失
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
}
从上面的分析可以看出,当我们调用Toast.makeText的时候,系统创建了一个Toast对象,为他设置了布局,同时设置了Toast的窗口属性,还创建了一个真正实现Toast显示和隐藏功能的TN对象.
下面再来分析下show方法:
/**
* Show the view for the specified duration.
*/
public void show() {
//这个mNextView是创建Toast的时候赋值的,一般不为null
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
//获取通知服务
INotificationManager service = getService();
//显示Toast的包名
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
//调用NotificationManagerService的enqueueToast方法
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
show方法本身非常简单,不用多说,直接看NotificationManagerService的enqueueToast方法:
//第一个参数是显示Toast的包名;第二个参数就是上面分析过的TN对象,
//在这里他是TN的一个代理对象;第三个参数是Toast的显示时间
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
//如果包名或者TN为空,那么直接返回
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
//是不是系统弹的Toast,isCallerSystemOrPhone方法很简单,不多说
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
//应用被挂起?
final boolean isPackageSuspended =
isPackageSuspendedForUser(pkg, Binder.getCallingUid());
//如果不是系统弹的Toast,而且允许Toast被阻塞,而且该应用
//允许弹通知或者挂起,那么该Toast将不会被弹出来
if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
(!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
|| isPackageSuspended)) {
return;
}
//mToastQueue是一个ToastRecord列表
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
//该包在Toast列表中的索引
int index;
// All packages aside from the android package can enqueue one toast at a time
//如果不是系统Toast的话,直接根据包名去找ToastRecord
if (!isSystemToast) {
index = indexOfToastPackageLocked(pkg);
} else {
//普通应用的话,不仅要根据包名,还要根据TN去找
index = indexOfToastLocked(pkg, callback);
}
//如果index大于0,说明此应用之前显示过Toast
if (index >= 0) {
//取出ToastRecord,然后更新;更新过程非常简单,
//就是重新设置duration和callback
record = mToastQueue.get(index);
record.update(duration);
record.update(callback);
} else {
//进入这个分支,说明此应用从没显示过Toast
Binder token = new Binder();
//处理token
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
//创建一个ToastRecord
record = new ToastRecord(callingPid, pkg, callback, duration, token);
//添加进ToastRecord列表(的末尾)
mToastQueue.add(record);
//此应用在Toast列表的索引
index = mToastQueue.size() - 1;
}
//处理进程,意思应该是一个正要显示Toast的应用应该处于活跃状态
keepProcessAliveIfNeededLocked(callingPid);
//如果该Toast位于Toast列表的第一个,那么调用showNextToastLocked显示它
if (index == 0) {
showNextToastLocked();
}
}finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
enqueueToast其实也不难,首先处理一些不应该显示Toast的场景;其次根据包名或者TN去查找ToastRecord,找到了就更新下,没找到就创建一个添加进Toast列表;接着就更新进程相关的信息;最后如果该Toast位于Toast列表的第一个,那么调用showNextToastLocked显示这个Toast:
@GuardedBy("mToastQueue")
void showNextToastLocked() {
//既然要显示,肯定先显示列表中的第一个,所以取出第一个ToastRecord
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
//调用TN的show方法显示Toast
record.callback.show(record.token);
//超时处理,不管
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
//如果Toast显示失败,那么将这个ToastRecord中列表中移除,然后显示下一个
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
可以看到,showNextToastLocked其实也很简单,就是调用TN的show方法;如果显示失败,接着显示下一个Toast.
到目前为止,Toast的framework层的流程已经分析完毕,很简单;下面分析TN是怎么显示Toast的:
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
--------------------------------------------------------------------------
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
......
}
--------------------------------------------------------------------------
public void handleShow(IBinder windowToken) {
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
//如果当前Toast正在等待消失或者隐藏,那么此Toast就不显示了,直接返回
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
//mView是上次Toast显示的view,mNextView是这次显示的view
if (mView != mNextView) {
//移除上次显示的view,简单,不多说
handleHide();
//下面一坨代码都是处理Toast的窗口属性
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
//下面几行代码的意思是如果此View有父View的话,就将View移除,不太懂
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
//将此View添加进WindowManager,添加进去后就会触发
//测量,布局,绘制三大步骤,然后Toast就显示出来了
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
总结:
Toast的显示流程包括三个部分,第一个是Toast的创建;第二个是Toast在framework的处理;第三是Toast的显示:
1.Toast的创建是由Toast.makeText触发的,makeText方法会导致一个空的Toast对象和TN对象的创建,空的Toast创建后,就会加载布局,默认是transient_notification.xml,然后把这个View赋值给mNextView;在创建空的Toast对象的时候,还会创建一个TN对象,在TN对象的构造函数里面会初始化Toast的窗口属性,然后里面还有个Handler,用于控制Toast的显示,隐藏,取消;
2.Toast在framework的处理流程从enqueueToast方法开始,首先他会判断要不要显示此Toast,如果不显示,那么直接return;接着根据包名或者包名+TN去Toast队列里面查找相应的ToastRecord,一个ToastRecord对应一个应用进程里面的Toast;如果找到了,就更新这个ToastRecord的duration和TN;如果没有找到,就创建一个ToastRecord对象,然后添加进Toast列表的末尾;如果添加进去的Toast是Toast队列的第一个Toast,那么就显示这个Toast;显示的流程也很简单,就是死循环,不停的从队列里面拿到ToastRecord,然后调用Toast对应的TN的show方法显示Toast,一直到没有Toast为止;
3.TN收到显示的消息后,就会创建WindowManager对象,设置窗口属性,然后将第一步的View添加进WindowManager,这样,Toast就显示出来了.
以上,就是Toast的显示流程
网友评论