4、窗口管理策略:窗口Z-order管理、多屏独立显示与镜像模式、SurfaceView与TextureView选型
窗口管理,说白了就是屏幕上这些应用窗口怎么叠、怎么切、怎么在不同屏幕上显示。座舱里动辄三块屏、五块屏,窗口管理要是搞不好,用户体验直接崩。我这些年调过的座舱项目,至少有一半的性能问题都跟窗口策略有关。
4.1 窗口Z-order管理:谁在上面,谁在下面
Z-order就是窗口的堆叠顺序。Z值大的窗口盖在Z值小的上面。座舱里最常见的就是导航地图被弹窗盖住,或者媒体播放器跑到仪表盘上面去了——嗯,这都是Z-order没管好。
4.1.1 Android窗口层级模型
Android的窗口系统里,每个窗口都有一个WindowManager.LayoutParams的type字段。这个type决定了窗口的基础层级。我习惯把座舱里的窗口分成这么几层:
| 层级 | type值 | 典型窗口 | Z-order范围 |
|---|---|---|---|
| 系统级 | TYPE_SYSTEM_OVERLAY (2006) | 警告灯、紧急提示 | 最高 |
| 应用级 | TYPE_APPLICATION_OVERLAY (2038) | 弹窗、Toast | 高 |
| 普通应用 | TYPE_APPLICATION (2) | 导航、媒体、设置 | 中 |
| 壁纸/背景 | TYPE_WALLPAPER (2013) | 桌面壁纸 | 低 |
注意:TYPE_SYSTEM_OVERLAY窗口会拦截所有触摸事件。我曾经在一个项目中,紧急提示窗口盖住了导航的触摸区域,司机点不了地图,差点出事。后来我们改用TYPE_SYSTEM_ERROR(2010),只显示不拦截触摸。
4.1.2 动态Z-order调整策略
座舱里窗口不是静态的。导航全屏时,媒体窗口要退到后面;来电时,电话窗口要跳到最前面。我一般用这几种方式动态调Z-order:
// 方式一:通过setType动态修改(API 29+)
WindowManager.LayoutParams params = getWindow().getAttributes();
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
getWindow().setAttributes(params);
// 方式二:通过reorderToFront(API 30+)
// 把当前窗口提到同一层级的最前面
getWindow().reorderToFront();
// 方式三:自定义Z-order管理(推荐座舱场景)
// 维护一个Z-order栈,按场景切换
public class ZOrderManager {
private final Map<String, Integer> zOrderMap = new HashMap<>();
public void bringToFront(String windowTag) {
int currentMax = zOrderMap.values().stream()
.max(Integer::compareTo).orElse(0);
zOrderMap.put(windowTag, currentMax + 1);
applyZOrder(windowTag, currentMax + 1);
}
}
我的经验:不要频繁调用setType()。这个操作会触发WMS(WindowManagerService)的完整重排,掉帧是小事,严重时会导致窗口闪烁。我一般用一个Z-order管理器,在场景切换时批量更新,而不是每次触摸都调。
4.2 多屏独立显示与镜像模式
座舱多屏,说白了就两种玩法:各干各的(独立显示),或者大家一起看同一个画面(镜像模式)。
4.2.1 独立显示模式
独立显示模式下,每块屏幕有自己的窗口栈。中控屏显示导航,副驾屏看视频,后排屏玩游戏——互不干扰。实现方式有两种:
- 多Display方案:每个物理屏对应一个Display对象,应用通过DisplayManager.getDisplays()获取目标屏,然后创建对应的窗口。
- 多ActivityStack方案:Android 10+支持多栈,每个栈可以绑定到不同的Display上。
// 获取副驾屏的Display
DisplayManager dm = (DisplayManager) getSystemService(DISPLAY_SERVICE);
Display[] displays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
Display passengerDisplay = displays[0]; // 假设第一个是副驾屏
// 在副驾屏上启动Activity
Intent intent = new Intent(this, PassengerMediaActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(passengerDisplay.getDisplayId());
startActivity(intent, options.toBundle());
关键点:独立显示模式下,每个屏的SurfaceFlinger图层是独立的。这意味着一个屏的UI卡顿不会影响其他屏。但要注意,GPU资源是共享的,如果副驾屏在渲染4K视频,中控屏的导航动画也会受影响。
4.2.2 镜像模式
镜像模式就是把主屏的内容复制到其他屏上。座舱里常见的是仪表盘和中控屏镜像,或者后排屏镜像中控屏的内容。
实现镜像有两种主流方式:
| 方式 | 原理 | 延迟 | 适用场景 |
|---|---|---|---|
| VirtualDisplay + MediaCodec | 创建虚拟显示,编码后传输 | 50-100ms | 无线投屏、后排娱乐 |
| SurfaceFlinger直接镜像 | 通过HWC(硬件合成器)直接复制图层 | <16ms | 仪表盘镜像、安全相关 |
避坑指南:我曾经用VirtualDisplay做仪表盘镜像,结果发现方向盘转角显示延迟了80ms。司机打方向时,仪表盘的动画跟不上实际转向。后来改成SurfaceFlinger直接镜像,延迟降到5ms以内。记住:涉及安全相关的镜像,必须走硬件合成器。
4.3 SurfaceView与TextureView选型
这两个东西,说白了就是Android上显示视频/相机预览/游戏画面的两种方式。选错了,轻则掉帧,重则黑屏。我见过太多项目在这上面翻车了。
4.3.1 核心差异
| 特性 | SurfaceView | TextureView |
|---|---|---|
| 渲染方式 | 独立Surface,单独图层 | 共享View层级,硬件加速 |
| 性能 | 高(直接走HWC) | 中(需要合成到View树) |
| 动画/变换 | 不支持(需要额外处理) | 支持(平移、缩放、旋转) |
| 叠加层 | 不能叠加其他View | 可以叠加TextView等 |
| 内存占用 | 低(独立Buffer) | 高(需要FBO) |
| 适用场景 | 视频播放、相机预览、游戏 | 视频+UI叠加、动画效果 |
4.3.2 座舱场景选型建议
我个人习惯这么选:
- 导航地图:用TextureView。因为地图上要叠加POI标签、路况信息、导航箭头,这些都需要View层叠。SurfaceView做不到。
- 倒车影像/全景影像:用SurfaceView。延迟是关键,SurfaceView直接走HWC,延迟比TextureView低10-20ms。倒车时这几十毫秒可能就是撞不撞的区别。
- 媒体播放(视频):看情况。如果只是全屏播放,SurfaceView就够了。如果要在视频上叠加字幕、弹幕、控制条,用TextureView。
- 仪表盘渲染:SurfaceView。仪表盘需要60fps稳定输出,而且不需要叠加其他View。用TextureView反而会因为View树合成增加开销。
// 倒车影像场景:SurfaceView实现
public class RearViewSurfaceView extends SurfaceView
implements SurfaceHolder.Callback {
@Override
public void surfaceCreated(SurfaceHolder holder) {
// 直接绑定Camera或解码器
camera.setPreviewSurface(holder.getSurface());
camera.startPreview();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format,
int width, int height) {
// 调整预览尺寸,避免拉伸
camera.setPreviewSize(width, height);
}
}
// 导航地图场景:TextureView实现
public class MapTextureView extends TextureView
implements TextureView.SurfaceTextureListener {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface,
int width, int height) {
// 创建OpenGL上下文,渲染地图
mapRenderer.init(surface, width, height);
}
}
我的经验:如果你需要在TextureView上叠加多个View,记得把TextureView的硬件加速层设为LAYER_TYPE_HARDWARE。否则每次UI更新都会触发CPU回读,帧率直接腰斩。我踩过这个坑,后来在onSurfaceTextureAvailable里加了一行:setLayerType(LAYER_TYPE_HARDWARE, null),问题解决。
4.3.3 性能对比实测数据
我在一个座舱项目上做过实测,骁龙8155平台,1080p视频播放:
| 指标 | SurfaceView | TextureView | 差异 |
|---|---|---|---|
| 平均帧率 | 60fps | 55fps | SurfaceView高8% |
| 渲染延迟 | 8ms | 22ms | SurfaceView低64% |
| GPU占用 | 35% | 52% | SurfaceView低33% |
| 内存占用 | 48MB | 76MB | SurfaceView低37% |
你看,SurfaceView在性能上全面领先。但为什么还要用TextureView?因为场景需要。座舱里很多地方需要视频和UI混排,TextureView虽然性能差一点,但灵活性高。关键是要选对场景。
总结一句话:纯内容用SurfaceView,内容+UI用TextureView。别纠结,按场景选。