文章已授权『郭霖』公众号发布
前言
很高兴遇见你~ 欢迎阅读我的文章
这篇文章讲解关于window token的问题,同时也是Context机制和Window机制这两篇文章的一个补充。如果你对Android的Window机制和Context机制目前位了解过,强烈建议你先阅读前面两篇文章,可以帮助理解整个源码的解析过程以及对token的理解。同时文章涉及到Activty启动流程源码,读者可先阅读Activity启动流程这篇文章。文章涉及到这些方面的内容默认读者已经阅读且了解,不会对这方面的内容过多阐述,如果遇到一些内容不理解,可以找到对应的文章看一下。那么,我们开始吧。
当我们想要在屏幕上展示一个Dialog的时候,我们可能会在Activity的onCreate方法里这么写:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dialog = AlertDialog.Builder(this)
dialog.run{
title = "我是标题"
setMessage("我是内容")
}
dialog.show()
}
他的构造参数需要一个context对象,但是这个context不能是ApplicationContext等其他context,只能是ActivityContext(当然没有ApplicationContext这个类,也没有ActivityContext这个类,这里这样写只是为了方便区分context类型,下同)。这样的代码运行时没问题的,如果我们使用Application传入会怎么样呢?
override fun onCreate(savedInstanceState: Bundle?) {
...
// 注意这里换成了ApplicationContext
val dialog = AlertDialog.Builder(applicationContext)
...
}
运行一下:
报错了,原因是You need to use a Theme.AppCompat theme (or descendant) with this activity.
,那我们给他添加一个Theme:
override fun onCreate(savedInstanceState: Bundle?) {
...
// 注意这里添加了主题
val dialog = AlertDialog.Builder(applicationContext,R.style.AppTheme)
...
}
好了再次运行:
嗯嗯?又崩溃了,原因是:Unable to add window -- token null is not valid; is your activity running?
token为null?这个token是什么?为什么同样是context,使用activity没问题,用ApplicationContext就出问题了?他们之间有什么区别?那么这篇文章就围绕这个token来展开讨论一下。
文章采用思考问题的思路来展开讲述,我会根据我学习这部分内容时候的思考历程进行复盘。希望这种解决问题的思维可以帮助到你。
对token有一定了解的读者可以看到最后部分的整体流程把握,再选择想阅读的部分仔细阅读。
什么是token
首先我们看到报错是在ViewRootImpl.java:907
,这个地方肯定有进行token判断,然后抛出异常,这样我们就能找到token了,那我们直接去这个地方看看。:
ViewRootImpl.class(api29)
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
int res;
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
...
if (res < WindowManagerGlobal.ADD_OKAY) {
...
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
/*
* 1
*/
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
...
}
...
}
...
}
我们看到代码就是在注释1的地方抛出了异常,是根据一个变量res
来判断的,这个res
来自方法addToDisplay
,那么token的判断肯定在这个方法里面了,res
只是一个 判断的结果,那么我们需要进到这个addToDisplay
里去看一下。mWindowSession的类型是IWindowSession,他是一个接口,那他的实现类是什么?找不到实现类就无法知道他的具体代码。这里涉及到window机制的相关内容,简单讲一下:
WindowManagerService是系统服务进程,应用进程跟window联系需要通过跨进程通信:AIDL,这里的IWindowSession只是一个Binder接口,他的具体实现类在系统服务进程的Session类。所以这里的逻辑就跳转到了Session类的
addToDisplay
方法中。关于window机制更加详细的内容,读者可以阅读Android全面解析之Window机制这篇文章进一步了解,限于篇幅这里不过多讲解。
那我们继续到Session的方法中看一下:
Session.class(api29)
class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
final WindowManagerService mService;
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,
outInsetsState);
}
}
可以看到,Session确实是继承自接口IWindowSession,因为WMS和Session都是运行在系统进程,所以不需要跨进程通信,直接调用WMS的方法:
public int addWindow(Session session, IWindow client, int seq,
LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState) {
...
WindowState parentWindow = null;
...
// 获取parentWindow
parentWindow = windowForClientLocked(null, attrs.token, false);
...
final boolean hasParent = parentWindow != null;
// 获取token
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
...
// 验证token
if (token == null) {
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
...//各种验证
}
...
}
WMS的addWindow方法代码这么多怎么找到关键代码?还记得viewRootImpl在判断res是什么值的情况下抛出异常吗?没错是WindowManagerGlobal.ADD_BAD_APP_TOKEN和WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN
,我们只需要找到其中一个就可以找到token的判断位置,从代码中可以看到,当token==null的时候,会进行各种判断,第一个返回的就是WindowManagerGlobal.ADD_BAD_APP_TOKEN
,这样我们就顺利找到token的类型:WindowToken。那么根据我们这一路跟过来,终于找到token的类型了。再看一下这个类:
class WindowToken extends WindowContainer<WindowState> {
...
// The actual token.
final IBinder token;
}