RK3576 Android15 框架扩展 — RkAi 数据流实战篇

电子说

1.4w人已加入

描述

在上一篇《RkAi 架构篇》中,我们搭建了 RkAi 子系统的整体框架:Binder Service 注册、AIDL 接口设计、客户端 API 封装。

本篇我们来追踪两条完整的数据流

ASR 上行:麦克风采集的音频数据→ AudioFlinger → JNI → Java 服务端 → App 回调

LLM 下行:App 发送的文本消息 → Binder → 服务端广播 → 各监听器

并深入分析 JNI 层的实现细节、线程模型、以及性能瓶颈。

 思维导图

Android

1. 前置知识

Android

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 完整路径图

Android

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(32位),但 Java 侧期望 short[](16位)。JNI 层做了 int → short 的逐元素截断转换。这暗示音频数据实际使用的就是 16bit 采样(标准 PCM 16bit 格式),用 vector 是 AudioFlinger 回调接口的历史设计。

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 路径图

Android

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 问题至关重要。

Android

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 性能热点

Android

每次回调涉及的主要开销(按从左到右):

操作 开销等级 说明
vector 构造 AudioFlinger 为每次回调构造新的 vector
int→short 逐元素 O(n) 逐元素转换,可优化为批量转换
NewShortArray JVM 堆分配
SetShortArrayRegion native → JVM 内存拷贝
Bundle.putShortArray Bundle 内部序列化/反序列化
beginBroadcast/finishBroadcast 内部锁 + 列表遍历
Binder oneway 传输 Parcel 序列化 RkAiData

5.2 可优化点

1. 消除逐元素拷贝
当前代码for (int i=0; i

2. 流控机制缺失
当前onAsrBuffer 不做任何流控——AudioFlinger 以多快频率回调,JNI 就以多快频率广播。如果 App 端的 listener 消费速度跟不上,没有背压机制,可能导致 App 侧的 Handler 消息队列堆积。
可以考虑的方案:在 JNI 层实现一个环形缓冲区 + 降采样逻辑。

3. Binder 死亡重连缺失
如果系统服务重启(比如 SystemServer crash),RkAiManager 持有的mService Binder 代理会变成死对象。当前代码没有重试或重连机制,所有后续 API 调用会抛出 RemoteException。

4. AudioSystem 回调线程优先级
如果onAsrBuffer 中执行了太重的操作(比如 Java 静态方法调用中的 Bundle 分配),可能拖慢 AudioFlinger 的处理线程。建议在 JNI 层就做一次线程切换,用独立的 Native 线程接收回调,再异步转发到 Java 层。

5.3 内存开销

为分析每次 ASR 回调的内存分配路径:

 

AudioFlinger:    vector  (4*len bytes)JNI:             jshort[]    (2*len bytes, JVM heap)                  + jBuffer  (2*len bytes, native heap, 临时)Java:            Bundle       (内部数组)Binder:          Parcel       (序列化后的数据)App:             Handler msg  + dispatch

 

实际上每次回调会有4-5 倍于原始数据量的临时内存分配。如果 ASR 频率很高(如 10ms 一次),这些临时对象的 GC 压力不小。

6. 调试方法

6.1 ASR 路径验证

 

# 开启所有 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

 

6.2 Native 层调试

 

# JNI 回调是否生效# 正常 logcat 应有:# RkAiManagerNative: nativeInit supportAsr=1# 如果 ASR 回调没到 Java,检查:# 1. AudioSystem::setRkAiCallback 是否成功# 2. BnRkAiCallback 的 onAsrBuffer 是否命中

 

6.3 常见问题排查

症状 可能原因 排查方法
服务注册失败 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 回调,理论上不应发生

7. 总结

通过本篇的详细追踪,可以看到 RkAi 子系统在两个方向上的数据流:

方向 路径 当前状态
ASR 上行 AudioFlinger → JNI → Service → App  启用
LLM 下行 App → Service → 广播监听器  已实现但编译关闭

完整的调用链及每层的职责:

Android

改进空间总结:

1.JNI 层的 int→short 逐元素拷贝可以优化

2.缺少 ASR 数据的流控/背压机制

3.RkAiManager 缺少 Binder 死亡重连

4.高频 ASR 回调下的 GC 压力需要评估

5.服务端不对callingPackage 做权限校验

审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分