Android窗口系统第三篇---WindowManagerService中窗口的组织方式

Android窗口系统第一篇---Window的类型与Z-Order确定
Android窗口系统第二篇---Window的添加过程

上面文章梳理了一个窗口的添加过程,系统中有很多应用,每个应用有多个Activity,一个Activity上有一个Window,WindowManagerService是怎么管理的?先adb shell dumpsys activity查看一下Activity.


image.png

Display #0 (activities from top to bottom):
Display对应窗口系统中的DisplayContent类,可以理解成一个屏幕,手机上一般就是一块屏幕,当然也可以有虚拟屏幕,最近中兴推出了一款双屏幕的手机,那么这款手机上DisplayContent的size就为2,#0代表当前屏幕的设备ID。

Stack #0:

Stack对应WindwoManagerService中的TaskStack类,如果是在ActivityManagerService就对应ActivityStack类,为什么要引入TaskStack和ActivityStack类呢?因为他们的作用是管理TASK,一个Stack中包含了多个Task。应用程序也可以在AndroidManifest.xml文件中通过android:launchMode
指定当前Activity运行在哪一个Task中。#0代表Stack的id,android 中规定了id为0的Stack是存放home桌面的窗口的。在Android N以前,是不支持多窗口的,所以只有两个Stack,一个是HOME Stack,也就是存在桌面的Stack,另外一个就是存在其他应用程序的Stack。在android N 以后,总共有5个Stack。这些Stack都是在ActivityManager.java中定义的。


/** First static stack ID. */
public static final int FIRST_STATIC_STACK_ID = 0;

/** Home activity stack ID. */
public static final int HOME_STACK_ID = FIRST_STATIC_STACK_ID;

/** ID of stack where fullscreen activities are normally launched into. */
public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;

/** ID of stack where freeform/resized activities are normally launched into. */
public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1;

/** ID of stack that occupies a dedicated region of the screen. */
public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1;

/** ID of stack that always on top (always visible) when it exist. */
public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1;

