RK3576 Android15 框架扩展 — RkAi 数据流实战篇 电子说
在上一篇《RkAi 架构篇》中,我们搭建了 RkAi 子系统的整体框架:Binder Service 注册、AIDL 接口设计、客户端 API 封装。
本篇我们来追踪两条完整的数据流:
•ASR 上行:麦克风采集的音频数据→ AudioFlinger → JNI → Java 服务端 → App 回调
•LLM 下行:App 发送的文本消息 → Binder → 服务端广播 → 各监听器
并深入分析 JNI 层的实现细节、线程模型、以及性能瓶颈。
思维导图

1. 前置知识

1.1 Android 音频系统概览
Android 音频系统的核心构件:
•AudioFlinger:音频系统的服务端,管理音频输入输出流
•AudioSystem:封装 AudioFlinger 的客户端 API,包括回调注册
•BnRkAiCallback:RK 自定义的 Binder 回调接口,用于从音频 HAL 实时获取语音数据
1.2 JNI 的 JNI_OnLoad 机制
当 Java 代码调用 System.loadLibrary() 时,Native 库的 JNI_OnLoad() 会被自动调用。该函数有两个职责:
1.通过RegisterNatives() 注册 Java native 方法的 C 实现
2.缓存jmethodID / jfieldID,避免运行时重复查找
// onload.cppextern "C" jint JNI_OnLoad(JavaVM* vm, void*) { register_com_android_server_RkAiManagerService(env); register_com_android_server_rkdisplay_RkDisplayModes(env); return JNI_VERSION_1_4;}
2. ASR 上行数据路径(Native → Java → App)
这是 RkAi 当前唯一激活的通路。麦克风采集到的音频数据经过层层传递,最终到达应用层的 OnRkAiListener。
2.1 完整路径图

