Flutter BLE高频数据处理:三种方案如何选?
写在前面
做Flutter BLE开发,特别是对接心电、听诊器这类医疗设备时,一定会遇到这个问题:设备notify数据频率很高(比如8kHz),数据该怎么处理?
常见的做法无非三种:
- 直接在Dart里解析
- 透传到原生,再交给C++
- 用dart FFI直接调C++
网上讲这三种方案的文章不少,但要么只谈性能不谈开发成本,要么只讲Android不讲iOS,看完还是不知道怎么选。
这篇文章试着把三个方案摆在同一张桌子上,从性能、开发成本、后期维护三个角度逐项对比,最后给出明确的选型建议。
先看数据流
先把三个方案的数据路径画出来,后面讨论问题时心里有个底。
方案一:Dart解析(极简方案)
硬件 → 原生蓝牙栈 → EventChannel → Dart Stream → Dart解析 → UI
方案二:透传原生(透传方案)
硬件 → 原生蓝牙栈 → EventChannel → Dart
→ BasicMessageChannel/Pigeon → Kotlin/Swift → JNI/桥接 → C++
→ 结果回Dart
方案三:FFI直接调C++(FFI方案)
硬件 → 原生蓝牙栈 → EventChannel → Dart → FFI → C++ → 结果回Dart
从路径长度看,方案一最短,方案二最长,方案三居中。但路径长短不等于性能好坏,后面会细说。

