29、SystemUI的测试与调试:使用dumpsys systemui命令,SysUI的单元测试与UI测试
SystemUI 这层,说白了就是 Android 系统的“脸面”。
状态栏、通知栏、导航栏、锁屏……用户每天跟它打交道的时间比跟任何 App 都长。但问题也来了——这玩意儿一旦出 Bug,用户感知极强。状态栏不显示、通知拉不下来、导航键没反应,随便一个都能让用户抓狂。
所以,SystemUI 的测试与调试,是每个做框架层开发的工程师必须掌握的硬功夫。今天我就把这块内容掰开了讲,从命令行调试到单元测试,再到 UI 自动化测试,一条龙说清楚。
29.1 调试利器:dumpsys systemui
先说说我最常用的调试命令——dumpsys systemui。
你想想看,SystemUI 是一个系统级进程,你不能像普通 App 那样随便加 Log 然后跑一遍。很多时候,Bug 是偶现的,或者发生在用户手上。这时候,dumpsys 就是你的救命稻草。
核心用法:
adb shell dumpsys systemui
这条命令会输出 SystemUI 当前的所有状态信息,包括通知列表、状态栏图标、亮度滑块位置、导航模式等等。
我个人习惯把它重定向到本地文件,然后慢慢分析:
adb shell dumpsys systemui > sysui_dump.txt
29.1.1 常用子命令
dumpsys systemui 其实支持很多子命令,我挑几个最实用的:
| 子命令 | 作用 | 我常用的场景 |
|---|---|---|
dumpsys systemui -t 10 |
设置超时时间(秒) | SystemUI 卡死时,防止 dump 命令也卡住 |
dumpsys systemui visibility |
查看各 UI 组件的可见性 | 排查状态栏图标消失问题 |
dumpsys systemui notifications |
查看通知列表详情 | 通知不显示时,看是不是被拦截了 |
dumpsys systemui statusbar |
状态栏内部状态 | 看图标排序、暗色模式状态 |
dumpsys systemui nav |
导航栏状态 | 手势导航 vs 三键导航切换问题 |
小技巧: 如果你只想看某个模块,可以用 dumpsys systemui | grep -A 20 "Notifications" 来过滤。我经常这么干,省得在几千行输出里翻来翻去。
29.1.2 实战:用 dumpsys 定位通知不显示问题
有一次,用户反馈说某个 App 的通知收不到。我远程连上设备,先跑了一发:
adb shell dumpsys systemui notifications
输出里有一段关键信息:
NotificationList:
packageName=com.example.app
key=0|com.example.app|123|null|10157
mPriority=0
mUserLockedFields=0
mShowBadge=true
mAlerted=true
mInterruptionFilter=3
mSuppressedVisualEffects=0
嗯,这里要注意——mInterruptionFilter=3 表示“重要程度为中等”,按理说应该显示。但再看下去,发现 mSuppressedVisualEffects 虽然为 0,但 mUserLockedFields 也是 0,说明用户没有手动屏蔽。
问题出在哪?我继续往下翻,发现这个通知的 mGroupKey 和另一个通知相同,但 mGroupAlertBehavior 被设成了 GROUP_ALERT_SUMMARY。说白了,这个通知被合并到群组里了,只显示摘要,不显示单独条目。
找到根因后,我让 App 开发改了通知渠道的 setGroupAlertBehavior 设置,问题解决。
避坑指南: 我曾经遇到过 dumpsys systemui 输出为空的情况。后来发现是 SystemUI 进程挂了,需要先 adb shell ps -A | grep systemui 确认进程是否存在。如果进程不在,先 adb shell am start-foreground-service com.android.systemui/.SystemUIService 重启。
29.2 SystemUI 的单元测试
SystemUI 的单元测试,和普通 App 的单元测试有相似之处,但也有不少坑。毕竟它依赖系统服务、Binder 调用、WindowManager 等底层组件。
29.2.1 测试框架选型
在 AOSP 里,SystemUI 的单元测试主要用 Robolectric + JUnit4。为什么不用 Instrumentation 测试?因为 Instrumentation 需要真机或模拟器,跑一次太慢了。Robolectric 可以在 JVM 上跑,速度快得多。
我建议你在 packages/apps/SystemUI/tests 目录下写测试,这是官方推荐的位置。
29.2.2 一个典型的单元测试例子
假设我们要测试 NotificationListener 的一个方法:
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 30)
public class NotificationListenerTest {
private NotificationListener mListener;
private TestableNotificationManager mTestManager;
@Before
public void setUp() {
mTestManager = new TestableNotificationManager();
mListener = new NotificationListener(mTestManager);
}
@Test
public void testOnNotificationPosted_shouldUpdateBadge() {
// 准备一个模拟通知
StatusBarNotification sbn = createTestNotification("test_pkg", 1);
// 执行
mListener.onNotificationPosted(sbn, null);
// 验证
assertTrue(mTestManager.isBadgeUpdated());
assertEquals("test_pkg", mTestManager.getLastBadgePackage());
}
private StatusBarNotification createTestNotification(String pkg, int id) {
// 构造一个模拟的 StatusBarNotification
// 这里省略具体实现
}
}
你看,这里用了 TestableNotificationManager 来替代真实的 NotificationManager。这就是依赖注入的好处——把依赖抽出来,测试时换成 Mock 或 Test Double。
个人经验: 写 SystemUI 单元测试时,最难搞的是那些 @Inject 的依赖。我建议你用 Dagger 的 TestModule 来替换生产环境的 Module。比如:
@Module
public class TestSysUIModule {
@Provides
public NotificationManager provideNotificationManager() {
return mock(NotificationManager.class);
}
}
29.2.3 测试系统服务调用
SystemUI 经常调用 WindowManager、StatusBarManager 等系统服务。在 Robolectric 里,你可以用 Shadow 来模拟这些服务:
@Test
public void testExpandNotificationsPanel_callsWindowManager() {
ShadowWindowManager shadowWm = Shadows.shadowOf(
(WindowManager) RuntimeEnvironment.application.getSystemService(Context.WINDOW_SERVICE));
mNotificationPanel.expand();
// 验证 WindowManager 是否收到了添加窗口的请求
assertTrue(shadowWm.getAddedWindowTokens().size() > 0);
}
嗯,这里要注意——Robolectric 的 Shadow 对象不是万能的。有些系统服务的 Shadow 实现不完整,这时候你就得自己写 Mock 了。
29.3 UI 测试:Espresso 与 UiAutomator
单元测试只能测逻辑,UI 交互还得靠 UI 测试。SystemUI 的 UI 测试,我主要用两种框架:
- Espresso:适合测单个 Activity 或 Fragment 内的 UI 交互
- UiAutomator:适合跨 App 的测试,比如从通知栏点击跳转到设置
29.3.1 用 Espresso 测试通知面板
假设我们要测试“下拉状态栏,点击通知,跳转到对应 App”:
@Test
public void testClickNotification_opensApp() {
// 先发送一个测试通知
NotificationManager nm = (NotificationManager)
InstrumentationRegistry.getInstrumentation().getTargetContext()
.getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = new Notification.Builder(
InstrumentationRegistry.getTargetContext(), "test_channel")
.setContentTitle("测试通知")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.build();
nm.notify(1, notification);
// 下拉状态栏
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.openQuickSettings();
// 点击通知
onView(withText("测试通知")).perform(click());
// 验证是否跳转到了目标 Activity
intended(hasComponent(TestActivity.class.getName()));
}
这段代码看起来简单,但实际跑起来坑不少。我遇到过最典型的问题是——通知面板展开后,onView 找不到那个通知。为什么?因为通知列表是 RecyclerView,需要滚动才能看到。
解决办法是加一个 scrollTo():
onView(withText("测试通知"))
.perform(scrollTo(), click());
29.3.2 用 UiAutomator 测试跨 App 场景
Espresso 只能测自己进程内的 UI。如果要测“从通知栏点击,跳转到浏览器”,就得用 UiAutomator:
@Test
public void testNotificationClick_opensBrowser() throws Exception {
// 发送通知
// ... 同上 ...
// 下拉通知栏
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
device.openNotification();
// 等待通知出现
UiObject notification = device.findObject(new UiSelector()
.textContains("测试通知"));
assertTrue("通知未显示", notification.waitForExists(3000));
// 点击通知
notification.click();
// 等待浏览器打开
UiObject browserTitle = device.findObject(new UiSelector()
.packageName("com.android.chrome")
.textContains("测试页面"));
assertTrue("浏览器未打开", browserTitle.waitForExists(5000));
}
避坑指南: 我曾经在 CI 上跑 UiAutomator 测试,发现 device.openNotification() 在某些设备上不生效。后来查了源码,发现这个方法依赖 StatusBarManager 的 expandNotificationsPanel(),而有些定制 ROM 把这个接口改了。我的建议是:如果 openNotification() 不行,试试用 device.swipe() 模拟手势下拉。
29.4 知识体系总览
我把 SystemUI 测试与调试的核心内容整理成了一张图,方便你对照着看:
这张图把调试、单元测试、UI 测试三个维度串起来了。你想想看,实际工作中,这三个环节是互相补充的:
- 开发阶段,用单元测试快速验证逻辑
- 集成阶段,用 UI 测试验证交互流程
- 线上问题,用 dumpsys 和日志定位根因
缺了任何一个环节,SystemUI 的质量都很难保证。我个人习惯是:每次提交代码前,先跑一遍单元测试,再跑几个关键的 UI 测试用例。虽然 CI 上也会跑,但本地跑一遍能省去来回提交的等待时间。
最后说一句: SystemUI 的测试,难的不是写代码,而是搭建测试环境。尤其是那些依赖系统服务的模块,Mock 起来特别麻烦。我的建议是——先从 dumpsys 入手,把调试工具用熟,再逐步建立单元测试和 UI 测试的覆盖。别想一口吃成胖子。
公众号:蓝海资料掘金营,微信deep3321