ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter鸿蒙适配实战:为蓝牙插件补全OpenHarmony原生实现

Flutter鸿蒙适配实战:为蓝牙插件补全OpenHarmony原生实现 做鸿蒙适配这一年我最深的感触是UI 和业务逻辑迁移其实没那么难真正卡住进度的往往是一个不起眼的原生插件。项目里用的是 Flutter 的蓝牙插件 flutter_blue_plus平时在 Android/iOS 上跑得好好的一搬到 OpenHarmony 上就哑火了——扫描没结果、连接没回调、服务发现直接超时。查了一圈发现插件压根没有鸿蒙端的原生实现社区里也找不到现成的轮子。没办法只能自己动手在 OpenHarmony 侧补一套原生逻辑把 flutter_blue_plus 的 Dart 调用接住。最后做出来的效果是上层业务代码一刀没改蓝牙扫描、连接、服务发现、特征读写、通知订阅全部跑通。这篇就是我整个适配过程的完整复盘包括底层原理、代码实现、踩坑记录希望能给同样在搞 Flutter 鸿蒙化的人省点时间。1. 为什么 Flutter 上鸿蒙蓝牙这件小事成了大麻烦1.1 项目背景Flutter 应用要跑在 OpenHarmony 上先说下背景。我手上的项目是一个设备控制类 App核心功能是通过蓝牙 BLE 连接硬件设备做参数配置和数据采集。App 本身是用 Flutter 写的主要跑 Android后来要适配鸿蒙生态目标系统是 OpenHarmony。刚开始我以为工作量不大。Flutter 的 OpenHarmony 适配社区已经做了一段时间基础 UI、网络、本地存储这些场景基本能跑。但等我把功能清单过一遍发现蓝牙这块完全没有着落。项目里的蓝牙能力全部依赖 flutter_blue_plus 这个插件。这个插件是 Flutter 社区目前最常用的 BLE 库底层在 Android 用 Kotlin 封装了系统蓝牙 API在 iOS 用 Swift 封装了 CoreBluetooth逻辑成熟、接口稳定。但它的官方实现里没有 OpenHarmony 这一端也就是说你在鸿蒙设备上跑这个插件会在原生层直接报“找不到实现”。当时的可选方案有三个等官方支持、换别的插件、自己适配。前两个都不可行——官方路线遥遥无期换插件意味着上层所有蓝牙业务代码要重写代价更大。唯一可行的路就是在 OpenHarmony 侧补一套原生实现让 flutter_blue_plus 的 Dart 层调用能找到“接盘侠”。1.2 插件生态是 Flutter 鸿蒙化的最大短板这个项目做完之后我更加确认了一个判断Flutter 迁移鸿蒙真正的难点从来不是 Flutter 框架本身而是插件生态。Flutter 框架的鸿蒙适配属于“社区推动型”已经有人在做核心渲染、Dart 运行时、PlatformView 这些基础能力陆续在补齐。但第三方插件就参差不齐了。像 flutter_blue_plus 这种涉及系统级能力的插件官方不会主动支持 OpenHarmony社区也没几个人去适配最后只能项目方自己干。而且蓝牙不是个“能跑就行”的功能。它牵扯系统权限、GATT 协议、多状态回调、线程切换任何一个环节断了表现就是扫描不到设备、连接不上、数据读不出来。这种功能一旦适配不彻底后面调试的成本比重新写一遍还高。所以我在动手之前就定了一个原则不能简单“能用”要尽量做到上层 Dart 代码零改动把 flutter_blue_plus 现有的调用语义完整地映射到鸿蒙侧。这样后续插件升级、业务扩展我们不需要再回头改适配层。1.3 适配方案怎么选fork、wrapper、还是补全端实现动手之前还有一个路线选择的问题。基于 flutter_blue_plus 做鸿蒙适配业内常见的做法无非三种。第一种是直接 fork 插件在源码里加一个 OpenHarmony 平台判断然后跳转到自己写的原生实现。好处是简单直接坏处是以后想同步插件上游更新会非常痛苦每次都得手动合并。第二种是封装一层“假插件”不真正实现蓝牙逻辑而是通过 MethodChannel 转发给另一个自定义插件处理。这种方案的好处是不动上游代码坏处是多了一层跳转调用链路变长调试起来会绕。第三种是采用 Flutter 官方的 federated plugin 机制把 flutter_blue_plus 改造成一个多端架构的插件为 OpenHarmony 单独注册一个平台实现包。这种做法是最规范的和 flutter_blue_plus 本身的架构也契合但它要求你先把插件源码内部结构吃透改造量最大。我最终选择了第三种思路的简化版不把工程改造成完整的 federated plugin而是把 flutter_blue_plus 的代码拉下来在它的 Android 实现旁边增加一个 OpenHarmony 平台的入口复用它的 MethodChannel 协议自己写鸿蒙端原生逻辑。这样上层业务代码不用动插件升级时只要重点看通道协议有没有变化就好。2. 先拆插件flutter_blue_plus 的通道路由设计2.1 Flutter 与原生通信的三个通道要适配 flutter_blue_plus先得搞明白 Flutter 和原生之间到底怎么通信。Flutter 定义了一套 Platform Channel 机制开发者常用的主要是三种通道。MethodChannel 是“你问我答”式的方法调用Flutter 侧发起一个方法名和参数原生侧处理后返回结果适合扫描、连接、读写这种一次性操作。EventChannel 是“你监听我推送”式的事件流原生侧主动往 Flutter 侧推送数据适合蓝牙状态变化、特征值通知这类持续回调。BasicMessageChannel 是双向消息传递用的场景相对少。flutter_blue_plus 的通信设计就是 MethodChannel 和 EventChannel 的组合拳Dart 层调用像 startScan、connect、writeCharacteristic 这样的方法全部走 MethodChannel而设备发现、连接状态变化、特征值通知这些事件则通过 EventChannel 或者 MethodChannel 的反向 invokeMethod 推给 Dart 层。这意味着如果鸿蒙侧想“冒充”Android关键不在于 UI 怎么写而在于你能不能精确复现这套通道协议让 Dart 层感知不到对面换了操作系统。2.2 flutter_blue_plus 能力清单与通道方法映射适配工程里最琐碎但也最重要的一步是把 flutter_blue_plus 全部的能力列出来逐一确认鸿蒙侧用什么 API 对应。我从源码里梳理出一份核心能力清单大致包括这些蓝牙状态获取与监听、设备扫描与停止扫描、连接与断开连接、服务发现、特征值读写、特征值通知开关与监听、MTU 设置以及一些设备基本信息获取。以扫描为例Dart 层调用 startScan会拼一个 Map 作为参数通过 MethodChannel 发到原生侧里面包括扫描的服务 UUID 过滤条件、超时时间、是否允许重复上报。原生侧扫到设备后需要把设备 ID、设备名、RSSI、广播数据、服务 UUID 列表等信息包装成固定结构往回传。这里要特别提醒一下flutter_blue_plus 的通道方法是动态拼接的也就是说方法名不是简单写死在 switch 里的而是带设备 ID、服务 UUID 这类参数组成一个唯一的 channel。最典型的例子是读取特征值这个方法传入的参数是特征值实例 ID你不能靠已知的设备地址去定位操作对象必须维护好“Dart 侧实例 ID 到鸿蒙侧 GATT 对象”的映射关系。这个映射关系很容易被忽略但恰恰是适配最容易出错的地方。2.3 为什么“序列化格式”是适配成败的关键很多人在适配时有个误区觉得只要把方法名对上能返回数据就行。但实际上flutter_blue_plus 的 Dart 层对原生侧返回的数据结构有严格的解析逻辑字段名错了、类型不对都会导致序列化失败或者运行时异常。举几个例子。设备扫描结果里deviceId 在 Android 端返回的是 MAC 地址字符串鸿蒙端返回的也必须是字符串而且最好保持同一种格式否则 Dart 层拿来当 map key 会出现奇奇怪怪的问题。蓝牙值数据的传输用的是字节数组Flutter 侧的 typed_data 在 MethodChannel 里会被序列化成标准类型鸿蒙侧必须做 Uint8Array 和 ArrayBuffer 之间的转换不能直接当普通数组处理。还有枚举状态值连接状态这个字段 Android 返回 0/1/2鸿蒙侧就得把系统状态码转换成 Dart 层认识的语义一字不差。这一步没有捷径只能老老实实对照 Android 端的实现代码把每个字段的类型和含义确认好。我建议在正式开始写鸿蒙代码之前先把 Android 端 FlutterBluePlusPlugin 里的 methodCallHandler 完整读一遍在纸上把方法名、参数 key、返回值结构画成一张表这张表就是后续所有工作的设计文档。3. OpenHarmony 蓝牙 API 摸底与能力对齐3.1 鸿蒙侧蓝牙模块bluetoothManager 能干什么OpenHarmony 系统本身提供了蓝牙能力API 封装在 bluetoothManager 这个模块里。整体能力上和 Android 原生蓝牙 API 高度相似毕竟 BLE 协议栈的底层逻辑是相通的。扫描能力方面bluetoothManager 支持 startScan 和 stopScan同时可以监听设备发现事件。连接能力方面它通过 createGattClientDevice 创建一个 GATT 客户端然后调用 connect 方法发起连接并监听连接状态变化。连接成功之后可以调用 getServices 获取服务列表然后进一步读取特征值、写入特征值、开启通知。有一点和 Android 不太一样。Android 的 BLE 扫描有比较灵活的 ScanFilter 和 ScanSettings可以按厂商数据、服务 UUID、信号强度做过滤OpenHarmony 的扫描 API 相对简洁全局扫描为主服务 UUID 过滤能力也有但参数没有 Android 那么细需要自己在回调里做二次过滤。另外从 API 版本演进来看老版本用的是ohos.bluetooth这种导入方式新版本推荐统一从kit.BluetoothKit里拿不同系统版本之间会有差异。开发前最好先确认目标设备的系统 API 版本避免出现模块找不到的问题。3.2 能力比对表Android/iOS/OpenHarmony 蓝牙能力差异我把 flutter_blue_plus 用到的核心能力在三个平台上的实现方式做了一张比对表这样列出来比较直观。能力项Android 实现思路iOS 实现思路OpenHarmony 实现思路权限声明蓝牙扫描/连接权限 定位权限Info.plist 描述文案module.json5 声明蓝牙权限设备扫描BluetoothLeScanner ScanCallbackCBCentralManager scanForPeripheralsbluetoothManager.startScan 设备发现监听连接管理BluetoothGatt.connect 回调CBCentralManager connectcreateGattClientDevice connect服务发现BluetoothGatt.discoverServicesdiscoverServices 回调gattClient.getServices特征读写writeCharacteristic/readCharacteristicwriteValue/readValueForCharacteristicgattClient.writeCharacteristicValue / readCharacteristicValue通知订阅setCharacteristicNotification 描述符写入setNotifyValuegattClient.setCharacteristicChangeNotification状态监听BroadcastReceiver 系统广播centralManagerDidUpdateStatebluetoothManager.on(stateChange)从表里能看出来OpenHarmony 的蓝牙能力覆盖得还是比较全的关键路径上一个没缺。这对适配工作是很大的利好说明不是“没得做”而是“怎么对齐”的问题。3.3 适配范围与取舍先跑通哪些后补哪些能力全覆盖是一回事实际项目要不要全部实现是另一回事。第一次做适配时我的建议是分阶段来不要一上来就追求 100% 功能一致。第一阶段只做最核心的链路初始化、扫描、连接、获取服务、读写特征值、通知开关与监听。这一套跑通App 的主业务流程基本就能用了。第二阶段再补齐增强能力MTU 协商、多连接管理、广播数据解析、重连机制、后台权限兼容等。这些功能虽然重要但不影响第一版 Demo 的验证没必要在第一周就全扑上去。我实际项目中第一阶段花了两周第二阶段陆续又花了一个多月边用边补。有些边角功能比如厂商私有扩展指令其实是后面硬件那边提出新需求才加的前期做了大概率也是白做。不要试图一次做完。适配的本质是“协议对齐”而协议本身是活的你只有先把主干跑起来才能在真机联调中发现哪些字段被漏了、哪些语义理解错了。4. 核心实操手写一个 flutter_blue_plus 的 OpenHarmony 原生实现4.1 工程准备Flutter、DevEco Studio、SDK 版本开始写代码之前先把环境搭好。适配 flutter_blue_plus 到 OpenHarmony本质上要做的是一个 Flutter 插件工程里的 OpenHarmony 端原生模块所以环境要具备两个能力能编译 Flutter能编译 OpenHarmony 应用。开发机上建议安装 Flutter SDK 和 DevEco StudioOpenHarmony 的 SDK 通过 DevEco Studio 的 SDK Manager 单独安装。另外需要注意Flutter 跑 OpenHarmony 需要用带鸿蒙适配的 Flutter SDK 分支不是官方主线社区有几个维护中的 fork选一个活跃度高的就好。这里还有个小坑。DevEco Studio 的版本和 OpenHarmony SDK 版本是有对应关系的版本不匹配会导致工程创建失败或者编译报错。建议直接用 DevEco Studio 默认配套的 SDK 版本不要手贱去升级到最新的 SDK因为 Flutter 鸿蒙适配分支不一定跟得上系统 API 的变化。工程结构上我是在 Flutter 插件工程里执行了flutter create --templateplugin --platformsohos这种思路先让工程具备 OpenHarmony 侧的平台目录然后把 flutter_blue_plus 的 Dart 源码作为普通依赖引进来调试。插件工程的 ohos 目录下通常包含一个 Index.ets 和对应的原生模块等等要做的核心工作都在这个模块里。4.2 第一步权限声明这是大多数人踩的第一个坑鸿蒙应用访问蓝牙必须在 module.json5 里声明权限这点和 Android 的 AndroidManifest 声明类似。如果漏了权限代码本身不会报错但系统会在 API 层静默拒绝表现为扫描不到任何设备。我用的权限声明配置如下{ module: { requestPermissions: [ { name: ohos.permission.USE_BLUETOOTH, reason: $string:app_name, usedScene: { abilities: [EntryAbility] } }, { name: ohos.permission.DISCOVER_BLUETOOTH, reason: $string:app_name, usedScene: { abilities: [EntryAbility] } }, { name: ohos.permission.ACCESS_BLUETOOTH, reason: $string:app_name, usedScene: { abilities: [EntryAbility] } } ] } }这里需要解释下三个权限的区别USE_BLUETOOTH 是使用蓝牙的基础权限DISCOVER_BLUETOOTH 是扫描发现设备需要的权限ACCESS_BLUETOOTH 则是进行蓝牙通信时需要的权限。简单理解就是发现用第二个通信用第三个基础开关用第一个场景不同缺一个都可能出问题。权限声明还有一个关联问题如果目标应用是首装后动态弹权限框还需要在代码里处理权限申请逻辑。比如在扫描前调用 requestPermissionsFromUser系统会弹出授权框用户同意后再执行 startScan。这个逻辑在真机上调试时非常关键我第一次就是权限弹窗没处理导致开发板一直扫不到设备。4.3 第二步写 MethodChannel 入口与路由权限搞定了接下来就是核心代码。我先在鸿蒙端建一个类专门负责和 Flutter 侧的 MethodChannel 通信。整体的通信架构可以这样设计Flutter 侧创建了一个 MethodChannelchannel name 是 flutter_blue_plus 约定的那个鸿蒙侧在初始化时给这个 channel 绑定 MethodCallHandlerFlutter 侧每次调用方法都会走到这里。核心的入口代码如下import { MethodChannel } from ohos/hypium; // 实际按工程里引入 export class FlutterBluePlusOhos { private channel: MethodChannel; private gattDevices: Mapstring, bluetoothManager.GattClientDevice new Map(); private connectedStateMap: Mapstring, number new Map(); constructor(channel: MethodChannel) { this.channel channel; channel.setMethodCallHandler((call) { return this.handleMethodCall(call); }); } private async handleMethodCall(call): Promiseany { const method call.method; const args call.arguments; switch (true) { case method startScan: return this.startScan(args); case method stopScan: return this.stopScan(); case method connect: return this.connect(args); case method disconnect: return this.disconnect(args); case method getServices: return this.getServices(args); case method readCharacteristic: return this.readCharacteristic(args); case method writeCharacteristic: return this.writeCharacteristic(args); case method setNotify: return this.setNotify(args); case method getPlatformState: return this.getPlatformState(); default: return Promise.reject({ code: UNIMPLEMENTED, message: method ${method} not implemented }); } } }这段代码的思路很简单维护一个方法名到处理函数的映射每来一个调用把它分发到对应的处理逻辑里。但这里有个非常关键的细节容易在第一步就被忽略flutter_blue_plus 的方法名是用“请求 ID 操作名”动态生成的不是一个完全固定的字符串。比如某个特征的读取方法名可能是read_characteristic#1234这种带后缀的格式。如果不做兼容光在源码里搜方法名是搜不到的必须看它 Dart 层是怎么拼出这个字符串的。我的做法是在 Dart 层临时打日志把所有到达原生侧的方法名和参数完整打印出来然后根据实际请求去对齐。这个方法虽然土但最有效。4.4 第三步扫描与设备订阅扫描功能是最先要跑通的也是很多问题的集中爆发点。鸿蒙侧扫描的第一步是调用系统蓝牙能力在扫描过程中要监听设备发现事件把发现的结果整理成 flutter_blue_plus 需要的结构通过 MethodChannel 反向推送回 Flutter 层。来看这段代码private startScan(args: any): Promisevoid { return new Promise((resolve, reject) { try { if (this.isScanning) { resolve(); return; } const serviceUuids args.serviceUuids || []; const onDeviceFind (device: bluetoothManager.BLEDevice) { // 过滤如果需要按服务 UUID 过滤在这里判断 if (serviceUuids.length 0 !this.deviceHasService(device, serviceUuids)) { return; } const scanResult { deviceId: device.deviceId, name: device.deviceName || , rssi: device.rssi || 0, manufacturerData: this.parseManufacturerData(device), serviceUuids: device.serviceUuids || [], rawAdvertisementData: [] }; // 回调给 Flutter 层 this.channel.invokeMethod(ScanResult, scanResult); }; bluetoothManager.on(BLUETOOTH_DEVICE_FIND, onDeviceFind); bluetoothManager.startScan(); this.isScanning true; this.scanCallback onDeviceFind; resolve(); } catch (e) { reject({ code: ScanFailed, message: e.message }); } }); } private stopScan(): Promisevoid { bluetoothManager.stopScan(); if (this.scanCallback) { bluetoothManager.off(BLUETOOTH_DEVICE_FIND, this.scanCallback); } this.isScanning false; return Promise.resolve(); }这里有几个细节要注意。deviceId 的稳定性问题。鸿蒙扫描返回的 deviceId 在部分系统版本上可能是随机地址或者动态变化的如果你用 deviceId 做缓存 key可能会出现设备列表越扫越多的诡异现象。建议把它和 deviceName 一起处理至少在日志里能看到每次扫描结果的变化轨迹。广播数据的解析。flutter_blue_plus 的 Dart 层对 manufacturerData 有特定解析方式如果你希望上层业务的厂商识别逻辑继续生效鸿蒙侧必须把广播数据按 BLE 广播包的格式解析出来否则这个字段永远是空的。这块可以从系统 API 的 scanResult 里拿到原始广播数据然后自己解析。扫描是高频回调场景。不建议每收到一个设备事件就无脑往 Flutter 侧推可以先在原生侧用 Map 做去重同一设备只推一次或者只在信号强度明显变化时更新减少 Dart 层的渲染压力。4.5 第四步连接、服务发现、特征读写与通知扫描通了之后连接链路是下一个大头。鸿蒙侧的 GATT 连接逻辑通过 createGattClientDevice 创建设备实例然后调用 connect。这里需要建立一个设备映射表关键点在于Dart 层的 deviceId 和鸿蒙侧的 GattClientDevice 实例必须一一对应。我用了一个 Map 来维护这个关系。核心实现如下private connect(args: any): Promisevoid { const deviceId args.deviceId; const gattClient bluetoothManager.createGattClientDevice(deviceId); // 监听连接状态变化 gattClient.on(BLEConnectionStateChange, (state) { const isConnected state.state 2; // 2 CONNECTED this.connectedStateMap.set(deviceId, state.state); // 回传连接状态给 Flutter 层 this.channel.invokeMethod(ConnectionStateChanged, { deviceId: deviceId, connected: isConnected, state: state.state }); }); gattClient.connect(); this.gattDevices.set(deviceId, gattClient); return Promise.resolve(); }连接状态监听这里最需要注意的是状态码的语义。不同平台的连接状态码不一定一致Android 的 STATE_CONNECTED 是 2OpenHarmony 这边如果返回的枚举不一样需要在适配层做一次转换绝对不能直接透传给 Dart 层否则上层基于状态码做的判断会全部失效。服务发现和特征值获取代码如下private getServices(args: any): Promiseany { const deviceId args.deviceId; const gattClient this.gattDevices.get(deviceId); if (!gattClient) { return Promise.reject({ code: DeviceNotConnected, message: device not found }); } return gattClient.getServices().then((services) { const result services.map((service) ({ uuid: service.uuid, characteristics: service.characteristics.map((char) ({ uuid: char.uuid, properties: char.properties, descriptors: (char.descriptors || []).map((desc) ({ uuid: desc.uuid })) })) })); return result; }); }读特征值的时候鸿蒙 API 里 readCharacteristicValue 返回的通常是 ArrayBuffer需要转成标准数字数组再回传。写特征值时则反过来要把 Dart 层传来的数组转成 ArrayBuffer。private writeCharacteristic(args: any): Promisevoid { const { deviceId, serviceUuid, characteristicUuid, value, type } args; const gattClient this.gattDevices.get(deviceId); const arrayBuffer new ArrayBuffer(value.length); const view new Uint8Array(arrayBuffer); value.forEach((byte, index) { view[index] byte; }); return gattClient.writeCharacteristicValue( serviceUuid, characteristicUuid, arrayBuffer, WRITE_DEFAULT ).then(() { // 部分设备需要等待写入完成的回调 return Promise.resolve(); }); }特征值通知的订阅是 IoT 场景下用的最多的一个功能。设备有数据变化时主动通过 GATT 通知推给手机App 不需要主动轮询。鸿蒙侧的做法是给 GattClientDevice 注册 BLECharacteristicChange 监听同时开启指定特征的通知开关。private setNotify(args: any): Promisevoid { const { deviceId, serviceUuid, characteristicUuid, enable } args; const gattClient this.gattDevices.get(deviceId); // 注册数据变化监听 gattClient.on(BLECharacteristicChange, (charChange) { if (charChange.characteristicUuid ! characteristicUuid) { return; } const value Array.from(new Uint8Array(charChange.value)); this.channel.invokeMethod(CharacteristicChanged, { deviceId: deviceId, characteristicUuid: characteristicUuid, value: value }); }); return gattClient.setCharacteristicChangeNotification( serviceUuid, characteristicUuid, enable ); }这里要特别注意通知开关在某些低功耗设备上有依赖顺序问题。严格来说先开启服务端特征值通知再写客户端特征描述符CCCD顺序反了可能收不到任何通知。flutter_blue_plus 在 Android 端是自动处理这个顺序的鸿蒙端的 setCharacteristicChangeNotification 如果你发现某些设备收不到通知可以检查一下是不是 CCCD 没有成功写入。4.6 让上层 Dart 代码“零改动”的收尾配置核心逻辑实现完之后还有一个收尾工程确保 Flutter 工程在构建时能正确引用到我们写的鸿蒙端实现而不是在找不到原生实现时报错。flutter_blue_plus 的依赖可以分为 Dart 层和原生层。Dart 层是纯逻辑可以直接复用原生层则需要替换成我们写的 OpenHarmony 实现。最简单的做法是把适配后的插件工程放到本地目录然后在 pubspec.yaml 里通过 path 依赖指向本地插件。dependencies: flutter_blue_plus: path: ./packages/flutter_blue_plus_ohos这样做的好处是编译的时候 Flutter 会自动把鸿蒙端模块的源码打包进去不需要额外配置。坏处是切回老版本的 flutter_blue_plus 时要改依赖路径稍微麻烦一点但换来的是上层代码零改动这个取舍完全值得。如果团队后续有多个 App 都要用这套适配建议做一次正式封装将 OpenHarmony 实现单独抽成一个插件包按 flutter_blue_plus 的 platform interface 规范注册。这个改造更规范但要花时间理解 flutter_blue_plus 的 inner lib 结构和接口定义属于二期工程。5. 常见问题排查与避坑记录5.1 扫描不到设备的经典原因在我适配和后续联调的过程中“扫描不到设备”是出现频率最高的问题。这里我整理了四个最可能的原因。权限没声明或没授权这是最常见的原因。你需要在 module.json5 里声明三个蓝牙权限同时还要确保应用运行时拿到了用户授权。尤其是第一次安装后的授权弹窗如果测试时点掉了没同意后续扫描永远会失败。建议每个新设备第一次调试时先手动进入系统设置确认应用权限状态。权限有了但扫描时机太早。系统蓝牙服务还没就绪的时候立刻调用 startScan部分鸿蒙版本会静默失败不抛异常也不产生回调。可以先用 API 查询蓝牙开关状态或者做一次重试机制。过滤条件写太死。如果传了 serviceUuids 过滤条件而设备广播里没有包含完全一致的 UUID往往会漏掉设备。前期验证时建议先不过滤扫到之后再逐步收紧。系统的安全限制。部分鸿蒙设备上如果目标手机与 App 之间没有完成某种配对关系应用是扫不到某些“受限广播”设备的。这个问题在国产设备上比较特殊遇到时可以从系统设置里的“允许被发现/可被连接”选项排查。5.2 连接状态一直对不上连接功能最常见的现象是Flutter 侧显示连接失败但设备实际上已经连上了或者反过来界面显示已连接但设备根本没在线。这种问题的根源基本都出在状态码语义不一致。flutter_blue_plus 的 Dart 层会根据自己的枚举值判断当前连接状态而鸿蒙系统返回的状态码如果不做转换就会造成误判。解决办法是在鸿蒙侧用一张映射表把系统状态码转成 flutter_blue_plus 约定好的语义码不要图省事直接透传。还有一个经验是connect 调用之后不要立即设置超时。BLE 连接过程涉及链路层连接、GATT 服务发现等多个阶段整体可能耗时几百毫秒到几秒不等。第一次连接还会触发系统的配对流程如果用户没及时点同意连接时间会拖得更长。超时时间给到 10 秒以上比较稳妥。5.3 特征值读写返回 0 或失败特征值读写失败往往不是适配层的问题而是对 GATT 协议栈的理解问题。一个很典型的场景是读取数据返回结果是 0 或者 null。大概率是这个特征值本身不支持 Read 操作只支持 Notify 或者 Write。flutter_blue_plus 在描述特征值时会带 properties 字段你可以先读一下这个字段确认该特征值支持哪些操作再做对应的读写调用。写入失败还有一种可能就是写入的数据长度超过了 MTU。默认 MTU 是 23 字节扣掉 3 字节的协议头用户数据实际只有 20 字节。如果App 一次性写入超过 20 字节的数据有些设备会直接返回失败有些设备会截断。解决方案是先做 MTU 协商或者业务层自己分包。分包逻辑一定要在 Flutter 侧做原生侧只负责透传不然每包之间的时序很难控制。5.4 通知收不到、线程卡顿通知收不到八成是 CCCD 描述符的问题。很多低功耗设备需要同时写入 0x0001 到 0x2902 这个客户端特征描述符才能开启通知。鸿蒙侧的 setCharacteristicChangeNotification 在不同的系统版本上行为不一致有的版本会自动处理有的版本不会。如果你发现只有部分设备能收到通知强烈建议手动检查 CCCD 描述符的写入状态。线程问题则出在原生回调的线程模型上。鸿蒙侧的回调事件有些不在主线程直接往 MethodChannel 里 invokeMethod 可能偶发时序问题。我的处理方式是把真正要回传的数据先推到主线程再统一走通道发送。否则偶尔会碰到回调先到数据还没准备好导致 Dart 层拿到的字段不完整。5.5 排查工具推荐最后分享几个我在调试时用的工具。第一是 hdc shell 的蓝牙日志。OpenHarmony 的系统日志里会有大量 Bluetooth 相关输出出现诡异问题时第一反应应该是去拉 hdc 日志看协议栈日志而不是盯着 Flutter 层打断点。第二是 Flutter 层的 MethodChannel 日志。我给鸿蒙侧的门禁函数加了统一的日志打印每个方法的请求参数和返回结果都会落日志。这个习惯帮我省了很多事尤其是联调第三方设备时对方一问“你发的是什么指令”我能直接翻日志回答。第三是一个小技巧在写适配代码时先在 Channel 入口打一行“收到方法 X参数 Y”的日志跑通之后再一条条打开具体业务日志。这样既能控制日志量又能快速定位到是调用没到原生侧还是结果没回来。综合这次适配经验我的体会有三条。第一适配工作开始前花一天时间把插件源码读透比动手后盲目试错省力得多尤其是 MethodChannel 的方法路由和序列化规则这是整个适配的地基。第二一定要用真机验证模拟器上扫描、连接的行为和真机完全不同很多坑只有真机才能暴露出来。第三优先保证主链路的完整闭环再逐步补充边缘能力不要试图一次性做全。如果你也在做 Flutter 鸿蒙化希望这份记录能帮你少走几个弯路。
返回列表