Android-Window机制原理之Token验证(为什么Application的Context不能show dialog)

5,614 阅读7分钟

概述

博客链接

注:本文基于Android 10源码,为了文章的简洁性,引用源码的地方可能有所删减。

今天在掘金上看到一篇解析为什么不能使用 Application Context 显示 Dialog的文章,看完之后感觉作者忽略了一个很重要的对象--parentWindow,因此讲解的时候无法完整地把源码逻辑串起来。在参考了之前对Android-Window机制原理的解析,重新阅读了源码,决定借助这个问题记录一下关于 Android WMS 在 addWindow 的时候Token验证的逻辑,借此也可以说明为什么不能使用 Application Context 显示 Dialog。

Android 不允许使用 Activity 之外的 Context 来显示普通的 Dialog(非 System Dialog 等)。当运行如下代码的时候,会报错:

val dialog = Dialog(applicationContext)
dialog.show()

// ------- error -------
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:840)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:356)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
    at android.app.Dialog.show(Dialog.java:329)
    // ...

如果添加一行代码,发现可以show成功(注意要在window?.attributes?.token被赋值后调用,可以使用延时或者View.post调用dialog.show):

val dialog = Dialog(applicationContext)
dialog.window?.attributes?.token = window?.attributes?.token
dialog.show()

接下来从源码角度来解析Token验证相关的逻辑。在开始这部分的内容之前,最好对 startActivity 启动源码和 Window 机制的原理有一定的理解,这里先将相关的流程做个梳理。从Android-Activity启动流程中可知,startActivity 的流程中与 Token 相关的步骤简要描述如下:

App进程调用 Context.startActivity 方法,然后交由 AMS(system_server进程) 做一些处理,下面的 Token 创建便是在这里完成的,此外,如果需要启动的 Activity 是一个新的进程,那么 system_server 会向 zygote 发起创建新进程的请求,在目标进程创建成功后,逻辑就由AMS转到了目标Activity进程,目标进程的 ActivityThread 会调用 performLaunchActivity 方法来创建目标 Activity 实例,然后调用 Activity.attach 方法,下面的 WindowManager 对象便是在这里创建的,后面会陆续回调 Activity 的生命周期方法,其中在 onCreate 的 setContentView 中创建了一个 DecorView 对象,然后在 onResume 回调完成后,会通过 WindowManager.addView 添加 DecorView 对象(见Android-Window机制原理,通过Binder调用,借助WMS完成)。

在大致了解了这个流程后(上面的流程是简化的,如果不想阅读 startActivity 相关源码的话可以先记住上面的流程,接下来的解析会用到),接下来看看Token是怎么在各个阶段去工作的。关于Binder相关的可以参考这里:Android-Binder原理系列,简言之就是 Binder IPC 方式是一个 C/S 架构,服务端和客户端进程分别持有 Binder 的引用与代理,二者之间可以跨进程调用。

最后的总结部分会将这个流程输出为一个流程图,如有问题,欢迎留言指正!

Token创建

根据上面的流程,我们先从AMS开始看一看Token是怎么创建的,如果阅读过 Activity 启动的源码的话,可以知道在 ActivityStarter.startActivity 方法中有如下代码(此时处于system_server进程的AMS线程,与WMS同进程不同线程,可以参考Android-init-zygote):

// ActivityStarter
private int startActivity(/*...*/) {
    // ...
    ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
            callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
            resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
            mSupervisor, checkedOptions, sourceRecord);
    // ...
}

然后我们看看 ActivityRecord 类:

final class ActivityRecord extends ConfigurationContainer implements AppWindowContainerListener {

    // Binder 服务端对象
    static class Token extends IApplicationToken.Stub {
        // 持有外部 ActivityRecord 的弱引用
        private final WeakReference<ActivityRecord> weakActivity;
        private final String name;

        Token(ActivityRecord activity, Intent intent) {
            weakActivity = new WeakReference<>(activity);
            name = intent.getComponent().flattenToShortString();
        }
        // ...
    }

    ActivityRecord(/*...*/) {
        appToken = new Token(this, _intent);
        // ...
    }
}

