[Android] Toast问题深度剖析(一)
2018-02-06 11:13
375 查看
欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~
作者:QQ音乐技术团队
第一篇,我们将分析
第二篇,将提供解决
(注:本文源码基于Android 7.0)
另外,在某些系统上,你没有看到什么异常,却会出现
可以看出,
“如果是系统窗口,那么,普通的应用进程为什么会有权限去生成这么一个窗口呢?”
实际上,
我们通过代码可以看出,当
(不去深究其他代码的细节,有兴趣可以自行研究,挑出我们所关心的Toast显示相关的部分)
我们会得到以下的流程(在
判断当前的进程所弹出的
生成一个
将该
代码到这里,我们已经看出
既然已经生成了这个窗口的
我们知道,
这里,
我们看到
那么
这时候
而这个显示窗口的方法非常简单,就是将所传递过来的窗口
上面我们解释了
而这个方法就是用于管理
远程调用
将给
上面我们就从源码的角度分析了一个Toast的显示和隐藏,我们不妨再来捋一下思路,
首先,这个异常发生在 Toast 显示的时候,原因是因为 token 失效。那么 token 为什么会失效呢?我们来看下下面的图:
通常情况下,按照正常的流程,是不会出现这种异常。但是由于在某些情况下,
我们先调用
果然如我们所料,我们复现了这个问题的堆栈。那么或许你会有下面几个疑问:
在
当然没用,按照我们的源码分析,异常是发生在我们的下一个 UI 线程消息中,因此我们在上一个 ui 线程消息中加入 try-catch 是没有意义的
为什么有些系统中没有这个异常,但是有时候
我们上面分析的是7.0的代码,而在8.0的代码中,
在
有哪些原因引起的这个问题?
引起这个问题的也不一定是卡顿,当你的
UI 线程执行了一条非常耗时的操作,比如加载图片,大量浮点运算等等,比如我们上面用
在某些情况下,进程退后台或者息屏了,系统为了减少电量或者某种原因,分配给进程的
Android OpenGL开发实践 - GLSurfaceView对摄像头数据的再处理
通过JS库Encog实现JavaScript机器学习和神经学网络
此文已由作者授权腾讯云+技术社区发布,转载请注明文章出处
作者:QQ音乐技术团队
题记
Toast作为
Android系统中最常用的类之一,由于其方便的api设计和简洁的交互体验,被我们所广泛采用。但是,伴随着我们开发的深入,
Toast的问题也逐渐暴露出来。本文章就将解释
Toast这些问题产生的具体原因。 本系列文章将分成两篇:
第一篇,我们将分析
Toast所带来的问题
第二篇,将提供解决
Toast问题的解决方案
(注:本文源码基于Android 7.0)
1. 异常和偶尔不显示的问题
当你在程序中调用了Toast的
API,你可能会在后台看到类似这样的
Toast执行异常:
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的源码。
2. Toast 的显示和隐藏
首先,所有Android进程的视图显示都需要依赖于一个窗口。而这个窗口对象,被记录在了我们的 WindowManagerService(后面简称 WMS) 核心服务中。WMS 是专门用来管理应用窗口的核心服务。当
Android进程需要构建一个窗口的时候,必须指定这个窗口的类型。
Toast的显示也同样要依赖于一个窗口, 而它被指定的类型是:
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//系统窗口
可以看出,
Toast是一个系统窗口,这就保证了
Toast可以在
Activity所在的窗口之上显示,并可以在其他的应用上层显示。那么,这就有一个疑问:
“如果是系统窗口,那么,普通的应用进程为什么会有权限去生成这么一个窗口呢?”
实际上,
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系统服务中:
//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中删除
3. 异常产生的原因
上面我们分析了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线程消息中
sleep10秒。当进程异常退出后我们截取他们的日志可以得到:
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的超时时间,还是会出现问题
UI 线程执行了一条非常耗时的操作,比如加载图片,大量浮点运算等等,比如我们上面用
sleep模拟的就是这种情况
在某些情况下,进程退后台或者息屏了,系统为了减少电量或者某种原因,分配给进程的
cpu时间减少,导致进程内的指令并不能被及时执行,这样一样会导致进程看起来”卡顿”的现象
相关阅读
一种Android App在Native层动态加载so库的方案Android OpenGL开发实践 - GLSurfaceView对摄像头数据的再处理
通过JS库Encog实现JavaScript机器学习和神经学网络
此文已由作者授权腾讯云+技术社区发布,转载请注明文章出处
相关文章推荐
- [Android] Toast问题深度剖析(二)
- [Android] Toast问题深度剖析(二)
- 【原创】构建高性能ASP.NET站点 第七章 如何解决内存的问题(前篇)—托管资源优化—垃圾回收机制深度剖析
- Android应用开发以及设计思想深度剖析(1)
- 深度分析Android out of memory问题
- Android中Toast重复显示问题解决
- SVN图标含义及常见问题解决方法深度剖析
- Android应用程序开发以及背后的设计思想深度剖析(3)
- Android应用程序开发以及背后的设计思想深度剖析(1)
- Android应用开发以及设计思想深度剖析(4) 推荐
- Android应用程序开发以及背后的设计思想深度剖析(4)
- Android应用开发以及设计思想深度剖析(5) 推荐
- Android应用程序开发以及背后的设计思想深度剖析(5)
- Android应用开发以及设计思想深度剖析(2) 推荐
- android中Toast重复显示问题解决
- 通过深度剖析Android之Launcher源码设计架构,创建HomeScreen的Shortcut(快捷方式)
- 深度剖析Android平台使用细则
- 如何去掉Android应用启动时带的标题栏及启动时的误解问题剖析?
- Android应用程序开发以及背后的设计思想深度剖析
- 【原创】构建高性能ASP.NET站点 第七章 如何解决内存的问题(前篇)—托管资源优化—垃圾回收机制深度剖析