从一个窗口覆盖问题出发,理解 Android Window 的层级与权限

一、引子:一个"似懂非懂"的起点

描述最初的问题:遇到窗口问题,想搞清楚 Display/Window/Activity 的关系。

首先通过 dumpsys window containers 可以看到 Android 的层级树:

  • 最外层是一个 Display 0
  • Display 中有孩子节点,如:
    • Leaf:36:36
    • #1 HideDisplayCutout:32:35
    • #0 WindowedMagnification:

然后在 WindowedMagnification: 中多层递进之下来到它的孩子辈 #0 DefaultTaskDisplayArea。

这里是管理 Task 的,然后 Task 是可以嵌套的(这不是重点),在 Task 中通过 ActivityRecord 来管理一个个 Activity。

Android 并不是简单地维护一个 Window 列表,而是维护了一棵 WindowContainer 树。

shennong:/ $ dumpsys window containers

ROOT type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#0 Display 0 name="内置屏幕" type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=0,01080,1920 bounds=0,01080,1920

#2 Leaf:36:36 type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

...

#0 WindowToken{e0bd122 android.os.BinderProxy@dad5eed} type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#0 70c97b3 ScreenDecorOverlay type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#1 HideDisplayCutout:32:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

...

#0 WindowedMagnification:0:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#1 Leaf:3:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#0 WindowToken{4b4dee7 android.os.BinderProxy@9c397e9} type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#0 cb2fe01 ShellDropTarget type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#0 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#3 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#0 Task=2 type=home mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

#0 ActivityRecord{99abbcd u0 app.lawnchair/.LawnchairLauncher t2} type=home mode=fullscreen override-mode=undefined requested-bounds=0,00,0 bounds=0,01080,1920

...

#0 Task=5 type=undefined mode=multi-window override-mode=multi-window requested-bounds=0,00,0 bounds=0,01080,1920

dumpsys window container 代码在 WindowManagerService.java 中。

大致调用链路如下(这里 mRoot 是一个根节点):

java 复制代码
frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
private void doDump(FileDescriptor fd, PrintWriter pw, String[] args, boolean useProto)
	...
    else if ("containers".equals(cmd))) {
        mRoot.dumpChildrenNames(pw, " ");          // ← 打印整棵窗口树
        pw.println(" ");
        mRoot.forAllWindows(w -> {pw.println(w);}, true);  // ← 再打印所有 WindowState
    }

如果想进一步理清上面节点的关系,可以去浏览一下这个博主的文章

二、窗口层级的理论模型

从 WMS 的 WindowContainer 树来看,可以先用下面这个简化模型理解应用窗口和系统窗口的组织方式,因为不同 Android 版本、不同 Window 类型的实际组织关系会有差异。

这里提到一个基类 WindowContainer 是一个泛型,很多类通过继承它来管理孩子节点。

如果从 WMS 管理具体 Window 的角度来看,可以先把 WindowState 理解为 WMS 对一个 Window 的服务端表示,那在窗口中的一个个组件如 layout、text、button 又属于上面层级中的哪一个部分呢?

UI 中的 LayoutTextViewButton 等属于 View 层级,它们由 ViewRootImpl 作为 View 树的根节点参与管理。ViewRootImpl 是应用侧 View 系统与 WMS 之间的重要桥梁。因此,ButtonTextView 等 View 并不是 WMS 直接管理的 Window,它们存在于某个 Window 内部的 View Tree 中。

可以参考博主文章

三、Android 中窗口是怎么显示的?

这里引入一个层级的关系,也就是一个窗口除去 x、y 轴的平面,实际上是一个个窗口的累加,这个一个个窗口就是 z 轴。Window Type 可以理解为 WMS 划分窗口层级的重要依据之一。不同 Type 位于不同的层级区间,但窗口最终的相对层级并不是简单地由 Type 数值大小决定,还会受到 WindowContainer 层级、Token 以及系统策略等因素影响。

这里可以通过 dumpsys window windows 输出解读(重点看 mAttrs 里的 ty= 字段)。这里的 ty 字段后面的 NAVIGATION_BAR_PANEL 字段就是一个 Type。

Type 类型定义在 frameworks/base/core/java/android/view/WindowManager.java 中。

可以看到显示如下,也就是说这个窗口的 type 是 2024。Window Type 是 WMS 判断窗口层级的重要依据之一。不同 Type 被划分到不同的窗口层级区间,WMS 会结合 Type、WindowContainer 层级、Token 以及系统策略等因素确定窗口的最终层级。

java 复制代码
/**
 * Start of system-specific window types. These are not normally
 * created by applications.
 */
public static final int FIRST_SYSTEM_WINDOW = 2000;
/**
Window type: Navigation bar panel (when navigation bar is distinct from status bar)
In multiuser systems shows on all users' windows.
@hide
*/
public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW + 24;

四、回头再看几个关键类

