前言
原生Toast的能力
- 提供有关操作的简单信息反馈
- 能够将队列中的View有序弹出,并能控制每个View的显示时长
- 反馈的信息可以为文本或者自定义View
能够满足的需求
- 轻量级的信息提示方式,不占据屏幕,且立马会消失
- 适用于即时消息提示
- 不打断当前操作,也不获取焦点
- 有序对消息进行展示
技术原理
-
创建方法
- 最简单的创建方式
Context context = getApplicationContext();
CharSequence text = "Hello Toast";
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();
- 自定义视图的创建方式
LinearLayout ll = new LinearLayout(this);
Toast toast = new Toast(this);
toast.setView(ll);
toast.setDuration(Toast.LENGTH_SHORT);
toast.setGravity.CENTER, 0, 0);
toast.show();
- Toast为系统级别窗口,层级较高
-
源码分析
- Toast的构造方法 构造方法中创建了TN对象,需要传入Context和Looper
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);
}
Looper其实是TN在使用,因为我们一般把Toast作为UI控件使用,在主线程中操控,传入的looper为null。而传入一个Looper的作用是在子线程中也能保证其正常显示。Looper.myLooper()为获取当前线程,主线程已经为我们创建好了一个线程,故无需传入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()");
}
}
TN继承自ITransientNotification.Stub,是Toast的内部私有静态类,实现了ITransientNotification的show(IBinder windowToken)和hide()方法。
/** @hide */
oneway interface ITransientNotification {
void show(IBinder windowToken);
void hide();
}
- show()方法
该方式是也是同样通过AIDL方式获取NotificationManagerService的接口,然后把TN对象以及参数传递给NotificationManagerService中
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
}
}
其中getService()方法通过单例模式来拿到notification的service对象
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
- enqueueToast(pkg, tn, duration) 该方法为NotificationManagerService类下的方法,先判断这个Toast是否为系统Toast,如果是系统Toast则会进入系统队列中,然后创建Token,将TN和Token等参数封装为ToastRecord对象添加到mToastQueue中。如果当前Toast的index为0,则会进行showNextToastLocked()方法。
synchronized (mToastQueue) {
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) {
index = indexOfToastPackageLocked(pkg);
} else {
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) {
record = mToastQueue.get(index);
record.update(duration);
try {
record.callback.hide();
} catch (RemoteException e) {
}
record.update(callback);
} else {
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) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
- showNextToastLocked()方法
callbake为ITransientNotification接口实现TN对象,这个show()方法为发送Message调用handleShow(windowToken)方法来进行显示。该token即为上面提到的Token,其作用为表示该Toast为系统窗口,具有较高权限和显示层级。该方法配置好WindowManager的相应参数后,将View弹出。
try {
record.callback.show(record.token);
scheduleDurationReachedLocked(record);
return;
}
- cancelToastLocked(index)
scheduleDurationReachedLocked(record)方法为开始计时,在计时到达预定的时间后,发送Message调用cancelToastLocked(index)方法。
而cancelToastLocked(index)方法是调用tn下的hide()方法发送Message调用handleHide()方法,将View移除掉,并释放掉各类资源。
在最后判断mToastQueue中是否还有参数,如果还有则会进行下一轮的View的创建和销毁。
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide();
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to hide notification " + record.callback
+ " in package " + record.pkg);
}
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
DEFAULT_DISPLAY);
scheduleKillTokenTimeout(lastToast.token);
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
showNextToastLocked();
}
}
-
UML图
路径:
Toast.java : \base\core\java\android\widget
ITransientNotification : \base\core\java\android\app
NotificationManagerService :\base\services\core\java\com\android\server\notification
INotificationManager :\base\core\java\android\app
类图:
流程图:
时序图:
-
技术总结
- 通信方式
总共用到两种通信方式,一种是AIDL消息机制,一种是Handler消息机制
- Activity销毁后Toast仍会显示
Toast的token为系统级别的,其次keepProcessAliveIfNeededLocked(callingPid)方法将当前的Toast设置为了前台进程,保证了不会被系统杀死
- 子线程调用Toast
子线程调用Toast需要自己新建Looper,上文有提到
-
Toast显示和隐藏的重点逻辑
- Toast类下的show()方法其实是将本地的TN类型的参数传递给NotificationManager中进行操作
- NotificationManager在收到Toast的show()请求后,会将一个系统级别的Token添加到ToastRecord中与TN封装在一起,传递给TN类下的show()方法,并开启计时监听
- TN对象收到消息就会将Toast中的View添加到WindowManagerService管理,并进行显示。
- 在时间到达设定点后,则会远程调用TN类下的hide()方法移除Toast窗口,然后判断mToastQueue中是否还有对象,如果有则继续进行show()和hide()操作。