Android蓝牙开发实战:从经典蓝牙到BLE,构建稳定通信应用
2026/9/4 4:35:41 网站建设 项目流程

简介:这是一套面向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 12(API 31)开始,如果应用需要扫描附近的蓝牙设备,除了位置权限,可能还需要声明BLUETOOTH_SCANBLUETOOTH_CONNECT权限,并注意其运行时申请策略。我们的项目以兼容更广泛版本为主,但你在开发新应用时务必查阅最新官方文档。

2.2 核心依赖与Gradle配置

项目本身不依赖第三方蓝牙库,完全使用Android SDK自带的android.bluetoothandroid.bluetooth.le包。这能让我们更透彻地理解原生API的工作机制。在app/build.gradle文件中,主要需要关注的是targetSdkVersionminSdkVersion的设定。为了兼顾较旧的设备和新的API特性,一个常见的配置如下:

android { compileSdk 34 defaultConfig { applicationId "com.example.bluetoothassistant" minSdkVersion 21 // 支持BLE的最低版本 targetSdkVersion 34 versionCode 1 versionName "1.0" } // ... 其他配置 }

minSdkVersion设为21(Android 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.0(API 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 设备发现、配对与列表展示

开启蓝牙后,下一步是发现周围的设备。这里有两种方式:发现已配对设备发现新设备

  • 获取已配对设备:这是瞬时的,直接通过适配器获取列表。
    Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
  • 发现新设备:这是一个异步过程,需要注册一个BroadcastReceiver来接收BluetoothDevice.ACTION_FOUND广播。在开始发现前,记得先调用bluetoothAdapter.cancelDiscovery()取消可能正在进行的上一次发现,然后调用bluetoothAdapter.startDiscovery()

DeviceListActivity中,我们将这两种来源的设备合并到一个ListViewRecyclerView中展示。用户点击某个设备项时,我们获取其BluetoothDevice对象,它包含了后续连接所必需的MAC地址。

3.3 建立Socket连接与后台服务管理

这是经典蓝牙通信的核心。我们选择使用BluetoothSocketBluetoothServerSocket来模拟串口通信。通常,一个设备作为客户端(Client),另一个作为服务端(Server)。在我们的项目中,手机通常作为客户端去连接一个蓝牙模块(如HC-05)。

连接流程如下:

  1. 客户端(手机):通过BluetoothDevice.createRfcommSocketToServiceRecord(UUID)创建一个Socket。这个UUID必须与服务器端一致,通常使用标准的SPP(串口协议)UUID:"00001101-0000-1000-8000-00805F9B34FB"
  2. 建立连接:在非UI线程(如AsyncTaskThread)中调用socket.connect()。这是一个阻塞调用,会尝试连接直到成功或超时。
  3. 服务端(蓝牙模块):模块通常已内置为服务端,一直在监听连接。

为了管理这个可能耗时的连接过程以及连接后的持续通信,我们将其封装在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(); List<ScanFilter> 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进行连接。

  1. 连接:使用BluetoothDevice.connectGatt(Context, autoConnect, BluetoothGattCallback)方法。autoConnect参数设为false表示直接连接,设为true是自动重连,但后者行为不可靠,通常建议设为false并在应用层自己管理重连逻辑。
  2. 服务发现:连接成功后,在BluetoothGattCallback.onServicesDiscovered()回调中,调用gatt.discoverServices()开始发现服务。发现完成后,会触发onServicesDiscovered(),此时我们可以遍历gatt.getServices()来找到目标服务及其特征值。
  3. 数据读写与通知
    • :调用gatt.readCharacteristic(characteristic),结果在onCharacteristicRead()回调中返回。
    • :调用gatt.writeCharacteristic(characteristic)。写操作分为“有响应写”(WRITE_TYPE_DEFAULT)和“无响应写”(WRITE_TYPE_NO_RESPONSE)。后者速度更快,但不保证数据到达,适用于频繁发送且允许丢包的场景,如传感器数据流。
    • 通知(Notify/Indicate):这是BLE最常用的数据接收方式。首先,需要找到具有NOTIFYINDICATE属性的特征值。然后,向其CCCD描述符写入BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUEENABLE_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; // 已连接

任何连接状态的改变(成功、失败、断开),都通过HandlerLiveData/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.0(API 26)及以上,如果应用进入后台,普通Service很快会被停止。解决方案是使用startForegroundService()并创建一个前台通知,告知用户应用正在后台保持连接。

此外,从Android 10开始,对后台应用获取位置信息有了更严格的限制。如果你的应用需要在后台扫描BLE设备,必须申请ACCESS_BACKGROUND_LOCATION权限,并且需要在Google Play上声明符合其后台位置政策,审核非常严格。因此,对于大多数应用,建议只在应用处于前台时进行蓝牙扫描。

6. 从源码到APK:调试与优化实战

6.1 日志与调试技巧

蓝牙调试,日志是你的眼睛。除了常规的Log.d(),有几点特别有用:

  1. 启用蓝牙HCI日志:在手机的“开发者选项”中,开启“蓝牙HCI信息收集日志”。这会将底层的蓝牙指令包保存为文件,可以用Wireshark等工具分析,是解决疑难杂症的终极武器。
  2. 过滤日志:在Android Studio的Logcat中,使用tag:Bluetoothtag:BluetoothGatt等标签过滤,快速定位系统蓝牙栈的日志。
  3. 状态可视化:在App的调试版本中,在UI上直接显示详细的连接状态、发送/接收的字节数、错误码等,比看日志方便得多。

6.2 功耗与性能优化

  • 经典蓝牙:连接建立后,功耗相对稳定。优化点在于及时释放资源:断开连接时,务必按顺序关闭InputStream,OutputStream,Socket
  • BLE:功耗优化的核心在于扫描策略连接参数
    • 扫描:使用SCAN_MODE_LOW_POWER模式,并设置合理的扫描时长(例如,扫描10秒,休息30秒)。
    • 连接后:如果没有数据需要传输,可以考虑主动断开连接,而不是一直保持。对于需要长期监听通知的设备(如手环),连接后功耗主要由连接间隔决定,可以尝试请求一个合理的间隔值(如100ms-200ms)。
    • 及时清理:在ActivityServiceonDestroy()中,务必调用gatt.close()来释放BLE连接相关的原生资源,防止内存泄漏。

6.3 兼容性处理:应对千奇百怪的安卓设备

不同厂商(小米、华为、OPPO、vivo等)的Android系统对蓝牙的实现可能有细微差别,这是安卓开发的老大难问题。

  • 经典蓝牙连接失败:如前所述,准备反射调用作为备选方案。
  • BLE扫描不到设备
    • 确认位置权限已授予。
    • 部分厂商(如小米)有额外的“后台弹出界面”或“自启动”权限需要手动授予,否则扫描会被系统阻止。在App设置中引导用户开启。
    • 尝试不同的扫描模式(SCAN_MODE_LOW_LATENCY)。
  • BLE连接不稳定:有些设备在屏幕关闭后,为了省电会主动降低蓝牙性能或断开BLE连接。可以尝试在Service中获取WakeLock(唤醒锁)来暂时阻止CPU休眠,但需谨慎使用,以免过度耗电。

这个“蓝牙助手”项目的源码,就像一张精心绘制的地图,标出了Android蓝牙开发这条路上的主要地标和潜在陷阱。通读并运行它,再结合本文提到的原理和坑点,你不仅能复制出一个可用的蓝牙应用,更能建立起一套应对各种蓝牙通信需求的系统性方法。真正的熟练,来自于在理解原理的基础上,亲手解决掉那些编译、运行、调试过程中跳出来的、独一无二的问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询