欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

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

程序员文章站 2022-08-29 12:22:26
概述注:本文基于Android 10源码,为了文章的简洁性,引用源码的地方可能有所删减。今天在掘金上看到一篇解析为什么不能使用 Application Context 显示 Dialog的文章,看完之后感觉作者忽略了一个很重要的对象–parentWindow,因此讲解的时候无法完整地把源码逻辑串起来。在参考了之前对Android-Window机制原理的解析,重新阅读了源码,决定借助这个问题记录一下关于 Android WMS 在 addWindow 的时候Token验证的逻辑,借此也可以说明为什么不...

概述

注:本文基于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 验证的流程(手里来了个新需求,时间仓促,如果流程图有问题欢迎指正,之前的解析如有问题也请指出改正!觉得不错的可以多点几个赞~):

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

面试复习笔记:

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
给文章留个小赞,就可以免费领取啦~

戳我领取:Android对线暴打面试指南超硬核Android面试知识笔记3000页Android开发者架构师核心知识笔记

《960页Android开发笔记》

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

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

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

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

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

资料已经上传在我的GitHub快速入手通道:(点这里)下载!诚意满满!!!

听说一键三连的粉丝都面试成功了?如果本篇博客对你有帮助,请支持下小编哦

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

快速入手通道:(点这里)下载!诚意满满!!!

Android高级面试精选题、架构师进阶实战文档传送门:我的GitHub

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

你的支持,我的动力;祝各位前程似锦,offer不断!!!

本文地址:https://blog.csdn.net/Androiddddd/article/details/110195969