
1. 短信模块在Android系统中的整体设计思路短信功能在Android系统里属于那种“看起来简单、做起来全是坑”的模块。很多刚接触Android系统开发的朋友会觉得短信嘛不就是调个API发出去、收回来显示一下但真正在系统层做过短信相关开发的人都知道从应用层到框架层再到无线通信层中间要穿过至少四五个进程边界涉及权限管控、多卡路由、PDU编解码、数据库并发、状态机管理等一系列问题。这篇文章就把我在实际项目中积累的短信模块理论知识和实操经验完整梳理一遍适合Android应用开发者、系统ROM定制人员、以及需要做短信自动化测试的工程师参考。先明确一下Android短信模块的边界。它主要包含三大块能力短信发送SMS Send、短信接收SMS Receive、短信存储与读取SMS Provider。此外还有两个容易被忽略但实际项目中一定会碰到的部分多卡管理Multi-SIM和短信状态报告Delivery Report。这五块内容构成了Android短信功能的完整闭环。从架构层次来看短信模块横跨了四个层面。最上层是应用层包括系统自带的短信应用和第三方应用通过SmsManager调用的接口。第二层是框架层主要是Telephony框架中的SmsManager、SmsController等类。第三层是无线通信接口层RIL负责把框架层的指令翻译成无线通信模块能理解的AT命令或HAL调用。最底层是无线通信硬件和协议栈处理实际的信号收发。理解这个分层结构非常关键因为后面讲到的所有问题排查本质上都是先定位问题出在哪一层然后再针对性处理。为什么Android要把短信模块设计得这么复杂核心原因有三个。第一是安全性短信涉及用户隐私和资费必须有多层权限校验。第二是多卡适配从Android 5.1开始系统原生支持双卡甚至多卡每张卡的短信通道需要独立管理。第三是运营商差异不同运营商的短信中心号码、编码方式、状态报告格式都不一样系统需要一套灵活的适配机制。这三点决定了短信模块不可能做得简单也决定了我们在开发和调试时必须考虑这些维度。2. 短信发送链路的核心细节与实操要点2.1 从应用到无线通信层的完整调用链短信发送的调用链是整个模块中最核心的部分。当你在短信应用里点击发送按钮后实际发生的事情远比你想象的复杂。应用层调用SmsManager.getDefault()或SmsManager.getSmsManagerForSubscriptionId()获取管理器实例然后调用sendTextMessage()或sendMultipartTextMessage()方法。这两个方法的区别在于前者用于单条短信不超过160个字符的GSM-7编码或70个字符的UCS-2编码后者用于长短信自动拆分。框架层收到请求后会经过SmsController进行权限校验和路由决策。这里有个关键点从Android 4.4开始只有默认短信应用才能调用短信发送接口。非默认短信应用调用sendTextMessage()时系统会抛出SecurityException。这个限制是为了防止恶意应用在后台偷偷发送短信产生资费。很多开发者第一次遇到这个异常时会懵其实就是没有把应用设为默认短信应用。权限校验通过后请求会进入IccSmsInterfaceManager再通过SmsDispatchersController分发到具体的SmsDispatcher实现类。这里根据网络类型不同会走不同的路径GSM网络走GsmSmsDispatcherCDMA网络走CdmaSmsDispatcher。分发器负责把短信内容编码成PDU格式然后通过RIL层发送出去。PDU编码是短信发送中最容易出问题的环节。PDUProtocol Data Unit是短信在无线通信网络中传输的标准格式包含短信中心号码、目标号码、编码方式、用户数据等多个字段。编码方式主要有两种GSM-7编码用于纯ASCII字符和UCS-2编码用于中文等Unicode字符。GSM-7编码下单条短信最多160个字符UCS-2编码下单条最多70个字符。超过限制就需要拆分成多条短信每条短信头部会增加6个字节的UDHUser Data Header用于拼接。注意长短信拆分后如果其中某一条丢失接收端可能无法完整拼接。这就是为什么有时候对方收到的短信是乱码或者不完整的原因之一。在实际项目中如果对短信到达率要求很高建议控制单条短信长度在70个字符以内避免拆分。2.2 多卡场景下的发送路由逻辑多卡场景是Android短信开发中绕不开的难点。从Android 5.1开始系统引入了SubscriptionManager来管理多张SIM卡。每张SIM卡都有一个唯一的subscriptionId短信发送时必须指定用哪张卡发送。如果你调用的是不带subscriptionId参数的sendTextMessage()系统会使用默认短信卡的subscriptionId。这里有个实际项目中经常踩的坑默认短信卡和默认数据卡是两回事。用户可能在设置里把卡1设为默认数据卡但默认短信卡仍然是卡2。如果你在代码里硬编码用卡1发送用户就会觉得“为什么我用卡2发短信结果扣的是卡1的费”。正确的做法是通过SubscriptionManager.getDefaultSmsSubscriptionId()获取当前默认短信卡的ID然后传给SmsManager.getSmsManagerForSubscriptionId()。多卡发送的另一个问题是状态报告的对应关系。每张卡发送的短信其状态报告会通过对应的RIL通道返回。如果应用层没有正确区分subscriptionId就可能出现“卡1发的短信状态报告显示在卡2上”的混乱情况。我在实际项目中遇到过这个问题排查了半天才发现是SmsTracker中的subscriptionId字段没有正确传递。2.3 短信发送的权限与限制清单在Android系统中发送短信需要满足以下条件条件说明常见问题SEND_SMS权限在AndroidManifest中声明Android 6.0以上需要动态申请默认短信应用Android 4.4以上非默认应用无法发送需要引导用户设置正确的subscriptionId多卡场景必须指定不指定会走默认卡有效的短信中心号码由运营商配置配置错误会导致发送失败网络注册状态必须已注册到网络无服务时发送会立即失败实操心得在调试短信发送功能时建议先用adb shell dumpsys telephony.registry查看当前网络注册状态和短信中心号码。如果短信中心号码为空说明SIM卡没有正确读取配置需要检查RIL层是否正常工作。3. 短信接收与解析的完整实现过程3.1 从无线通信层到应用层的接收链路短信接收的链路和发送正好相反但复杂度一点不低。当无线通信模块收到一条短信后会通过RIL层上报给RILReceiver然后经过SmsDispatchersController分发到InboundSmsHandler。InboundSmsHandler是短信接收的核心类它负责PDU解码、去重、存储和广播。PDU解码是接收环节的第一个关键步骤。收到的PDU数据需要按照3GPP TS 23.040标准进行解析提取出短信中心号码、发送方号码、编码方式、用户数据、时间戳等信息。如果编码方式是GSM-7还需要进行7位到8位的解包操作。这个过程如果出错就会出现乱码或者号码显示错误。解码完成后InboundSmsHandler会进行去重检查。为什么要去重因为在某些网络环境下同一条短信可能会被重复上报。系统通过比对短信的PDU数据和接收时间来判断是否重复。如果不去重用户就会收到两条一模一样的短信。这个去重逻辑在InboundSmsHandler的dispatchMessage()方法中实现具体是通过SmsBroadcastUndelivered和RawTable来管理的。3.2 短信存储与SMS Provider的读写机制解码后的短信需要存储到数据库中这个数据库就是SMS Provider对应的URI是content://sms。SMS Provider本质上是一个SQLite数据库包含sms、raw、draft、outbox、sent等多个表。其中raw表存储原始PDU数据sms表存储解析后的短信内容。这里有个重要的设计细节短信的读写权限是分开的。读取短信需要READ_SMS权限写入短信需要WRITE_SMS权限。从Android 4.4开始只有默认短信应用才能写入SMS Provider。第三方应用即使有WRITE_SMS权限也无法直接写入必须通过Telephony.Sms.Inbox等接口间接操作。在实际项目中我遇到过SMS Provider数据库损坏导致短信应用崩溃的情况。排查方法是先用adb shell进入设备然后执行sqlite3 /data/data/com.android.providers.telephony/databases/mmssms.db打开数据库检查表结构是否完整。如果数据库损坏严重可以删除数据库文件让系统重建但这样会丢失所有短信记录操作前一定要备份。注意SMS Provider的数据库路径在不同Android版本和不同设备上可能不同。有些设备是mmssms.db有些是telephony.db。建议先用adb shell find /data -name *.db | grep -i sms来定位。3.3 短信广播与通知机制短信存储完成后InboundSmsHandler会发送一个SMS_RECEIVED_ACTION广播。这个广播有一个特殊之处它是一个有序广播Ordered Broadcast优先级高的接收者可以先处理并且可以调用abortBroadcast()来阻止广播继续传递。这就是为什么某些安全软件可以拦截短信的原因。从Android 4.4开始只有默认短信应用才能接收SMS_RECEIVED_ACTION广播。其他应用即使注册了接收器也收不到这个广播。这个限制同样是为了保护用户隐私。如果第三方应用需要读取短信必须通过ContentResolver查询SMS Provider并且需要用户授权READ_SMS权限。短信通知的显示由默认短信应用负责。当收到新短信时默认短信应用会调用NotificationManager显示通知。这里有个细节Android 8.0以上必须使用NotificationChannel否则通知不会显示。很多开发者在适配新版本时忘记创建Channel导致短信通知不显示排查起来很费时间。4. 常见问题与排查技巧实录4.1 短信发送失败排查速查表短信发送失败是最常见的问题原因可能出在多个环节。下面这张表是我在实际项目中总结的排查思路按优先级从高到低排列排查项检查方法可能原因解决方案权限是否授予adb shell dumpsys package 包名未动态申请SEND_SMS引导用户授权是否默认短信应用adb shell settings get secure sms_default_application非默认应用引导用户设置短信中心号码adb shell dumpsys telephony.registry号码为空或错误检查SIM卡配置网络注册状态adb shell dumpsys telephony.registry未注册或仅限紧急呼叫等待网络恢复subscriptionId日志中搜索SmsTracker多卡路由错误指定正确的卡PDU编码日志中搜索PDU编码方式错误检查字符集RIL层错误日志中搜索RILJ无线通信模块异常重启无线通信模块实操心得排查短信发送问题时最有效的工具是adb logcat -b radio。这个命令会输出无线通信相关的日志包括RIL层的交互过程。如果看到SEND_SMS请求发出后没有SEND_SMS_COMPLETE响应说明问题出在RIL层或无线通信模块。如果看到了SEND_SMS_COMPLETE但状态是ERROR说明是网络侧拒绝需要检查短信中心号码或账户余额。4.2 短信接收乱码与延迟问题短信接收乱码通常有两个原因编码方式不匹配和PDU解析错误。编码方式不匹配是指发送方用GSM-7编码接收方按UCS-2解码或者反过来。这种情况在跨运营商短信中比较常见。解决方法是检查PDU中的DCSData Coding Scheme字段确保解码方式与编码方式一致。短信接收延迟的原因更复杂。可能是网络侧的问题也可能是设备侧的问题。设备侧最常见的原因是Doze模式。Android 6.0引入了Doze模式在设备长时间静止时会对后台应用进行限制可能导致短信广播延迟。解决方法是在默认短信应用中申请REQUEST_IGNORE_BATTERY_OPTIMIZATIONS权限或者引导用户将短信应用加入电池优化白名单。另一个容易被忽略的原因是短信去重逻辑误判。如果两条不同的短信恰好PDU数据相同比如验证码短信内容一样去重逻辑可能会把第二条当成重复短信丢弃。这个问题的排查方法是查看InboundSmsHandler的日志搜索duplicate关键字。如果确认是去重误判可以调整去重的时间窗口或者比对字段。4.3 多卡短信状态报告混乱问题多卡场景下状态报告混乱是一个高频问题。具体表现是用卡1发送的短信状态报告却显示在卡2的会话中。这个问题的根源在于SmsTracker中的mSubscriptionId字段没有正确传递到状态报告的处理逻辑中。排查方法是先在日志中搜索SmsTracker找到发送时的subscriptionId然后搜索DeliveryReport看状态报告返回时的subscriptionId是否一致。如果不一致说明在SmsDispatchersController分发状态报告时没有正确匹配。解决方法是修改SmsDispatchersController中的handleSmsStatusReport()方法确保用subscriptionId来匹配对应的SmsTracker。注意修改框架层代码需要重新编译系统或者使用Xposed等框架。如果是应用层开发无法直接修改框架层逻辑只能通过SmsManager.getSmsManagerForSubscriptionId()来确保发送时指定了正确的卡减少状态报告混乱的概率。5. 短信模块的进阶话题与扩展方向5.1 短信与RCS的融合趋势传统短信基于电路交换或分组交换的独立通道而RCSRich Communication Services是基于IP的多媒体消息服务。目前主流运营商都在推进RCS商用Android系统也从Android 5.0开始内置了RCS支持。在实际项目中短信模块需要同时处理传统短信和RCS消息这对架构设计提出了新的要求。RCS消息的发送和接收走的是完全不同的通道但用户体验上需要和传统短信无缝融合。系统通过SmsManager的扩展接口来支持RCS应用层可以通过isImsSmsSupported()判断当前是否支持RCS。如果支持优先走RCS通道如果不支持回退到传统短信通道。这个回退逻辑在实际项目中非常关键因为RCS的网络覆盖还不完善经常需要回退。5.2 短信自动化测试的实操方案短信功能的自动化测试是一个独立的话题。由于短信涉及真实的无线通信网络很难在模拟器上完整测试。实际项目中通常采用以下方案使用真实的测试设备插入测试SIM卡通过adb命令模拟短信的发送和接收。发送短信的adb命令是adb shell service call isms但这个命令的参数非常复杂需要构造PDU数据。更简单的方法是使用adb shell am start启动短信应用然后通过UI自动化工具如UiAutomator模拟点击操作。接收短信的模拟可以通过adb emu sms send命令实现但这个命令只在模拟器上有效。实操心得在真实设备上做短信自动化测试时建议使用专门的测试SIM卡避免产生真实资费。另外测试环境要尽量屏蔽外部干扰比如把设备放在信号稳定的位置关闭其他可能发送短信的应用。我试过在信号不稳定的环境下测试结果短信发送成功率只有70%排查了半天才发现是信号问题不是代码问题。5.3 短信模块的功耗优化短信模块的功耗主要来自两个方面无线通信模块的待机功耗和短信应用的后台唤醒。无线通信模块的待机功耗是硬件层面的应用层无法直接优化。但短信应用的后台唤醒是可以优化的。默认短信应用需要注册SMS_RECEIVED_ACTION广播接收器这个接收器会在收到短信时唤醒应用。如果应用在onReceive()中做了耗时操作会导致设备长时间无法进入休眠。优化方法是在onReceive()中只做最必要的操作比如把短信存储到数据库然后通过JobScheduler或WorkManager异步处理通知显示等后续操作。另一个优化点是短信数据库的查询效率。SMS Provider中的短信数量可能达到数万条如果查询时没有正确使用索引会导致查询缓慢进而增加功耗。建议在查询时使用Telephony.Sms.DATE字段排序并且限制返回数量避免一次性加载所有短信。6. 个人实操经验与避坑总结做了这么多年的Android短信模块开发踩过的坑确实不少。有几个经验我觉得特别值得分享。第一个是关于短信中心号码的读取。很多开发者以为短信中心号码是存在SIM卡里的直接读取就行。但实际上短信中心号码的存储位置和读取方式在不同设备上差异很大。有些设备存在SIM卡中有些存在NV项中有些由运营商在开机时下发。在实际项目中最可靠的方式是通过TelephonyManager.getSmsCenterAddress()获取如果返回空再尝试从SIM卡读取。如果都失败就需要引导用户手动设置。第二个是关于长短信的拼接。长短信的拼接依赖UDH中的参考号和序列号。如果接收端在拼接时没有正确处理序列号就会出现短信内容错乱。我在实际项目中遇到过一个问题发送方把长短信拆成3条序列号是1、2、3但接收端收到的顺序是2、1、3结果拼接出来的内容是乱的。这个问题的根源在于网络侧不保证短信的到达顺序。解决方法是接收端在拼接时先按序列号排序再拼接。第三个是关于短信数据库的并发访问。SMS Provider是一个SQLite数据库支持多进程并发读写。但在高并发场景下比如同时收到多条短信可能会出现数据库锁竞争导致写入失败。解决方法是使用ContentResolver的bulkInsert()方法批量插入减少事务次数。另外查询时尽量使用CursorLoader异步加载避免在主线程中查询数据库。第四个是关于短信权限的适配。Android 6.0引入动态权限后短信权限的申请变得复杂。很多应用在申请SEND_SMS权限时用户拒绝了就再也不申请了导致功能不可用。正确的做法是在用户拒绝后显示一个解释说明告诉用户为什么需要这个权限然后再引导用户去设置中手动开启。如果用户再次拒绝就不要再频繁弹窗了而是提供一个降级方案比如通过分享功能让用户手动发送短信。第五个是关于短信广播的优先级。SMS_RECEIVED_ACTION是一个有序广播优先级高的接收者先收到。默认短信应用的优先级是系统预设的第三方应用无法超过。但如果第三方应用需要优先处理短信比如做短信拦截可以通过IntentFilter.setPriority()设置较高的优先级。不过要注意从Android 4.4开始只有默认短信应用才能收到这个广播所以第三方应用的优先级设置实际上没有意义。最后分享一个调试小技巧如果怀疑短信模块有问题但不确定是应用层还是框架层的问题可以先用adb shell dumpsys telephony.registry查看当前电话服务的状态然后用adb logcat -b radio -b main同时抓取无线通信日志和主日志。如果无线通信日志中有SMS相关的错误说明问题在框架层或RIL层如果只有主日志中有错误说明问题在应用层。这个二分法可以快速缩小排查范围节省大量时间。