Id等于0:Home Stack,就是Launcher所在的Stack。但是还有一些系统界面也运行在这个Stack上,比如近期任务的界面。
Id等于1:FullScren Stack,全屏的Activity所在的Stack。 但其实在分屏模式下,Id为1的Stack只占了半个屏幕。
Id等于2:Freeform模式的Activity所在Stack
Id等于3:Docked Stack 在分屏模式下,屏幕有一半运行了一个固定的应用,这就是Docked Stack
Id等于4:Pinned Stack 这是画中画Activity所在的Stack

 Stack #0:
   Task id #142
     TaskRecord{a11ed77 #142 A=com.miui.home U=0 sz=1}
     Intent { act=android.intent.action.MAIN cat=[android.intent.category.HOME] flg=0x10800000 cmp=com.miui.home/.launcher.Launcher }
       Hist #0: ActivityRecord{2661ad9 u0 com.miui.home/.launcher.Launcher t142}
         Intent { act=android.intent.action.MAIN cat=[android.intent.category.HOME] flg=0x10800000 cmp=com.miui.home/.launcher.Launcher }
         ProcessRecord{7da5d1c 2606:com.miui.home/u0a16}

所以上面的dum就可以这样解释,在id等于0这个stack管理一个id等于142的TASK,TASK中存放一个ActivityRecord,这个ActivityRecord就是HOME。了解了上面,就可以看下面这张图。


image.png

WindwoManagerService中需要管理多个显示屏幕,这些显示屏对象被保存在mDisplayContents列表里面,每一个显示屏对象中有一个mStackBoxs列表,这个列表包含多个StackBox,StackBox被组织成二叉树的形式,mFirst指向第一个节点的TaskStack对象,mSecond指向第二个节点的TaskStack对象,mStack指向当前TaskStack对象,一个TaskStack对象内部有mTask列表,这个列表中存储的对象是TASK,一个TASK中有很多Activity,一个Activity对应一个AppWindowToken。一个AppWindowToken中有一个windows列表,所以一个AppWindowToken对应多个WindowState。

DisPlayContent、TaskStack都解释过,现在看看什么是AppWindowToken?

说AppWindowToken需要先说WindowToken,WindowToken的子类AppWindowToken,你可以把WindowToken理解成是一个显示令牌,无论是系统窗口还是应用窗口,添加新的窗口的时候必须使用这个令牌向WMS表明自己的身份,添加窗口的时候会创建WindowToken,销毁窗口的时候移除WindowToken(removeWindowToken方法)。

WMS使用WindowToken将同一个应用组件(Activity,InputMethod,Wallpaper,Dream)的窗口组织在一起,换句话说,每一个窗口都会对应一个WindowToken,并且这个窗口中的所有子窗口将会对应同一个WindowToken,就如下面这个图的关系,假设窗口A是播放器中的一个窗口,除了主窗口外,还有三个子窗口,这些窗口的WindowToken都是一样的。

WindowToken和WindowState是1对多的关系.png

所以说WindowToken就像一个“户口本”,表示一家人都要在一个本本上。

AppWindowToken:每个App的Activity对应一个AppWindowToken。其中的appToken为IApplicationToken类型,连接着对应的AMS中的ActivityRecord::Token对象,有了它就可以顺着AppWindowToken找到AMS中相应的ActivityRecord。

这里为什么要针对Activity弄出一下AppWindowToken呢?我觉得有以下几个原因:
1、AppWindowToken是AMS中ActivityRecord::Token的产物,也就是ActivityRecord::Token可以看作ActivityRecord的一个远端句柄,在WMS中以AppWindowToken形式存在,ActivityRecord::Token实现了IApplicationToken。当WMS要通知AMS窗口变化时,就是用的这个接口。

2、一次连续性的操作(Task)可以打开多个Activity,每个Activity可以包含多个窗口(对应WindowState),WMS中TaskStack维护了Task的列表mTask,Task维护了AppWindowToken列表mAppTokens,AppWindowToken维护了相关WindowState的列表windows。即AppWindowToken被TaskStack管理,TaskStack中的mTasks是按历史顺序存放的,最老的Task在最底下,这个也是与AMS中ActivityStack的mTaskHistory顺序保持一致。如果没有AppWindowToken,直接用WindowToken,那么AMS的负担被增加,因为这些非Activity窗口,AMS是不care的。

AppWindowToken是什么时候被创建的呢?

在Activity窗口添加之前,AMS会调用addConfigOverride向WMS登记。

    void addConfigOverride(ActivityRecord r, TaskRecord task) {
        final Rect bounds = task.updateOverrideConfigurationFromLaunchBounds();
        // TODO: VI deal with activity
        mWindowManager.addAppToken(task.mActivities.indexOf(r), r.appToken,
                r.task.taskId, mStackId, r.info.screenOrientation, r.fullscreen,
                (r.info.flags & FLAG_SHOW_FOR_ALL_USERS) != 0, r.userId, r.info.configChanges,
                task.voiceSession != null, r.mLaunchTaskBehind, bounds, task.mOverrideConfig,
                task.mResizeMode, r.isAlwaysFocusable(), task.isHomeTask(),
                r.appInfo.targetSdkVersion, r.mRotationAnimationHint);
        r.taskConfigOverride = task.mOverrideConfig;
    }

参数r.appToken,是ActivityRecord:Token对象。r.task.taskId是这个Activity运行的Task的ID, mStackId是这个Actiivty的Task所在的Stack的ID。

    @Override
    public void addAppToken(int addPos, IApplicationToken token, int taskId, int stackId,
            int requestedOrientation, boolean fullscreen, boolean showForAllUsers, int userId,
            int configChanges, boolean voiceInteraction, boolean launchTaskBehind,
            Rect taskBounds, Configuration config, int taskResizeMode, boolean alwaysFocusable,
            boolean homeTask, int targetSdkVersion, int rotationAnimationHint) {
  
        ....
        synchronized(mWindowMap) {
            AppWindowToken atoken = findAppWindowToken(token.asBinder());
            if (atoken != null) {
                Slog.w(TAG_WM, "Attempted to add existing app token: " + token);
                return;
            }
           //创建AppWindowToken
            atoken = new AppWindowToken(this, token, voiceInteraction);
            atoken.inputDispatchingTimeoutNanos = inputDispatchingTimeoutNanos;
            atoken.appFullscreen = fullscreen;
            atoken.showForAllUsers = showForAllUsers;
            atoken.targetSdk = targetSdkVersion;
            atoken.requestedOrientation = requestedOrientation;
            atoken.layoutConfigChanges = (configChanges &
                    (ActivityInfo.CONFIG_SCREEN_SIZE | ActivityInfo.CONFIG_ORIENTATION)) != 0;
            atoken.mLaunchTaskBehind = launchTaskBehind;
            atoken.mAlwaysFocusable = alwaysFocusable;
            if (DEBUG_TOKEN_MOVEMENT || DEBUG_ADD_REMOVE) Slog.v(TAG_WM, "addAppToken: " + atoken
                    + " to stack=" + stackId + " task=" + taskId + " at " + addPos);
            atoken.mRotationAnimationHint = rotationAnimationHint;
            //mTaskIdToTask是一个SparseArray对象,先尝试取出Task,没有就创建一个
            Task task = mTaskIdToTask.get(taskId);
            if (task == null) {
                task = createTaskLocked(taskId, stackId, userId, atoken, taskBounds, config);
            }
            //将atoken放到task中的addPos位置
            task.addAppToken(addPos, atoken, taskResizeMode, homeTask);
            //atoken放到mTokenMap中
            mTokenMap.put(token.asBinder(), atoken);

            // Application tokens start out hidden.
            atoken.hidden = true;
            atoken.hiddenRequested = true;
        }
    }
    private Task createTaskLocked(int taskId, int stackId, int userId, AppWindowToken atoken,
            Rect bounds, Configuration config) {
        if (DEBUG_STACK) Slog.i(TAG_WM, "createTaskLocked: taskId=" + taskId + " stackId=" + stackId
                + " atoken=" + atoken + " bounds=" + bounds);
        //这里是能够取到的,因为AMS在调用addAppWindowToken之前,会调用WMS的attachStack方法,提前将TaskStack创建出来。
        final TaskStack stack = mStackIdToStack.get(stackId);
        if (stack == null) {
            throw new IllegalArgumentException("addAppToken: invalid stackId=" + stackId);
        }
        EventLog.writeEvent(EventLogTags.WM_TASK_CREATED, taskId, stackId);
        Task task = new Task(taskId, stack, userId, this, bounds, config);
        //把上面创建的Task放到mTaskIdToTask中
        mTaskIdToTask.put(taskId, task);
        //把task放到stack中,返回task
        stack.addTask(task, !atoken.mLaunchTaskBehind /* toTop */, atoken.showForAllUsers);
        return task;
    }

现在从Activity的角度来看Activiy窗口的组织方式应该如下图。


image.png

TaskRecord和ActivityRecord:Token是AMS包下面的类,Task和DisplayContent是WMS包下面的类,其中TaskRecord和Task对应,TaskRecord中有mActivities列表,这个列表中放了若干ActivityRecord,每一个ActivityRecord都有一个Token,这个Token要通过WMS的addAppToken给加入到WMS中的Task中去,AMS和WMS都是运行在system_server进程下面,所有addAppToken并不是一个IPC过程。每一个AppWindwToken可以对应一个WindowState或者对应多个WindowState,图中的WindowState-Z是主窗口,WindowState-Z-1是子窗口,每一个WindowState是属于一个屏幕(DisplayContent)中,这些WindowState按照Z序大小的方式排列。另外不仅App的窗口添加的时候需要调用addAppToken注册Token,其他类型的窗口添加也需要向WMS注册token,比如墙纸类型的窗口在添加的时候,也需要提前调用addWindowToken向WMS中注册token,这样后面执行addWindow的过程才不会失败。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容