00-android车载入门
如何选择页面展示的屏幕
- Presentation – 指定display的Dialog
1 | public class Presentation extends Dialog { |
本质是dialog
传入display
Presentation 的上下文与其宿主 Activity 的上下文相互独立。加载布局、资源时必须使用 Presentation 自身的上下文进行布局填充与资源加载,才能保证加载出适配目标显示器尺寸、屏幕密度的资源素材。
1 | public Presentation( Context outerContext, Display display, int theme, |
- 当 Presentation 所依附的显示器被移除时,Presentation 会自动关闭(对应 Dialog.cancel())。宿主 Activity 在自身执行暂停 / 恢复生命周期时,需要自行控制 Presentation 内部正在播放的内容,同步执行暂停、恢复操作。
- ActivityOptions – 启动Activity时指定Display
1 |
|
adb am start -n <activity> --display
display的获取
通过媒体路由(MediaRouter)选择演示显示器
最简单的方式是使用 MediaRouter API。媒体路由服务会持续跟踪系统内可用的音视频路由。当路由选中、取消选中,或是某条路由的首选演示显示器发生变更时,媒体路由会发出通知。应用只需监听这类通知,即可自动在首选演示显示器上拉起或关闭 Presentation。
首选演示显示器:媒体路由推荐、应用可用来展示内容的副屏。若不存在首选演示显示器,应用应当直接在本机屏幕展示内容,无需启用 Presentation。
下面示例借助 android.media.MediaRouter.RouteInfo.getPresentationDisplay(),通过媒体路由在首选演示显示器上创建并展示 Presentation:
1 | MediaRouter mediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); |
ApiDemos 提供示例代码,演示如何监听设备状态:有可用演示显示器时切换到 Presentation 展示,无可用屏幕时回退至主 Activity 界面展示内容。
直接调用 DisplayManager API。
显示管理器会记录系统全部显示器,但并非所有显示器都适合承载 Presentation。例如:如果尝试在主屏弹出 Presentation,会覆盖应用自身界面(相当于在 Activity 上层弹出对话框)。在主屏创建 Presentation 并调用 show () 时,会抛出 WindowManager.InvalidDisplayException 异常。
下方代码使用 DisplayManager.getDisplays(String) 配合分类常量 DisplayManager.DISPLAY_CATEGORY_PRESENTATION,筛选可用的演示显示器:
1 | DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); |
WMS 四大核心容器:层级关系 + 作用(Android 12+ 通用容器树模型)
所有类全部继承自 WindowContainer,WMS 使用树形层级管理全部窗口;
先给最简层级链路(简化版,省略中间 DisplayArea/Task 分支):
1 | RootWindowContainer(根) |
补充:应用 Activity 窗口走另一条分支:
DisplayContent → TaskDisplayArea → Task → ActivityRecord(AppWindowToken) → WindowState
1. RootWindowContainer
定位:全局窗口树唯一根节点,WMS 内部单例
作用
管理系统所有屏幕:持有全部 DisplayContent,按 displayId 区分主屏、副屏、虚拟屏、Presentation 投屏屏;
全局统一调度:焦点分发、屏幕旋转、配置变更、壁纸管理、多屏之间交互协调;
对外提供查询入口:根据 displayId 查找对应 DisplayContent;
监听 DisplayManager 屏幕插拔事件,动态创建 / 销毁 DisplayContent。
关键特点
WMS 初始化时创建唯一实例;
子节点只能是 DisplayContent;
多屏场景所有 DisplayContent 的父容器。
2. DisplayContent
定位:代表一块逻辑显示屏(物理屏 / 虚拟屏 / Presentation 副屏),一屏一个实例
作用
隔离屏幕:不同 DisplayContent 的窗口互相独立,Z 序、布局、动画互不干扰(多屏核心基础);
维护当前屏幕属性:分辨率、density、旋转角度、cutout、显示模式;
内部划分多个 DisplayArea:
TaskDisplayArea:承载普通 App Activity 窗口栈(Task/ActivityRecord)
DisplayArea.Tokens:承载非 Activity 窗口:输入法、壁纸、状态栏、Dialog、Presentation
维护本屏幕内所有 WindowToken 映射表;
执行单屏内布局计算、窗口动画、输入命中查找。
和你前面代码关联:
setLaunchDisplayId(int displayId) 本质就是指定 Activity 投放至哪个 DisplayContent。
Presentation 内部窗口最终也归属副屏对应的 DisplayContent。
3. WindowToken
定位:窗口分组容器,一组同源窗口的 “令牌”,逻辑分组层
作用
分组管理多个 WindowState:同一个 Token 下的窗口具备相同归属、生命周期;
依靠 IBinder token 作为跨进程唯一标识;
两大分支:
AppWindowToken(子类 = ActivityRecord):一个 Activity 对应一个 AppWindowToken。
Activity 主窗口、Dialog、PopupWindow 全部归属同一个 AppWindowToken;Activity 销毁 → Token 销毁 → 所有窗口清理。
普通 WindowToken:不属于 Activity 的窗口组:输入法窗口、壁纸、系统弹窗、Presentation 窗口。
权限与生命周期控制:Token 销毁时,自动清理旗下全部 WindowState。
通俗理解
Token = “户口本”,一个户口本下可以有多个窗口(WindowState)。
4. WindowState
定位:WMS 服务端一个具体窗口实例 **,窗口最小管理单元 **
作用
一一对应应用侧 Window(PhoneWindow、Dialog 窗口、Presentation 窗口);
保存窗口全部属性:位置、大小、Z-order、窗口类型、flag、Insets、Surface、可见性;
和 SurfaceControl 绑定,最终交给 SurfaceFlinger 图层合成;
处理窗口添加、移除、布局、动画、焦点、输入点击命中;
父容器一定是某个 WindowToken。
通俗理解
WindowState = “户口本里单独一个人”,对应屏幕上真实渲染的一块窗口图层。
完整关系总结
RootWindowContainer:总管所有屏幕
└── DisplayContent:单块屏幕(主屏 / 外接副屏 / Presentation 投屏屏)
├── TaskDisplayArea → Task → ActivityRecord (AppWindowToken) → WindowState(Activity、Activity 内 Dialog)
└── DisplayArea.Tokens → WindowToken → WindowState(输入法、壁纸、Presentation、系统 Dialog)
Window / Surface / ViewRootImpl / DecorView 完整关系梳理
DecorView 是根 View;ViewRootImpl 是连接 View 树与 Window/Surface/WMS 的桥梁;Window 是窗口抽象;Surface 是图形缓冲区。
层级从上到下(应用进程客户端)
1 | Window(PhoneWindow) |
逐个类作用详解
Window(窗口抽象,实现类 PhoneWindow)
定位:逻辑窗口管理器,承载窗口策略、样式、主题
每个 Activity 默认持有一个 PhoneWindow(Window 唯一实现类)
职责:
持有 DecorView;
管理窗口特征:Title、ActionBar、WindowFlags、软键盘模式;
setContentView() 最终委托 PhoneWindow 完成布局加载,把布局添加到 DecorView 的内容区域;
❗ Window 不是 View,也不负责绘制,只是容器策略对象。DecorView
定位:整个窗口的根 View,FrameLayout 子类
由 PhoneWindow 创建:installDecor()
结构固定:
DecorView 内部包含:
标题栏区域(TitleBar/ActionBar)
android.R.id.content(内容容器,setContentView 的布局就放进这里)
特点:
DecorView 在没有被 ViewRootImpl 绑定前,无法真正参与绘制、无法接收事件。
Dialog、Presentation 内部同样拥有自己独立 PhoneWindow + DecorView。ViewRootImpl
重中之重:View 体系与窗口系统的桥梁(中介)
关键时机:
1 | wm.addView(decorView, layoutParams) |
作用:
建立 DecorView 和 WMS 的跨进程连接
通过 IWindow Binder 对象,WMS 服务端对应 WindowState。
创建 Surface,管理 Surface 生命周期
请求 WMS 分配 Surface,Surface 是一块图形缓冲区,所有 View 绘制最终输出到 Surface。
驱动 View 绘制三大流程:测量→布局→绘制(performTraversals)
分发触摸事件、窗口事件(尺寸变化、旋转、Insets 变化)
控制窗口参数更新、添加 / 移除窗口
关键结论:一个窗口(Window)对应唯一 ViewRootImpl。
Activity 窗口、Dialog、Presentation 各自独立 ViewRootImpl。
Surface
定位:图形渲染缓冲区(生产者)
不是 View,和 View 体系无关,属于图形栈层
ViewRootImpl 向 WMS 申请得到 Surface;
Canvas 绘制、OpenGL 渲染最终都输出到 Surface;
SurfaceFlinger 消费多个 Surface,合成后输出到屏幕(对应前面 WMS 的 DisplayContent 屏幕)。
一个窗口通常对应一个 Surface(普通窗口);SurfaceView、TextureView 会额外拥有独立 Surface。
完整启动链路(Activity 标准流程)
Activity.attach() → new PhoneWindow
window.setContentView ()
→ PhoneWindow.installDecor () 创建 DecorView
→ 将业务布局 add 进 DecorView 的 content 容器
Activity.makeVisible()
→ WindowManager.addView(DecorView, lp)
WindowManagerGlobal 创建 ViewRootImpl,调用 viewRoot.setView(DecorView)
ViewRootImpl.setView:
通过 Binder 调用 WMS,注册窗口,服务端生成 WindowToken + WindowState
请求分配 Surface
触发第一次 performTraversals,开始测量布局绘制
几组极易混淆的边界区分Window vs DecorView
Window:逻辑窗口管理对象(策略、Flag、主题)
DecorView:View 树根节点,真正承载 UI 视图DecorView vs ViewRootImpl
DecorView:UI 控件;
ViewRootImpl:控制器、桥梁,本身不是 View,持有 DecorView 引用。ViewRootImpl vs Surface
ViewRootImpl 管理 Surface 生命周期;
Surface 只是一块画布缓冲区。
结合你之前关心的 Presentation 场景补充
Presentation 本质是一个独立 Dialog:
拥有独立 PhoneWindow、独立 DecorView
addView 时新建独立 ViewRootImpl
在目标副屏 DisplayContent 下创建 WindowToken / WindowState
拥有独立 Surface,输出到外接显示屏
和 Activity 使用 setLaunchDisplayId 启动到副屏,两套窗口载体,但底层都走这套客户端架构。
极简记忆口诀
PhoneWindow 管窗口样式;
DecorView 是视图树根;
ViewRootImpl 连通 WMS、驱动绘制;
Surface 是绘图画布缓冲区。