Android之window机制token验证 - Go语言中文社区

Android之window机制token验证


文章已授权『郭霖』公众号发布

前言

很高兴遇见你~ 欢迎阅读我的文章

这篇文章讲解关于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;
}

官方告诉我们里面的token变量才是真正的token,而这个token是IBinder对象。

好了到这里关于token是什么已经弄清楚了:

  • token是一个IBinder对象
  • 只有利用token才能成功添加dialog

那么接下来就有更多的问题需要思考了:

  • Dialog在show过程中是如何拿到token并给到WMS验证的?
  • 这个token在activity和application两者之间有什么不同?
  • WMS怎么知道这个token是合法的,换句话说,WMS怎么验证token的?

dialog如何获取到context的token的?

首先,我们解决第一个问题:Dialog在show过程中是如何拿到token并给到WMS验证的?

我们知道导致两种context(activity和application)弹出dialiog的不同结果,原因在于token的问题。那么在弹出Dialog的过程中,他是如何拿到context的token并给到WMS验证的?源码内容很多,我们需要先看一下token是封装在哪个参数被传输到了WMS,确定了参数我们的搜索范围就减小了,我们回到WMS的代码:

parentWindow = windowForClientLocked(null, attrs.token, false);
WindowToken token = displayContent.getWindowToken(
        hasParent ? parentWindow.mAttrs.token : attrs.token);

我们可以看到token和一个attrs.token关系非常密切,而这个attrs从调用栈一路往回走到了viewRootImpl中:

ViewRootImpl.class(api29)
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
   ...
}

可以看到这是一个WindowManager.LayoutParams类型的对象。那我们接下来需要从最开始show()开始,追踪这个token是如何被获取到的:

Dialog.class(api30)
public void show() {
    ...
    WindowManager.LayoutParams l = mWindow.getAttributes();
    ...
    mWindowManager.addView(mDecor, l);
    ...
}

这里的mWindowmWindowManager是什么?我们到Dialog的构造函数一看究竟:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    // 如果context没有主题,需要把context封装成ContextThemeWrapper
    if (createContextThemeWrapper) {
        if (themeResId == Resources.ID_NULL) {
            final TypedValue outValue = new TypedValue();
            context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
            themeResId = outValue.resourceId;
        }
        mContext = new ContextThemeWrapper(context, themeResId);
    } else {
        mContext = context;
    }
    // 初始化windowManager
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    // 初始化PhoneWindow
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    ...
    // 把windowManager和PhoneWindow联系起来
    w.setWindowManager(mWindowManager, null, null);
    ...
}

初始化的逻辑我们看重点就好:首先判断这是不是个有主题的context,如果不是需要设置主题并封装成一个ContextThemeWrapper对象,这也是为什么我们文章一开始使用application但是没有设置主题会抛异常。然后获取windowManager,注意,这里是重点,也是我当初看源码的时候忽略的地方。这里的context可能是Activity或者Application,他们的getSystemService返回的windowManager是一样的吗,看代码:

Activity.class(api29)
public Object getSystemService(@ServiceName @NonNull String name) {
    if (getBaseContext() == null) {
        throw new IllegalStateException(
                "System services not available to Activities before onCreate()");
    }
    if (WINDOW_SERVICE.equals(name)) {
        // 返回的是自身的WindowManager
        return mWindowManager;
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}

ContextImpl.class(api29)
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}

Activity返回的其实是自身的WindowManager,而Application是调用ContextImpl的方法,返回的是应用服务windowManager。这两个有什么不同,我们暂时不知道,先留意着,再继续把源码看下去寻找答案。我们回到前面的方法,看到mWindowManager.addView(mDecor, l);我们知道一个PhoneWindow对应一个WindowManager,这里使用的WindowManager并不是Dialog自己创建的WindowManager,而是参数context的windowManager,也意味着并没有使用自己创建的PhoneWindow。Dialog创建PhoneWindow的目的是为了使用DecorView模板,我们可以看到addView的参数里并不是window而只是mDecor。

我们继续看代码,,同时要注意这个l参数,最终token就是封装在里面。addView方法最终会调用到了WindowManagerGlobaladdView方法,具体调用流程可以看我文章开头的文章:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ...
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
	...
    ViewRootImpl root;
    ...
    root = new ViewRootImpl(view.getContext(), display);
	...
    try {
        root.setView(view, wparams, panelParentView);
    } 
    ...
}