这些问题属于进一步阅读 WMS 源码时比较容易遇到的概念,本文先不展开,只保留几个入口,后续可以单独分析,也建议直接参考下面引用的博主的文章。

  • RootWindowContainer 为什么是根?(继承链:ConfigurationContainer → WindowContainer → RootWindowContainer)
  • DisplayContent 的真实孩子 和各个 Android 版本有关,这里可以参考这个博主的一系列文章,自己对照源码梳理。
  • ActivityRecord extends WindowToken 意味着什么?
  • 一个 Activity 为什么有多个 WindowState?(Dialog/PopupWindow 都是独立 WindowState)

到这里可以先得到一个结论:Window 能不能显示在另一个 Window 上面,不是单纯由应用层 View 决定的。View 属于某个 Window,而 Window 本身由 WMS 管理。WMS 会结合 Window Type、WindowContainer 层级、Token 和权限等因素决定窗口是否能够被创建以及处于什么层级。

五、窗口类型(type)的设计哲学

源码在frameworks/base/core/java/android/view/WindowManager.java中

三大区间:

  1. 应用窗口(1~99)
  2. 子窗口(1000~1999)
  3. 系统窗口(2000~2999)
java 复制代码
/**
 * End of types of system windows.
 */
public static final int LAST_SYSTEM_WINDOW = 2999;

那如果一个应用希望创建一个能够显示在其他普通应用之上的窗口,需要满足什么条件?

对于一些系统级、高层级窗口,调用方还需要具备相应的系统权限。WMS 在 addWindow() 过程中会根据窗口 Type、调用者权限、Token 等条件进行检查,权限不足时会直接拒绝窗口创建。

一个应用想创建一个能够显示在其他应用之上的窗口,需要同时满足窗口类型和系统权限/策略的要求。

例如普通应用的悬浮窗口可以使用 TYPE_APPLICATION_OVERLAY,但仍然受到系统权限和 WindowManager 策略限制。

而某些系统级窗口则需要更高的系统权限。

一个应用层大概这样实现:

java 复制代码
WindowManager.LayoutParams params =
        new WindowManager.LayoutParams(
                WRAP_CONTENT,
                WRAP_CONTENT,
                TYPE_APPLICATION_OVERLAY,
                FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT);

wm.addView(view, params);

那这个 wm.addView() 后续在 Android 中又走什么样的链路呢?大致如下:

不同 Window Type 对调用者的权限要求并不完全相同。例如,对于特殊的 Rounded Corner Overlay 窗口,WMS 会额外检查调用者是否具有 INTERNAL_SYSTEM_WINDOW 权限。权限不满足时,窗口添加会被拒绝。

java 复制代码
App: WindowManager.addView()
 → ViewRootImpl.setView()
   → Session.addToDisplay() [IPC]
   (windowmanagerservice)
     → ==WMS.addWindow()==
     ├─ 检查窗口添加权限
     │   └─ 根据 Window Type、调用者权限以及特殊窗口属性进行检查
     │
     ├─ 获取 DisplayContent
     │   └─ 确定窗口属于哪个 Display
     │
     ├─ 查找 / 创建 WindowToken
     │
     ├─ 创建 WindowState
     │   └─ 加入 WindowContainer 层级
     │
     └─ 返回添加结果App: WM.addView()

→ 后续 relayout()
→ WMS.relayoutWindow()
→ createSurfaceControl()
→ SurfaceFlinger.createLayer()
→ App 绘制 → BufferQueue → SF 合成显示

到这里,从一个应用调用 WindowManager.addView() 开始,窗口如何进入 WMS、如何参与 Window 层级管理,以及最终如何进入 SurfaceFlinger 完成显示,整个链路就串起来了。

参考文章

https://juejin.cn/post/7338808893529423883

相关推荐
古法安卓4 小时前
Android-SELinux 策略调试实战:从 AVC 日志到策略修复
android·java·android studio
Dovis(誓平步青云)5 小时前
DevEco Studio 6.1.1 Windows 安装实录:从下载校验到首次启动
android·开发语言·数据库·人工智能·windows·harmonyos
Zender Han5 小时前
Flutter 自适应(Adaptive)与响应式(Responsive)设计实践:官方推荐方案详解
android·flutter·ios
我命由我123455 小时前
Android 开发问题:TopAppBar 和 topAppBarColors API is experimental...
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
造火箭6 小时前
Android UI自动化测试可行性评估SKILL
android·功能测试·ui
hunterandroid7 小时前
[Android 从零到一] ViewPager2 与 Fragment 生命周期协同:从预加载到状态一致性
android·前端
mmsx8 小时前
一个黑边 Bug 修了两版:自己算矩阵直接黑屏,借库重建只用了一行 setZoom
android·前端
枢影Kernel8 小时前
Android CLI 与 Android Skills 最佳实践:把 AI Agent 接入可验证的 Android 开发流程
android
Kapaseker9 小时前
没想到吧!Skill 也可以测试 — 小白都看得懂的 Skill 教程
android·人工智能·kotlin