因此在 startActivity 过程中,ActivityRecord 对象中的 appToken 被实例化了。接着再往下走,来到了 ActivityStack.startActivityLocked 方法:

void startActivityLocked(ActivityRecord r, ActivityRecord focusedTopActivity,
        boolean newTask, boolean keepCurTransition, ActivityOptions options) {
    // ...
    r.createWindowContainer();
    // ...
}

// ActivityRecord
void createWindowContainer() {
    mWindowContainerController = new AppWindowContainerController(taskController, appToken, /*...*/);
    // ...
}

// AppWindowContainerController
public AppWindowContainerController(TaskWindowContainerController taskController, IApplicationToken token, /*...*/) {
    atoken = createAppWindow(mService, token, /*...*/);
}

AppWindowToken createAppWindow(WindowManagerService service, IApplicationToken token, /*...*/) {
    return new AppWindowToken(service, token,  /*...*/);
}

// AppWindowToken --> WindowToken
AppWindowToken(WindowManagerService service, IApplicationToken token,  /*...*/) {
    super(service, token != null ? token.asBinder() : null, TYPE_APPLICATION, /*...*/);
    appToken = token;
    mVoiceInteraction = voiceInteraction;
    mFillsParent = fillsParent;
    mInputApplicationHandle = new InputApplicationHandle(this);
}

// WindowToken
WindowToken(WindowManagerService service, IBinder _token, int type, /*...*/) {
    super(service);
    token = _token;
    windowType = type;
    mPersistOnEmpty = persistOnEmpty;
    mOwnerCanManageAppTokens = ownerCanManageAppTokens;
    mRoundedCornerOverlay = roundedCornerOverlay;
    onDisplayChanged(dc);
}

void onDisplayChanged(DisplayContent dc) {
    dc.reParentWindowToken(this);
    // ...
}

// DisplayContent
void reParentWindowToken(WindowToken token) {
    // ...
    addWindowToken(token.token, token);
}

private void addWindowToken(IBinder binder, WindowToken token) {
    // HashMap<IBinder, WindowToken> mTokenMap
    // key--ActivityRecord.Token(IApplicationToken.Stub); value--WindowToken
    mTokenMap.put(binder, token);
    // ...
}

上面的代码只给出了关键的步骤,可以清楚地看到,客户端进程调用 startActivity 去启动一个 Activity,然后在AMS(system_server进程的AMS线程)的处理流程中,创建了一个 IApplicationToken.Stub 的对象,这是一个 Binder 服务端,然后又创建了一个 AppWindowToken 对象,并将其存入 DisplayContent.mTokenMap 中。这里 AMS 和 WMS 都处于 system_server 进程中,后续 WMS.addWindow 中会使用到 mTokenMap 来检验 Token(此处关于 mTokenMap 是否存在线程安全问题,有兴趣可以深入看看细节)。

WindowManager对象获取

然后看一下使用 Activity 的 Context 调用 getSystemService 方法和使用 Application 的 Context 调用 getSystemService 方法的区别(只针对WMS服务):

// Activity
public Object getSystemService(@ServiceName @NonNull String name) {
    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
    }
    // ...
}

// Application 调用的是父类 ContextImpl 的方法
// ContextImpl
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}

// SystemServiceRegistry
registerService(Context.WINDOW_SERVICE, WindowManager.class, new CachedServiceFetcher<WindowManager>() {
    @Override
    public WindowManager createService(ContextImpl ctx) {
        return new WindowManagerImpl(ctx);
    }});

Activity 中获取的是 mWindowManager 对象,它在 Activity.attach 方法中赋值,从Android-Activity启动原理可知该方法是在 startActivity 过程中回调的(AMS处理后,通过Binder调用目标Activity的方法):

// Activity
final void attach(/*...*/) {
    attachBaseContext(context);
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
        mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    mWindowManager = mWindow.getWindowManager();
    // ...
}

// Window
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    mAppToken = appToken;
    mAppName = appName;
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

// WindowManagerImpl
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    return new WindowManagerImpl(mContext, parentWindow);
}

public WindowManagerImpl(Context context) {
    this(context, null);
}

private WindowManagerImpl(Context context, Window parentWindow) {
    mContext = context;
    mParentWindow = parentWindow;
}

