Toast显示流程:源码分析
前提:不熟悉Toast基础知识的可以先查看我的上一篇文章Toast解析《一》
时序图如下:
Toast创建
Toast创建分为两种:直接使用构造函数调用和使用makeText调用,我们以经常使用的makeText进行讲解;
makeText源码:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);
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;
}
makeText函数有带有3个参数和4个参数,我们经常使用3个参数,但最终都是调用4个参数;首先介绍4个参数
- context
The context to use. Usually your {@link android.app.Application} or {@link android.app.Activity} object.
上下文,需使用Application或者Activity的context;其他Context不可以。 - text
The text to show. Can be formatted text.
显示的文本。 - looper
Make a standard toast to display using the specified looper.If looper is null, Looper.myLooper() is used.
looper为TN构造方法所需参数,用于构建mHandler。 - duration
How long to display the message. Either {@link #LENGTH_SHORT} or{@link #LENGTH_LONG}
Toast显示的时长
然后makeText主要做了两件事:构建Toast和填充Toast。
1. 构建Toast
构造函数代码如下:
public Toast(Context context) {
this(context, null);
}
/**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
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);
}
其中最主要的任务就是构建mTN及初始化mTN的相关变量。
TN的构造方法
TN(String packageName, @Nullable Looper looper) {
// 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;//将mParams引用传递给params
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;
mPackageName = packageName;
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()");
}
}
// 初始化mHandler,looper为makeText传入的参数,用于处理Notification和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;
}
/**省略代码,处理不同的消息**/
}
};
}
2. 填充Toast
使用LayoutInflater解析布局并将相关组件赋值个Toast。
##Toast显示(show)
主要分析当我们调用show方法,系统怎么显示Toast
源码如下:
/**
* Show the view for the specified duration.
*/
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();//使用NotificationService的Binder,客户端的引用
String pkg = mContext.getOpPackageName();// 包名
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);//交由NotificationService进行管理
} catch (RemoteException e) {
// Empty
}
}
主要分析service.enqueueToast(pkg, tn, mDuration)(源码位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java):
enqueueToast源码:
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (DBG) {
Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
+ " duration=" + duration);
}
//如果包名 或者 TN回调为空,直接返回
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
// 判断是否为系统弹出的Toast;
// System进程、Phone进程、UID为0,或者包名为android;则为系统Toast
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
//调用进程是否被挂起
final boolean isPackageSuspended =
isPackageSuspendedForUser(pkg, Binder.getCallingUid());
// 特殊条件下过滤该消息
if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
(!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
|| isPackageSuspended)) {
Slog.e(TAG, "Suppressing toast from package " + pkg
+ (isPackageSuspended
? " due to package suspended by administrator."
: " by user request."));
return;
}
//首先是线程同步的,保证队列线程同步,防止出现异常
synchronized (mToastQueue) {
// 记录调用方的Pid 和 并清除调用者相关信息。
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index;
// All packages aside from the android package can enqueue one toast at a time
if (!isSystemToast) {
// 查找当前队列中包名与当前消息的pkg相等的位置,如果不存在返回-1,存在返回位置index;
// 作用:对于同一个包名的消息,队列中只允许存在一个。
index = indexOfToastPackageLocked(pkg);
} else {
// 和上面类似,但条件苛刻,包名和callback均需要相同
index = indexOfToastLocked(pkg, callback);
}
// If the package already has a toast, we update its toast
// in the queue, we don't move it to the end of the queue.
if (index >= 0) {
// 复用原来的队列中的Toast
record = mToastQueue.get(index);
record.update(duration);
record.update(callback);
} else {
// 新增ToastRecord并将他放置队列尾部
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
}
// 保证调用线程不被杀死
keepProcessAliveIfNeededLocked(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) {
// 展示Toast
showNextToastLocked();
}
} finally {
// 恢复Binder信息,与Binder.clearCallingIdentity()对应。
Binder.restoreCallingIdentity(callingId);
}
}
}
那么当处于队列首位时,执行showNextToastLocked进行Toast的展示
代码
@GuardedBy("mToastQueue")
void showNextToastLocked() {
// 获取队列的头元素
ToastRecord record = mToastQueue.get(0);
// 当头元素不为空时,进行show操作;若该操作发生异常,移除异常元素,继续遍历下一个。
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
record.callback.show(record.token);//回调到Toast的TN,进行Toast的展示;
//使用handler postdelay进行将Toast隐藏,这也是Toast自动隐藏的机制;
//隐藏过后,又会去获取队列头元素进行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);
}
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
流程图如下:
那么最后通过callback回调到Toast,此处的callbak就是Toast中的TN:
我们来看下show方法:
/**
* 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进行处理,查看mHandler的定义:
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;
}
}
}
};
然后实际执行显示的入口为handleShow(token)
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
// 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.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
// 如果上一次显示view与这次不同
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
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);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
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;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 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;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
其实看到最后就是使用WindowManager的addview将Toast显示出来。
##小结
至此Toast的显示源码分析就完成了;有如下几个疑点还需要研究确认。
- package:getOpPackageName、getBasePackageName、getPackageName具体含义及区别
- Token:作用是什么,ToastRecord数据结构中包含一个token;然后LayoutParams mParams中mParams.token = windowToken;
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
Toast中设计的好处,使用了哪些模式和方法。