
简介BleSolution.zip 是一份面向 C# 开发者的 BLE 4.0 低功耗蓝牙通信解决方案适合在物联网场景中需要与健康监测器、智能手表、智能家居等设备交互的 Windows 桌面或移动应用开发者尤其适合具备一定 C# 基础、希望快速上手 BLE 开发的中高级程序员。资源包含 46 个文件压缩包仅 1.87MB以 14 个 C# 源码文件为核心涵盖 BleCore、BluetoothLECode、BleEnum 等模块另有 4 个可执行程序、4 个配置文件、2 个 DLL 和 Windows 运行时元数据winmd并附带完整 sln/csproj 工程文件目录结构清晰可快速打开编译。已有 5815 人学习浏览适合作为 BLE 开发的实战参考。通过研究代码可以掌握从 BluetoothLEDevice.FromIdAsync 获取设备、ConnectGattAsync 建立连接到遍历 GattServices 发现服务与特征、使用 ReadValueAsync/WriteValueAsync 读写数据、订阅特征通知的完整流程并学习如何封装便捷 API规避异步处理和权限配置中的常见问题从零开始理解 GATT 服务模型。1. 解压之前先弄清楚BleSolution在解决什么问题BleSolution.zip 这个压缩包第一眼看上去就是个普通的工程打包解压完才知道这里面是一整套蓝牙低功耗BLE开发的解决方案。它不只是一个 Android Demo而是把 BLE 开发里最常遇到的“扫描慢、连接断、数据乱”这三个老大难问题连同代码封装、协议设计、调参经验一起打包了。如果你正在做智能硬件 App 联调、运动健康设备接入或者手上有个设备要通过 BLE 定期上报数据这个方案基本覆盖了“从扫描到稳定通信”的全流程。有人可能会问系统不是已经把 BLE API 封装好了吗直接调不就行了实际做过就知道裸调 API 只能保证流程走通走通不等于走得稳。真正的坑都在流程之外扫描过滤条件怎么写才能又快又准连接成功之后是不是马上就能传数据连接参数怎么调才能兼顾功耗和稳定性超过 MTU 的大包怎么安全地拆开再拼回去。这套方案做的工作就是把上面这些“经验层”的东西沉淀成可以直接用的代码。对新手来说它是一份能抄作业的模板按 README 里的步骤跑一遍一套能用的 BLE 通信链路就出来了对老手来说它的价值在于分包协议和连接参数那部分可以直接拿来对比自己的项目有没有埋伏雷。我建议解压后先别急着编译花十分钟看一下工程结构和文档弄清楚每一层在干什么后面调起 Bug 来会顺手很多。1.1 压缩包里的工程结构长什么样解压后目录是这样的BleSolution/ ├── app/ │ └── src/main/java/com/example/blesolution/ │ ├── BleClient.kt // 对外统一入口封装扫描连接收发 │ ├── BleScanner.kt // 扫描策略管理 │ ├── BleConnectionManager.kt // 连接生命周期与回调分发 │ ├── BlePacketProtocol.kt // 分包/拼帧协议 │ └── BleConstants.kt // UUID、服务、特征定义 ├── hardware/ │ └── peripheral_firmware_example/ // 对端固件示例工程 └── README.md这个结构想表达一件事BLE 不能只写手机端。对端固件的行为——广播间隔、连接参数、特征值定义——会直接决定手机端能不能稳定工作。我见过太多项目只改了 App却不知道设备固件用的是默认的 20 字节 MTU、默认的广播间隔结果手机端怎么做都优化不上去。所以这个包里除了 Android 工程还附带一个简单的固件示例方便你对着改对端来配合调试。BleConstants.kt 里集中放了服务 UUID、特征 UUID、分包协议的帧头定义。这个习惯很重要BLE 联调中有一大半的沟通成本来自“双方 UUID 对不上”“字段定义不一致”把它们集中到一个文件里至少你自己这边不会乱。等跟硬件同事对接时直接把这份文件发过去比口头传话高效得多。1.2 为什么统一封装一层会更可靠BleSolution 的核心思路是在系统 BLE API 上面包了一层自己的客户端。这层封装的目的不是炫技而是把流量控制和状态管理收拢到一个地方。比如扫描回调、连接状态回调、特征值变化回调系统 API 会把这些回调抛到不同的线程和逻辑里如果不统一收口业务层很容易出现“回调里做耗时操作”或者“状态没同步”的竞态问题。封装之后业务层只需要面向 BleClient 这个门面编程连接、断开、发数据、收数据都是同步语义的方法底层是线程切换还是回调拼接对调用方不可见。这样还有一个额外好处后续如果想从 Android 切到 iOS或者换一套底层库业务代码的改动面会小很多。这个道理跟做后端时封装数据访问层是一样的——底层实现可以换上层的业务逻辑不该跟着频繁重写。2. 核心链路拆解扫描、连接、找服务、读特征、收通知BLE 通信听起来很玄但一条数据从设备到手机走的链路其实是固定的先扫描到设备然后发起连接连接建立后去发现服务和特征最后往特征里写数据或者订阅通知。BleSolution 里的核心代码基本就是沿着这条链路组织的下面按阶段拆开讲每个阶段都结合代码说为什么要这么写。2.1 扫描不是越久越好过滤和回调要一起设计扫描阶段最容易犯的错误是把startScan的ScanCallback写得很大觉得回调越多越好。实际上扫描回调是高频回调同一台设备可能几十毫秒就广播一次回调里一旦做了耗时操作主线程直接卡顿掉帧。正确做法是先用过滤条件把范围缩小再在回调里做轻量处理。val settings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() val filters listOf( ScanFilter.Builder() .setServiceUuid(ParcelUuid(SERVICE_UUID)) .build() ) scanner.startScan(filters, settings, scanCallback)这里有两个细节。第一个是setScanModeLOW_LATENCY模式扫描窗口长结果速更快适合冷启动后快速发现设备如果做后台常驻扫描建议用LOW_POWER不然耗电极快。第二个是ScanFilter用服务 UUID 过滤能挡掉一屋子无关设备这在现场有几十个 Beacon 同时广播的环境下特别有用。实测下来加了 UUID 过滤之后从扫码到列表出现设备的时间通常能压到两三秒以内远比全量扫描再自己过滤要稳。扫描回调里建议只做一件事把设备名和 MAC 地址塞进一个列表等用户点击后统一处理。不要在回调里直接弹窗、跳页面那是灾难。2.2 连接之后先聊 MTU别急着收发数据很多联调现场的第一步卡在“连上了但数据发不出去”或者“发小包没事发大包就丢”。这大概率是 MTU 没有协商。BLE 的默认 MTU 是 23 字节扣掉 ATT 头单包有效载荷只有 20 字节。如果你的业务数据超过 20 字节要么自己分包要么先把 MTU 协商上去。Android 5.0 之后可以通过requestMtu发起协商建议连接成功并发现服务后立刻调用override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState BluetoothGatt.STATE_CONNECTED) { gatt.requestMtu(247) } }然后等onMtuChanged回调里面会回传实际协商结果。这里要注意requestMtu(247)只是请求外设端如果不支持大 MTU协商结果可能还是 23。所以业务层拿到的 MTU 值必须来自回调不能想当然用请求值。一个容易忽略的点协商 MTU 的时机。如果在连接后立刻读写特征这时候 MTU 可能还没协商完成数据还是会按 20 字节拆包行为会表现成“写入成功但对方收不全”。稳妥的顺序是连接成功 → 发现服务 → 请求 MTU → MTU 回调返回 → 再开启通知或读写也就是把数据链路相关的动作都排在 MTU 协商之后。2.3 通知订阅把数据流从“拉”改成“推”设备上报数据有两种模式一种是你主动去读特征值Read一种是设备主动往通知特征里推Notify/Indicate。绝大多数硬件上报场景用的都是后者因为主动轮询既慢又费电。订阅通知在 Android 上是一个两步操作代码长这样gatt.setCharacteristicNotification(characteristic, true) val descriptor characteristic.getDescriptor(CCCD_UUID) descriptor.value BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor)这里有个非常经典的新手坑只调了setCharacteristicNotification没有写 CCCD 描述符结果设备端不推数据过来。前者是告诉手机本地接收这个特征的通知后者是真正往设备端写值告诉对端“我开始订阅了”。两步都完成之后设备端才会真正把数据推下来。订阅完成后数据会陆续出现在onCharacteristicChanged回调里。这个阶段如果发现数据乱、收不全不要怀疑回调丢了大概率是分包和拼帧的问题这部分在第四节展开讲。3. 功耗与稳定性之间的隐性三角连接间隔、从机延迟和超时时间围绕 BLE 通信稳定性绕不开三个连接参数连接间隔Connection Interval、从机延迟Slave Latency和超时时间Supervision Timeout。很多人把它们当默认配置能连就行但项目的坑往往就藏在这三个参数里。它们的关系可以用一句话概括连接间隔决定“多久说一次话”从机延迟决定“没话说时可以跳几次”超时时间决定“多久没说话就判定断开了”。3.1 三个参数各自的物理含义和数量级先把量级说清楚。连接间隔的单位是 1.25ms允许范围是 6 到 3200对应时间就是 7.5ms 到 4 秒。常用的值是 30ms 或 50ms也就是主从设备每 30 到 50 毫秒同步一次时钟并交换数据。从机延迟的单位是“个连接事件”允许范围是 0 到 499。为 0 表示每个连接事件都必须参与为 2 表示可以跳过 2 个连接事件再到这样能显著降低从机功耗但代价是数据延迟变大。超时时间的单位是 10ms允许范围是 100ms 到 32 秒。它有一个硬性的约束公式超时时间 (1 从机延迟) × 2 × 连接间隔如果主从两端在这一项上算出来的值不满足条件连接就会在几秒内被判定为超时断开。这个公式是协议栈里的底层要求不少诡异断连问题查到最后都是这条没满足。3.2 一套从“能连上”到“连得稳”的参数调整流程我自己的习惯是拿到一套新设备先按默认参数跑通链路然后分两轮调参。第一轮追求实时性。比如做运动手环、车钥匙这种对延迟敏感的产品我倾向于把连接间隔设到 30ms从机延迟设为 0超时时间设 2 秒。这种组合的优点是数据延迟低按下按键到手机收到反馈基本感觉不到延迟缺点是从机每一两个连接事件都要醒着耗电明显。第二轮追求续航。像温湿度传感器、门锁状态上报这种低频场景把连接间隔调到 50ms从机延迟调到 4超时时间设到 6 秒以上。这样设备大部分时间可以睡过去数据延迟增加几十毫秒到几百毫秒但续航可能从“一周一充”变成“一个月一充”。调参时需要和硬件同事配合因为从机端固件里也有对应的参数配置。很多时候手机端发起请求对端固件不响应反过来也一样得两边一起改。这也是为什么我在开头强调这个包附带固件示例的原因——只调手机端效果会大打折扣。3.3 连接参数修改失败Android 和 iOS 的不同表现Android 做主机时可以通过requestConnectionPriority请求调整连接参数但它不像 MTU 那样可以指定任意值只有三档CONNECTION_PRIORITY_LOW_POWER、CONNECTION_PRIORITY_BALANCED、CONNECTION_PRIORITY_HIGH。如果你需要精确调到某个值Android 这一端是没有办法直接指定的得靠对端固件主动发起连接参数更新请求Android 侧被动接受。iOS 这边的机制更明显作为中心设备时iOS 不允许 App 主动修改连接参数连接参数的更新只能由外设端发起iOS 的 CoreBluetooth 在收到更新请求后会自动决定是否接受。所以如果你在做的是一个 iOS App 配合自研硬件请务必在固件端实现连接参数更新请求并且把参数设置在 iOS 能接受的范围内否则你在手机上把代码翻出花来也没用。另外还有一个常见的“修改失败”原因时机不对。在 GATT 发现服务还没完成时就调用requestConnectionPriority或requestMtu协议栈会直接忽略请求。一定要等onServicesDiscovered之后再发起这个顺序我踩过不只一次。4. 实测里最容易翻车的三个问题断连、分包和兼容性代码写完了链路走通了真正的地方在联调现场。这一节记录我在实际使用 BleSolution 过程中反复遇到、排查过的三个高发问题每个都是完整链路排查不是直接给答案。4.1 高频断连的完整排查链路现象是设备连接成功后少则几秒多则几十秒就会自动断开。反复重连反复断。我的排查顺序是这样走的。第一步先看onConnectionStateChange里的status值。Android 回调里的status是最直接的线索133表示远端设备主动终止了连接19表示连接被远端断开8表示超时。如果日志里反复出现8基本可以断定是连接超时下一步直接查连接参数。第二步照着 3.1 的公式算一下超时时间和连接间隔、从机延迟的关系。我遇到过一个案例对端固件把连接间隔设成了 100ms从机延迟设成 6超时时间只有 1 秒。按公式算(1 6) × 2 × 0.1 1.4 秒已经超过 1 秒的超时阈值了不断才怪。跟硬件同事确认固件参数后一改断连立刻消失。第三步如果参数没问题就要考虑环境干扰。BLE 跑在 2.4GHz跟 Wi-Fi 同频段现场如果路由器密集很容易出现丢包重传导致的隐性断连。判断方法是用抓包工具长时间抓广播和连接事件看看是不是有明显的丢包重传。第四步排查手机系统省电策略。这个问题国产 ROM 上特别明显App 一旦切到后台系统会主动休眠蓝牙或者杀掉连接。解决思路是使用前台服务保活并且在应用内申请合理的后台运行权限。这一步跟 BLE 本身关系不大但在实际项目里占了断连问题的相当比例。4.2 超过 MTU 的大包如何分包和拼帧默认 MTU 23 时的有效载荷只有 20 字节就算协商到 247最多也就 244 字节。如果你要传一组传感器校准数据或者升级包几百上千字节几乎是必然的。BleSolution 的做法是在应用层定义一套分包协议。大致思路是发送方先把完整数据切成长度不超过 MTU 有效载荷的小块每块加上帧头。帧头里包含帧类型、包序号、总包数接收方收到后按包序号重组。协议核心代码大概长这样class BlePacketProtocol(private val mtuPayloadSize: Int) { fun encode(data: ByteArray): ListByteArray { val totalPackets (data.size mtuPayloadSize - 1) / mtuPayloadSize return (0 until totalPackets).map { index - val payload data.copyOfRange( index * mtuPayloadSize, minOf((index 1) * mtuPayloadSize, data.size) ) buildFrame(index, totalPackets, payload) } } fun decode(frames: ListByteArray): ByteArray { val sorted frames.sortedBy { frameIndex(it) } val payloadSize sorted.sumOf { framePayload(it).size } val result ByteArray(payloadSize) var offset 0 for (frame in sorted) { val payload framePayload(frame) payload.copyInto(result, offset) offset payload.size } return result } }这里要特别提醒BLE 的数据包不保证按顺序到达尤其多点并发传输时后发先到很正常所以协议里必须带包序号接收端要按序号排序再拼帧。很多做串口出身的人习惯按顺序流式接收在这上面容易吃大亏。分包大小建议拿 MTU 回调的真实值减 3 算出来比如协商到 247单帧就取 244 字节。如果把分包大小写死在 20 字节那 MTU 协商做得再好也白搭。4.3 系统行为差异从权限到回调都值得单独测BLE 开发里最折腾人的是 Android 和 iOS 的行为差异同一个硬件两端表现完全不同。整理几个我踩过的第一个是权限模型。Android 12 之前只需要位置权限Android 12 之后必须动态申请BLUETOOTH_SCAN和BLUETOOTH_CONNECT少了任一权限扫描直接返回空。iOS 侧从 13 开始必须提供NSBluetoothAlwaysUsageDescription文案否则直接崩溃。这些是硬门槛打包前要专门检查。第二个是回调线程。Android 的 BLE 回调默认跑在 Binder 线程不是主线程如果直接在回调里更新 UI 会崩溃。iOS 的 CoreBluetooth 回调在主线程但主线程一卡回调也会延迟。两边行为不一样封装层最好统一把回调抛到业务指定的线程里避免业务代码到处写runOnUiThread。第三个是后台行为。iOS 在蓝牙后台模式下可以继续接收通知但必须提前在 Capabilities 里打开Uses Bluetooth LE accessories。Android 这边某些厂商深度定制的系统后台杀得狠如果只是用 10 分钟就断开先查这一条。5. 从 demo 到业务验证清单和扩展方向代码能跑通跟真正能上线中间还隔着一整套验证流程。BleSolution 的方案拿来用之后我通常建议团队先跑一遍下面这份清单再决定要不要进入业务开发。我自己在实际使用中每次改完连接参数或者分包逻辑也都会拿这份清单回归一遍防止新逻辑把老链路弄挂了。5.1 五个可以自检的验收场景冷启动场景手机重启后第一次打开 App从扫描到连接成功时间应控制在 3 秒以内。超过 5 秒可以怀疑扫描逻辑存在重复回调或者过滤条件过严。连续收发场景固定往设备端发 200 个 20 字节的小包再发 50 个 244 字节的大包统计丢包率。一步都不丢是不可能的但小包丢包率应低于千分之一大包要保证拼帧后完整还原。静置场景连接建立后不做任何操作放在那里 30 分钟看是否出现 Supervison Timeout。这个场景专门验证连接参数设置是否合理。断线重连场景手动关掉设备电源再打开确认 App 能在 5 秒内发现断连并自动发起重连。重连逻辑一定要有退避策略否则设备一回来就可能被十几条连接请求打挂。前后台切换场景App 切后台 10 分钟再切回来确认连接状态是否正确收数是否中断。这一步能暴露系统省电策略和后台服务是否正常工作。5.2 按需扩展的方向如果项目顺利跑过了验证阶段后续扩展通常围绕三个方向。第一个是吞吐量优化。如果要做 OTA 升级或者传输大文件普通 BLE 4.0 的速度是远远不够的。办法是启用 BLE 5.0 的 2M PHY 和长包模式在 Android 侧通过BluetoothLePhy相关接口设置iOS 侧则可以在centralManager(_:didUpdateANCSAuthorizationFor:)周边能力里配合。这个扩展能把吞吐从几十 KB/s 拉到一两百 KB/s但需要两端固件都支持不是改 App 就能解决。第二个是多设备连接。BleSolution 默认是单连接模型但很多智能家居场景需要同时连接多个传感器。扩展思路是把BleConnectionManager改成用设备地址做 key 的复用池每个设备一个独立 GATT 实例同时注意 Android 系统并发连接数限制以及回调线程和主线程的切换逻辑避免多个 GATT 的回调交叉时状态互相污染。第三个是配对与安全。如果设备涉及个人健康数据建议增加配对绑定和加密链路。BLE 的配对过程涉及 LE Legacy Pairing 和 LE Secure Connections 两套机制绑定之后双方会存储 Long Term Key后续连接可以快速加密。但配对逻辑会增加研发量且不同外设芯片支持程度差异大适合项目进入量产阶段后再投入。最后分享一个我个人的调试习惯开发 BLE 最怕“设备灯亮了功能却对不上”。每次拿到新的硬件我一定会先在两个不同品牌的手机上跑同一套流程再动固件代码。如果只能带一个工具我会选抓包器它能把广播、连接、断开的整个过程摊开在时间轴上大多数连接异常看到波形就明白了。本文还有配套的精品资源点击获取