四个维度逐项对比
一、性能
1. 数据拷贝次数
拷贝次数直接决定CPU占用和延迟。
| 方案 | 拷贝路径 | 总次数 |
|---|---|---|
| 方案一 | 原生 → Dart | 1次 |
| 方案二 | 原生 → Dart → 原生 → C++ | 3次 |
| 方案三 | 原生 → Dart(C++通过Pointer直接读) | 1次(零拷贝) |
方案一和方案三拷贝次数最少。方案二多了两次跨通道拷贝,高频下CPU缓存命中率会明显下降。
2. 线程切换开销
| 方案 | 线程切换 | 说明 |
|---|---|---|
| 方案一 | 无 | 解析在Dart线程同步完成,可能阻塞UI |
| 方案二 | 频繁 | 原生线程 ↔ Dart UI线程 ↔ 原生线程,8kHz下切换成本很高 |
| 方案三 | 无 | FFI调用同步执行,无线程切换 |
方案二的线程切换在8kHz下是个实实在在的性能杀手。每次notify都涉及线程切换,每秒8000次,这部分开销叠加起来不可忽视。
3. 序列化开销
| 方案 | 是否需要序列化 |
|---|---|
| 方案一 | 否,直接操作ByteData |
| 方案二 | 是,BasicMessageChannel/Pigeon需要编码解码 |
| 方案三 | 否,直接传递内存指针 |
方案二中的序列化(即使是用BinaryCodec)在8kHz频率下会产生额外CPU消耗。方案一和方案三避开了这个环节。
性能结论:方案三(FFI)性能最优,零拷贝 + 无线程切换 + 无序列化。方案一次之,方案二最差。
二、开发工作量
方案一
只需要写Dart代码,解析逻辑全部在Flutter侧完成。
- 需要处理:Dart字节解析 + 业务逻辑
- 不需要处理:原生层任何代码
工作量最小。如果解析逻辑不复杂,一个人一天就能搞定。
方案二
需要在四个地方写代码:
- Dart侧:BasicMessageChannel/Pigeon的收发逻辑
- Android侧:Kotlin/Java接收数据,写JNI胶水层
- iOS侧:Swift/ObjC接收数据,写C++桥接
- C++侧:算法逻辑
而且Android和iOS的桥接代码写法完全不同,同一份逻辑要维护两套调用代码。Pigeon能解决Dart到原生的类型安全问题,但解决不了原生到C++的桥接工作量。
工作量最大,容易出bug的地方也最多。
方案三
需要在两个地方写代码:
- Dart侧:用ffigen生成FFI绑定,或手写绑定代码
- C++侧:算法逻辑,编译为动态库(Android的.so,iOS的.dylib)
无需写任何Java/Kotlin/Swift/ObjC代码。C++库编译好之后,放在对应目录下即可。
工作量适中,比方案一多一个编译动态库的步骤,但比方案二少很多。
开发工作量结论:方案一开发最快,方案三次之,方案二最慢且最复杂。
三、扩展性与维护成本
方案一
算法逻辑写在Dart里,好处是修改方便(热重载),坏处是:
- Dart的数值计算性能有限,复杂算法(如FFT、数字滤波)跑起来吃力
- 换算法要改Dart代码,涉及Flutter发版
如果算法持续迭代,每次都要走Flutter发版流程,比较麻烦。
方案二
算法在C++里,换算法只需更新动态库。但有一个问题:Dart到原生的接口、原生到C++的桥接,Android和iOS是两套代码。新增算法接口时,两边都要改,很容易出现「Android改了、iOS忘改」的情况,行为不一致的bug很难排查。
方案三
算法也在C++里,换算法只需更新动态库。区别在于:Dart通过FFI直接调用C++的导出函数,接口由C++头文件统一定义。Android和iOS共用同一份C++库(分别编译),不存在平台桥接不一致的问题。
一套接口,一套C++代码,两端通用。
扩展性结论:方案三扩展性最强,维护成本最低。方案二的双端维护成本是隐形的大坑。
四、调试与排查难度
方案一
Dart代码调试最简单,断点、日志、热重载都方便。遇到问题排查链路短。
方案二
排查问题的链路最复杂。数据经过:Dart → Native → JNI/Swift桥接 → C++,任何一个环节出问题都要跨语言排查。JNI的崩溃往往不报具体行号,Swift桥接的ARC问题可能导致野指针,这类bug非常耗时间。
方案三
Dart → FFI → C++,链路比方案二短。但FFI调用的崩溃同样难定位(C++段错误),需要在Xcode/Android Studio中调试C++库。好在链路短,嫌疑范围小一些。
调试结论:方案一最容易调试,方案三居中,方案二最难。
一张表总结
| 对比维度 | 方案一:Dart解析 | 方案二:透传原生 | 方案三:FFI调C++ |
|---|---|---|---|
| 数据拷贝次数 | 1次 | 3次 | 1次(零拷贝) |
| 线程切换开销 | 低 | 高 | 低 |
| 序列化开销 | 无 | 有 | 无 |
| 开发工作量 | ⭐ 最小 | ⭐⭐⭐ 最大 | ⭐⭐ 中等 |
| 跨端维护成本 | 低 | 高(双端桥接) | 低(一份C++) |
| 调试难度 | 低 | 高 | 中 |
| 算法扩展性 | 低(需发版) | 中(双端接口) | 高(统一接口) |
怎么选?直接给结论
什么情况选方案一(Dart解析):
- 数据频率不高(<500次/秒)
- 解析逻辑简单(如只读几个特征值)
- 团队没有C++能力
- 项目快速验证阶段
什么情况选方案二(透传原生):
- 已经有现成的Android/iOS原生SDK,且不想动它
- 数据频率较低(<500次/秒)
- 能接受双端维护的成本
⚠️ 8kHz高频场景下,不推荐方案二。拷贝+序列化+线程切换的叠加开销会很明显,而且双端桥接的维护成本会随着项目迭代越来越重。
什么情况选方案三(FFI):
- 数据频率高(如ECG 8kHz、听诊器数据)
- 算法复杂(滤波、FFT、AI推理)
- 追求长期维护成本最低
- 团队有C++开发能力(或愿意引入)
对于心电、听诊器这类高频医疗设备,方案三是最优选。前期多花一点时间把C++库的编译和FFI绑定搭好,后续算法的迭代、扩展都会非常顺畅。
顺便说一句:无论选哪种方案,从原生侧到Dart传递notify数据时,记得用 EventChannel 推流,不要用MethodChannel轮询。前者是专为数据流设计的,后者是给RPC用的,用错了通道类型,再好的架构也救不了性能。
最后
其实这三个方案没有绝对的“好”与“坏”,关键看场景。低频指令走方案一,高频数据流走方案三,有现成原生SDK甩不掉时走方案二——把方案和场景匹配对了,就是好方案。
希望这篇文章能帮你在面对BLE高频数据时,少走一些弯路。

262

被折叠的 条评论
为什么被折叠?



