欢迎你们前往云+社区,获取更多腾讯海量技术实践干货哦~java
做者:QQ音乐技术团队
Toast
做为 Android
系统中最经常使用的类之一,因为其方便的api设计和简洁的交互体验,被咱们所普遍采用。可是,伴随着咱们开发的深刻,Toast
的问题也逐渐暴露出来。本文章就将解释 Toast
这些问题产生的具体缘由。 本系列文章将分红两篇:android
Toast
所带来的问题Toast
问题的解决方案(注:本文源码基于Android 7.0)api
当你在程序中调用了 Toast
的 API
,你可能会在后台看到相似这样的 Toast
执行异常:bash
android.view.WindowManager$BadTokenException
Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
android.view.ViewRootImpl.setView(ViewRootImpl.java:826)
android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:369)
android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
android.widget.Toast$TN.handleShow(Toast.java:459)复制代码
另外,在某些系统上,你没有看到什么异常,却会出现 Toast
没法正常展现的问题。为了解释上面这些问题产生的缘由,咱们须要先读一遍 Toast
的源码。网络
首先,全部 Android
进程的视图显示都须要依赖于一个窗口。而这个窗口对象,被记录在了咱们的 WindowManagerService(后面简称 WMS) 核心服务中。WMS 是专门用来管理应用窗口的核心服务。当 Android
进程须要构建一个窗口的时候,必须指定这个窗口的类型。 Toast
的显示也一样要依赖于一个窗口, 而它被指定的类型是:机器学习
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//系统窗口复制代码
能够看出, Toast
是一个系统窗口,这就保证了 Toast
能够在 Activity
所在的窗口之上显示,并能够在其余的应用上层显示。那么,这就有一个疑问:ide
“若是是系统窗口,那么,普通的应用进程为何会有权限去生成这么一个窗口呢?”函数
实际上,Android
系统在这里使了一次 “偷天换日” 小计谋。咱们先来看下 Toast
从显示到隐藏的整个流程:源码分析
// code Toast.java
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();//调用系统的notification服务
String pkg = mContext.getOpPackageName();
TN tn = mTN;//本地binder
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}复制代码
咱们经过代码能够看出,当 Toast
在 show
的时候,将这个请求放在 NotificationManager
所管理的队列中,而且为了保证 NotificationManager
能跟进程交互, 会传递一个 TN
类型的 Binder
对象给 NotificationManager
系统服务。而在 NotificationManager
系统服务中:post
//code NotificationManagerService
public void enqueueToast(...) {
....
synchronized (mToastQueue) {
...
{
// 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++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
//上限判断
return;
}
}
}
}
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token,
WindowManager.LayoutParams.TYPE_TOAST);//生成一个Toast窗口
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveIfNeededLocked(callingPid);
}
....
if (index == 0) {
showNextToastLocked();//若是当前没有toast,显示当前toast
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}复制代码
(不去深究其余代码的细节,有兴趣能够自行研究,挑出咱们所关心的Toast显示相关的部分)
咱们会获得如下的流程(在 NotificationManager
系统服务所在的进程中):
Toast
数量是否已经超过上限 MAX_PACKAGE_NOTIFICATIONS
,若是超过,直接返回TOAST
类型的系统窗口,而且添加到 WMS
管理Toast
请求记录成为一个 ToastRecord
对象代码到这里,咱们已经看出 Toast
是如何偷天换日的。实际上,这个所须要的这个系统窗口 token
,是由咱们的 NotificationManager
系统服务所生成,因为系统服务具备高权限,固然不会有权限问题。不过,咱们又会有第二个问题:
既然已经生成了这个窗口的 Token
对象,又是如何传递给 Android
进程并通知进程显示界面的呢?
咱们知道, Toast
不只有窗口,也有时序。有了时序,咱们就可让 Toast
按照咱们调用的次序显示出来。而这个时序的控制,天然而然也是落在咱们的NotificationManager
服务身上。咱们经过上面的代码能够看出,当系统并无 Toast
的时候,将经过调用 showNextToastLocked();
函数来显示下一个Toast
。
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
...
try {
record.callback.show(record.token);//通知进程显示
scheduleTimeoutLocked(record);//超时监听消息
return;
} catch (RemoteException e) {
...
}
}
}复制代码
这里,showNextToastLocked
函数将调用 ToastRecord
的 callback
成员的 show
方法通知进程显示,那么 callback
是什么呢?
final ITransientNotification callback;//TN的Binder代理对象复制代码
咱们看到 callback
的声明,能够知道它是一个 ITransientNotification
类型的对象,而这个对象实际上就是咱们刚才所说的 TN
类型对象的代理对象:
private static class TN extends ITransientNotification.Stub {
...
}复制代码
那么 callback
对象的show
方法中须要传递的参数 record.token
呢?实际上就是咱们刚才所说的NotificationManager
服务所生成的窗口的 token
。 相信你们已经对 Android
的 Binder
机制已经熟门熟路了,当咱们调用 TN
代理对象的 show
方法的时候,至关于 RPC
调用了 TN
的 show
方法。来看下 TN
的代码:
// code TN.java
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);//处理界面显示
}
};
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(0, windowToken).sendToTarget();
}复制代码
这时候 TN
收到了 show
方法通知,将经过 mHandler
对象去 post
出一条命令为 0 的消息。实际上,就是一条显示窗口的消息。最终,将会调用handleShow(Binder)
方法:
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
....
mParams.token = windowToken;
...
mWM.addView(mView, mParams);
...
}
}复制代码
而这个显示窗口的方法很是简单,就是将所传递过来的窗口 token
赋值给窗口属性对象 mParams
, 而后经过调用 WindowManager.addView
方法,将 Toast
中的mView
对象归入 WMS
的管理。
上面咱们解释了 NotificationManager
服务是如何将窗口 token
传递给 Android
进程,而且 Android
进程是如何显示的。咱们刚才也说到,NotificationManager
不只掌管着 Toast
的生成,也管理着 Toast
的时序控制。所以,咱们须要穿梭一下时空,回到 NotificationManager
的showNextToastLocked()
方法。你们能够看到:在调用 callback.show
方法以后又调用了个 scheduleTimeoutLocked
方法:
record.callback.show(record.token);//通知进程显示
scheduleTimeoutLocked(record);//超时监听消息复制代码
而这个方法就是用于管理 Toast
时序:
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);
}复制代码
scheduleTimeoutLocked
内部经过调用 Handler
的 sendMessageDelayed
函数来实现定时调用,而这个 mHandler
对象的实现类,是一个叫作 WorkerHandler
的内部类:
private final class WorkerHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord)msg.obj);
break;
....
}
}
private void handleTimeout(ToastRecord record)
{
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}复制代码
WorkerHandler
处理 MESSAGE_TIMEOUT
消息会调用 handleTimeout(ToastRecord)
函数,而 handleTimeout(ToastRecord)
函数通过搜索后,将调用cancelToastLocked
函数取消掉 Toast
的显示:
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
....
record.callback.hide();//远程调用hide,通知客户端隐藏窗口
....
ToastRecord lastToast = mToastQueue.remove(index);
mWindowManagerInternal.removeWindowToken(lastToast.token, true);
//将给 Toast 生成的窗口 Token 从 WMS 服务中删除
...复制代码
cancelToastLocked
函数将作如下两件事:
ITransientNotification.hide
方法,通知客户端隐藏窗口Toast
生成的窗口 Token
从 WMS
服务中删除上面咱们就从源码的角度分析了一个Toast的显示和隐藏,咱们不妨再来捋一下思路,Toast
的显示和隐藏大体分红如下核心步骤:
Toast
调用 show
方法的时候 ,其实是将本身归入到 NotificationManager
的 Toast
管理中去,期间传递了一个本地的 TN
类型或者是ITransientNotification.Stub
的 Binder
对象NotificationManager
收到 Toast
的显示请求后,将生成一个 Binder
对象,将它做为一个窗口的 token
添加到 WMS
对象,而且类型是 TOAST
NotificationManager
将这个窗口 token
经过 ITransientNotification
的 show
方法传递给远程的 TN
对象,而且抛出一个超时监听消息scheduleTimeoutLocked
TN
对象收到消息之后将往 Handler
对象中 post
显示消息,而后调用显示处理函数将 Toast
中的 View
添加到了 WMS
管理中, Toast
窗口显示NotificationManager
的 WorkerHandler
收到 MESSAGE_TIMEOUT
消息, NotificationManager
远程调用进程隐藏 Toast
窗口,而后将窗口 token
从 WMS
中删除上面咱们分析了 Toast
的显示和隐藏的源码流程,那么为何会出现显示异常呢?咱们先来看下这个异常是什么呢?
Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
android.view.ViewRootImpl.setView(ViewRootImpl.java:826)
android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:369)复制代码
首先,这个异常发生在 Toast 显示的时候,缘由是由于 token 失效。那么 token 为何会失效呢?咱们来看下下面的图:
一般状况下,按照正常的流程,是不会出现这种异常。可是因为在某些状况下, Android
进程某个 UI 线程的某个消息阻塞。致使 TN
的 show
方法 post
出来 0 (显示) 消息位于该消息以后,迟迟没有执行。这时候,NotificationManager
的超时检测结束,删除了 WMS
服务中的 token
记录。也就是如图所示,删除token
发生在 Android
进程 show
方法以前。这就致使了咱们上面的异常。咱们来写一段代码测试一下:
public void click(View view) {
Toast.makeText(this,"test",Toast.LENGTH_SHORT).show();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}复制代码
咱们先调用 Toast.show
方法,而后在该 ui
线程消息中 sleep
10秒。当进程异常退出后咱们截取他们的日志能够获得:
12-28 11:10:30.086 24599 24599 E AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@2e5da2c is not valid; is your activity running?
12-28 11:10:30.086 24599 24599 E AndroidRuntime: at android.view.ViewRootImpl.setView(ViewRootImpl.java:679)
12-28 11:10:30.086 24599 24599 E AndroidRuntime: at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
12-28 11:10:30.086 24599 24599 E AndroidRuntime: at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
12-28 11:10:30.086 24599 24599 E AndroidRuntime: at android.widget.Toast$TN.handleShow(Toast.java:434)
12-28 11:10:30.086 24599 24599 E AndroidRuntime: at android.widget.Toast$TN$2.handleMessage(Toast.java:345)复制代码
果真如咱们所料,咱们复现了这个问题的堆栈。那么或许你会有下面几个疑问:
在 Toast.show
方法外增长 try-catch 有用么?
固然没用,按照咱们的源码分析,异常是发生在咱们的下一个 UI 线程消息中,所以咱们在上一个 ui 线程消息中加入 try-catch 是没有意义的
为何有些系统中没有这个异常,可是有时候 toast
不显示?
咱们上面分析的是7.0的代码,而在8.0的代码中,Toast
中的 handleShow
发生了变化:
//code handleShow() android 8.0
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}复制代码
在 8.0
的代码中,对 mWM.addView
进行了 try-catch
包装,所以并不会抛出异常,但因为执行失败,所以不会显示 Toast
有哪些缘由引发的这个问题?
TN
抛出消息的时候,前面有大量的 UI
线程消息等待执行,而每一个 UI
线程消息虽然并不卡顿,可是总和若是超过了 NotificationManager
的超时时间,仍是会出现问题sleep
模拟的就是这种状况cpu
时间减小,致使进程内的指令并不能被及时执行,这样同样会致使进程看起来”卡顿”的现象一种Android App在Native层动态加载so库的方案
Android OpenGL开发实践 - GLSurfaceView对摄像头数据的再处理
经过JS库Encog实现JavaScript机器学习和神经学网络
此文已由做者受权腾讯云+技术社区发布,转载请注明文章出处