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

资讯详情

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

Android蓝牙开发实战:从经典蓝牙到BLE,构建稳定通信应用

Android蓝牙开发实战:从经典蓝牙到BLE,构建稳定通信应用 简介这是一套面向Android开发学习者与物联网通信实践者的蓝牙应用开发完整源码聚焦移动端蓝牙设备发现、配对连接、双向数据传输等核心功能实现适用于智能硬件调试、蓝牙调试工具开发及移动通信课程实训。资源共47个文件包含11个XML布局与配置文件定义界面结构与权限声明、7个Java源文件承载蓝牙扫描、Socket通信、UI控制器等核心逻辑、5个PNG图标资源及多个构建配置文件如project.properties、.classpath、AndroidManifest.xml整体压缩包仅1.22MB轻量易读。已有76人下载学习适合从零掌握Android蓝牙通信全链路——涵盖UI交互设计、系统权限适配、后台服务保活、广播状态监听、异常容错处理及轻量数据持久化等关键环节代码模块清晰、注释规范可直接导入AS运行并按需拓展BLE或跨平台通信能力。1. 项目背景与核心价值如果你正在学习Android开发或者想给自己的智能硬件项目加一个手机控制端那么蓝牙通信绝对是一个绕不开的实战课题。市面上的教程要么太老要么只讲个大概真到自己动手时从权限申请到数据收发再到连接稳定性处理处处是坑。今天我就以一个完整的“蓝牙助手”项目源码为例带你从零到一彻底搞懂Android蓝牙应用开发的核心流程和那些官方文档里不会写的细节。这个项目麻雀虽小五脏俱全涵盖了经典蓝牙Bluetooth Classic和低功耗蓝牙BLE两种模式从设备搜索、配对连接到数据收发、连接状态管理最后打包成一个可安装的APK。无论你是想做一个遥控小车、智能家居控制面板还是与单片机通信这套代码和思路都能直接拿来用。2. 项目架构与环境搭建2.1 技术选型与项目结构这个“蓝牙助手”项目采用标准的Android原生开发语言是Java开发环境是Android Studio。选择Java而非Kotlin主要是考虑到其广泛的受众基础和教程资源方便更多开发者理解和上手。项目结构清晰遵循了Android开发的最佳实践MainActivity: 应用的主界面和入口负责管理经典蓝牙的设备列表、连接状态显示和发送数据。BleActivity: 专门处理低功耗蓝牙BLE的界面包含BLE设备的扫描、连接、服务与特征值发现及数据读写。BluetoothService: 一个后台Service用于管理经典蓝牙的Socket连接。将连接逻辑放在Service中是为了避免因Activity生命周期如屏幕旋转导致的连接中断这是保证连接稳定性的关键设计。BluetoothLeService: 对应BLE连接的后台Service封装了Android BLE API的核心操作如连接、发现服务、设置通知等。DeviceListActivity: 用于弹出设备选择列表的Activity在经典蓝牙和BLE扫描中都会用到。权限与配置: 在AndroidManifest.xml中声明了必要的蓝牙权限包括BLUETOOTH,BLUETOOTH_ADMIN以及Android 6.0需要动态申请的位置权限ACCESS_FINE_LOCATION因为蓝牙扫描需要。注意从Android 12API 31开始如果应用需要扫描附近的蓝牙设备除了位置权限可能还需要声明BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限并注意其运行时申请策略。我们的项目以兼容更广泛版本为主但你在开发新应用时务必查阅最新官方文档。2.2 核心依赖与Gradle配置项目本身不依赖第三方蓝牙库完全使用Android SDK自带的android.bluetooth和android.bluetooth.le包。这能让我们更透彻地理解原生API的工作机制。在app/build.gradle文件中主要需要关注的是targetSdkVersion和minSdkVersion的设定。为了兼顾较旧的设备和新的API特性一个常见的配置如下android { compileSdk 34 defaultConfig { applicationId com.example.bluetoothassistant minSdkVersion 21 // 支持BLE的最低版本 targetSdkVersion 34 versionCode 1 versionName 1.0 } // ... 其他配置 }将minSdkVersion设为21Android 5.0可以确保绝大多数设备支持BLE。如果你的应用只针对经典蓝牙可以降到更低的版本。3. 经典蓝牙Bluetooth Classic通信全解析经典蓝牙适用于持续、大数据量的传输场景比如音频流、文件传输以及我们项目中实现的这种双向串口式通信。3.1 权限申请与蓝牙适配器检查一切操作始于BluetoothAdapter。它是蓝牙功能的门户。首先我们需要在代码中检查设备是否支持蓝牙以及蓝牙是否已开启。BluetoothAdapter bluetoothAdapter BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter null) { // 设备不支持蓝牙 Toast.makeText(this, 此设备不支持蓝牙, Toast.LENGTH_SHORT).show(); finish(); return; } if (!bluetoothAdapter.isEnabled()) { // 蓝牙未开启可以发送Intent请求用户开启或直接启用需要BLUETOOTH_ADMIN权限 Intent enableBtIntent new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }对于Android 6.0API 23及以上版本必须在运行时动态申请位置权限否则扫描不到任何设备。这是一个非常容易踩坑的地方。if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_LOCATION_PERMISSION); }3.2 设备发现、配对与列表展示开启蓝牙后下一步是发现周围的设备。这里有两种方式发现已配对设备和发现新设备。获取已配对设备这是瞬时的直接通过适配器获取列表。SetBluetoothDevice pairedDevices bluetoothAdapter.getBondedDevices();发现新设备这是一个异步过程需要注册一个BroadcastReceiver来接收BluetoothDevice.ACTION_FOUND广播。在开始发现前记得先调用bluetoothAdapter.cancelDiscovery()取消可能正在进行的上一次发现然后调用bluetoothAdapter.startDiscovery()。在DeviceListActivity中我们将这两种来源的设备合并到一个ListView或RecyclerView中展示。用户点击某个设备项时我们获取其BluetoothDevice对象它包含了后续连接所必需的MAC地址。3.3 建立Socket连接与后台服务管理这是经典蓝牙通信的核心。我们选择使用BluetoothSocket和BluetoothServerSocket来模拟串口通信。通常一个设备作为客户端Client另一个作为服务端Server。在我们的项目中手机通常作为客户端去连接一个蓝牙模块如HC-05。连接流程如下客户端手机通过BluetoothDevice.createRfcommSocketToServiceRecord(UUID)创建一个Socket。这个UUID必须与服务器端一致通常使用标准的SPP串口协议UUID00001101-0000-1000-8000-00805F9B34FB。建立连接在非UI线程如AsyncTask或Thread中调用socket.connect()。这是一个阻塞调用会尝试连接直到成功或超时。服务端蓝牙模块模块通常已内置为服务端一直在监听连接。为了管理这个可能耗时的连接过程以及连接后的持续通信我们将其封装在BluetoothService这个后台Service中。Service内部维护了连接线程、数据发送线程和接收线程。连接状态管理是重中之重。我们需要在Service中定义一系列Handler消息用于在后台线程和主线程UI线程之间传递连接状态如连接中、已连接、连接断开和数据。这样MainActivity只需绑定这个Service并注册一个回调接口就能实时更新UI。3.4 数据收发与线程安全连接建立后通过socket.getInputStream()和socket.getOutputStream()获取流对象就可以进行数据读写了。发送数据在Service中提供一个sendData(byte[] data)方法。该方法将数据写入一个线程安全的队列如LinkedBlockingQueue由一个专门的发送线程从队列中取出数据并通过OutputStream写入。// 伪代码示例 public void sendData(byte[] data) { if (mSendQueue ! null isConnected()) { mSendQueue.offer(data); // 非阻塞式入队 } }接收数据在一个独立的线程中循环调用InputStream.read()。这里的关键是缓冲区大小和协议解析。蓝牙传输是流式的对方发送的“一包”数据在接收端可能会被拆分成多次read调用。因此必须根据自定义的应用层协议例如固定包头、包长度、校验和来重组数据包。一个简单的做法是约定以特定字符如换行符\n作为帧结束符。踩坑经验直接在主线程或单一线程中进行阻塞式读写是绝对要避免的这会导致界面卡死。必须使用多线程并且妥善处理线程间的通信与同步。OutputStream.write()本身不是线程安全的如果多个地方同时调用发送需要加锁或使用单一发送线程。4. 低功耗蓝牙BLE通信深度剖析BLE是为间歇性、小数据量传输设计的功耗极低常见于手环、传感器等设备。4.1 BLE核心概念GATT、服务、特征值与描述符理解BLE通信必须掌握GATT通用属性协议模型。你可以把它想象成一个树形结构设备Device树的根代表一个BLE外设如心率带。服务Service树干上的主要枝干代表一个完整的功能单元如“电池服务”、“心率服务”。每个服务有一个唯一的UUID标识。特征值Characteristic枝干上的叶子是实际进行数据交互的单元。一个服务包含多个特征值。例如“心率服务”下可能有“心率测量特征值”用于通知心率数据和“心率控制点特征值”用于写入命令。特征值也有UUID并具有属性Properties如READ,WRITE,NOTIFY,INDICATE这决定了你能对它做什么操作。描述符Descriptor叶子上的更细小的部分用于描述或配置特征值。最常用的是Client Characteristic Configuration Descriptor (CCCD)用于开启或关闭NOTIFY/INDICATE通知。我们的BluetoothLeService核心工作就是连接设备 → 发现服务 → 找到我们感兴趣的特征值 → 根据其属性进行读写或订阅通知。4.2 BLE设备扫描与过滤BLE扫描使用BluetoothLeScanner。与经典蓝牙的广播发现不同BLE扫描可以更精确地过滤。BluetoothLeScanner scanner bluetoothAdapter.getBluetoothLeScanner(); ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 扫描模式平衡、低功耗、低延迟 .build(); ListScanFilter filters new ArrayList(); // 可以按设备名、MAC地址或服务UUID过滤 // filters.add(new ScanFilter.Builder().setDeviceName(MyDevice).build()); scanner.startScan(filters, settings, scanCallback); // scanCallback 处理扫描结果扫描优化建议在界面显示扫描结果列表时使用设备的MAC地址作为唯一标识来去重因为同一个设备可能会被扫描到多次。扫描是非常耗电的操作在找到目标设备或扫描超时后务必调用scanner.stopScan(scanCallback)。4.3 连接、服务发现与通信在BleActivity中用户点击扫描到的设备后我们通过BluetoothLeService进行连接。连接使用BluetoothDevice.connectGatt(Context, autoConnect, BluetoothGattCallback)方法。autoConnect参数设为false表示直接连接设为true是自动重连但后者行为不可靠通常建议设为false并在应用层自己管理重连逻辑。服务发现连接成功后在BluetoothGattCallback.onServicesDiscovered()回调中调用gatt.discoverServices()开始发现服务。发现完成后会触发onServicesDiscovered()此时我们可以遍历gatt.getServices()来找到目标服务及其特征值。数据读写与通知读调用gatt.readCharacteristic(characteristic)结果在onCharacteristicRead()回调中返回。写调用gatt.writeCharacteristic(characteristic)。写操作分为“有响应写”WRITE_TYPE_DEFAULT和“无响应写”WRITE_TYPE_NO_RESPONSE。后者速度更快但不保证数据到达适用于频繁发送且允许丢包的场景如传感器数据流。通知Notify/Indicate这是BLE最常用的数据接收方式。首先需要找到具有NOTIFY或INDICATE属性的特征值。然后向其CCCD描述符写入BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE或ENABLE_INDICATION_VALUE来启用通知。启用后设备发送的数据会通过onCharacteristicChanged()回调推送过来。一个巨大的坑BLE API的回调BluetoothGattCallback是异步的并且很多操作如write, read不能同时进行。你必须串行化这些操作。一个常见的做法是创建一个命令队列在前一个操作的回调完成后再执行下一个。否则你会遇到133等神秘错误。5. 项目源码中的关键实现与避坑指南5.1 连接状态机与UI同步无论是经典蓝牙还是BLE维护一个清晰的连接状态机至关重要。在我们的Service中通常定义几个常量public static final int STATE_NONE 0; // 未连接 public static final int STATE_LISTENING 1; // 监听中服务端 public static final int STATE_CONNECTING 2; // 连接中 public static final int STATE_CONNECTED 3; // 已连接任何连接状态的改变成功、失败、断开都通过Handler或LiveData/EventBus等机制通知UI层更新按钮、列表和状态提示。永远不要在后台线程直接操作UI。5.2 经典蓝牙的连接超时与重连socket.connect()默认超时时间较长且不可控。在实际项目中我强烈建议为其包装一个超时机制try { socket.connect(); } catch (IOException e) { try { // 第一种连接方式失败尝试反射调用隐藏API兼容某些设备 Method m device.getClass().getMethod(createRfcommSocket, new Class[] {int.class}); BluetoothSocket fallbackSocket (BluetoothSocket) m.invoke(device, 1); fallbackSocket.connect(); socket fallbackSocket; } catch (Exception e2) { // 连接失败通知UI updateState(STATE_NONE, e2.getMessage()); return; } }对于自动重连不要在线程里写死循环。应该使用一个可控制的Handler在连接断开后发送一个延迟消息如5秒后尝试重连并设置最大重试次数。5.3 BLE的“连接参数”与“连接间隔”这是一个高级但影响巨大的话题。连接间隔Connection Interval是主设备手机和从设备手环两次通信之间的时间间隔范围在7.5ms到4s之间。较短的间隔如20ms延迟低、数据吞吐快但耗电高较长的间隔如1s则相反。Android应用作为中心设备Central可以向外围设备Peripheral发起“连接参数更新请求”。但这需要设备固件支持。在BluetoothGattCallback.onConnectionStateChange()连接成功后可以尝试调用gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)来请求高速模式。但请注意这只是一个请求最终决定权在外围设备。5.4 后台运行与权限适配我们的BluetoothService被设计为startService()启动这样即使Activity退出蓝牙连接也能保持。但这也带来了新的问题Android系统对后台服务的限制越来越严格。在Android 8.0API 26及以上如果应用进入后台普通Service很快会被停止。解决方案是使用startForegroundService()并创建一个前台通知告知用户应用正在后台保持连接。此外从Android 10开始对后台应用获取位置信息有了更严格的限制。如果你的应用需要在后台扫描BLE设备必须申请ACCESS_BACKGROUND_LOCATION权限并且需要在Google Play上声明符合其后台位置政策审核非常严格。因此对于大多数应用建议只在应用处于前台时进行蓝牙扫描。6. 从源码到APK调试与优化实战6.1 日志与调试技巧蓝牙调试日志是你的眼睛。除了常规的Log.d()有几点特别有用启用蓝牙HCI日志在手机的“开发者选项”中开启“蓝牙HCI信息收集日志”。这会将底层的蓝牙指令包保存为文件可以用Wireshark等工具分析是解决疑难杂症的终极武器。过滤日志在Android Studio的Logcat中使用tag:Bluetooth或tag:BluetoothGatt等标签过滤快速定位系统蓝牙栈的日志。状态可视化在App的调试版本中在UI上直接显示详细的连接状态、发送/接收的字节数、错误码等比看日志方便得多。6.2 功耗与性能优化经典蓝牙连接建立后功耗相对稳定。优化点在于及时释放资源断开连接时务必按顺序关闭InputStream,OutputStream,Socket。BLE功耗优化的核心在于扫描策略和连接参数。扫描使用SCAN_MODE_LOW_POWER模式并设置合理的扫描时长例如扫描10秒休息30秒。连接后如果没有数据需要传输可以考虑主动断开连接而不是一直保持。对于需要长期监听通知的设备如手环连接后功耗主要由连接间隔决定可以尝试请求一个合理的间隔值如100ms-200ms。及时清理在Activity或Service的onDestroy()中务必调用gatt.close()来释放BLE连接相关的原生资源防止内存泄漏。6.3 兼容性处理应对千奇百怪的安卓设备不同厂商小米、华为、OPPO、vivo等的Android系统对蓝牙的实现可能有细微差别这是安卓开发的老大难问题。经典蓝牙连接失败如前所述准备反射调用作为备选方案。BLE扫描不到设备确认位置权限已授予。部分厂商如小米有额外的“后台弹出界面”或“自启动”权限需要手动授予否则扫描会被系统阻止。在App设置中引导用户开启。尝试不同的扫描模式SCAN_MODE_LOW_LATENCY。BLE连接不稳定有些设备在屏幕关闭后为了省电会主动降低蓝牙性能或断开BLE连接。可以尝试在Service中获取WakeLock唤醒锁来暂时阻止CPU休眠但需谨慎使用以免过度耗电。这个“蓝牙助手”项目的源码就像一张精心绘制的地图标出了Android蓝牙开发这条路上的主要地标和潜在陷阱。通读并运行它再结合本文提到的原理和坑点你不仅能复制出一个可用的蓝牙应用更能建立起一套应对各种蓝牙通信需求的系统性方法。真正的熟练来自于在理解原理的基础上亲手解决掉那些编译、运行、调试过程中跳出来的、独一无二的问题。本文还有配套的精品资源点击获取
返回列表