2.2 第 ① 步:音频采集与回调注册
RkAi 服务启动时,nativeInit(true) 做了两件事:
// com_android_server_RkAiManagerService.cppstatic int nativeInit(JNIEnv* env, jobject obj, jboolean supportAsr) { if (supportAsr) { mRkAiAsrCallback = sp::make(); AudioSystem::setRkAiCallback(mRkAiAsrCallback); } return 1;}
关键点:
•JRkAiAsrCallback 继承自BnRkAiCallback(Android Binder 框架的 Native 端实现)
•通过AudioSystem::setRkAiCallback() 注册到 AudioFlinger
•AudioFlinger 在有音频输入时,回调 onAsrBuffer()
2.3 第 ② 步:JNI 回调与类型转换
这是最核心的 C++ 代码:
// JRkAiAsrCallback 的构造函数JRkAiAsrCallback() { env = AndroidRuntime::getJNIEnv(); clazz = env->FindClass("com/android/server/RkAiManagerService");}// onAsrBuffer 回调binder::Status onAsrBuffer(const std::vector& buffer, int32_t len) override { JNIEnv* env = AndroidRuntime::getJNIEnv(); jshortArray jArray = env->NewShortArray(len); jshort *jBuffer = new jshort[len]; if (jArray != NULL && jBuffer != NULL) { // 逐元素拷贝:vector → short[] for (int i = 0; i < len; ++i) { jBuffer[i] = static_cast(buffer[i]); } env->SetShortArrayRegion(jArray, 0, len, jBuffer); jint jLen = static_cast(len); // 调用 Java 静态方法 env->CallStaticVoidMethod(clazz, gClassInfo.asrBufferFromNative, jArray, jLen); env->DeleteLocalRef(jArray); } delete[] jBuffer; return binder::ok();}
值得注意的设计细节:
1.类型不匹配 — AudioFlinger 送出 vector
2.手动分配临时缓冲区 — new jshort[len] + SetShortArrayRegion 的组合,没有直接用NewShortArray 的原生指针写入。这里有一次额外的内存拷贝(jBuffer → JVM 管理的 jshortArray)。
3.JNIEnv 线程问题 — 构造函数中获取的 env 在回调线程中可能无效,所以onAsrBuffer 中每次都重新调用AndroidRuntime::getJNIEnv() 获取当前线程的 JNIEnv。这是一种防御性写法。
4.static jmethodID 缓存 — gClassInfo.asrBufferFromNative 在JNI_OnLoad 时通过GetStaticMethodID 缓存下来,避免了每次回调时重复查找。
2.4 第 ③ 步:Java 静态方法 asrBufferFromNative
// RkAiManagerService.javaprivate static void asrBufferFromNative(final short[] buffer, final int len) { Bundle bundle = new Bundle(); bundle.putShortArray(RkAiManager.EXTRA_ASR_BUFFER, buffer); bundle.putInt(RkAiManager.EXTRA_ASR_BUFFER_LEN, len); sendRkAiMsg(new RkAiData(RkAiManager.RKAI_TYPE_ASR, bundle));}
这个方法有几个重要设计:
•static 方法 — 因为从 JNI 只能直接调用静态方法,无需对象实例
•Bundle 封装 — 用 Bundle 作为灵活的数据容器,可以方便地扩展键值对
•final 参数 — final 阻止方法将 buffer 重新指向另一个数组(不能 buffer = new short[...]),加强代码安全
2.5 第 ④ 步:RemoteCallbackList 广播
// RkAiManagerService.javaprivate static void sendRkAiMsg(RkAiData data) { final int num = mListeners.beginBroadcast(); try { for (int i = 0; i < num; i++) { try { mListeners.getBroadcastItem(i).dispatchRkAiListener(data); } catch (Exception e) { e.printStackTrace(); } } } finally { mListeners.finishBroadcast(); }}
RemoteCallbackList 是 Android 系统服务中管理跨进程回调的标准方式:
•beginBroadcast() — 锁定内部列表,获取当前注册的 listener 快照
•getBroadcastItem(i) — 获取第 i 个 listener 的 Binder 代理
•finishBroadcast() — 解锁
广播期间如果某个 listener 的进程已死亡,调用 dispatchRkAiListener() 会抛出DeadObjectException,被 catch 块捕获后继续广播剩下 listener。
2.6 第 ⑤ 步:回到 App 主线程
IOnRkAiListener 接口声明为oneway,所以服务端的广播调用是非阻塞的。在 App 端,Binder 回调到达的线程是 Binder 线程池中的线程,不能直接做 UI 操作。
oneway 关键字意味着:Binder 调用在事务发送后立即返回,不等待服务端执行完毕;不保证发送顺序与服务端处理顺序一致;服务端的 DeadObjectException(进程死亡)无法被oneway 调用检测到(Binder 驱动不会回复错误)。
// RkAiManager.java — mServiceListenerprivate final IOnRkAiListener.Stub mServiceListener = new IOnRkAiListener.Stub() { @Override public void dispatchRkAiListener(RkAiData data) { mHandler.post(() -> { reportRkAiMsg(data); }); }};
•mHandler.post() — 通过 Handler 将 reportRkAiMsg() 投递到创建RkAiManager 时所在的线程(通常是 App 的主线程)
•这意味着 App 不需要额外做线程同步——回调的最终分发总在主线程
2.7 第 ⑥ 步:reportRkAiMsg 分发
// RkAiManager.javavoid reportRkAiMsg(RkAiData data) { Object[] listeners; synchronized (mListeners) { final int N = mListeners.size(); if (N <= 0) return; listeners = mListeners.toArray(); // 快照,避免遍历时被修改 } for (int i = 0; i < listeners.length; i++) { switch (data.getType()) { case RKAI_TYPE_ASR: { Bundle info = data.getInfo(); if (null != info) { short[] buffer = info.getShortArray(EXTRA_ASR_BUFFER); int len = info.getInt(EXTRA_ASR_BUFFER_LEN, 0); ((OnRkAiListener)listeners[i]).onRkAiAsrBuffer(buffer, len); } break; } // LLM 类型同理处理 } }}
这里用了 Copy-on-Write 模式:在 synchronized 块内调用 toArray() 拿到监听器列表的快照,然后在锁外出逐个调用回调。这样:
•添加/删除 listener 的操作与回调互不干扰
•即使遍历过程中有 listener 被移除,已拿到的快照依然有效
3. LLM 下行数据路径(App → Service → 广播)
ASR 是 Native → Java 的上行方向,LLM 则是 App → Service 的下行方向。不过当前 SUPPORT_RKAI_LLM = false,所以这条路径处于"有代码但未启用"状态。
3.1 路径图

3.2 LLM 消息的独特之处
与 ASR 不同,LLM 消息的源是 App 本身:
// RkAiManager.javapublic void sendRkAiLlmMsg(String selectText, String contextText) { Bundle bundle = new Bundle(); bundle.putString(EXTRA_SELECT_TEXT, selectText); bundle.putString(EXTRA_CONTEXT_TEXT, contextText); RkAiData data = new RkAiData(RKAI_TYPE_LLM, bundle); mService.sendRkAiMsg(data, ...);}
当前SUPPORT_RKAI_LLM = false,但这套代码的架构思路是:
1.App 通过 Binder 把选中文本和上下文文本发给服务端
2.服务端广播给所有注册的 listener
3.监听器(比如另一个 App 进程的后台服务)解析文本,调用 LLM 推理
4.推理结果再通过另一个通道返回
这种"发一条消息播报给所有人"的模式与 ASR 的"单路采集多路分发"本质上是相同的。
4. 线程模型深度分析
理解 RkAi 的线程模型,对排查音频延迟、卡顿、ANR 问题至关重要。

