ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android消息监听工具实战:统一捕获短信与通知,附后台保活方案

2026/8/27 5:12:52 拓冰建站 浏览量
Android消息监听工具实战:统一捕获短信与通知,附后台保活方案 市面上聊这类工具的文章大多停在“写个广播接收器就能收到短信”的层面真到要稳定跑、能聚合、敢拿数据做展示时一堆隐藏问题就冒出来了。正好最近整理了一个叫Phone Message Monitor的实践项目把手机上的短信、通知、来电等消息统一监听、统一存储、统一展示顺便接了一些自动化测试的联动需求。这篇就把完整思路拆开聊一聊。这个项目最核心的价值不是“换了个壳的消息盒子”而是把 Android 系统里的几套消息监听机制——短信广播、NotificationListenerService、无障碍服务、辅助通道——按照各自能力边界做了合理分工然后围绕消息的归一化、存储、脱敏、规则过滤和后台存活做了完整工程化设计。不管你是想做一个自用的消息留档工具还是给自动化测试流程补上“验证码自动获取”这一环或者是纯粹想研究 Android 系统消息机制这篇内容都应该能给你一些可以落地的方案。1. 为什么自己动手写一个消息监听工具1.1 从“看验证码”到“消息中枢”的起点先说个我能遇到的典型场景。自动化测试跑 UI 用例的时候很多流程都牵扯到短信验证码比如注册、登录、找回密码。测试脚本执行到这一步常规做法是停在那里等人工看一眼手机再把验证码填进去。折腾过的人都懂这种等待不仅慢还容易因为手机锁屏、通知延迟导致整个用例超时。当时手里一堆测试机品牌各不相同系统版本从 Android 9 到 Android 14 都有。我就想要一个工具能实时把所有机器的短信验证码抓到同一个页面里脚本需要的时候直接从这个页面取数。后来需求又扩展了家里有些智能设备会推送告警通知平时不可能一直守着手机如果能把这些通知集中到一台设备上统一查看体验会舒服很多。这就是Phone Message Monitor最初的原型定位一个跑在手机端、负责监听短信和通知、并把消息统一存储和展示的消息聚合工具。它的核心不是一个具体的功能而是一整套消息处理链路的设计。1.2 现成方案为什么不够用可能会有人问这种需求直接用现成的消息同步 App 不就行了我试过几类方案都不是很满意。第一类是厂商自带的消息云同步。华为、小米、荣耀都有自己的云服务能把短信和通知同步到同一品牌的其他设备上。但问题也明显——不同品牌之间生态隔离家里设备品牌不一的话这套方案根本没法统一。而且这一类服务的数据最终都走了厂商云端敏感信息留存在人家的服务器上心里总有点不踏实。第二类是第三方推送转发类工具。功能确实全但是要么广告太多要么权限申请范围大得吓人。一个简简单单的消息转发工具恨不得把通讯录、定位、存储全都要走。我本身做一些自动化测试和个人开发对这种权限滥用是比较敏感的还是希望能自己控制所有代码逻辑。第三类叫“辅助”的偏门方案更不用提有些基于无障碍服务的工具表面上说是消息监控背地里干了什么不好说。所以自己写一套最大的收益是三个数据可控消息只在本地存储不经过任何第三方服务器。可定制验证码提取、规则过滤、远程联动想要什么自己做。学底层机制把 Android 的广播、通知监听、辅助服务真正吃透一次。1.3 项目范围与合规前提动手之前需要先明确一个边界。这类工具只应该用在你自己拥有的设备、你所在团队的测试机上或者已经获得明确授权的设备上。任何未经授权的消息监听行为既违反了 Android 的用户知情原则也可能带来法律风险。我在项目里也刻意做了轻量化的设计只监听回调事件不做屏幕内容抓取数据默认只存在本地导出必须手动触发。这个原则不是在为难自己是想让工具始终保持在“个人/团队效率工具”这个合规范围内。后面讲无障碍服务的时候还会再强调一次。2. 监听层怎么选广播、通知服务还是无障碍服务这一节是整个项目的地基。监听机制选错了后面的稳定性、功耗和合规性都会出问题。2.1 三条监听路线的能力对比Android 系统里能感知“消息到来”的通道主流的有这三条通道能拿到什么时效性权限门槛后台可持续性合规顾虑短信广播接收器短信内容、号码、SIM 卡信息高短信一到就触发需要RECEIVE_SMS权限静态广播高版本受限动态广播存活依赖进程低只限短信NotificationListenerService通知标题、正文、包名、时间中通知弹出后触发用户到系统设置里手动开启系统 bind 的独立服务存活率很高中能看见所有应用通知AccessibilityService屏幕节点、通知内容甚至界面结构中用户手动开启高危权限存活率强但系统会严格审查高权限过大用一张表总结短信广播是最直接的短信通道通知监听是覆盖最广的消息入口无障碍服务能力最强但代价也最大。2.2 首选 NotificationListenerService 的原因在我的方案里核心监听层选择了 NotificationListenerService而不是短信广播理由其实很现实。第一通知监听的覆盖面广。短信会弹通知微信会弹通知支付宝账单会弹通知智能家居 App 的告警也会弹通知。只要注册了通知监听服务所有这些消息都能在一个回调里拿到。短信广播只能拿短信覆盖面完全不同。第二通知监听后台存活率远高于普通 Service。因为它是由系统通过 bindService 方式主动绑定并维护的独立服务只要用户没有手动关掉开关系统一般会保持它的运行。相比之下普通 Service 在 Android 8.0 之后一旦进入后台随时可能被系统回收。第三NotificationListenerService 能直接拿到通知的标题和正文也就是Notification.extras里的EXTRA_TITLE和EXTRA_TEXT。对验证码类短信来说这些字段里通常已经包含验证码了不需要再去读短信数据库。一个基本实现的核心代码长这样class MonitorNotificationService : NotificationListenerService() { override fun onNotificationPosted(sbn: StatusBarNotification) { val notification sbn.notification val extras notification.extras val title extras.getString(Notification.EXTRA_TITLE) ?: val text extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: val packageName sbn.packageName val postTime sbn.postTime // 把这条消息交给统一处理链路 MessageDispatcher.dispatch( MessageEvent( type MessageType.NOTIFICATION, packageName packageName, title title, content text, timestamp postTime ) ) } override fun onNotificationRemoved(sbn: StatusBarNotification?) { // 通知被划掉时触发可以做“已读”状态同步 } }这段代码里的MessageDispatcher.dispatch()是自定义的消息入口所有被监听的消息都会流到这里再做统一处理。这样设计的好处是后续想加新的监听源比如短信广播只需要再写一个入口最终数据都汇到同一条流水线上。2.3 什么时候需要叠加 AccessibilityService很多人一上来就想用无障碍服务拿消息理由是它能获取到通知监听拿不到的内容比如某些应用把通知内容折叠了或者只在应用内部展示消息。但我要说的是无障碍服务优先不碰。它有几点不可回避的问题权限弹窗描述得非常恐怖用户几乎都会犹豫。应用商店对上架无障碍类应用审查极严如果没有明明白白的使用场景很容易被拒。无障碍服务在部分系统上会明显增加耗电因为系统要持续跟踪界面节点变化。在实际项目中我只有在一个场景下才会引入无障碍服务需要抓取的是应用内界面状态而不是系统通知的时候比如某个测试 App 的内部通知页只在应用内部展示不弹系统通知。这时候通知监听拿不到只能靠 AccessibilityService 去读取界面文本。如果用也建议把逻辑做得非常克制。只在目标应用前台时启用其他时候立刻释放服务不要在无障碍回调里做任何多余的事情。合规边界比功能完整更重要。2.4 短信热路径SMS Receiver 与通知监听的分工虽然通知监听已经能覆盖到短信通知了但我在项目里还是保留了短信广播接收器作为一条独立的短信处理链路。原因有两个有些短信到达时系统通知可能还没弹出广播接收器响应最快。对于验证码这种时效性很强的场景早一步拿到内容早一步完成测试流程。有些系统 Message 应用的折叠策略会让通知正文里不显示完整验证码但短信广播拿到的原始内容一定是最完整的。注册方式用的是 Java 层的动态广播因为纯静态广播在 Android 8.0 之后对隐式广播的限制非常多动态注册可以绕开这个限制前提是进程必须活着。所以我在方案里把短信广播定位成“辅助通道”主通道仍然是 NotificationListenerService。一个简单的短信接收器代码class SmsReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action ! Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return val messages Telephony.Sms.Intents.getMessagesFromIntent(intent) val content messages.joinToString(\n) { it.displayMessageBody } val sender messages.firstOrNull()?.originatingAddress ?: unknown MessageDispatcher.dispatch( MessageEvent( type MessageType.SMS, packageName com.android.mms, title sender, content content, timestamp System.currentTimeMillis() ) ) } }短信广播和通知监听并存会带来一个重复消息问题。解决办法我放在后面专门开一节讲这里先记住一个结论双通道是为了兜底不是为了让消息出现两遍。3. 消息的存储、归一化与安全脱敏3.1 统一消息模型不然后面全是坑监听层把消息收上来之后如果直接往数据库里塞后期做时间线展示、导出、过滤都会非常痛苦。因为短信、通知、来电在结构上差异太大短信有发送号码通知有包名来电有通话状态。如果每个类型各建一张表后面查询就全是 join。我在项目里定义了一个统一的消息模型data class MessageEvent( val id: Long? null, val type: MessageType, // SMS / NOTIFICATION / CALL val packageName: String, // 来源应用包名短信固定为 com.android.mms val title: String, // 短信的号码、通知的标题、来电号码 val content: String, // 核心正文 val timestamp: Long, // 事件时间戳 val sequence: Long, // 单调递增序列号解决时间戳相同导致乱序 val simSlot: Int -1, // SIM 卡槽位 val read: Boolean false, // 是否已读 val hash: String // 去重用哈希 )保存到数据库后所有的消息都在这一个表里按timestamp DESC, sequence DESC排序天然形成一条完整的时间线。展示端不管消息来自短信还是通知拿到的都是同一个结构体省掉了很多烦琐的分支判断。3.2 存储选型与写入策略对于这类工具数据库选型我直接用了 Room。理由很简单Room 提供了编译期 SQL 校验表结构改动不容易写错。支持协程 Flow消息列表可以做成响应式来一条消息界面自动刷新。内置的迁移机制对后续版本升级很友好。从数据量角度说一个普通用户一天也就收到几十到几百条通知一个月下来累计几千条。这个量级对 SQLite 来说毫无压力真正要关注的反而是写入性能和索引设计。我会做这几件事在timestamp、type、packageName上建联合索引满足大多数查询场景。所有插入操作走单条insert不搞批量因为监听回调本身不密集没必要为了省一点点 CPU 让代码复杂度上升。选一个内存中的分发队列监听回调只往队列里写由独立协程异步批次落库。这样既不会卡住回调线程也能减少数据库写入频率。核心代码参考Dao interface MessageDao { Query(SELECT * FROM message_event ORDER BY timestamp DESC, sequence DESC LIMIT :limit OFFSET :offset) fun pagedMessages(limit: Int, offset: Int): FlowListMessageEvent Insert suspend fun insert(event: MessageEvent): Long Query(DELETE FROM message_event WHERE timestamp :beforeTime) suspend fun deleteOlderThan(beforeTime: Long) }3.3 脱敏与隐私边界监听工具最容易翻车的点是隐私处理。自己做的话可以省掉三方的隐私风险但也要主动把风险控制住。我在项目里做了这几层脱敏默认不存联系人姓名。短信记录只存号码不关联通讯录。验证码自动提取。用正则从短信正文里提取验证码提取成功后正文只保留“验证码已提取”的占位文案避免明文验证码长时间存在本地。导出前二次确认。任何导出操作都会弹窗提醒“导出的文件包含您的消息内容请在安全环境处理”。验证码提取的正则简单版可以这么写fun extractVerificationCode(content: String): String? { val patterns listOf( Regex(\b(\d{4,8})\b), Regex([验证码|校验码|动态码][: ]?([A-Za-z0-9]{4,8})) ) patterns.forEach { pattern - val match pattern.find(content) if (match ! null) return match.groupValues[1] } return null }这段代码只适合自用如果接到自动化测试平台里还是建议用更严格的上下文规则来提取避免误提取正文里的订单号这类非验证码数字。4. 让监听服务稳定存活进程保活与厂商适配4.1 后台进程为什么会死“明明监听着过了一会儿怎么不推送了”这是做这类工具最常被问的问题。原因基本都指向一个点厂商的激进省电策略。从 Android 6.0 开始系统引入了 Doze 模式到 8.0后台执行限制进一步收紧再到国产 ROM 上小米、华为、OPPO、vivo 几乎每家都有自己的一套后台清理逻辑。它们杀的不只是普通应用有时候连高优先级的服务也照杀不误。这不是技术 bug说直白点这是生态里功耗和体验的博弈。做这类工具只能顺着系统规则来逆着系统干结果就是上架被拒、设备卡顿、用户骂街。4.2 前台服务与常驻通知最基础的一层保障是把监听服务做成前台服务附带一条常驻通知让系统知道这个服务正在被用户使用不容易被回收。Android 对前台服务的要求一直在变。在 targetSdk 34Android 14上前台服务必须声明类型而且并不存在一个通用的“给我随便开个前台服务”的通道。对 Phone Message Monitor 这类场景比较合理的是声明为specialUse类型并在AndroidManifest.xml里写明用途说明。Manifest 声明service android:name.MonitorService android:enabledtrue android:exportedfalse android:foregroundServiceTypespecialUse property android:nameandroid.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE android:valuemessage_monitoring_for_dashboard / /service启动前台服务的代码if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { val manager getSystemService(NotificationManager::class.java) val channel NotificationChannel( monitor_service, 消息监听服务, NotificationManager.IMPORTANCE_LOW ) manager.createNotificationChannel(channel) } val notification NotificationCompat.Builder(this, monitor_service) .setSmallIcon(R.drawable.ic_stat_monitor) .setContentTitle(消息监听运行中) .setContentText(正在监听短信与通知) .setOngoing(true) .build() startForeground(1001, notification)这里有个容易忽略的点监听工具自己的常驻通知建议把渠道重要性设成IMPORTANCE_LOW不要弹声音、不要震动否则用户看到一条“我在监控”的通知疯狂响第一反应就是卸载。4.3 厂商白名单引导前台服务只能保证系统不主动杀但用户或系统一键清理仍然能把后台任务清掉。这时候就得靠引导用户把应用加入厂商的白名单。这部分属于“一次性配置一劳永逸”的活。每个厂商的入口名称不一样但路径大同小异厂商入口路径小米/MIUI设置 - 应用设置 - 应用管理 - 自启动管理华为/EMUI设置 - 应用 - 应用启动管理 - 手动管理OPPO/ColorOS设置 - 电池 - 应用耗电管理 - 允许后台运行vivo/OriginOS设置 - 电池 - 后台耗电管理 - 允许后台高耗电我一般在首次启动的引导页里做三步展示一张截图标出各厂商入口的位置。检测系统是不是某厂商系统直接弹出对应入口的说明。顺便提示用户如果某个设备上用了省电模式省电模式下所有后台服务都会被冻结。引导页不用做太花哨一张清晰截图加一句“请允许后台运行”就够了。4.4 掉线自愈监听器的心跳与看门狗即便做了上述所有配置还是会有意外某些系统更新之后白名单权限被重置或者用户为了省电主动关了自启动。我这边做了一个轻量的“看门狗”机制用 WorkManager 每天定时检查一次监控服务是否还活着如果发现NotificationListenerService没有被系统绑定就在通知栏提醒用户“监听服务已被系统断开请重新开启”。具体实现思路class ServiceWatchdogWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result { val isListening NotificationListenerService .getActiveNotifications(applicationContext) ?.isNotEmpty() true if (!isListening) { NotificationHelper.notifyWatchdog(applicationContext) } return Result.success() } }这里用getActiveNotifications()来判断服务是否活着有一个前提这个方法是静态的只要系统里已经绑定了一个通知监听服务能正常调用它拿到当前通知列表就说明服务活着。周期性任务的调度val request PeriodicWorkRequestBuilderServiceWatchdogWorker( 1, TimeUnit.DAYS ).setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.NOT_REQUIRED) .build() ).build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( watchdog, ExistingPeriodicWorkPolicy.KEEP, request )WorkManager 是 Android 官方推荐的后台任务方案它能根据不同系统版本自动选择 JobScheduler、AlarmManager 等底层实现比自己在代码里硬写 Alarm 兼容各版本靠谱得多。5. 实测踩坑记录那些官方文档不会写的细节这节说几个我实测中遇到的坑每个都花了不止一个晚上。5.1 通知被折叠时contentIntent 拿不到的坑刚开始用 NotificationListenerService 的时候我一直想通过sbn.notification.contentIntent去拉取通知对应的 Activity 信息以为这样能拿到完整内容。后来在部分系统上发现通知被折叠状态时contentIntent直接是 null。这个现象在 Android 12 之后的个别定制系统上比较明显因为系统为了省内存会把不重要的通知的 PendingIntent 释放掉。解决办法不要依赖contentIntent核心内容从Notification.extras里拿。EXTRA_TITLE、EXTRA_TEXT、EXTRA_BIG_TEXT这三个字段基本覆盖了标题和正文而且不随折叠状态变化。5.2 双卡双待的 SIM 卡信息错乱做多卡设备适配时发现Telephony.Sms.Intents里拿 SIM 卡信息不同厂商的行为差异非常大。有的厂商在intent.getStringExtra(simId)返回0或1有的返回-1还有的直接不提供这个字段。想通过SubscriptionManager拿卡槽和号码也需要 READ_PHONE_STATE 权限而且 Android 10 之后对读取 SIM 序列号、手机号的限制越来越严格。我的兜底策略是优先尝试intent.getIntExtra(slot, -1)兼容 AOSP 字段。再尝试intent.getIntExtra(simId, -1)兼容国内部分厂商自定义字段。两个都拿不到就标记为simSlot -1不在界面上强绑卡槽。这种妥协方案不完美但它稳定。工具的定位是消息监听不是 SIM 卡管理不值得在这上面耗费太多优先级。5.3 Android 14 对通知权限的收紧在 targetSdk 34 的设备上如果你的应用没有主动申请POST_NOTIFICATIONS运行时权限那么应用发不出任何通知包括前台服务的常驻通知。如果是通过 startForeground 启动前台服务还会因为通知被系统拦截而报错。所以权限申请时机要前置。建议应用启动时立刻调用if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { val hasPermission ContextCompat.checkSelfPermission( this, Manifest.permission.POST_NOTIFICATIONS ) PackageManager.PERMISSION_GRANTED if (!hasPermission) { requestPermissions( arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_CODE_NOTIFICATION ) } }这里有个体验细节授权弹窗应该跟工具的核心功能绑定在一起解释比如说“需要通知权限才能显示监听运行状态”而不是干巴巴地弹系统框。用户一旦拒绝后面服务怎么都稳定不了。5.4 瞬时多条验证码消息的乱序问题做时间线展示时我遇到一个很经典的乱序问题用户一次性收到多条验证码短信由于系统通知并发到达onNotificationPosted回调的先后顺序和短信实际到达时间并不完全一致。如果只用timestamp排序这几条消息的先后就是随机排列。后来我在消息模型里加了一个sequence字段每次事件进入分发层时从原子计数器取一个递增序号。写入数据库时保存sequence排序用timestamp DESC, sequence DESC双字段。这样即使时间戳相同也能保证后到的消息排在后面展示逻辑符合人的直觉。5.5 消息重复与去重双通道方案带来的第一个问题就是重复一条短信短信广播也收到了通知监听也收到了最终数据库里出现两条一模一样的消息。去重方案我做了两层第一层在分发层生成消息哈希val hash MessageHash.generate(type, packageName, title, content, timestampWindow)第二层入库前查重相同哈希且时间窗在 3 秒内的消息直接丢弃Query(SELECT COUNT(*) FROM message_event WHERE hash :hash AND ABS(timestamp - :timestamp) 3000) suspend fun countDuplicate(hash: String, timestamp: Long): Int这里要注意timestampWindow的设计不能精确到毫秒做哈希否则同一条短信即使内容完全一样时间戳差异也会导致哈希不同去重失效。3 秒窗口是一个折中值既不会误杀正常消息也能把广播和通知两个通道产生的重复消息覆盖到。6. 从“能用”到“好用”过滤规则、远程联动与扩展思路6.1 规则引擎让监听器只搬你关心的消息消息一旦多起来全部展示等于没展示。我加了一个轻量规则引擎支持三类条件包名白名单/黑名单比如只关心 com.android.mms、com.tencent.mm。关键词匹配比如包含“验证码”“告警”才记录。正则匹配比如只保留验证码格式的短信。规则对象data class Rule( val id: Long, val name: String, val type: RuleType, // INCLUDE / EXCLUDE val field: RuleField, // PACKAGE / TITLE / CONTENT val pattern: String, // 关键词或正则表达式 val regex: Boolean false )在分发层里逐条过规则命中的进存储没命中的直接丢弃。这样设计的好处是规则引擎和监听层完全解耦以后想加“只转发包含订单号的通知”这种规则改规则匹配器就够了。6.2 远程联动要不要上 WebSocket/HTTP自动测试场景里监听器不能只在本机看测试脚本需要通过网络直接把验证码抓走。这个需求我实际搞了两套方案局域网内首选 HTTP 轮询实现简单、容易调。监听器提供一个 HTTP 接口测试脚本定时拉取未读消息。这种方式对测试框架最友好不依赖长连接断了也不心疼。低延迟推送场景用 WebSocket适合消息中转站的需求监听端把消息实时推给同一局域网内的接收端。注意这里只做局域网直连不搞任何公网穿透避免数据暴露在不可控链路里。无论哪种方案都建议做一层 token 认证不用多复杂一个预设密钥做请求头校验即可。安全不是靠复杂实现的是每一步都留校验造成的。6.3 电量与性能优化跑在手机上的工具电量是绕不开的话题。我实测下来的体会是监听回调里绝不能做 IO不管数据库写得有多快。一条短信进来通知监听回调触发如果你直接在这个线程里做数据库插入随着消息量增加主线程卡顿、系统 ANR 这些问题都会出现。优化方案集中在三点回调线程里只负责构建事件对象放进ConcurrentLinkedQueue。独立单线程协程从队列里拉数据批量插入数据库。通知列表 UI 使用 Flow数据库更新后自动刷新 View。实测对比优化前单条消息处理耗时在回调线程里平均 5ms优化后回调线程耗时降到 1ms 以下UI 也不再出现列表刷新卡顿。对于这种低频率消息工具这样的优化已经相当够用了。6.4 后续还可以这样扩展项目做到这个阶段性能、稳定性和功能闭环基本都能用了。后续的扩展空间也很大折叠屏/平板适配消息时间线的双栏布局。做更聪明的智能摘要比如直接从通知内容里提取快递单号、航班号、取件码。结合本地模型做敏感信息自动分类把支付验证码和普通通知分开管理。不过这些都建立在稳定监听这个底座上。底座不稳功能再多也白搭。从实践角度讲我最想分享的是一个认知改变做这种消息监听工具最大的难点从来不是你写不出监听代码而是你要清楚这套机制里每一层系统的边界在哪里。通知监听能做什么不能做什么前台服务在哪个版本上会被限制厂商省电策略为什么要杀掉你的服务把这些边界都摸清了工具自然就稳了。另外一个实际体会是如果你也是要在测试机群上跑这个工具建议先从最小可用版本开始——只做短信广播 通知监听捕获到的消息直接写进本地日志文件。等日志里的数据格式稳定了再一步步加数据库、加规则引擎、加远程接口。一上来就追求功能大而全后续排查问题的成本反而会成倍增加。动手写一个自己的消息监听工具真的是理解 Android 系统消息机制最好的方式。踩过那些版本差异的坑你的底线和耐心都会比之前稳得多。