第二十五章节:通知的测试与调试

通知开发完了,怎么验证它对不对?

说实话,我见过太多开发者在模拟器上点两下就觉得「嗯,没问题了」,结果一上线就被用户骂「收不到通知」或者「通知样式全乱了」。通知这东西,坑特别多——厂商定制、权限管理、渠道配置,任何一个环节出问题,用户感知都很强烈。

所以这一章,我带你系统地把通知的测试与调试捋一遍。我会从三个角度切入: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,说明用户手动关闭了这个渠道。你代码里发再多通知也没用。

核心思路:调试通知问题时,先查渠道是否被阻塞,再查通知评分是否被降权,最后看 flags 是否正确。按这个顺序排查,90% 的问题都能定位。

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();
}
个人经验:测试 PendingIntent 时,记得加上 FLAG_IMMUTABLE 标志。Android 12 开始,可变 PendingIntent 会被系统警告,甚至可能被直接拒绝。

25.4 知识体系总览

这一章的内容比较多,我画了一张图帮你梳理整体结构:

通知测试与调试知识体系 adb 命令发送通知 am broadcast / cmd notification 优先级模拟(high/low) 通知渠道创建与测试 dumpsys 查看状态 --all 查看所有通知 --channels 检查渠道 --history 查看历史 单元测试 Robolectric 环境搭建 发送与渠道测试 PendingIntent 验证 调试流程建议 1. 用 adb 快速验证 UI 效果 → 2. 用 dumpsys 定位问题 → 3. 用单元测试固化逻辑 三者结合,覆盖开发、调试、回归全流程

这张图把三种调试手段的关系说清楚了。我个人建议的调试流程是:先用 adb 快速验证通知能不能正常展示,如果发现问题,立刻用 dumpsys 查系统状态定位根因,最后把修复后的逻辑写成单元测试,防止以后回归。

通知的测试与调试,说白了就是「发出去、查状态、写测试」这三个步骤。把这套流程跑熟了,你就能从「通知怎么又没了」的焦虑中解脱出来。


公众号:蓝海资料掘金营,微信deep3321