第二十五章节:通知的测试与调试
通知开发完了,怎么验证它对不对?
说实话,我见过太多开发者在模拟器上点两下就觉得「嗯,没问题了」,结果一上线就被用户骂「收不到通知」或者「通知样式全乱了」。通知这东西,坑特别多——厂商定制、权限管理、渠道配置,任何一个环节出问题,用户感知都很强烈。
所以这一章,我带你系统地把通知的测试与调试捋一遍。我会从三个角度切入:adb 命令模拟通知、dumpsys 查看通知状态、以及单元测试怎么写。这三板斧用熟了,通知相关的 bug 基本逃不出你的手掌心。
25.1 用 adb 命令发送通知
你想想看,有时候你正在调试某个通知逻辑,但触发条件特别麻烦——比如需要服务器推送、或者需要特定时间点。这时候用 adb 直接发一条通知,效率就高多了。
我个人习惯在开发阶段准备一个脚本,里面放几条常用的 adb 通知命令,随时调用来验证 UI 效果。
25.1.1 基础命令:am broadcast
Android 系统提供了一个广播动作 android.intent.action.NOTIFICATION,但说实话,这个方式比较古老,而且限制很多。我更推荐用 am broadcast 配合自定义的广播接收器来触发通知。
不过,如果你只是想快速验证通知的展示效果,可以用 cmd notification 命令:
# 发送一条简单的通知
adb shell cmd notification post \
--category call \
--tag my_tag \
--priority high \
"com.example.app" "1" \
"标题:来电通知" \
"内容:张三正在呼叫你"
这条命令会直接向你的应用发送一条通知。注意参数顺序:包名、通知 ID、标题、内容。我刚开始用的时候老搞混顺序,后来干脆写了个别名脚本。
--content-intent 参数,指定一个 PendingIntent 的 URI。比如 --content-intent "intent:#Intent;action=android.intent.action.VIEW;end"。
25.1.2 模拟不同优先级的通知
通知的优先级直接影响它在状态栏的展示方式。高优先级的通知会弹出 Heads-up 横幅,低优先级的可能直接被折叠。
用 adb 可以很方便地切换优先级来测试:
# 高优先级(会弹出横幅)
adb shell cmd notification post \
--priority high \
"com.example.app" "2" \
"紧急" "服务器负载过高"
# 低优先级(静默通知)
adb shell cmd notification post \
--priority low \
"com.example.app" "3" \
"提醒" "你有3条未读消息"
我在项目中遇到过一个问题:某款国产手机把低优先级通知直接屏蔽了,用户完全看不到。后来我们统一把重要通知的优先级提到 high,才解决了这个问题。
25.1.3 模拟通知渠道
Android 8.0 之后,通知必须绑定渠道。用 adb 创建渠道也很简单:
# 创建通知渠道
adb shell cmd notification create_channel \
"com.example.app" \
"channel_id_1" \
"重要通知" \
4
最后一个数字是重要性等级:1=最低,2=低,3=默认,4=高,5=紧急。这个等级决定了通知是否弹窗、是否发出声音。
25.2 用 dumpsys 查看通知状态
通知发出去了,但用户到底收没收到?系统是怎么处理这条通知的?这时候 dumpsys notification 就是你的救命稻草。
我记得有一次线上反馈说某些用户收不到推送,我远程连上用户的设备,跑了一遍 dumpsys,发现通知被系统的「通知过滤」功能拦截了——因为我们的通知频率太高,被系统判定为骚扰。
25.2.1 查看所有通知
adb shell dumpsys notification --all
这条命令会输出当前系统上所有应用的通知状态。信息量很大,我一般会配合 grep 来过滤:
# 只看自己应用的通知
adb shell dumpsys notification --all | grep -A 20 "com.example.app"
输出内容大致长这样:
NotificationRecord(0x12345)
uid=10123 user=0 pkg=com.example.app
id=1 tag=null
postingTime=2024-01-15 10:30:00
priority=2
score=0
key=0|com.example.app|1|null|10123|0
status=active
channel=channel_id_1
groupKey=null
flags=0x10
visibility=-1000
关键字段我解释一下:
- status:通知状态,
active表示正在显示,canceled表示已取消 - score:系统对通知的评分,负值表示被降权
- flags:标志位,比如 0x10 表示
FLAG_AUTO_CANCEL - visibility:锁屏可见性,负值表示隐藏
25.2.2 检查通知渠道配置
有时候通知发不出来,是因为渠道配置有问题。用 dumpsys 可以查看渠道的详细信息:
adb shell dumpsys notification --channels | grep -A 10 "channel_id_1"
输出示例:
ChannelRecord(channel_id_1)
name=重要通知
importance=4
canShowBadge=true
canBypassDnd=false
isBlocked=false
userLockedFields=0
这里 isBlocked 如果为 true,说明用户手动关闭了这个渠道。你代码里发再多通知也没用。
25.2.3 查看通知历史
通知被取消了怎么办?别急,系统会保留最近的通知历史:
adb shell dumpsys notification --history
这个命令会列出最近 100 条被取消或替换的通知记录。每条记录包含发送时间、取消时间、原因等。我在追查「通知闪一下就消失」的问题时,就是靠这个命令发现是系统因为内存压力主动回收了通知。
25.3 通知的单元测试
手动测试只能验证「当前这次」对不对。要想保证代码质量,还得靠单元测试。
说实话,通知的单元测试在 Android 圈子里一直是个「老大难」——因为 NotificationManager 是个系统服务,直接 new 不出来。不过好在有 Robolectric 和 AndroidX Test 库,我们可以模拟系统环境。
25.3.1 搭建测试环境
首先在 build.gradle 里加上依赖:
testImplementation 'org.robolectric:robolectric:4.10.3'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'androidx.test.ext:junit:1.1.5'
然后写一个基础的测试类:
@RunWith(RobolectricTestRunner.class)
public class NotificationHelperTest {
private NotificationManager notificationManager;
private Context context;
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
notificationManager = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
}
}
25.3.2 测试通知发送
我最常用的测试模式是:发送通知,然后通过 NotificationManager 的 API 验证它是否存在:
@Test
public void testSendNotification() {
// 准备数据
String channelId = "test_channel";
String title = "测试标题";
String content = "测试内容";
// 执行发送
NotificationHelper.sendNotification(context, channelId, title, content);
// 验证:通过 dumpsys 模拟的方式获取通知
StatusBarNotification[] notifications =
notificationManager.getActiveNotifications();
assertThat(notifications).isNotEmpty();
assertThat(notifications[0].getNotification().extras
.getString(Notification.EXTRA_TITLE))
.isEqualTo(title);
}
这里有个坑:getActiveNotifications() 在 Robolectric 中默认返回空列表。你需要确保测试的 Shadow 版本支持。我一般会加一个 @Config(sdk = {Build.VERSION_CODES.O}) 注解来指定 API 级别。
25.3.3 测试通知渠道创建
渠道创建是通知功能的基础,必须测试:
@Test
public void testCreateNotificationChannel() {
String channelId = "my_channel";
String channelName = "我的渠道";
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationHelper.createChannel(context, channelId, channelName, importance);
NotificationChannel channel =
notificationManager.getNotificationChannel(channelId);
assertThat(channel).isNotNull();
assertThat(channel.getName().toString()).isEqualTo(channelName);
assertThat(channel.getImportance()).isEqualTo(importance);
}
我曾经踩过一个坑:在测试中创建了渠道,但忘记在 @After 方法里删除它,导致后续测试互相影响。所以记得加清理代码:
@After
public void tearDown() {
notificationManager.deleteNotificationChannel("my_channel");
}
25.3.4 测试通知点击行为
通知的点击事件通常通过 PendingIntent 实现。测试时我们可以验证 Intent 是否正确:
@Test
public void testNotificationClickIntent() {
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra("from_notification", true);
Notification notification = new NotificationCompat.Builder(context, "channel_id")
.setContentTitle("点击测试")
.setContentText("点击后跳转")
.setContentIntent(PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
.build();
// 验证 PendingIntent 中的 Intent
PendingIntent pendingIntent = notification.contentIntent;
Intent[] intents = pendingIntent.getIntent();
assertThat(intents).isNotEmpty();
assertThat(intents[0].getBooleanExtra("from_notification", false)).isTrue();
}
FLAG_IMMUTABLE 标志。Android 12 开始,可变 PendingIntent 会被系统警告,甚至可能被直接拒绝。
25.4 知识体系总览
这一章的内容比较多,我画了一张图帮你梳理整体结构:
这张图把三种调试手段的关系说清楚了。我个人建议的调试流程是:先用 adb 快速验证通知能不能正常展示,如果发现问题,立刻用 dumpsys 查系统状态定位根因,最后把修复后的逻辑写成单元测试,防止以后回归。
通知的测试与调试,说白了就是「发出去、查状态、写测试」这三个步骤。把这套流程跑熟了,你就能从「通知怎么又没了」的焦虑中解脱出来。
公众号:蓝海资料掘金营,微信deep3321