Android9.0针对Toast的特殊处理
前言
我们都清楚,Toast显示时长有两个选择,长显示是3.5秒,端显示是2秒。那如果想要做到长时间显示,该怎么做呢?有个历史遗留的app通过开一个线程,不断调用show方法进行实现,这些年也没出过问题,直到系统版本更新到了Android9.0。实现方式大概如下:
mToast = new Toast(context);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.setView(layout);
...
mToast.show(); //在线程里不断调用show方法,达到长时间显示的目的
在Android9.0上,Toast闪现了一下就不见了,并没有如预期那样,长时间显示。为什么呢?
概述
这里我们先来大概了解下Toast的显示流程。
Toast使用
一般使用Toast的时候,比较简单的就是如下方式:
Toast.makeText(mContext, "hello world", duration).show();
这样就可以显示一个toast。还有一种是自定义view的:
mToast = new Toast(context);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.setView(layout);
mToast.show();
原理都一样,先new 一个Toast,然后设置显示时长,设置toast中要显示的view(text也是view),然后就可以show出来。
Toast原理
Toast实现
先看看Toast的实现:
//frameworks/base/core/java/android/widget/Toast.java
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);
}
Toast的构造函数很简单,主要就是mTN这个成员,后续对Toast的操作都在这里进行。紧接着就是设置Toast显示时长和显示内容:
public void setView(View view) {
mNextView = view;
}
public void setDuration(@Duration int duration) {
mDuration = duration;
mTN.mDuration = duration;
}
Toast显示
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
}
}
show方法简单,最终是调用了通知服务的enqueueToast方法:
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
...
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
...
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);
}
}
}
Toast的管理是通过ToastRecord类型列表集中管理的,NotificationManagerService会将每一个Toast封装为ToastRecord对象,并添加到mToastQueue中,mToastQueue的类型是ArrayList。在enqueueToast中,首先会判断应用是否为系统应用,如果是系统应用,则通过indexOfToastLocked来寻找是否有满足条件的Toast存在:
int indexOfToastLocked(String pkg, ITransientNotification callback)
{
IBinder cbak = callback.asBinder();
ArrayList<ToastRecord> list = mToastQueue;
int len = list.size();
for (int i=0; i<len; i++) {
ToastRecord r = list.get(i);
if (r.pkg.equals(pkg) && r.callback.asBinder().equals(cbak)) {
return i;
}
}
return -1;
}
判断的依据是包名和callback,这里的callback其实就是上文说到的TN类,这是一个Binder类型,继承自ITransientNotification.Stub。如果条件符合,则返回对应索引,否则返回-1。首次show Toast的时候,肯定返回-1,则此时会new一个ToastRecord对象,并且加入到mToastQueue中,此时的index则为0:
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
那么就会走到如下分支了:
if (index == 0) {
showNextToastLocked(); //显示Toast
}
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
record.callback.show(record.token); //调用TN类的show方法
scheduleDurationReachedLocked(record); //时间到就隐藏Toast
return;
} catch (RemoteException e) {
...
}
}
}
该方法也简单,就是回调TN类的show方法,上文提过,TN类对外提供show,hide, cancel等方法,在这些方法中,再通过内部handler进行处理:
//frameworks/base/core/java/android/widget/Toast.java
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
//贴出部分handleMessage方法
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
public void handleShow(IBinder windowToken) {
...
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...
try {
mWM.addView(mView, mParams); //交给WMS进行下一步的操作,最终显示出我们的view
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
调用show方法,最终会调用到handleshow方法,在该方法中使用WMS服务将view显示出来。
Toast隐藏
显示说完了,什么时候隐藏消失?在scheduleDurationReachedLocked方法中:
//frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
private void scheduleDurationReachedLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
这里也是使用了一个handler来进行处理,delay的时长取决于我们之前设置的Toast显示时长。长时间为3.5秒,短时间为2秒。
MESSAGE_DURATION_REACHED消息处理如下:
case MESSAGE_DURATION_REACHED:
handleDurationReached((ToastRecord)msg.obj);
break;
private void handleDurationReached(ToastRecord record)
{
if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide(); //隐藏掉该Toast
} catch (RemoteException e) {
...
}
ToastRecord lastToast = mToastQueue.remove(index); //已经显示完毕的Toast,从列表中移除掉
...
if (mToastQueue.size() > 0) { //如果还有待显示Toast
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
showNextToastLocked();
}
}
该方法调用TN的hide方法隐藏掉Toast,然后再将Toast从列表中移除。看看隐藏的过程:
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null; //这里会把view清掉
break;
}
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
...
mWM.removeViewImmediate(mView);
...
mView = null;
}
}
隐藏的过程,其实也简单,将view从窗口中移除,然后将mNextView和mView置Null。
到此Toast的显示和隐藏已经讲完。下面说说多次show为什么会导致Toast消失。
Toast的消失
想象一个场景,如果一个全局Toast(此次出问题的app中就是一个全局Toast),我们不断的去调用Toast的show方法,那么就意味着上文说的mToastQueue列表不为空,存在Toast,就会走到如下分支:
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(); //如果存在已经显示的Toast,这里会先进行hide
} catch (RemoteException e) {
}
record.update(callback);
}
}
hide的流程我们已经清楚,会将资源释放,将mNextView和mView置为Null。执行到这里会导致第一个Toast消失,之后调用showNextToastLocked()方法显示第二个Toast,最终调用到TN的handleShow方法:
public void handleShow(IBinder windowToken) {
// ...
if (mView != mNextView) {
// ...
mView = mNextView;
// ...
mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// ...
mWM.addView(mView, mParams);
// ...
}
}
由于所有的Toast都对应一个TN对象,因此此时mView和mNextView均为null,不会执行mWM.addView(),Toast也就不会显示。
解决方法
在Android9.0中如果想要一直显示某个Toast,怎么做?使用局部Toast,不要使用全局Toast。
但有一点比较奇怪的是,查看了Android10.0代码,发现Android10.0将这个机制回滚了。即Android10.0上又可以一直显示Toast:
//这里就不执行hide的操作了
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
}
结语
Android多个系统版本中,唯独Android9.0做了这个特殊处理,无非就是禁用应用长时间显示Toast。但10.0版本又取消了这个处理,难道是发现这样处理并不合适?
微信公众号
我在微信公众号也有写文章,更新比较及时,有兴趣者可以扫描如下二维码,或者微信搜索【Android系统实战开发】,关注有惊喜哦!
上一篇: seaborn ---- 绘制热力图
下一篇: seaborn 热力图