ARTICLE DETAIL

资讯详情

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

Android BLE开发入门:BluetoothLeGatt Demo核心代码与实战踩坑

Android BLE开发入门:BluetoothLeGatt Demo核心代码与实战踩坑 简介针对安卓官方低功耗蓝牙通信示例工程面向需要实现设备扫描、建立连接、断开连接、发现服务与特征值读写等功能的移动应用开发者尤其适合有安卓基础但初次接触蓝牙4.0的工程师。压缩包内含完整工程目录共70个文件以Java源码、XML界面与配置文件、class字节码和jar依赖库为主同时保留了可安装运行的APK包体仅1.34兆字节便于快速导入开发环境或直接安装体验。已有五百余人学习下载热度较高反映该示例在实际开发中对理解蓝牙协议栈和官方接口调用顺序具有参考价值。开发者可对照源码理清扫描回调、服务发现回调及UUID匹配等关键逻辑也可参考工程中的资源划分与构建配置为自己的低功耗蓝牙模块提供直接可落地的实现思路例如基于该系统源码扩展自定义服务或特征值操作。 做Android开发的人多多少少都会碰一次蓝牙BLE。尤其是当你要对接智能手环、体脂秤、空气检测仪这类硬件的时候蓝牙BLE基本是绕不开的通道。官方的那个BluetoothLeGatt demo我从第一次接触到现在翻过好几遍说它是Android上BLE开发的“最小可运行教科书”真不过分。它不像很多第三方库一样把所有细节封装得严严实实而是把扫描、连接、发现服务、读写特征值、订阅通知这些关键流程用原生Android API一层层摊开给你看。这篇文章不是官方文档翻译而是我结合真实调试经验把这个demo从环境搭建到核心代码拆开讲清楚告诉你每段代码在解决什么问题、什么场景下最容易踩坑以及最后怎么把它改成自己项目里能用的东西。不论你是刚开始接触BLE的Android开发还是做嵌入式想快速验证协议这篇都适合接着往下读。1. 项目概述与目标场景1.1 官方demo到底在做什么官方示例在Android samples里通常叫BluetoothLeGatt是一个整体非常精简的工程。它模拟了一个最常见的BLE中心设备场景手机作为中心设备去扫描周围正在广播的BLE外设点击某个设备后发起连接连接成功后把外设上暴露出来的GATT服务和特征值逐条列出来。你可以在列表里点“Read”去读取某个特征值也可以输入内容点“Write”往特征值里写入数据还能打开“Notify”订阅设备端的通知实时接收外设推送过来的数据。说白了它就是一个“可见即可调”的BLE调试器。这个demo虽然小但把BLE开发的核心链路全走了一遍。很多人写BLE项目写到后面发现问题根本不是UI怎么做而是链路里某个环节没有理顺。官方demo最大的价值就是给你一个没有被业务污染过的基线遇到问题可以回溯到这个demo去验证是外设的锅还是代码的锅。1.2 为什么值得仔细看一遍首先是代码足够简单。整个工程涉及的类不多一个Activity管扫描一个Activity管交互一个Service统一管理GATT一个常量类放UUID。这个结构对理解“BLE该怎么分层”特别有帮助。很多商业项目里的BLE代码为了兼容各种场景动不动就上千行封装反而是官方demo能让你一眼看清系统API在什么时候回调、在哪个线程回调。第二个原因是它直接对应官方文档API。网上碎片教程很多但版本不同写法也可能不一样。官方demo和Android开发者官网的BLE指南是配套的你按demo代码去查文档基本都能对上。第三方封装库虽然好用但一旦遇到Android 12权限变更、厂商兼容性问题你还得回到原生API层面去找原因。把demo吃透等于给自己留了一个兜底方案。1.3 适合什么样的人看如果你是第一次做BLE并且手里已经有一个支持BLE的外设那这个demo是最快的起跑线。你不需要懂太多GATT协议的背景先把它跑起来把外设名字扫出来然后再看代码理解每一步对应什么动作。如果你是老开发但只在某个业务里用过现成SDK没看过底层这个demo也能帮你补上GATT服务发现和特征值操作这些短板。嵌入式工程师同样适合因为通过这个demo你能快速确认自己板子广播的service UUID对不对特征值的属性有没有设置正确。换句话说它是个跨岗位的沟通工具App端和硬件端各拿一份demo联调起来会顺畅很多。2. 准备工作把demo跑起来2.1 软硬件环境要求先泼一盆冷水安卓模拟器基本没法用来调试BLE因为模拟器默认没有蓝牙协议栈某些版本虽然有虚拟蓝牙但行为跟真机差得远。所以你需要一台Android真机系统版本尽量新一点我建议至少Android 8以上这样权限逻辑和API行为比较统一。开发工具用Android Studio版本不强制最新但建议别太老因为Gradle和AGP版本兼容性会折腾人。外设方面如果你是做产品开发用你们自己的硬件模组如果只是学习随便一个支持BLE的心率带、手环、ibeacon都行。目标外设要支持BLE这是前提。有些老设备只支持经典蓝牙BR/EDR不在这个demo的讨论范围内。现在很多IoT设备都同时支持BLE和经典蓝牙但广播形式完全不同刷出来的结果也会不一样。先确认外设处于可广播状态连接阶段会省很多麻烦。2.2 获取官方示例工程官方仓库在Android samples里最常见的路径是android/connectivity-samples里面有一个BluetoothLeGatt模块。你可以直接用Git把仓库clone下来也可以去developer.android.com的示例页面下载zip。我更推荐直接clone整个connectivity-samples仓库因为它里面还有其他连接类示例对比着看会更有收获。如果你网络下载GitHub很慢可以用国内的镜像站或者直接去码云搜索别人同步好的副本。不管从哪里拿关键判断标准是看工程结构是否完整里面有没有app/src/main/java/.../DeviceScanActivity.java这个核心文件。有说明版本基本没问题。拿到工程后先别急着改代码先同步一遍Gradle让IDE把依赖和构建工具都准备好。2.3 导入Android Studio并修改版本配置下载完后用Android Studio打开工程我遇到的第一个问题通常是SDK版本不匹配。官方样本更新有滞后性经常会出现compileSdk比当前安装的SDK版本低或者Gradle插件版本太老导致无法同步。我的习惯是先把这三行对齐到我自己电脑上稳定可用的版本compileSdk 34 minSdk 21 targetSdk 34如果你用的是新版Android Studio会遇到提示把Gradle插件升级到某个版本直接点升级一般没问题。编译报错时优先看网络问题因为Gradle首次同步会下载一堆依赖。如果同步太慢可以给Gradle配国内镜像源。我踩过的最典型坑是官方demo可能依赖了已经移除的旧support库需要手动改成AndroidX不过近几年官方示例基本已经迁移完了这个问题出现概率不大。2.4 工程结构一眼看懂打开工程后默认包名下主要有这几个文件DeviceScanActivity扫描页负责扫描BLE设备并展示为列表。DeviceControlActivity连接后的详情页列出服务和特征值。BluetoothLeService一个后台Service封装了GATT连接、服务发现、读写和通知。SampleGattAttributes存放常用GATT UUID比如心率的Service UUID、电池电量的UUID等。BluetoothLeAdvertiser部分版本还带了一个模拟外设的广播示例方便你测试。整体结构是典型的MVP思路Activity只管和用户交互实际的GATT调用放在Service里Service通过Binder把状态回调给Activity。这样即使Activity重建连接也不容易丢。官方故意这么设计就是告诉你生产项目中BLE服务应该独立管理而不是直接写在Activity里。3. 核心细节拆解从扫描到GATT通信3.1 权限声明与动态申请BLE权限是整个demo里最容易被忽略的地方。传统Android版本你需要BLUETOOTH和BLUETOOTH_ADMIN两个权限其中前者负责常规蓝牙操作后者负责扫描和连接。从Android 6开始扫描BLE还没那么复杂但从Android 12API 31开始系统把蓝牙权限分得更细新增了BLUETOOTH_SCAN、BLUETOOTH_CONNECT和BLUETOOTH_ADVERTISE。扫描需要SCAN连接需要CONNECT广播外设需要ADVERTISE。demo里会在扫描前检查这些权限并动态请求。这里有个容易踩的坑如果你的targetSdk是31及以上但还在用旧权限App不会崩溃只是扫描结果永远为空或者系统直接拒绝连接。反过来如果你的targetSdk仍然低于31那新权限适配不做也能跑但上架时可能被应用市场要求适配。我的建议是直接用新权限不论设备系统版本用ActivityCompat.requestPermissions请求一套兼顾老版本。权限申请时有几点要特别留意。第一Android 12以上BLUETOOTH_SCAN和BLUETOOTH_CONNECT是运行时权限不能再只写在Manifest里需要在界面弹窗申请。第二部分国产ROM在蓝牙权限之外还会让你手动开启“附近设备”开关这个属于厂商定制代码里检测不到只能引导用户去设置页打开。第三早期版本需要的定位权限在Android 12已经被解耦不需要额外申请但有些老旧外设扫描还是依赖定位服务开关所以如果扫不到设备先把系统定位服务打开试试。3.2 BLE扫描流程扫描是BLE通信的第一步。demo里通过BluetoothLeScanner发起扫描核心代码如下mBluetoothLeScanner.startScan(null, settings, mScanCallback);第一个参数是ScanFilter列表传null表示不过滤扫所有广播设备settings可以配置扫描模式demo里默认是低功耗模式。回调mScanCallback里会收到ScanResult里面包含设备地址、信号强度RSSI和广播数据。有一点非常关键扫描回调不能保证在主线程但更新UI必须在主线程所以demo里用了runOnUiThread包装。另外扫描是有功耗代价的官方建议扫描到目标设备之后马上stopScan。很多新手会在页面上保留一个“刷新”按钮每次点击先stop再start这个思路是正确的。还有一个容易忽略的问题是扫描回调里拿到的设备对象可能是同一个设备多次回调需要做去重demo里通过维护一个Map或List判断设备地址是否已存在避免列表疯狂重复。3.3 连接与GATT服务发现扫描到设备后demo会调用device.connectGatt(this, false, mGattCallback)建立连接。第二个参数autoConnect设为false表示直连适合主动连接场景如果设为true系统会在设备可用时自动重连更适合长时间跟踪设备。直连速度更快但重连逻辑要自己做自动连接省心但首次连接可能慢很多。连接过程中最重要的回调是onConnectionStateChange。当收到BluetoothProfile.STATE_CONNECTED时demo会调用mBluetoothGatt.discoverServices()去发现GATT服务发现成功后系统会回调onServicesDiscovered只有在这个回调里你才能安全地拿到service和characteristic。我见过不少人直接在连接回调里读写特征结果返回失败就是因为服务还没发现完。这个demo把GATT的回调都放在一个BluetoothLeService里然后通过广播或Binder通知UI。为什么不直接在Activity里用回调因为GATT连接是异步的Activity可能会因为屏幕旋转或返回键销毁如果把回调放在Activity里很可能连接还没完成Activity就没了。用Service持有连接即使页面重建底层连接也能保留。官方这个设计很值得学习。3.4 特征值读写与通知拿到特征值后demo里提供了三个操作Read、Write、Notify。Read通过readCharacteristic(characteristic)发起读取结果在onCharacteristicRead回调里返回Write通过writeCharacteristic(characteristic, value)写入结果在onCharacteristicWrite回调里返回。这两个回调都属于异步操作不要在回调外部调用返回值。同时要注意一个GATT连接同一时刻最好只发起一个操作如果你连续调用多个read或者write系统底层会把命令排队但回调顺序可能跟你的调用顺序不一致容易造成业务数据错乱。Notify订阅稍微绕一点。除了调用setCharacteristicNotification(characteristic, true)之外还要对特征值的CCCD描述符写入开启通知的指令。CCCD是客户端特征配置描述符标准UUID是0x2902demo里通过BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE写入这样才能真正收到外设主动上报的数据。很多新手只调了setCharacteristicNotification发现收不到通知就是因为漏了这个步骤。3.5 MTU协商与分包处理BLE默认的ATT层有效载荷只有23字节去掉3字节头真正留给应用数据的只有20字节。这是很多刚接触BLE的人最先踩的坑你往特征值里写一个超过20字节的字符串要么写入失败要么被系统截断。要解决这个问题就得协商MTU。demo里虽然没有把MTU协商做成一个显眼功能但在连接成功后可以调用requestMtu来请求更大的MTU。协商结果会通过onMtuChanged回调返回常见的MTU值是247表示单个ATT包最多承载244字节应用数据。这个值不是越高越好因为MTU越大出错重传的代价越大而且外设端不一定支持。你可以在硬件工程师提供协议的时候顺便确认一下设备端最大支持多少MTU两边取最小值使用。如果你的数据超过单包长度仍然要自己拆包。比如要发500字节的升级固件你需要在应用层定义帧格式包括序号、总包数、载荷然后分包发送接收端再按序号重组。这个逻辑官方demo没有实现因为它是示例而不是完整产品但这恰恰说明官方只是把最小链路打通了真正工程化还是得靠你自己。4. 实操复盘与问题排查4.1 把demo跑通的完整流程我建议你拿到demo后别急着读代码先做一次“黑盒调试”。第一步用Android Studio把工程跑到一个真机上系统会自动申请蓝牙权限全部允许。第二步把外设电源打开让它进入可广播模式手环或者开发板类设备通常都有配对开关或指示灯。第三步打开demo点击右上角扫描按钮等几秒外设名字就会出现在列表里。点击设备名称进入详情页后你会看到一列service继续展开会看到每个service下面挂的characteristic。这个页面在真机调试时非常有用因为它能直接看到硬件端暴露了哪些服务。如果你的外设是自己做的此刻对照设计的GATT表检查就行。如果某一项service的UUID不在SampleGattAttributes里详情页会显示原始UUID这很正常很多私有服务都是这样。接着你可以点一个characteristic进入操作界面。先点Read看能不能读出来再点Write输入hex或者字符串。这里要注意demo界面默认输入框是文本如果你要发十六进制数据最好改成hex输入否则有些字符会被当成UTF-8编码后长度变长。最后打开Notify让外设周期上报数据观察回调是否持续触发。整个流程走通你对demo的运作就有了直观认识再去读代码会轻松很多。4.2 常见问题速查表在真机调试过程中几乎每个人都会遇到下面这几个问题。我把它们整理成一张表方便你直接对号入座。问题现象可能原因解决办法扫描列表一直为空没有蓝牙权限或蓝牙未开启检查权限、开启蓝牙打开系统定位服务扫描到但连接不上外设连接数已达上限或距离过远让硬件重启广播靠近手机等待几秒再试连接后立刻断开GATT服务发现失败或设备端主动断连查看logcat中onConnectionStateChange的status值没有任何service显示服务发现回调没执行或UUID过滤错误确认调用了discoverServices等待onServicesDiscovered写不进数据特征值不支持Write或数据超长检查特征属性协商MTU拆包发送收不到Notify没写CCCD描述符调用setCharacteristicNotification后写入0x2902描述符Android 12一打开就崩缺少BLUETOOTH_CONNECT/SCAN动态权限在运行时申请新蓝牙权限这张表只覆盖高频问题更奇葩的情况通常和厂商协议有关。排查时记住一个原则先看硬件是否正常工作再用logcat确认回调状态码最后再看代码逻辑。别一上来就怀疑手机系统很多时候是外设没有进入广播模式。4.3 不同Android版本的兼容性差异做BLE应用最头疼的不是协议而是版本碎片化。Android 6到Android 11扫BLE需要定位权限很多用户不明白为什么一个蓝牙App要定位权限实际上是系统需要扫描附近设备这导致应用市场审核时经常要解释。Android 12之后系统总算把蓝牙相关权限单独拎出来了但代价是新增了更细的权限声明不做适配可能直接连不上。国产ROM的适配更是千奇百怪。某些品牌手机上即使你申请了所有权限系统默认还是禁止App在后台扫描蓝牙你需要在“电池优化”或“自启动管理”里手动允许。还有的设备在连接时会弹窗询问“是否允许此应用随时连接蓝牙设备”这个不是标准Android行为但出现频率不低。遇到这种问题别慌引导用户去检查或者在权限申请失败时多给几个提示文案避免用户以为App坏了。另外同一个API在不同系统版本上的行为也有差异。比如startScan在Android 7以下需要先获取BluetoothAdapter而在Android 8以上基本不会返回false。如果你的demo目标SDK设得太高反而可能在老设备上出现不兼容。所以生产项目里我一般会根据minSdk和targetSdk分别写分支而不是无脑用最新API。4.4 把demo改造成自己的项目骨架看完demo后最实用的方法是把它复制到自己的项目里作为BLE模块的起点。第一步把包名改成你们公司的域名类名可以保留避免以后混淆。第二步把SampleGattAttributes里的UUID换成你们协议里定义的UUID没有的服务删掉就行。第三步把扫描列表的过滤条件加上比如按Service UUID过滤只显示目标产品不然市场上有大量杂七杂八的BLE设备用户体验很差。再往下你可能会需要把BluetoothLeService从Activity代码里解耦出来。官方demo是通过bindService的方式和Activity交互的但这个方式在生命周期复杂时很容易出问题。我建议改成单例或者由Repository持有在Application初始化时创建连接管理类Activity只负责订阅状态和数据流。这样即使Activity重建连接状态也不会丢。最后如果你们的产品有OTA升级需求还需要在Service里加入队列机制防止多包发送时的回调混乱。5. 从demo到产品的几个坑5.1 后台连接与进程回收官方demo是纯前台运行没有处理后台保活。但在真实产品里App常常需要在退到后台后继续接收设备数据比如运动手表App锁屏后还要同步数据。Android系统的省电机制会杀掉后台进程BLE连接也会随之断开。业界通用的方案是使用前台服务并附上相应通知这样系统不会轻易回收进程。不过前台服务涉及通知权限和类型声明Android 14之后还要指定foregroundServiceType这点要提前设计好。还有一点要提醒如果业务只是需要周期性同步不一定非要长时间持有GATT连接。你可以用WorkManager定时唤醒打开蓝牙扫描并连接同步完马上断开释放资源。这样对电池最友好也更容易过系统审核。很多新手上来就做常驻连接结果被用户骂耗电其实是没区分清楚“实时通信”和“周期同步”这两个场景。5.2 多设备并发处理的正确姿势官方demo只管理一个设备连接但真实场景可能是家里有多个传感器App要同时连接。这时每个设备需要一个独立的BluetoothGatt对象对应的回调也各自独立看起来只是复制几份但实际上有个很容易踩的大坑同一个BluetoothGatt对象不能同时执行多个GATT操作。比如你对一个特征值同时调用读和写系统会出错因为底层是串行通道。解决方案是给每个设备的操作请求建一个队列。发起操作前把任务放到队列里等前一个操作的回调返回后再执行下一个。这个机制写起来不复杂但维护状态机需要细心。有些第三方库已经实现了队列但如果你就是想用官方demo做基础自己写队列反而是很好的练习。多设备场景下还要注意每个会话的资源释放断开一个设备时要把对应的队列清空不然回调里会拿到一个已经关闭的gatt对象。5.3 数据粘包与缓冲处理BLE通道本身对单个ATT包的大小有限制但对连续多条消息并没有“消息边界”概念所以你会遇到和TCP类似的粘包问题。比如外设分三包发过来一个JSON字符串你收到的回调可能是一次全量、也可能是两次零散数据完全取决于设备和系统怎么调度。处理方式一般是维护一个发送缓冲区和一个接收缓冲区在应用层协议里加长度字段或结束标记然后每次收到数据后按协议解析拼包成功后返回给上层。这个点官方demo根本没有涉及因为示例只做单条数据的读写。但你在做真实项目时百分之百会碰到。我第一次做BLE手环同步时就是没做缓冲结果收到一半的包就解析导致数据经常错乱。后来在协议层加上长度字段再把接收函数改成积累到目标长度才触发回调问题立刻解决了。这个经验虽然不是官方demo直接教的但从demo往产品走的过程中一定会遇到。最后再说一个我常用的判断方法拿到一个从来没调过的BLE设备先别急着写业务逻辑直接拿官方demo去连一次把service和characteristic全部列出来基本就能摸清这块板子的能力边界。很多问题看起来像代码bug实际是协议理解问题。官方demo的好处就在于足够简单你可以拿它当“调试标尺”所有封装和业务逻辑都围绕着它慢慢加。等你自己也踩过几个坑之后对BLE这潭水的深浅也就心里有数了。本文还有配套的精品资源点击获取
返回列表