22、SystemUI与AMS/WMS的交互:通过Binder调用获取任务栈信息,窗口焦点变化对SystemUI的影响
说实话,SystemUI 在 Android 系统里就像个「前台管家」。它既要管状态栏、导航栏这些面子工程,又要跟 AMS、WMS 这些底层大佬打交道。今天我们就聊聊 SystemUI 怎么通过 Binder 跟 AMS/WMS 通信,以及窗口焦点变化时,SystemUI 内部到底发生了什么。
22.1 SystemUI 与 AMS 的 Binder 交互
AMS 管理着所有的 Activity 和任务栈。SystemUI 需要知道当前哪个 App 在前台,有几个任务,这些信息都藏在 AMS 里。
我刚开始做这块时,以为 SystemUI 直接持有 AMS 的 Binder 引用。后来才发现,其实是通过 IActivityManager 这个接口来调用的。说白了,SystemUI 拿到的是一个代理对象,真正的实现在 system_server 进程里。
22.1.1 获取任务栈信息
SystemUI 里有个 TaskStackChangeListeners 类,专门监听任务栈变化。它内部维护了一个 ITaskStackListener 的 Stub 对象,注册到 AMS 里。
// frameworks/base/packages/SystemUI/src/com/android/systemui/recents/TaskStackChangeListeners.java
public class TaskStackChangeListeners {
private final ITaskStackListener.Stub mTaskStackListener = new ITaskStackListener.Stub() {
@Override
public void onTaskStackChanged() {
// 任务栈发生变化,通知所有注册的监听器
mHandler.removeMessages(ON_TASK_STACK_CHANGED);
mHandler.sendEmptyMessage(ON_TASK_STACK_CHANGED);
}
@Override
public void onTaskCreated(int taskId, ComponentName componentName) {
// 新任务创建
mHandler.obtainMessage(ON_TASK_CREATED, taskId, 0, componentName).sendToTarget();
}
@Override
public void onTaskRemoved(int taskId) {
// 任务被移除
mHandler.obtainMessage(ON_TASK_REMOVED, taskId, 0).sendToTarget();
}
};
}
嗯,这里要注意:回调是在 Binder 线程池里触发的,所以 SystemUI 内部用 Handler 切到了主线程。我曾经踩过这个坑——直接在回调里更新 UI,结果 ANR 了。
22.1.2 获取当前运行的任务列表
有时候 SystemUI 需要主动查询当前有哪些任务。比如用户按了「最近任务」键,SystemUI 就要拉取所有任务信息。
// 通过 ActivityManager 获取运行中的任务
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.AppTask> tasks = am.getAppTasks();
for (ActivityManager.AppTask task : tasks) {
TaskInfo taskInfo = task.getTaskInfo();
// taskInfo.taskId, taskInfo.baseActivity 等
}
你想想看,这个 getAppTasks() 底层其实走的是 Binder 调用,跨进程到 AMS 去拿数据。如果任务很多,这个调用可能会有点慢。我个人习惯在后台线程做这件事,避免卡主线程。
22.2 SystemUI 与 WMS 的 Binder 交互
WMS 管的是窗口。SystemUI 需要知道当前哪个窗口获得了焦点,因为焦点变化会直接影响状态栏的显示行为。
22.2.1 窗口焦点变化的监听
SystemUI 里有个 WindowManagerService 的代理,通过 IWindowManager 接口通信。焦点变化时,WMS 会回调 WindowManagerPolicy 里的方法。
实际上,SystemUI 的 StatusBar 实现了 WindowManagerPolicy.WindowManagerFuncs 接口,WMS 在焦点变化时会调用 focusChangedLw 方法。
// frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@Override
public void focusChangedLw(IBinder focusedToken) {
// 窗口焦点变化,更新状态栏的显示策略
mHandler.post(() -> {
// 判断当前焦点窗口是否是全屏应用
boolean isFullscreen = isFullscreenApp(focusedToken);
// 更新沉浸模式状态
updateImmersiveMode(isFullscreen);
});
}
22.2.2 焦点变化对 SystemUI 的影响
窗口焦点变化,说白了就是「当前哪个 App 在跟用户交互」。这个变化对 SystemUI 的影响很大:
- 状态栏显示策略:全屏应用(比如视频播放器)会隐藏状态栏,SystemUI 需要根据焦点窗口调整显示模式
- 导航栏按键响应:焦点变化时,导航栏的「返回」键行为会变化——如果焦点在桌面,返回键可能直接回到桌面
- 通知面板的交互:焦点变化时,通知面板的展开/收起行为需要重新判断
- 沉浸模式切换:焦点窗口进入全屏时,SystemUI 要隐藏系统栏;退出全屏时再显示
举个例子,用户在看视频时点击了屏幕,焦点从视频播放器切换到某个弹窗。这时候 SystemUI 需要判断:弹窗是不是全屏?如果不是,状态栏要不要显示?嗯,这里逻辑挺复杂的。
22.3 任务栈与窗口焦点的联动
任务栈和窗口焦点其实是联动的。任务栈变化往往伴随着窗口焦点变化,但两者不是一回事。
我画了一张图,帮你理清它们的关系:
从图上你能看到:AMS 和 WMS 分别通过 Binder 回调 SystemUI,SystemUI 内部用 Handler 做线程切换,最终更新 UI 显示策略。这两个回调是独立的,但 SystemUI 需要把它们关联起来——比如任务栈变化后,焦点可能还没变,这时候状态栏该不该隐藏?
22.4 实际项目中的坑
我在做 Android 12 的 SystemUI 适配时,遇到过一个经典问题:用户从全屏视频切换到桌面,状态栏没有立即显示。原因是任务栈回调先到了,但焦点回调还没到,SystemUI 以为还在全屏模式。
解决方案是:在 onTaskStackChanged 回调里,主动查询当前焦点窗口的状态。说白了,就是「不等 WMS 通知我,我自己去问」。
// 在 TaskStackChangeListeners 中主动查询焦点
private void onTaskStackChangedInternal() {
// 主动查询当前焦点窗口
IBinder focusedToken = mWindowManager.getFocusedWindowToken();
boolean isFullscreen = isFullscreenWindow(focusedToken);
// 更新状态栏
mStatusBar.updateSystemBars(isFullscreen);
}
22.5 总结
SystemUI 与 AMS/WMS 的交互,核心就是通过 Binder 跨进程通信。AMS 告诉 SystemUI 任务栈变了,WMS 告诉 SystemUI 焦点变了。SystemUI 拿到这些信息后,决定状态栏、导航栏怎么显示。
这里有几个关键点:
- Binder 回调在 Binder 线程池,SystemUI 内部要用 Handler 切到主线程
- 任务栈变化和焦点变化是独立的,SystemUI 需要自己协调
- 主动查询比被动等待更可靠,但要注意性能
- 沉浸模式的切换逻辑最复杂,需要同时考虑任务栈和焦点状态
嗯,这块内容其实挺深的。如果你在实际开发中遇到 SystemUI 显示异常的问题,不妨先看看是不是 AMS/WMS 的回调没处理好。我曾经花了两天时间排查一个状态栏闪烁的问题,最后发现是 Binder 回调的线程问题——说起来都是泪啊。
公众号:蓝海资料掘金营,微信deep3321