ARTICLE · 1118072
安卓 / iOS / 鸿蒙 NEXT:同一套 BLE,三套平台规则
03 手机扫不到你的 BLE 设备?九成不是固件的锅
05 BLE notify 收不到、数据还重复两遍?订阅的坑在这
有个做 App 的兄弟找我,说他的代码在安卓机上一切正常,同一台设备,换 iPhone 就扫不到。
他改了三天代码,越改越乱。
其实他一行代码都不用改——问题出在两个系统的规矩不一样:他的扫描里写了服务 UUID 过滤,而设备的广播里没带这个 UUID。安卓放过了,iOS 没放过。
这不是个例。BLE 最坑的地方在于:协议是标准的,但三个系统的实现规矩是各自的。你在安卓上积累的所有"经验",搬到 iOS 或鸿蒙 NEXT 上,可能全部作废。
一、四个维度,一次看清差别
| 权限 | ACCESS_FINE_LOCATION,不给就搜不到);12+ 改 BLUETOOTH_SCAN / CONNECT / ADVERTISE | NSBluetoothAlwaysUsageDescription | ohos.permission.ACCESS_BLUETOOTH |
| 设备地址 | UUID,不是 MAC | 虚拟 MAC | |
| MTU | requestMtu() | 没有主动设置 MTU 的 API | |
| 后台 |
二、设备地址:这是最容易翻车的一条
三家的地址规则没有一个是一样的:
• 安卓:6.0 起连本机自己的真实 MAC 都拿不到;至于扫到的设备地址稳不稳,取决于那台设备用没用随机地址——设备轮转随机地址时,你看到的 MAC 就会变; • iOS:压根不给 MAC,给的是一个 UUID,而且这个 UUID 会变; • 鸿蒙 NEXT:给的是虚拟 MAC,配对后固定,重启蓝牙或取消配对就会变。
后果是什么?你不可能靠"地址"来稳定识别一台设备。
我见过太多代码把 MAC 当主键写进数据库,结果用户重启一次蓝牙,设备就"变成另一台"了。
✅ 唯一在三端都成立的识别原则:靠设备名称 + 服务 UUID + 广播里的自定义字段(比如序列号)来认设备,别依赖地址。
鸿蒙官方也明确建议依赖名称与服务 UUID,而不是 MAC。iOS 官方同样。这不是我的偏好,是三家的共识。
三、MTU:一个能写死就能出事的数字
MTU 就是"一次能传多少字节",具体概念我放 P6 细讲,这里只说平台差异:
• 安卓:你主动调 requestMtu(517),系统去协商,Android 14 起系统甚至会自己按 517 发起,并且忽略你在同一连接上的后续请求——你想改小都改不了;• iOS:没有给你设置 MTU 的 API。系统自己协商,你只能调 maximumWriteValueLength(for:)去问当前允许写多少(它返回的是最大写入长度,不是 MTU——常见协商结果 MTU 185 时,返回约 182,且这不是固定值);• 鸿蒙 NEXT:需要主动发起协商 + 监听实际结果,没有安卓那种自动顶格的策略,流程没走完整就容易出问题。
⚠️ 这条既是写稿红线,也是写代码红线:185、517、20 这些数字一个都不许写死——它们取决于机型、系统版本、对方设备。正确做法永远是运行时动态获取当前连接允许的最大写入长度。
四、后台:别指望在三端得到一致行为
• 安卓:后台扫描和连接都有限制,不同厂商(小米、华为、OPPO)的省电策略还各不相同; • iOS:最严格,你得声明后台模式,而且系统依然不保证你能一直连着; • 鸿蒙 NEXT:策略以华为官方文档为准,写这个的时候我没查到确定结论,建议你落地前先去官网核一遍,别听二手总结。
共同结论:别把"长时间后台保持连接"当成产品承诺。真要长期在线,老老实实做"断开重连"的逻辑。
五、还有一个 iOS 专属的坑
前面 P1 提过,这里再强调一次:iOS 扫描时如果指定了服务 UUID 过滤,而设备广播里没带这个 UUID,就直接扫不到。安卓宽松得多。
这是"安卓能搜、iPhone 搜不到"的头号原因。解决方法是让固件在广播包里带上服务 UUID——这条在三个平台上都成立,改固件比改 App 划算。
进阶选读:三端差异背后的机制
为什么 iOS 的 identifier 会变?如果设备广播用的是RPA(可解析随机地址,通常 15 分钟级轮转),那它本来就是设计成"认不出同一台"的。只有配对过、手里有 IRK 的一方才解得开这层;没配对时,系统只能按"当前看到的地址"派生 UUID——地址一换,UUID 跟着换。
安卓的 MTU 也有规范依据:一次连接只允许做一次 Exchange MTU 协商。Android 14 只是把这条执行得彻底了——第一次请求直接顶到 517,后续请求按规范就该被拒。
鸿蒙的虚拟 MAC 是同一思路的另一端:系统层做地址隔离,应用看到的地址根本不是空口里那个。
还有一类差异藏得比较深:连接参数三端"能改的程度"不一样。
BLE 里连接参数名义上由主设备(手机)定,外设只能通过 L2CAP 的 connection parameter update request 去"请求"。三端对这件事的支持完全两样:
| 安卓 | requestConnectionPriority 的高 / 均衡 / 低功率——不是精确值 | onConnectionUpdated 回调(API 26+)里的实际 interval / latency / timeout |
| iOS | 没有 API | |
| 鸿蒙 NEXT |
所以"我给固件配了 15 ms,怎么安卓上像生效了、iPhone 上完全没变"——因为 iPhone 压根没打算听你的。iPhone 通常按自己的策略给一个偏保守的间隔,你只能在外设侧把 min / max / latency / timeout 四元组配成一个它能接受的宽松区间(比如 min 30 ms、max 200 ms),给它台阶下。
这也解释了 P2 那条"三端各读一遍实际生效值"为什么重要:你以为你在调一套参数,其实你在跟三个不同的系统谈判。
顺手能用的东西
排查这类"换个手机就不行"的问题,最烦的是你手边不可能备齐三个系统的机器。
搏哥开发了一个三端可用的 BLE蓝牙调试工具箱 小程序,不用安装,安卓/苹果/纯鸿蒙 都可以使用,点击下方:
小程序的好处是同一套逻辑在三端表现一致——微信把平台差异兜住了。所以现场遇到"客户说扫不到、我这边明明能扫"的时候,我会让他用小程序扫一遍:小程序能扫到,说明设备没问题,问题在客户 App 的权限或过滤规则上;小程序也扫不到,那才是设备的事。
收个尾
1. 别依赖设备地址识别设备——三端规则全不一样,用名称 + 服务 UUID + 广播自定义字段; 2. MTU 一个数字都别写死,运行时动态取; 3. 后台保持连接不是承诺,做好断开重连; 4. 连接参数也不是承诺:安卓只有三档、iOS 连接口都不给,固件要配的是"三端都愿意接受的一个区间",不是某一个精确值。
下一篇讲数据侧最容易混的一笔账:HEX 和 ASCII 到底什么关系、为什么你收到的是乱码(P5)。
这个合集我会接着往下写:数据怎么切包和解析、交付时怎么给设备做二维码、验收该测哪 10 项。
你要是正好在做 BLE 项目,可以关注下「搏哥聊技术」,后面更新了能第一时间看到。
