[Framework] 聊聊 Android Activity 中的 Token

[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 组件,这些组件有 FragmentDialogPopupWindow 等等。其中 FragmentDialogPopupWindow 有一些差别,FragmentView 还是在原有的 ActivityViewTree 中,Fragment 只是为这些 View 提供了一个生命周期的管理;DialogPopupWindow 就不太一样,它俩会新建一个 Window 在原有的 ActivityWindow 之上显示,他们的 ViewTree 也是独立于 ActivityViewTree 存在;这里又有一个问题了,上面说到 DialogPopupWindow 是依附于 Activity 存在的,那怎么来描述这种依附关系呢?就是上面崩溃信息中所描绘的 token,每一个 Activity 正常显示的实例都有一个对应的 token,在 Activity 创建后 WindowManagerService 也会保存这个 token,当 DialogPopupWindow 想要添加自己的 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() 方法,它其实只是一个 binderClientbinderServersystem_server 进程中的 ActivityManagerService,IPC 过程中传递了许多的关键参数,ApplicationThread 是一个 binderServer 用来接收 ActivityManagerService 发送过来的消息,它是在进程启动时在 ActivityThread 中创建的;intent 包含了启动的目标 Activity 中的关键信息;token 就是当前 Activitytoken

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、目标 Activityintent、需要回复的 ActivityRecord 和 需要回复的 Activitytoken

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 对象中什么方法都没有实现,虽然他是一个 binderServer,其中只有一个弱应用指向 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 进程请求开启新的进程,新的进程会执行 ActivityThreadmain 函数,这也是我们应用进程梦开始的地方,这时会接着初始化 Application 相关的生命周期,还有重要的 ApplicationThread (前面已经说到,ApplicationThread 是用来接收 system_server 进程发送消息的 binderServer),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 对象其实就是上面提到的 ApplicationThreadAMS 中的 binderClient,经过 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 为什么必须要有无参的构造函数,然后会调用 Activityattach() 方法,然后会把那个 token 传过去,再后续会执行 ActivityonCreateonStart()onResume() 等生命周期方法。这里就先不具体看了,看看 Activityattach() 方法:

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 异常抛出的地方

无论是 ActivityDialog 还是 PopupWindow,最终要显示新的 window 时,通过获取到的 WindowManager#addView() 方法,这个方法传递的参数也包含 Activity 中的 token,而 WindowManager 最终的实现类是 WindowManagerGlobalWindowManagerGlobalActivity 启动时,都会去检查它的状态,如果没有初始化就去初始化:

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 去拿到 WMSbinderClient 端,然后保存到 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,一个是 WWindowSessionWindowManagerService 一样是一个单利,是通过 WMS 创建的,他也是一个 binderClientW 是一个 binderServer,每个 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 只是一个 binderClient,经过 IPC 后最终会到达 WMS,如果我们的 Activity 已经销毁,那么对应的 token 校验就会失败,然后就会抛出我们熟悉的异常崩溃。

相关推荐
深海呐2 小时前
Android 最新的AndroidStudio引入依赖失败如何解决?如:Failed to resolve:xxxx
android·failed to res·failed to·failed to resol·failed to reso
解压专家6662 小时前
安卓解压软件推荐:高效处理压缩文件的实用工具
android·智能手机·winrar·7-zip
Rverdoser2 小时前
Android 老项目适配 Compose 混合开发
android
️ 邪神4 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】标题栏
android·flutter·ios·鸿蒙·reatnative
努力遇见美好的生活5 小时前
Mysql每日一题(行程与用户,困难※)
android·数据库·mysql
图王大胜7 小时前
Android Framework AMS(17)APP 异常Crash处理流程解读
android·app·异常处理·ams·crash·binderdied·讣告
ch_s_t7 小时前
电子商务网站之首页设计
android
nameofworld7 小时前
前端面试笔试(二)
前端·javascript·面试·学习方法·数组去重
摇光938 小时前
promise
前端·面试·promise
豆 腐9 小时前
MySQL【四】
android·数据库·笔记·mysql