4.1 各线程的角色
| 线程 | 所属进程 | 类型 | 关键特征 |
| AudioFlinger 线程 | system_server | Native 回调 | 实时优先级,不可被 Java 调用阻塞 |
| JNI 回调体 | system_server | Native 执行 | 在 AudioFlinger 回调上下文中运行 |
| Binder 线程池 | system_server | Java 执行 | beginBroadcast+Binder IPC |
| App Binder 线程 | App | Java Binder 回调 | 处理dispatchRkAiListener |
| App Main Thread | App | Handler Looper | 最终回调触发处 |
4.2 "切线程"的设计考量
从 Native 到 App 回调,经历了两次线程切换:
AudioFlinger 线程 → (1) JNI 回调 → (2) Binder IPC → (3) Handler Post
为什么不在 JNI 直接广播?
因为RemoteCallbackList 的操作(beginBroadcast)需要在 Java 线程上下文中执行,且 Binder 调用本身就需要线程池。JNI 层只做类型转换和 Java 静态方法调用,是合理的设计选择。
为什么 App 端要用 Handler?
因为IOnRkAiListener 虽然是oneway(非阻塞),但回调仍然在 Binder 线程池中执行。如果 App 直接在 Binder 回调中处理音频数据:
•会占用 Binder 线程,影响跨进程通信的吞吐
•Binder 线程不能进行 UI 操作
•线程不匹配可能导致 App 内部死锁
mHandler.post() 保证了 App 的回调总在预期线程(主线程或自定义 Looper 线程)中执行。
5. 性能分析与优化思考
5.1 性能热点

每次回调涉及的主要开销(按从左到右):
| 操作 | 开销等级 | 说明 |
|
vector |
中 | AudioFlinger 为每次回调构造新的 vector |
| int→short 逐元素 | 高 | O(n) 逐元素转换,可优化为批量转换 |
| NewShortArray | 低 | JVM 堆分配 |
| SetShortArrayRegion | 中 | native → JVM 内存拷贝 |
| Bundle.putShortArray | 中 | Bundle 内部序列化/反序列化 |
| beginBroadcast/finishBroadcast | 低 | 内部锁 + 列表遍历 |
| Binder oneway 传输 | 中 | Parcel 序列化 RkAiData |
5.2 可优化点
1. 消除逐元素拷贝
2. 流控机制缺失
3. Binder 死亡重连缺失
4. AudioSystem 回调线程优先级
5.3 内存开销
为分析每次 ASR 回调的内存分配路径:
实际上每次回调会有4-5 倍于原始数据量的临时内存分配。如果 ASR 频率很高(如 10ms 一次),这些临时对象的 GC 压力不小。
6. 调试方法
6.1 ASR 路径验证
6.2 Native 层调试
6.3 常见问题排查
7. 总结
通过本篇的详细追踪,可以看到 RkAi 子系统在两个方向上的数据流:
完整的调用链及每层的职责:
改进空间总结:
1.JNI 层的 int→short 逐元素拷贝可以优化
2.缺少 ASR 数据的流控/背压机制
3.RkAiManager 缺少 Binder 死亡重连
4.高频 ASR 回调下的 GC 压力需要评估
5.服务端不对callingPackage 做权限校验
审核编辑 黄宇
当前代码for (int i=0; i
当前onAsrBuffer 不做任何流控——AudioFlinger 以多快频率回调,JNI 就以多快频率广播。如果 App 端的 listener 消费速度跟不上,没有背压机制,可能导致 App 侧的 Handler 消息队列堆积。
可以考虑的方案:在 JNI 层实现一个环形缓冲区 + 降采样逻辑。
如果系统服务重启(比如 SystemServer crash),RkAiManager 持有的mService Binder 代理会变成死对象。当前代码没有重试或重连机制,所有后续 API 调用会抛出 RemoteException。
如果onAsrBuffer 中执行了太重的操作(比如 Java 静态方法调用中的 Bundle 分配),可能拖慢 AudioFlinger 的处理线程。建议在 JNI 层就做一次线程切换,用独立的 Native 线程接收回调,再异步转发到 Java 层。
AudioFlinger: vector
# 开启所有 RkAi 相关日志adb logcat -s RkAiManager RkAiManagerService RkAiManagerNative# 确认服务已注册adb shell service check rkai_management# 查看 ASR 数据流# 关键词:asrBufferFromNative — JNI 到 Java 的入口# sendRkAiMsg — 广播# dispatchRkAiListener — 回调到 App# 监测 ASR 回调频率adb logcat -s RkAiManagerNative | grep -o "onAsrBuffer" | wc -l
# JNI 回调是否生效# 正常 logcat 应有:# RkAiManagerNative: nativeInit supportAsr=1# 如果 ASR 回调没到 Java,检查:# 1. AudioSystem::setRkAiCallback 是否成功# 2. BnRkAiCallback 的 onAsrBuffer 是否命中
症状
可能原因
排查方法
服务注册失败
feature 未使能
adb shell pm list features | grep rockchip.software.ai
ASR 回调无数据
AudioFlinger 未触发回调
logcat RkAiManagerNative 看onAsrBuffer 打印
App 收不到回调
Binder 代理失效 / Listener 未注册
检查addListener 返回值
音频延迟大
JNI 层逐元素拷贝 / Bundle 分配
在 onAsrBuffer 打时间戳
服务端 ANR
Binder 广播被慢 listener 拖住
oneway 回调,理论上不应发生
方向
路径
当前状态
ASR 上行
AudioFlinger → JNI → Service → App
启用
LLM 下行
App → Service → 广播监听器
已实现但编译关闭

全部0条评论
快来发表一下你的评论吧 !