尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android蓝牙开发源码全解析:从经典蓝牙到BLE实践指南

Android蓝牙开发源码全解析:从经典蓝牙到BLE实践指南 简介这份Android平台蓝牙开发源码包面向需要实现BLE扫描、连接与数据传输的开发者尤其适用物联网和设备间通信场景。代码围绕蓝牙管理器、蓝牙适配器、扫描回调、蓝牙GATT等核心接口展开直观演示了从设备发现、服务发现到特性读写的完整链路。资源以RAR压缩包发布共23个文件既有Java源文件也有编译生成的class文件、XML配置、界面布局和图片资源还打包了可安装APK与工程构建信息。整个包体仅91KB精简而完整便于快速阅读和二次调试。目前已有208人学习下载。对有Android蓝牙开发经验但希望结合源码理解扫描限制、权限适配、连接流程的开发者而言这份小而全的代码包能提供从静态代码到运行产物的双重参考节省自行搭建样例的时间。代码中尤其涉及扫描模式选择、权限声明等易错点读代码即可对应实现。 做 Android 蓝牙开发的人多少都有过这样的经历网上找了一份“android 蓝牙开发源码”下载下来跑一遍发现要么扫描不到设备要么连上了收不到数据最后只能对着源码发愁。我这些年手里过过的蓝牙项目不算少从经典的蓝牙串口通信到 BLE 低功耗设备控制再到基于 RSSI 的信号测距基本把蓝牙开发里能踩的坑都踩了一遍。这篇博文就从一个拿来即用的源码项目视角把 Android 蓝牙开发的整体思路、核心代码、权限细节和排错经验一次说清楚。这篇文章适合以下几类人刚接触蓝牙开发、想找个源码作为起点的 Android 工程师需要在经典蓝牙和 BLE 之间做技术选型的开发者以及被蓝牙权限扫描、稳定性、兼容性折磨过想系统性补课的人。内容不会只贴代码更多的是解释“为什么这么做”这样你拿到任何一份蓝牙源码都能快速看懂它在干嘛、应该怎么改。1. 开发前的关键技术选型1.1 经典蓝牙BR/EDR还是低功耗蓝牙BLE这是拿源码动手前必须先想清楚的事。很多人一上来就搜“android 蓝牙开发源码”根本不区分蓝牙类型结果拿着一份 BLE 扫描的代码去做串口透传设备怎么都连不上其实不是代码有问题是协议栈根本不对。经典蓝牙也就是 BR/EDR特点是传输带宽大、连接建立后适合持续传输数据。日常看到的蓝牙音箱、蓝牙耳机播放音乐走的是 A2DP蓝牙打印机、蓝牙串口模块比如 HC-05走的是 SPP/RFCOMM这些都建立在经典蓝牙基础上。BLE 则是低功耗蓝牙功耗只有经典蓝牙的几分之一但数据传输速率和单包长度都受限适合传感器数据上报、设备控制、广播信标这类小数据量场景。我个人的选型经验是如果设备是电池供电的 IoT 硬件比如温湿度传感器、智能防丢器、ESP32 做的遥控模块直接选 BLE如果是要持续传音频、大批量数据或者对接的是工业串口模块、蓝牙打印机那就走经典蓝牙。1.2 源码工程里常见的模块划分拿到一份组织良好的蓝牙源码通常能看到几个固定模块设备扫描模块、连接管理模块、数据收发模块、UI 交互模块。设备扫描负责搜索周围设备连接管理负责配对和建立通信链路数据收发负责读写字节流或 GATT 特征值。有些项目还会单独抽出一个 BleManager 或者 BluetoothClient 类把系统 API 封装起来。这个模块化思路非常重要。我见过很多 demo 源码把蓝牙逻辑全部塞进 Activity 里Activity 一销毁整个连接就断了。真正能落地的源码至少应该有独立的蓝牙服务层用 Service 或 ViewModel 持有连接对象。看源码时先看它有没有把扫描、连接、收发分离开如果全部堆在一个类里复用时就很痛苦。1.3 需要先认识的几个蓝牙 ProfileSPP串口仿真协议经典蓝牙下最常见的透传方式蓝牙模块和手机建立起一条类似串口的通道双向收发字节流蓝牙打印机、单片机透传模块基本都是走 SPP。A2DP高级音频分发蓝牙音频播放的标准协议手机放歌到耳机、音箱走的就是它。HFP免提协议蓝牙通话场景涉及 SCO 音频通道。GATT通用属性协议BLE 的核心机制基于 Service 和 Characteristic 来组织数据所有 BLE 设备通信都建立在 GATT 之上。这里多说一句网上热词的“蓝牙 a2dp切sco模式”实际上就是音频路由切换。A2DP 负责放音乐SCO 负责通话语音在 Android 上切换需要操作 AudioManager 的 startBluetoothSco 和 setBluetoothScoOn而不是直接操作蓝牙 socket。这属于音频类应用的特殊需求普通数据传输开发不需要关心。2. 环境搭建与权限配置2.1 Android Studio 环境与中文插件源码拿到手第一件事是把工程跑起来。Android 蓝牙开发用的是 Android Studio这个应该没有争议。很多新手卡在环境上常见的问题包括 Gradle 同步失败、SDK 版本不匹配。我的建议是先看工程的 compileSdk、targetSdk、minSdk 这三个版本参数把你的 Android Studio 里的 SDK Manager 把对应版本装上再同步就不会报错。至于“android studio怎么设置中文”这个问题直接在 Settings 里选择 Plugins搜索 Chinese安装 Chinese (Simplified) Language Pack 插件重启之后就完整汉化了。这种汉化只影响开发工具界面不会影响你写的代码放心装。2.2 权限声明和动态申请蓝牙权限在 Android 各版本之间变化很大这是蓝牙源码最容易出的问题。早年的 Android 5.0 只需要声明两个权限就能跑BLUETOOTH 和 BLUETOOTH_ADMIN。但从 Android 6.0 开始扫描蓝牙设备还需要定位权限 ACCESS_FINE_LOCATION因为系统认为扫描周围蓝牙设备会暴露位置信息。到了 Android 12API 31更是把蓝牙权限细分为 BLUETOOTH_SCAN、BLUETOOTH_ADVERTISE、BLUETOOTH_CONNECT 三个。一个兼容性最好的做法是直接按高版本声明同时在代码里做兼容uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT /运行时动态申请要特别注意Android 12 以上需要申请 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECTAndroid 11 及以下需要申请 ACCESS_FINE_LOCATION。我的习惯是写一个权限申请函数根据 Build.VERSION.SDK_INT 分开申请避免在低版本上请求一个不存在的权限导致崩溃。2.3 BluetoothAdapter 初始化和设备能力检查拿到蓝牙源码后先看它的蓝牙适配器是怎么获取的。传统写法是 BluetoothAdapter.getDefaultAdapter()但在 API 18 以后推荐通过 BluetoothManager 获取BluetoothManager manager (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); BluetoothAdapter adapter manager.getAdapter();这种写法能正确区分蓝牙和 BLE 适配器在一些双模设备上更可靠。拿到 adapter 之后要做三件事判断是否为 null设备不支持蓝牙、判断是否开启!adapter.isEnabled()、判断是否正在扫描adapter.isDiscovering()。很多源码没做这个检查直接 startDiscovery结果在飞行模式或者蓝牙关闭状态下直接崩溃这就是基础防护没做到位。3. 经典蓝牙开发源码拆解3.1 设备发现广播接收与扫描经典蓝牙扫描和 BLE 扫描不一样它是通过系统广播异步返回结果的。核心动作是调用 adapter.startDiscovery()然后在广播接收器里监听 BluetoothDevice.ACTION_FOUND 这个 Action。看这份源码时重点关注三个细节是不是在开始扫描前注册了广播扫描结束后是不是调用了 cancelDiscovery广播接收器退出了有没有注销。这三个细节少一个就会出现要么收不到设备、要么内存泄漏、要么扫描永远不停止的问题。一个常见误区是扫到设备后立刻去连接。在部分低端设备上startDiscovery 过程中发起连接会失败因为系统还在扫描状态。正确做法是先 cancelDiscovery等扫描停止后再 connect。这个坑我踩过好几次尤其是对接某些国产蓝牙模块时特别明显。3.2 配对与绑定状态监听经典蓝牙连接设备之前通常需要先配对。配对有两种触发方式一是调用 device.createBond() 主动发起二是系统弹出配对确认框由用户确认。蓝牙打印机类的设备一般会自动弹框输入 PIN 码就能配对。源码里建议注册一个 ACTION_BOND_STATE_CHANGED 的广播监听配对状态的变化。状态有 BOND_NONE未配对、BOND_BONDING配对中、BOND_BONDED已配对三个值。只有在收到 BOND_BONDED 之后再发起 socket 连接才不会因为配对没完成而失败。有些设备在 createBond() 之后不会弹配对框比如部分工业蓝牙模块。这时需要反射调用 BluetoothDevice 的 setPin 方法预置 PIN 码完成配对。这种 hack 方式在正规 app 里不推荐但对接特定设备时确实是有效的解决方案。3.3 RFCOMM 连接与数据收发配对完成后的核心是建立 RFCOMM socket。客户端需要用设备的 MAC 地址创建一个 BluetoothDevice然后调用设备对象的 createRfcommSocketToServiceRecord(uuid) 拿到 socket 并 connect()。这里有一个关键参数 UUID。SPP 服务常用的 UUID 是 00001101-0000-1000-8000-00805F9B34FB这个是蓝牙串口服务的基础 UUID大多数安卓蓝牙串口源码里用的都是它。但如果你对接的是某个厂商自研的蓝牙模块对方可能用了自定义 UUID那就要以硬件文档为准。源码里如果写死了这个 UUID连不上时第一件事就是检查 UUID 是否匹配。数据收发方面socket.getInputStream() 和 socket.getOutputStream() 分别拿到输入输出流。这里务必注意读写操作不能放在主线程必须开子线程。读取时要用循环不断从输入流读字节读到 -1 表示流关闭写入时 byte 数组要注意编码中文要转成 UTF-8 发送否则对端收到的会是乱码。4. BLE 低功耗蓝牙开发源码要点4.1 BLE 扫描ScanFilter 与回调BLE 的开发思路和经典蓝牙完全不同。BLE 设备不会自动弹配对框也不需要通过 createBond 去配对而是通过 GATT 协议直接连接。先看扫描部分BLE 扫描 API 从 API 21 开始推荐使用 BluetoothLeScannerBluetoothLeScanner scanner bluetoothAdapter.getBluetoothLeScanner(); ScanFilter filter new ScanFilter.Builder() .setServiceUuid(new ParcelUuid(UUID.fromString(0000ffe0-0000-1000-8000-00805f9b34fb))) .build(); ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build(); scanner.startScan(Collections.singletonList(filter), settings, scanCallback);注意 ScanSettings 的 SCAN_MODE_LOW_LATENCY 会明显提高扫描速度但耗电也更快。如果只是做设备发现用默认的 SCAN_MODE_BALANCED 就行。扫描回调里能拿到 ScanResult包含设备对象、信号强度 RSSI 和广播数据包。利用 ScanRecord 可以解析出设备名称、广播的服务 UUID这些信息可以用来过滤设备避免把周围的智能手环、耳机全都显示出来。很多源码在扫描时用一个布尔变量防止重复启动扫描这个细节值得学习。另外 BLE 扫描是持续性的拿到目标设备后一定要调用 stopScan不然手机会一直处于高功耗扫描状态电量肉眼可见地往下掉。4.2 GATT 连接与服务发现找到目标设备后调用 BluetoothDevice.connectGatt(context, false, gattCallback) 建立 GATT 连接。第二个参数如果传 true 表示自动重连实际开发中建议先传 false手动控制重连逻辑避免系统在信号差时频繁自动重连导致连接状态不稳定。连接结果会通过 gattCallback 中的 onConnectionStateChange 回调返回。newState 为 BluetoothProfile.STATE_CONNECTED 说明连接建立成功这时需要调用 gatt.discoverServices() 去发现设备上的服务。服务发现完成后会进入 onServicesDiscovered 回调只有在收到这个回调之后才能去读取特征值。我见过不少源码直接在 onConnectionStateChange 里就对特征值执行读写结果怎么都失败原因就是服务还没发现完。正确的顺序一定是连接成功 - discoverServices - onServicesDiscovered - 再获取 Service 和 Characteristic。这个顺序不能乱。4.3 特征读写与通知订阅BLE 设备的数据交互都是围绕 Characteristic 展开的。每个特征有它的属性支持读、写、通知中的一种或多种。写数据时调用 writeCharacteristic读数据时有两种方式直接 readCharacteristic 主动读或者开启通知让设备主动推送数据。开启通知订阅是 BLE 开发里最容易出问题的地方。除了调用 setCharacteristicNotification(characteristic, true) 之外还必须去写通知描述符 CCCD客户端特征配置描述符UUID 是 00002902-0000-1000-8000-00805f9b34fb写入 0x0001 才能打开通知。这才算真正完成订阅。很多源码漏了这一步导致设备端明明发了数据手机却收不到。对于用蓝牙 app 控制 ESP32 这类物联网场景整体链路就是手机 BLE 扫描到 ESP32 广播的服务 UUID连接后找到对应的写特征通过 writeCharacteristic 下发控制指令ESP32 执行完后通过通知特征上报状态。理解了 GATT 的服务-特征模式这套流程非常清晰。5. 蓝牙测距与信号强度应用5.1 RSSI 测距原理热词里频繁出现“蓝牙测距”这里聊一聊。蓝牙设备本身不直接返回“距离”返回的是信号强度 RSSI单位是 dBm。RSSI 测距的原理是利用信号在空间传播时会衰减距离越远信号越弱通过路径损耗公式反推距离。常用的简化公式是d 10^((A - RSSI) / (10 * n))其中 A 是设备距离目标 1 米时的参考 RSSI一般是 -59 到 -65 之间n 是环境衰减因子空旷空间取 2室内有墙壁反射时取 3 到 4。举个例子假设 A -59n 2实测 RSSI 为 -70带入公式就是 10 的 ((59 - 70) / 20) 次方约等于 3.55 米。参数选得准不准直接决定测距误差大不大。5.2 实测校准与工程落地在实际工程里我强烈不建议直接套用网上的默认参数因为不同手机的蓝牙天线灵敏度不一样同一台手机在不同环境下的信号波动也很夸张。需要做校准拿着手机在距离设备 1 米处记录 RSSI多采几组取平均值作为 A在 2 米、3 米处分别测几组反推吃掉 n 的合理区间。另一个关键经验是信号滤波。蓝牙 RSSI 抖动非常大同一位置可能一次是 -55下一次变成 -75。我在项目里用滑动窗口平均加中值滤波取最近 5 次 RSSI 去掉最大最小值再求平均测距稳定性明显提升。再进阶一些可以上卡尔曼滤波但大多数场景下中值滤波已经够用。这类测距技术在智能防丢器、室内定位、人员接近检测里都有应用。需要注意的是 WiFi 和人体干扰很严重人站在设备和手机之间会直接挡住信号使用场景要考虑这个限制。6. 常见问题排查与源码复用建议6.1 设备扫描不到结果这是蓝牙开发中第一高频问题。排查顺序先看权限Android 6 到 11 有没有申请定位权限Android 12 以上有没有申请 BLUETOOTH_SCAN。再看蓝牙是否真的开启了、设备是否处在可被发现模式。最后看代码里有没有加 UUID 或 MAC 过滤器过滤条件错了同样扫不到。部分手机上如果开启了省电模式或者电池优化白名单设置异常扫描会被系统静默中断这种情况下需要提示用户关闭电池优化。6.2 配对成功后连接卡住连接卡住最常见的诱因是 UUID 不匹配。其次是在扫描还没停止时就发起了 socket 连接系统还占着发现资源连接就会一直卡在底层。第三步是子线程问题蓝牙 socket 的连接必须在子线程但很多人用 Thread 直接 new 然后 start一旦连接超时没有超时机制线程就会一直阻塞。我的处理办法是给 socket 连接包一层超时控制用 CountDownLatch 配合 try-catch超时 10 秒就释放连接避免用户点了一次连接后整个界面无响应。6.3 连接后收不到数据经典蓝牙里收不到数据多半是输入流读取线程没有正确启动或者对端设备根本没有发送数据。BLE 里收不到数据优先检查有没有写 CCCD 开启通知其次检查服务 UUID、特征 UUID 是否对得上。开发时可以利用 Wireshark 抓取蓝牙包来分析数据链路配合日志打印 RSSI 和连接状态能更快定位问题。6.4 源码复用建议最后说一下怎么把一份源码真正吃透并在新项目里复用。我的建议是不要直接复制整个工程而是把蓝牙相关的核心类抽出来整理成独立的模块。重点是确认它封装了扫描、连接、收发、状态回调四件事并且状态回调是异步的与 UI 解耦。拿到源码后我会先打印一份连接状态脑图空闲、扫描中、连接中、已连接、收发数据中、断开。把状态机理清楚蓝牙开发的复杂度能下降一大半。然后针对不同设备把设备名、服务 UUID、特征 UUID 等配置抽成常量或配置文件新项目接入时只需改配置不需要动核心代码。实测下来这种改造方式新设备从拿到手到跑通功能半天时间就够用了。我自己在实际项目中还有一个习惯每份源码拿到手先跑一次扫描把周围所有设备的 MAC、广播名称、RSSI 打出来。这既能验证环境没问题又能直观感受不同设备在信号表现上的差异。蓝牙开发的代码逻辑往往不复杂真正的难点全在细节和兼容性上。把扫描、连接、收发、回调这套链路理解透了你在任何蓝牙源码面前都不会慌。本文还有配套的精品资源点击获取
返回列表