Appearance
车载 Native 数据引擎架构拆解
module_xdata 是车端所有信号数据的 Native 中枢:感知、导航、车控信号从 Java 进 JNI,经 SignalDispatcher 分发到十几个服务,解析后桥接 Unity 渲染,同时支持录制落盘、回放复现和跨进程分发。我把这个工程的 Core / Service / x3d / Render / Client 五层源码完整读了一遍,这篇是架构层面的拆解笔记。
信号数据源9 种分流
协议解析器84+ parser
共享内存 IPC10MB 环形缓冲
背景
一张图要处理的车端数据通路远比普通 App 复杂:行车/泊车传感器原始协议字节、导航 LD 瓦片、车控系统信号,全都要从 Android Java 层进入 C++,解析成结构化数据后供 Unity 渲染;同时要支持信号录制落盘、开发机回放调试、HUD/仪表等独立进程订阅。module_xdata 就是承载这一切的 Native 数据引擎,编译产出 xdata_core / xdata_service / libRenderExtend / libxdata_client 等一系列库。
工程整体是严格的六层单向依赖:jni → sdk → service → core,render 和 client 只依赖 core。Core 不依赖任何上层,是纯基础设施。
问题
这类高频数据引擎的难点集中在四个地方:
- 分发要无锁:信号回调线程不能持锁遍历观察者,否则一个长回调堵死整条总线。
- 内存要稳:高频协议解析如果每次都 malloc/free,抖动直接传导到渲染帧率。
- 编译要解耦:十几个服务如果互相 include 实现头,改一个文件全量重编。
- 回放不能阻塞主进程:回放文件解析耗时,不能影响主进程 Unity 的渲染帧率。
方案
整体架构一句话:所有信号汇聚到 SignalDispatcher 这个唯一中枢,按 SourceType 分流到实时分发、录制、回放三条路径,各 Service 以观察者身份订阅自己需要的信号码。
mermaid
flowchart TD
subgraph Core["Core 层 基础设施"]
SD["SignalDispatcher<br>9 种 source 分流 · COW"]
MEM["TLS 混合内存池<br>6 档预设 · 跨线程释放"]
SHM["共享内存 IPC<br>10MB 环形缓冲"]
end
subgraph SVC["Service 层 业务服务"]
X3D["x3d 协议解析<br>84+ parser 双路径"]
SIM["simulate 录制回放<br>64MB 分片 · 双线程双缓冲"]
DS["driving_strategy<br>20 信号码 · 队列消费"]
UT["unity_transport<br>Unity 桥接"]
end
subgraph OUT["Render / Client"]
RE["RenderExtend<br>LD 瓦片零拷贝解析"]
CL["libxdata_client<br>跨进程订阅"]
end
SD --> X3D
SD --> SIM
SD --> DS
SD --> UT
X3D -.回投新信号码.-> SD
SD --> SHM --> CL
UT --> RESourceType 九种数据源是分流的根基:Android / Native / Unity 实时源走 inner_dispatch 通知观察者;RECORD 源只进录制器;REPLAY 源走回放回调或改标后重新进入实时分发链——回放数据与实时数据走同一条分发链路,业务观察者对来源无感知,这是整个回放调试体系的基石。观察者注册时可携带 SourceMask 位掩码声明只收哪些来源,分发时用位运算快速过滤,例如 jni_transport 只收 NATIVE/UNITY/NATIVE_REPLAY,ANDROID_REPLAY 数据就不会误发到 Java。
跨进程回放用独立 :replay 进程解决阻塞问题:
mermaid
sequenceDiagram
participant RP as ":replay 进程"
participant RRD as "RemoteReplayDispatcher<br>Socket Server"
participant RCV as "主进程 ReplayDataReceiver<br>Socket Client"
participant SD as "主进程 SignalDispatcher"
RP->>RP: Replayer 双线程解析播放
RP->>RRD: setRemoteReplaySender<br>信号不过本地 dispatcher
RRD-->>RCV: Socket 跨进程发送
RCV->>SD: dispatch source=ANDROID_REPLAY
SD->>SD: inner_dispatch 分发各 Service文件解析和时间戳对齐播放都在 :replay 进程完成,主进程只接收已对齐的信号,回放再慢也不影响渲染帧率。:replay 进程会做与主进程相同的全量 SDK 初始化,但 x3d/navi/driving_strategy 等服务实例收不到任何信号,实际只有 Replayer 在工作。
关键实现
COW 无锁分发。 SignalDispatcher 的观察者列表是 shared_ptr<const vector<ObserverEntry>>:注册时用写锁深拷贝一份新 vector 再替换指针,正在遍历的读线程持有的旧快照不受影响;分发时用读锁取快照后立即放锁,遍历回调全程无锁。注册即补发——新观察者注册后查最新值缓存,立即补回调一次,不会错过上一个状态。每个回调包 try-catch 做异常隔离,Native 实时源分发超 40ms 打告警。录制/回放回调用原子自由函数读写 shared_ptr(Android NDK libc++ 拒绝非平凡类型的 atomic shared_ptr),支持热替换。
TLS 混合内存池。 分配走 thread_local 线程本地池,接近无锁;池只服务对齐 16 字节以内的常规请求,超出直接 new。释放的精妙在 pool_keeper 强引用:deleter 持有池的 shared_ptr,释放时判断当前线程是否 owner——是则归还 free list,否则推入 remote stack 等 owner drain,跨线程释放不需要全局查表;强引用还保证 owner 线程退出后池不被析构,直到最后一块内存归还。六档预设(SMALL 1MB 初始给低频回调线程,XXLARGE 20MB 初始给渲染 Tile 解析)按线程名前缀批量配置,优先级:TLS 直设 > 线程名前缀 > 默认。payload 前的 PoolAllocHeader 带 magic 0x5844504D('XDPM')校验,内存持有超 5000ms 打日志防泄漏。x3d 的 IX3DData 直接把 FlatBufferBuilder 的分配器接到内存池上,高频协议解析零 malloc 抖动。
节流日志与并发设施。 LFLogger 解决高频通路的日志洪泛:TLS 每线程 64 槽缓存 key→logger 指针(xxHash32 算哈希,查找无锁),canLog() 用 compare_exchange_weak 更新 lastLogTime,多线程同时触发只有一个能打,还记录跳过次数和最大间隔。dispatch 按 30s、record 按 60s 节流。配套的还有:XThreadPool 五种拒绝策略(默认 DiscardOldest)+ Per-worker 缓存行对齐统计(稀疏检查,每 32 次任务查一次时间);LockedQueue 只在队列从空变非空时 notify_one。
Service 层 Factory 化。 Phase 4.5 重构后,每个服务接口头里嵌入 createAndInit() 工厂声明,manager 通过 addServiceHelper(type_index, createAndInitXxx(...)) 一行完成构造+注册,0 反向 include——manager 不感知任何实现类,子模块可独立编译。init() 按依赖顺序注册 15 个服务:IThreadService 最先(其他服务依赖线程池),X3DService、DrivingStrategy 靠后;构造函数 bootstrap 阶段提前注册 UnityReceiveService,让 Unity 在 SDK 全量 init 前就能 QueryFromNative。所有服务遵循统一 release 五步法:CAS 防重入 → 注销观察者 → 停标志+唤醒 CV → 等线程退出 → 清资源,注销观察者优先于资源释放,杜绝悬空回调。
x3d 双路径与多供应商。 x3d 是最大子模块,facade(DataServicePlugin,Phase 3.2 从 627 行单文件拆成 ~300 行 facade + 3 个辅助类)注册两组观察者:协议路径走 ProtocolManager 查 parserMap 解析原始字节,经 Proxy 处理后以新信号码回投 SignalDispatcher(needCache=false 防缓存膨胀),是转换链路;模块路径走 ModuleManager 的 IProtocolChannel 入队、线程池异步更新 IDataModel 供查询,是状态链路。多供应商适配在 XP_XPU_Module::onInit:按车型特性 DRIVING_PROTOCOL_TYPE 动态选 XP/DIDI/BOSCH 行车 parser、PARKING_PROTOCOL_TYPE 选 XP/DIDI/HK 泊车 parser,三供应商 parser 结构对称、同名各一份,供应商切换业务无感。84+ parser 里小鹏根目录 14 个、博世 9 个、滴滴 9 个。Unity 桥接侧用 X3DTripleBuffer 三缓冲无锁读写:write/ready/read 三索引,readyIndex.exchange(acq_rel) 交换,单生产单消费零等待。
录制回放与共享内存。 录制器 noticeAnySignal 入队、独立线程 drainRecordQueue 编码写盘,默认二进制格式,64MB 分片(MAX_RECORD_FILE_SIZE)写满自动切新文件。回放器双线程双缓冲:parseFileThread 读文件解码写 Buffer[writeIndex],playThread 按录制时间戳对齐播放、dispatch 回投递,mBuffers[2] 取模轮转 + 条件变量互通知;不同回放源还选专用线程池互不阻塞。跨进程走 SharedMemoryHeader 环形缓冲:10MB 默认容量、进程间 pthread 互斥锁+条件变量、DataBlockHeader 紧凑排列自带 signal_code/format/source/timestamp,接收方无需协议协商;Unix Socket 用抽象命名空间(\0GuideEngine1222,回放 \0GuideEngineReplay),连接状态机七态。Client 侧回调路由用「锁内查找 + 锁外调用」防死锁,ensureRouterRegistered 用 CAS 保证路由 lambda 只注册一次。
指标
| 维度 | 关键数字 |
|---|---|
| 信号分发 | 9 种 SourceType 分流;COW 快照遍历无锁;Native 分发超 40ms 告警 |
| 内存池 | 6 档预设(SMALL 1MB → XXLARGE 20MB 初始);跨线程释放免查表 |
| Service 层 | 15 个服务 Factory 注册,manager 0 反向 include |
| x3d | 84+ parser;40+ 模块信号码;facade 627 行 → ~300 行 |
| 录制回放 | 64MB 分片写盘;双线程双缓冲;回放复用实时分发链 |
| 跨进程 | 10MB 环形缓冲;:replay 进程 Socket 解耦 |
复盘
- 热路径无锁不是炫技,是分层换来的。 COW 能成立的前提是「读远多于写」——观察者注册是低频操作,分发是高频操作,这个不对称性看准了,才敢用拷贝换无锁。
- 「回放即实时」是最值钱的一个决策。 REPLAY 源改个标就重新进入实时分发链,业务层零感知,录制回放体系不需要任何并行实现。对称可插拔省掉的是整套回放专用逻辑。
- 跨线程释放的解法是转移信息而不是查表。 pool_keeper 把「这块内存属于哪个池」这个信息挂进 deleter 随身带,省掉了全局注册表和它的锁——对象自己知道自己的来路。
- 编译解耦和运行解耦要分开做。 观察者模式解决运行时耦合,Factory + 接口头解决编译期耦合,0 反向 include 之后子模块才能真正独立演进。
- 这套引擎对上是 Native-Unity 信号链路 的 C++ 底座,对调试侧支撑了回放工具 75 倍提速 的工作流——读源码时把这三篇对照着看,链路就完整了。