从上面的源码可以看出使用 Activity 的 Context 调用 getSystemService 方法和使用 Application 的 Context 调用 getSystemService 方法的区别在于: Activity 中的 WindowManager 对象中 parentWindow 为 Activity 中的 PhoneWindow 对象,而 Application 中的 WindowManager 对象中 parentWindow 为 null。

至于上面的 mToken 对象(这是客户端进程的mToken,与上面AMS端创建的Token对象不一样!)从何而来,可以看看 Activity.attach 的调用:

// ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    activity.attach(appContext, this, getInstrumentation(), r.token, /*...*/);
}

可以看到 mToken 对象是 ActivityClientRecord.token,注意到这时我们所处的是目标Activity所在的进程,直接从Activity启动源码解析可以知道,这个 ActivityClientRecord.token 是 AMS 中 ActivityRecord.token 的 Binder 代理,具体的对象传递代码不贴了,贴多了代码看着无聊,这里想看的可以直接参考之前的博客。

总而言之就是,目标Activity进程中的 mAppToken 是一个 Binder 代理对象,其Binder服务端是 AMS 的 ActivityRecord 中的 Token 对象(IApplicationToken.Stub)。

WindowManager.addView

接着我们就到了 WindowManager.addView 添加 DecorView 的流程了,此时 Activity 才刚启动,界面还没有可见。

// WindowManagerImpl
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
        // 由上面可知在Activity中的WindowManager里,parentWindow是PhoneWindow对象
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
    // ...
    ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
    // ...
    root.setView(view, wparams, panelParentView);
}

上面的代码处于Activity所在的客户端进程,由于 parentWindow 不为空,是PhoneWindow对象,因此看看 Window.adjustLayoutParamsForSubWindow 方法:

// Window
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
    if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
        wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        // ...
    } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
        wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
        // ...
    } else {
        // 由于这是Application级别的window,因此走这个流程
        if (wp.token == null) {
            wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
        }
    }
}

可以看到 LayoutParams.token 取的是上面 Token 对象在客户端的 Binder 代理。记住这里,下面要用的。接下来看一下 ViewRootImpl.setView 的相关逻辑:

// ViewRootImpl
public ViewRootImpl(Context context, Display display) {
    mContext = context;
    // 继承于IWindow.Stub的W对象
    mWindow = new W(this);
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);
    // ...
}

// View.AttachInfo
AttachInfo(IWindowSession session, IWindow window, Display display,
        ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
        Context context) {
    mWindow = window;
    mWindowToken = window.asBinder();
    mViewRootImpl = viewRootImpl;
    // ...
}

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            mWindowAttributes.copyFrom(attrs);
            // 通过mWindowSession会调用到WMS.addWindow
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                    getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                    mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
            // ...
        }
    }
}

上面 View.AttachInfo 构造方法可以注意一下,下面也要用,这个类表示 View 的 attach 信息。接下来就到了 WMS 的逻辑:

public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, /*...*/) {
    synchronized(mWindowMap) {
        AppWindowToken atoken = null;
        final boolean hasParent = parentWindow != null;
        // 这个逻辑先不看,在后面Dialog添加再说
        WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
        // 创建WindowState实例
        final WindowState win = new WindowState(this, session, client, token, parentWindow,
            appOp[0], seq, attrs, viewVisibility, session.mUid, session.mCanAddInternalSystemWindow);
        mWindowMap.put(client.asBinder(), win);
        // ...
    }
}

这里的client是上面 ViewRootImpl 中的 Binder 服务端--mWindow。上述代码只贴了相关的逻辑,startActivity 流程中添加 Window 的过程可以只看到这里。WMS.addWindow方法中的WindowToken token对象便是用来做检验的,后面要讲的 Dialog 崩溃便是在这里!

Dialog.show

在上面大致讲了一下 Activity 启动后,添加 DecorView 的过程。接着我们可以开始研究 Dialog 调用 show 方法后发生了什么,以及它与 Token 和 Activity/Application 的关系。

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    // ...
}

public void show() {
    onStart();
    // ...
    mDecor = mWindow.getDecorView();
    mWindowManager.addView(mDecor, l);
    mShowing = true;
}

