Android中Toast的显示和隐藏分析
PS:本文系转载文章,阅读原文可读性会更好,文章末尾有原文链接
ps:源码是基于 android api 27 来分析的,demo 是用 kotlin 语言写的。
Toast 作为 Android 系统中最常用的类之一,因为它方便的 API 设计和简洁的交互体验,所以我们会经常用到,也所以深入学习 Toast 也是很有必要的;在 Android 中,我们知道凡是有视图的地方就会有 Window,Toast 显示出来的提示也属于视图,所以 Toast 依赖于 Window,而且还是系统窗口,Window 对象是 WindowManagerService 这个类所管理;根据 Type 参数可划分 Window 类型,Window 可分为应用 Window、子 Window 和系统 Window;Window 是有分层的,其中应用 Window 的层级范围是1~99,子 Window 的层级范围是1000~1999,系统 Window 的层级范围是2000~2999,这些层级范围对应着 WindowManager.LayoutParams 的 type参数;最大的层级其他 Window 的最顶层,所以系统 Window 在显示问题上具有最高优先级。为什么说 Toast 是系统 Window 呢?我们来看 Toast 的指定类型;
WindowManager 的内部类 LayoutParams 中的常量定义;
public static final int FIRST_SYSTEM_WINDOW = 2000;
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
可以看出 Toast 的 Type 值为2005,属于系统 Window,确保了在 Activity 所在的窗口之上以及其他的应用上层显示。
好了,到了这里,我们要开始分析 Toast 的显示和隐藏的源码了,先从 Toast 的 makeText(Context context, CharSequence text, @Duration int duration) 方法开始;
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
//1、
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;
}
Toast 的 makeText(Context context, CharSequence text, @Duration int duration) 方法最终调用了 Toast 中注释1 中的方法,该注释1 中的方法是创建一个 Toast 对象并用 transient_notification.xml 布局创建一个视图,从该视图中获取 TextView,用这个 TextView 设置提示的内容,然后将视图赋值给 Toast 对象的 mNextView,将 duration 赋值给 Toast 对象的 mDuration,最后返回 Toast 对象。
我们来看 Toast.LENGTH_SHORT 和 Toast.LENGTH_LONG 这2个真正的延长时间是多少,看一下 NotivicationManagerService 的 scheduleTimeoutLocked 方法;
@GuardedBy("mToastQueue")
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);
}
看一下 NotivicationManagerService 的 LONG_DELAY 和 SHORT_DELAY 声明;
static final int LONG_DELAY = PhoneWindowManager.TOAST_WINDOW_TIMEOUT;
static final int SHORT_DELAY = 2000; // 2 seconds
再看一下 PhoneWindowManager 的 TOAST_WINDOW_TIMEOUT 声明;
public static final int TOAST_WINDOW_TIMEOUT = 3500; // 3.5 seconds
所以 Toast.LENGTH_SHORT 的延时时间为 2s,Toast.LENGTH_LONG 的延时时间为 3.5s。
我们来看一下 Toast 的 show 方法;
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}//2、
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {//3、
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
注释2 中拿到的 INotificationManager 对象,它具体的实现类是 NotificationManagerService 的 IBinder 类型的 mService 属性;注释3 中将请求放在 NotificationManagerService 中 IBinder 类型的 mService 属性的 enqueueToast 方法,并传递一个实现了 ITransientNotification.Stub 类的 Toast 内部类 TN,TN 是一个 Binder 对象,能进行跨进程通信。
我们看一下 NotificationManagerService 中 IBinder 类型的 mService 属性的 enqueueToast 方法;
public class NotificationManagerService extends SystemService {
......
private final IBinder mService = new INotificationManager.Stub() {
// Toasts
// ============================================================================@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
......
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
if (index >= 0) {
......
} else {
// 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++;
//4、
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
//5、
record = new ToastRecord(callingPid, pkg, callback, duration, token);
//6、
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.
//7、
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
......
};
}
注释4 表示判断当前的进程所弹出的 Toast 数量是否已经超过上限 MAX_PACKAGE_NOTIFICATIONS ,这里的 MAX_PACKAGE_NOTIFICATIONS 值是50,如果超过,直接返回;注释5 表示把 TN 封装在 ToastRecord 中;注释6 表示把 ToastRecord 保存在 ArrayList 类型的 mToastQueue 属性中;注释7 表示如果没有其他的 Toast 了,那么就显示当前的 Toast。
我们来看 NotificationManagerService 的 showNextToastLocked 方法;
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
//8、
record.callback.show(record.token);
//9、
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
......
}
}
}
注释8 表示显示 Toast,record.callback 是 ITransientNotification 类型的对象,ITransientNotification.Stub 实现了 ITransientNotification,Toast 的内部类 TN 又继承了 ITransientNotification.Stub,所以我们看 Toast 的内部类 TN 的 show 方法;
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
......
}
}
};
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
我们看到 TN 的 show 方法调用了 TN 的内部类 Handler,Handler 根据 msg.what 又调用了 TN 的 handleShow 方法,我们看 handleShow 方法;
public void handleShow(IBinder windowToken) {
......
if (mView != mNextView) {
......
mParams.token = windowToken;
......
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
这个方法将所传递过来的窗口 token 赋值给 Toast 的窗口属性对象 mParams, 然后通过调用 WindowManager.addView 方法,将 Toast 中的 mView 对象纳入 WMS 的管理,WindowManager.addView 方法最终完成 Toast 的显示。
我们回看 NotificationManagerService 的 showNextToastLocked 方法中注释9 的代码,也就是 NotificationManagerService 的 scheduleTimeoutLocked 方法;
@GuardedBy("mToastQueue")
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);
}
在该方法中的 Handler 的实现类是 NotificationManagerService 内部类 WorkerHandler,将 Message 的 what 置为 MESSAGE_TIMEOUT 就延时发送一条消息,我们且看 WorkerHandler 对应的处理;
private final class WorkerHandler extends Handler {
......
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord) msg.obj);
break;
......
}
}
}
WorkerHandler 根据 msg.what == MESSAGE_TIMEOUT 又调用了 NotificationManagerService 的 handleTimeout 方法;
private void handleTimeout(ToastRecord record) {
......
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
handleTimeout 方法经过搜索后调用 NotificationManagerService 的 cancelToastLocked 方法取消掉 Toast 的显示,往下看 cancelToastLocked 方法;
@GuardedBy("mToastQueue")
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {//10、
record.callback.hide();
} catch (RemoteException e) {
......
}ToastRecord lastToast = mToastQueue.remove(index);
//11、
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
......
}
我们先看注释11 中的代码,WindowManagerInternal 的 实现类是 WindowManagerService 的内部类 LocalService,我们看 LocalService 的 removeWindowToken 方法;
@Override
public void removeWindowToken(IBinder binder, boolean removeWindows, int displayId) {
synchronized(mWindowMap) {
......
WindowManagerService.this.removeWindowToken(binder, displayId);
}
}
从 LocalService 的 removeWindowToken 方法可以看出,Toast 生成的窗口 Token 从 WMS 服务中删除。
我们回看 NotificationManagerService 的 cancelToastLocked 方法中注释10 的代码;前面说过 record.callback 是 TN 的具体对象,我们看看 TN 的 hide 方法;
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
......
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
......
}
}
};
TN 的 hide 方法发送一条消息给 Handler,Handler 又调用了 TN 的 handleHide 方法;
public void handleHide() {
......
if (mView != null) {
......
if (mView.getParent() != null) {
......
mWM.removeViewImmediate(mView);
}mView = null;
}
}
TN 的 handleHide 方法通过 WindowManagerService 删除了 Toast 的 View,成功实现对 Toast 的隐藏。
这里我们有一个疑问,如果在主线程中调用 Toast 的 show 方法,然后在延时4s会看到 Toast 提示吗?
我们写个 demo 验证一下;
(1)新建一个 kotlin 语言类型的 Activity,名叫 MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}fun onClick(v: View) {
Toast.makeText(this@MainActivity, "被点击了", Toast.LENGTH_SHORT).show()
SystemClock.sleep(4 * 1000)
}
}
(2)新建一个 MainActivity 对应的布局文件 activity_main.xml :
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.xe.postdemo.MainActivity">
程序运行后,界面如下所示(注意:我是在 Android API 29 的手机上运行的):
图片
点击 “Toast的show方法被调用后,主线程加了个4s的延时” 按钮后,发现没有 Toast 提示,是什么原因呢?是 Activity 超时无响应吗?不是这个原因,Activity 超时无响应是发生延时大于等于5s的条件下;是因为 Toast 的 show 方法是跨进程通信,Toast 的 show 方法发起系统 NotificationManagerService 内部的 INotificationManager.Stub 请求后,App 这边的主线程就进行4s的延时;其中 INotificationManager.Stub 请求是属于系统进程,App 这边的主线程属于非系统进程,当系统进程回调 TN 的 show 方法时,发现 App 这边的主线程进行延时,系统进程回调 TN 的 show 方法就会处于消息阻塞状态,等到4s延时结束后就会执行 TN 的 show 方法,但是4s延时结束之前由于 Toast.LENGTH_SHORT 那么长时间后,也就是2s后就进行了 NotificationManagerService 的 cancelToastLocked 方法调用,cancelToastLocked 方法执行了 WindowManagerService 对 窗口 token 的删除;当4s延时结束后,TN 最终会执行 handleShow 方法,但是这时候发现 handleShow 方法的 IBinder 类型的 windowToken 不存在了,所以就显示不了 Toast 的提示内容了。
如果在 Android API 25 及其以下 API 版本的手机,有的手机会出现报错,我先列出 API 25 中 TN 的 handleShow 方法;
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
......
//
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
【Android中Toast的显示和隐藏分析】是由于 mWM.addView(mView, mParams) 语句没有进行异常捕获引起的。
推荐阅读
- 热闹中的孤独
- android第三方框架(五)ButterKnife
- Shell-Bash变量与运算符
- JS中的各种宽高度定义及其应用
- 2021-02-17|2021-02-17 小儿按摩膻中穴-舒缓咳嗽
- 深入理解Go之generate
- 异地恋中,逐渐适应一个人到底意味着什么()
- 我眼中的佛系经纪人
- 《魔法科高中的劣等生》第26卷(Invasion篇)发售
- “成长”读书社群招募