20、Telephony与RIL架构:RILJ、RILD、AT指令与调制解调器通信
说到Android的通信能力,很多人第一反应是“打电话、发短信、上网”。但这些东西到底是怎么从App走到SIM卡,再走到基站的呢?嗯,这背后就是Telephony框架和RIL架构在干活。我当年第一次接触这块时,也被那一堆JNI、Socket、AT指令搞得头大。今天咱们就把这条链路彻底捋清楚。
整体架构:从App到Modem的“四层跳转”
先看一张我手绘的架构图,帮你建立整体认知:
说白了,整个通信链路就是四层:App → Telephony框架 → RILJ → RILD → Modem。每一层各司其职,咱们一层层拆开看。
RILJ:Java世界的“翻译官”
RILJ的全称是RIL Java,它位于frameworks/opt/telephony/src/java/android/internal/telephony/ril目录下。它的核心任务就一个:把Java层的请求序列化,通过Socket扔给RILD。
我个人习惯把RILJ看作一个“邮差”。它不关心你要打电话还是发短信,它只负责把信封装好、贴上标签、投递出去。
核心类:RIL.java
这个类大概有3000多行,里面定义了所有RIL请求的编号和序列化方法。比如:
// RIL.java 片段
public void dial(String address, int clirMode, UUSInfo uusInfo, Message result) {
// 构造RIL请求
RILRequest rr = RILRequest.obtain(RIL_REQUEST_DIAL, result);
rr.getStream().writeString(address);
rr.getStream().writeInt(clirMode);
// ... 写入UUSInfo
send(rr); // 通过Socket发送给RILD
}
你看,每个请求都有一个对应的RIL_REQUEST_XXX常量。这些常量在RILD那边也会有一份完全对应的定义,两边通过编号来匹配。嗯,这里要注意:编号必须一致,否则就会鸡同鸭讲。
避坑指南
我曾经在移植一个新平台时,发现打电话一直失败。查了两天才发现,RILJ和RILD里的RIL_REQUEST_DIAL编号不一致——一个定义的是10,另一个定义的是11。从那以后,我每次做平台适配都会先检查这个映射表。
RILD:Native层的“调度中心”
RILD是一个C++实现的守护进程,路径在hardware/ril/rild/。它启动后会创建一个Socket服务端,等待RILJ连接。连接建立后,RILD就进入一个事件循环,不断从Socket读取请求、解析、然后分发给具体的硬件抽象层。
RILD内部有几个关键组件:
- Event Loop:基于select/epoll的事件驱动循环
- Request Handler:解析RIL请求,调用对应的处理函数
- Unsolicited Handler:处理Modem主动上报的事件(如来电、信号变化)
你想想看,RILD其实就是一个“中间人”。它一边要听懂RILJ的“Java话”,一边要能指挥Modem干活的“AT指令话”。
AT指令:与Modem对话的“通用语言”
AT指令是Hayes标准指令集,最早用于调制解调器控制。Android里,RILD最终会把RIL请求翻译成AT指令,通过串口或USB发送给Modem。
举个例子,打电话的流程:
// RILD收到RIL_REQUEST_DIAL后,发送AT指令
ATD+8613800138000; // 拨号指令
// Modem回复
OK // 成功
或
NO CARRIER // 无载波
或
BUSY // 忙
常见的AT指令还有:
| AT指令 | 功能 | 回复示例 |
|---|---|---|
| AT+CSQ | 查询信号强度 | +CSQ: 20,99 |
| AT+CREG? | 查询网络注册状态 | +CREG: 0,1 |
| AT+CMGS | 发送短信 | +CMGS: 45 |
| AT+CGATT | GPRS附着 | OK |
注意:AT指令的坑
不同Modem厂商对AT指令的实现有差异。比如高通平台和联发科平台,同一个AT+CSQ返回的格式可能不同。我遇到过最离谱的一次,某个平台的AT+CSQ返回的是十六进制,而RILD默认解析的是十进制,结果信号强度显示永远满格——用户投诉说“手机信号假的”。
通信流程:一次完整的拨号
咱们把整个流程串起来,看看一次拨号到底经历了什么:
- App层:用户点击拨号按钮,调用
TelephonyManager.dial() - Telephony框架:Phone对象收到请求,调用
RILJ.dial() - RILJ:构造RILRequest,序列化参数,通过Socket发送给RILD
- RILD:从Socket读取请求,解析出RIL_REQUEST_DIAL,调用对应的AT指令生成函数
- AT指令:通过串口发送
ATD+8613800138000;给Modem - Modem:执行拨号,返回OK或错误码
- 反向路径:OK逐层返回,最终App收到拨号成功回调
整个过程看起来简单,但实际调试时每一步都可能出问题。我建议你在做RIL开发时,先在RILD层加日志,确认AT指令是否发出、Modem是否回复。这是最有效的排查手段。
RIL架构的演进:从HAL到HIDL
Android 8.0之前,RILD直接加载厂商的libreference-ril.so或libsec-ril.so等动态库。这种方式耦合太紧,厂商改一个库可能影响整个系统。
Android 8.0之后,Google引入了HIDL(HAL Interface Definition Language),把RIL拆成了两部分:
- RIL Service:系统侧,负责与RILJ通信
- RIL HAL:厂商侧,负责与Modem通信
两者通过HIDL接口通信,实现了系统与厂商实现的解耦。说白了,就是给RIL加了一层“标准化接口”,厂商只要实现这个接口,系统就能正常工作。
我的建议
如果你在做新平台的RIL适配,优先使用HIDL方式。虽然学习成本高一点,但后续维护会轻松很多。我见过太多厂商还在用老式的reference-ril方式,每次升级Android版本都要改一堆代码,苦不堪言。
调试技巧:如何抓取RIL日志
调试RIL问题,日志是最重要的武器。这里分享几个我常用的方法:
- RILJ日志:设置
persist.radio.ril.debug为true,RILJ会打印详细的请求/响应日志 - RILD日志:通过
logcat -b radio查看,RILD的日志标签通常是RILC或RIL - AT指令日志:设置
persist.radio.atlog为true,可以抓到RILD与Modem之间收发的原始AT指令 - Modem侧日志:通常需要厂商提供的工具,比如高通的QXDM或QPST
我曾经遇到一个诡异的问题:手机能打电话,但接不到来电。查了RILJ和RILD日志都正常,最后打开AT指令日志才发现,Modem上报的来电通知格式不对,RILD解析失败直接丢弃了。嗯,这种问题不看原始AT指令根本找不到原因。
总结
Telephony与RIL架构,说白了就是一条从App到Modem的“命令传递链”。RILJ负责序列化,RILD负责调度,AT指令负责执行。每一层都有各自的职责和坑,但只要理解了整体流程,调试起来就有方向了。
我个人觉得,RIL架构最精妙的地方在于它的分层设计——每一层都可以独立测试和替换。你可以在不修改App的情况下,换掉整个RILD实现;也可以在不碰Modem的情况下,优化RILJ的逻辑。这种松耦合的设计,才是Android能适配上千种不同Modem的底气所在。