可知也是调用的 WM.addView 方法。于是可以接着看上面给出的 WindowManagerGlobal.addView 方法,这里分两种情况:

  • 传给 Dialog 的是 Activity 上下文,则 WindowManager 的 parentWindow 不为空
  • 传给 Dialog 的是 Application 上下文,则 WindowManager 的 parentWindow 为空

我们已经知道,根据 parentWindow 是否为空,会选择是否调用其 parentWindow.adjustLayoutParamsForSubWindow(wparams) 方法:

// Window
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
    if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        // 这里是普通对话框,因此走这个流程
        if (wp.token == null) {
            View decor = peekDecorView(); // 通过PhoneWindow拿到DecorView对象
            if (decor != null) {
                wp.token = decor.getWindowToken();
            }
        }
    }
    // ...
}

// View
public IBinder getWindowToken() {
    return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
}

上面的 getWindowToken 方法返回的就是我们前面看到的 mAttachInfo.mWindowToken 对象!也就是之前 ViewRootImpl 中创建的 mWindow 对象(一个Binder服务端)。而如果这个 Context 是 Application,那么 wp.token 将会是 null。

WMS.addView

于是我们接着看展示 Dialog 过程中,WMS 的表演:

public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, /*...*/) {
    WindowState parentWindow = null;
    if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
        // 普通Dialog会走这个流程,获取parentWindow对象
        parentWindow = windowForClientLocked(null, attrs.token, false);
        if (parentWindow == null) {
            // parentWindow为null,返回bad
            return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
        }
        if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
            // parentWindow为普通window,返回bad
            return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
        }
    }
    // ...
}

final WindowState windowForClientLocked(Session session, IBinder client, boolean throwOnError) {
    WindowState win = mWindowMap.get(client);
    // ...
    return win;
}

我们先看看 parentWindow 的逻辑,windowForClientLocked 方法中 client 参数即是上一节讲的 wp.token

  • Context是Application的情况,它为null,则WMS中返回的parentWindow也是null,那么添加 Window 失败,返回 bad code。
  • Context是Activity的情况,它是 ViewRootImpl 中 mWindow 对象的 Binder 代理,在上面解析 startActivity 添加 DecorView 的过程中我们看到,我们为 mWindowMap 添加了一个 key 为 mWindow 对象代理,value 为当时创建的 WindowState 对象,也将作为这次的 parentWindow 返回。

我们接着往下看:

public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, /*...*/) {
    // ...
    AppWindowToken atoken = null;
    final boolean hasParent = parentWindow != null;
    // 取parentWindow.mAttrs.token
    WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
    if (token == null) {
        if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
            return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
        }
        if (rootType == TYPE_INPUT_METHOD) {
            return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
        }
    }
    // ...
}

// DisplayContent
WindowToken getWindowToken(IBinder binder) {
    // key--ActivityRecord.Token(IApplicationToken.Stub); value--WindowToken
    return mTokenMap.get(binder);
}

对于传入是 Activity 的情况,因为 parentWindow 不为空,可知 hasParent = true,从上面可以知道,parentWindow 的 LayoutParams.token 取的是 AMS创建的 Token 对象在客户端的 Binder 代理,而 mTokenMap 中早就添加过这个 key 的元素了!因此这里如果是 Activity 的话,则返回的 token 是有值的,其值为 AMS 创建的 AppWindowToken 对象。

现在应该知道,为啥在最开始我们加了这行代码 dialog.window?.attributes?.token = window?.attributes?.token 后,Dialog就可以正常展示了!因为我们手动给 Dialog 自己设置了 token,token 值就是启动 Activity 时创建的 mAppToken(代理)。

异常抛出

上面WMS返回后,回到 ViewRootImpl.setView 方法:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            res = mWindowSession.addToDisplay(/*...*/)
            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?");
                    // ...
                }
            }
            // ...
        }
    }
}

看到这里,我们也终于找到最开始看到的崩溃日志是怎么回事了。

总结

用一张图来总结一下 Token 验证的流程(手里来了个新需求,时间仓促,如果流程图有问题欢迎指正,之前的解析如有问题也请指出改正!觉得不错的可以多点几个赞~):

Window-Token验证