这里我们只看WindowManager.LayoutParams参数,parentWindow是与windowManagerPhoneWindow,所以这里肯定不是null,进入到adjustLayoutParamsForSubWindow方法进行调整参数。最后调用ViewRootImpl的setView方法。到这里WindowManager.LayoutParams这个参数依旧没有被设置token,那么最大的可能性就是在adjustLayoutParamsForSubWindow方法中了,马上进去看看:

Window.class(api29)
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
    CharSequence curTitle = wp.getTitle();
    if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        // 子窗口token获取逻辑
        if (wp.token == null) {
            View decor = peekDecorView();
            if (decor != null) {
                wp.token = decor.getWindowToken();
            }
        }
        ...
    } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
        // 系统窗口token获取逻辑
        ...
    } else {
        // 应用窗口token获取逻辑
        if (wp.token == null) {
            wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
        }
        ...
    }
    ...
}

终于看到了token的赋值了,这里分为三种情况:应用层窗口、子窗口和系统窗口,分别进行token赋值。

应用窗口直接获取的是与WindowManager对应的PhoneWindow的mAppToken,而子窗口是拿到DecorView的token,系统窗口属于比较特殊的窗口,使用Application也可以弹出,但是需要权限,这里不深入讨论。而这里的关键就是:这个dialog是什么类型的窗口?以及windowManager对应的PhoneWindow中有没有token?

而这个判断跟我们前面赋值的不同WindowManagerImpl有直接的关系。那么这里,就必须到Activity和Application创建WindowManager的过程一看究竟了。

Activity与Application的WindowManager

首先我们看到Activity的window创建流程。这里需要对Activity的启动流程有一定的了解,有兴趣的读者可以阅读Activity启动流程。追踪Activity的启动流程,最终会到ActivityThread的performLaunchActivity

ActivityThread.class(api29)
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
	// 最终会调用这个方法来创建window
    // 注意r.token参数
    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, window, r.configCallback,
        r.assistToken);
    ...
}

这个方法调用了activity的attach方法来初始化window,同时我们看到参数里有了r.token这个参数,这个token最终会给到哪里,我们赶紧继续看下去:

Activity.class(api29)
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) {
    ...
	// 创建window
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    ...
	// 创建windowManager
    // 注意token参数
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    mWindowManager = mWindow.getWindowManager();
    ...
}

attach方法里创建了PhoneWindow以及对应的WindowManager,再把创建的windowManager给到activity的mWindowManager属性。我们看到创建WindowManager的参数里有token,我们继续看下去:

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

这里利用应用服务的windowManager给Activity创建了WindowManager,同时把token保存在了PhoneWindow内。到这里我们知道Activity的PhoneWindow是拥有token的。那么Application呢?


Application的创建具体流程可以看到这篇文章contentProvider启动流程,因为contentProvider是伴随着应用的启动而启动的。最终Application的启动会来到ActivityThread的handleBindApplication方法:

/frameworks/base/core/java/android/app/ActivityThread.java
private void handleBindApplication(AppBindData data) {
    ...
    // 获取到Application实例
    Application app;
    ...
    try {
        app = data.info.makeApplication(data.restrictedBackupMode, null);
        ...
    }
    ...
}

这里的data.info是LoadedApk对象,我们深入这个方法看一下:

LoadedApk.java(api29)
public Application makeApplication(boolean forceDefaultAppClass,
        Instrumentation instrumentation) {
    if (mApplication != null) {
        return mApplication;
    }
    ...
    Application app = null;
    ...
    try {
        java.lang.ClassLoader cl = getClassLoader();
        ...
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        app = mActivityThread.mInstrumentation.newApplication(
                cl, appClass, appContext);
        appContext.setOuterContext(app);
    } 
    ...
    mActivityThread.mAllApplications.add(app);
    mApplication = app;

    if (instrumentation != null) {
        try {
            instrumentation.callApplicationOnCreate(app);
        } 
        ...
    }
    ...
    return app;
}

构建Application的逻辑也不复杂。首先判断Application如果存在则直接返回。后面通过Instrumentation来创建Application对象,最后再通过Instrumentation来回调Application的onCreate方法,我们分别看一下这两个方法:

Instrumentation.java(api29)
public Application newApplication(ClassLoader cl, String className, Context context)
        throws InstantiationException, IllegalAccessException, 
        ClassNotFoundException {
    Application app = getFactory(context.getPackageName())
            .instantiateApplication(cl, className);
    app.attach(context);
    return 
                        
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_43766753/article/details/109060496
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-06-13 12:10:48
  • 阅读 ( 1009 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