[Framework] 聊聊 Android Activity 中的 Token
相信很多人都遇到过以下的崩溃:
android.view.WindowManager$BadTokenException
sql
Unable to add window -- token android.os.BinderProxy@5cbbe42 is not valid; is your activity running?
当我们第一次遇到这个问题后,也是懵逼的,然后我们就上网搜索这个问题是什么原因造成的,然后网上的回答告诉我们这是由于 Activity
销毁后,我们继续使用它的 Context
去显示 PopupWindow
或者 Dialog
导致的崩溃,作为新手得到这个信息后我们修改就好了,知错就改就是好孩子。
但是工作几年后我就想为什么会这样呢?崩溃的信息中还提到 token
非法。首先我们要先理解 Android 的 UI 层级,所有的应用 UI 显示都需要依赖于 Activity
才能显示(这里先排除特殊悬浮窗权限的 UI),Android 创造了一些依附于 Actviity
的 UI 组件,这些组件有 Fragment
、Dialog
和 PopupWindow
等等。其中 Fragment
和 Dialog
与 PopupWindow
有一些差别,Fragment
的 View
还是在原有的 Activity
的 ViewTree
中,Fragment
只是为这些 View
提供了一个生命周期的管理;Dialog
和 PopupWindow
就不太一样,它俩会新建一个 Window
在原有的 Activity
的 Window
之上显示,他们的 ViewTree
也是独立于 Activity
的 ViewTree
存在;这里又有一个问题了,上面说到 Dialog
和 PopupWindow
是依附于 Activity
存在的,那怎么来描述这种依附关系呢?就是上面崩溃信息中所描绘的 token
,每一个 Activity
正常显示的实例都有一个对应的 token
,在 Activity
创建后 WindowManagerService
也会保存这个 token
,当 Dialog
和 PopupWindow
想要添加自己的 Window
时就要通过 binder
告知 WindowManagerService
,通过 binder
传递过去的参数就包含这个 token
,当这个 Activity
已经销毁后 WindowManagerService
就会阻止它显示,然后会出现上面提到的异常信息,如果没有销毁就正常显示。
我们在 Activity
的成员变量中我们可以看到这个 token
:
Java
// ...
@UnsupportedAppUsage
private IBinder mToken;
// ...
这里问题又来了,这个 token
怎么还是一个 binder
,难道说还会使用这个 token
IPC 通信?不了解 binder
的同学可以看看我写的这篇文章 Android Binder 工作原理,后面我就从 Activity
的启动过程来看看这个 token
。
启动 Activity 的源进程
我们启动新的 Activity
通常使用 startActivity()
方法,经过几轮跳转最后会到以下的方法中:
Java
public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
// ...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess();
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
其中调用了 ActivityManagerNative#startActivity()
方法,它其实只是一个 binder
的 Client
,binder
的 Server
是 system_server
进程中的 ActivityManagerService
,IPC 过程中传递了许多的关键参数,ApplicationThread
是一个 binder
的 Server
用来接收 ActivityManagerService
发送过来的消息,它是在进程启动时在 ActivityThread
中创建的;intent
包含了启动的目标 Activity
中的关键信息;token
就是当前 Activity
的 token
。
system_server 系统进程
在 AMS
在启动 Activity
时,会处理很多的事情,包括 Activity Stack
切换、Activity Flag
处理、Activity 选择
、 窗口动画
,权限控制
等等,这些代码非常复杂,简直是噩梦。我们跳过这些噩梦,去找我们关心的代码,希望你还没有忘记我们的目标是 token
。
我们在启动过程中找到了以下代码:
Java
// ...
ActivityRecord r = new ActivityRecord(mService, callerApp, callingUid, callingPackage,
intent, resolvedType, aInfo, mService.mConfiguration, resultRecord, resultWho,
requestCode, componentSpecified, voiceSession != null, this, container, options);
// ...
ActivityRecord
类是 AMS
用来记录 Activity
信息,里面有很多的重要信息,其中包括的一些主要信息有:源进程的 ApplicationThread
代理、源进程的 PID
、目标 Activity
的 intent
、需要回复的 ActivityRecord
和 需要回复的 Activity
的 token
。
Java
private ActivityRecord(ActivityTaskManagerService _service, WindowProcessController _caller,
int _launchedFromPid, int _launchedFromUid, String _launchedFromPackage,
@Nullable String _launchedFromFeature, Intent _intent, String _resolvedType,
ActivityInfo aInfo, Configuration _configuration, ActivityRecord _resultTo,
String _resultWho, int _reqCode, boolean _componentSpecified,
boolean _rootVoiceInteraction, ActivityTaskSupervisor supervisor,
ActivityOptions options, ActivityRecord sourceRecord, PersistableBundle persistentState,
TaskDescription _taskDescription, long _createTime)
{
super(
_service.mWindowManager, new Token (), TYPE_APPLICATION, true,
null /* displayContent */, false /* ownerCanManageAppTokens */
);
// ...
}
我们看到在 ActivityRecord
构造函数中我们看到创建了一个 Token
对象,这个就是我们找的那个 token
。
Java
private static class Token extends Binder {
@NonNull WeakReference<ActivityRecord> mActivityRef;
@Override
public String toString() {
return "Token{" + Integer.toHexString(System.identityHashCode(this)) + " "
+ mActivityRef.get() + "}";
}
}
这个 Token
对象中什么方法都没有实现,虽然他是一个 binder
的 Server
,其中只有一个弱应用指向 ActivityRecord
,这也解释了开头提出的问题,token
虽然是 binder
但是不是用来 IPC 通信的,他其实是一个标识对应 Activity
状态的作用,通过 token
中的对 ActivityRecord
的引用能够很容易获取到 Activity
的各种信息,当然也包括是否存活等信息,当然在应用程的 token
是不能拿到 ActivityRecord
信息的,这是由于 binder
特性决定的。那为什么这个 token
一定要用 binder
对象呢?其他对象可以吗?当然不可以,如果用的其他对象虽然每次应用层发送的是同一个 token
,但是系统进程每次收到的都是一个新的 token
实例。如果对于 binder
还有疑问的参考我之前的文章:Android Binder 工作原理
到这里我们已经知道了 token
创建的地方,以及为什么是一个 binder
对象和它的作用。我们继续看看这个 token
是如果到达目标 Activity
的。
在启动的目标 Activity
对应的进程还没有启动时,AMS
会发送消息给 Zygote
进程请求开启新的进程,新的进程会执行 ActivityThread
的 main
函数,这也是我们应用进程梦开始的地方,这时会接着初始化 Application
相关的生命周期,还有重要的 ApplicationThread
(前面已经说到,ApplicationThread
是用来接收 system_server
进程发送消息的 binder
的 Server
),Application
初始化完成后通过 binder
通知 AMS
,然后 AMS
继续 Activity
的启动流程。
然后又经过了一堆复杂的操作后,会执行以下代码:
Java
// ...
app.thread.scheduleLaunchActivity(
new Intent (r.intent),
r.appToken,
System.identityHashCode(r),
r.info,
new Configuration (mService.mConfiguration),
new Configuration (stack.mOverrideConfig),
r.compat,
r.launchedFromPackage,
task.voiceInteractor,
app.repProcState,
r.icicle,
r.persistentState,
results,
newIntents,
!andResume,
mService.isNextTransitionForward(),
profilerInfo
);
// ...
这个 thread
对象其实就是上面提到的 ApplicationThread
在 AMS
中的 binder
的 Client
,经过 IPC 调用后就会到目标 Activity
的应用进程,其中 appToken
变量也就是我们上面提到的 token
。
目标 Activity 进程
Java
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Configuration curConfig, Configuration overrideConfig, CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, PersistableBundle persistentState, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;
r.ident = ident;
r.intent = intent;
r.referrer = referrer;
r.voiceInteractor = voiceInteractor;
r.activityInfo = info;
r.compatInfo = compatInfo;
r.state = state;
r.persistentState = persistentState;
r.pendingResults = pendingResults;
r.pendingIntents = pendingNewIntents;
r.startsNotResumed = notResumed;
r.isForward = isForward;
r.profilerInfo = profilerInfo;
r.overrideConfig = overrideConfig;
updatePendingConfiguration(curConfig);
sendMessage(H.LAUNCH_ACTIVITY, r);
}
在 ApplicationThread#scheduleLaunchActivity()
方法中它把这些关键信息全部放在了 ActivityClientRecord
中,也包括我们的 token
,然后通过 H
Handler
发送到主线程来处理 Activity
的生命周期。
Java
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
} break;
...
}
}
Java
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
ComponentName component = r.intent.getComponent();
if (component == null) {
component = r.intent.resolveActivity(
mInitialApplication.getPackageManager());
r.intent.setComponent(component);
}
if (r.activityInfo.targetActivity != null) {
component = new ComponentName(r.activityInfo.packageName,
r.activityInfo.targetActivity);
}
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
// ...
}
// ...
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
// ...
return activity;
}
这里会通过反射的方法创建一个 Activity
实例,这里也解释了 Activity
为什么必须要有无参的构造函数,然后会调用 Activity
的 attach()
方法,然后会把那个 token
传过去,再后续会执行 Activity
的 onCreate
,onStart()
和 onResume()
等生命周期方法。这里就先不具体看了,看看 Activity
的 attach()
方法:
Java
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
IBinder shareableActivityToken) {
// ...
mToken = token;
// ...
}
最后看看 BadToken 异常抛出的地方
无论是 Activity
,Dialog
还是 PopupWindow
,最终要显示新的 window
时,通过获取到的 WindowManager#addView()
方法,这个方法传递的参数也包含 Activity
中的 token
,而 WindowManager
最终的实现类是 WindowManagerGlobal
,WindowManagerGlobal
在 Activity
启动时,都会去检查它的状态,如果没有初始化就去初始化:
Java
@UnsupportedAppUsage
public static void initialize() {
getWindowManagerService();
}
Java
@UnsupportedAppUsage
public static IWindowManager getWindowManagerService() {
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));
try {
if (sWindowManagerService != null) {
ValueAnimator.setDurationScale(
sWindowManagerService.getCurrentAnimatorScale());
sUseBLASTAdapter = sWindowManagerService.useBLAST();
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowManagerService;
}
}
这段代码那就非常的熟悉了,如果还不熟悉的同学再去看看我之前聊过的 binder
相关的文章。首先通过 ServiceManager
去拿到 WMS
的 binder
的 Client
端,然后保存到 sWindowManagerService
变量中。
我们再看看它的 addView()
方法:
Java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
// ...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
我们看到会创建一个 ViewRootImpl
,然后调用 ViewRooImpl#setView()
方法添加最后的 UI.
我们看看 ViewRootImpl
的构造函数:
Java
public ViewRootImpl(Context context, Display display) {
this(context, display, WindowManagerGlobal.getWindowSession(),
false /* useSfChoreographer */);
}
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session) {
this(context, display, session, false /* useSfChoreographer */);
}
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
mWindowSession = session;
mWindow = new W(this);
// ..
}
这里有个非常重要的两个东西,一个是 WindowSession
,一个是 W
,WindowSession
和 WindowManagerService
一样是一个单利,是通过 WMS
创建的,他也是一个 binder
的 Client
,W
是一个 binder
的 Server
,每个 ViewRootImpl
都会有一个对应的 W
。
说得简单一点就是 WindowSession
是应用用来向 WMS
发送消息的,而 W
是应用来接收 WMS
主动发送过来的消息。
Java
@UnsupportedAppUsage
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
// Emulate the legacy behavior. The global instance of InputMethodManager
// was instantiated here.
// TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
}
然后看看 ViewRootImpl#setView()
方法:
Java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
mView = view;
// ...
try {
// ...
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets,
mTempControls);
// ...
} catch (RemoteException e) {
// ...
} finally {
// ...
}
// ...
if (res < WindowManagerGlobal.ADD_OKAY) {
// ...
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
// ...
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
// ...
}
}
}
然后看到了调用 Session#addToDisplay()
方法请求显示 Window
,由前面我们知道,Session
只是一个 binder
的 Client
,经过 IPC 后最终会到达 WMS
,如果我们的 Activity
已经销毁,那么对应的 token
校验就会失败,然后就会抛出我们熟悉的异常崩溃。