Android BLE(Bluetooth Low Energy,低功耗蓝牙)连接流程涉及多个步骤,主要包括扫描、配对、连接三个阶段。以下是详细的流程梳理:
一、前提条件
1.1. 设备支持:确保Android设备支持BLE功能。从Android 4.3(API 级别 18)开始,Android系统内置了对BLE的支持。
1.2. 权限申请:在AndroidManifest.xml中申请必要的权限,包括BLUETOOTH、BLUETOOTH_ADMIN、BLUETOOTH_SCAN和BLUETOOTH_CONNECT等。
1.3. 动态权限请求:对于Android 6.0(API 级别 23)及以上版本,还需要在运行时请求位置权限(ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION),因为从Android 6.0开始,蓝牙扫描需要位置权限。
二、扫描BLE设备
在BLE建立连接之前,需要进行设备之间的扫描。BLE扫描主要分为两种模式:主动扫描(Active Scanning)和被动扫描(Passive Scanning)。这两种扫描模式在功耗、扫描范围和响应方式上存在显著差异。
2.1.BLE扫描的模式
2.1.1. 主动扫描(Active Scanning)
特点:
- 主动扫描模式下,扫描设备会主动发送扫描请求(Scan Request)给周围的BLE广播设备。
- 收到扫描请求的BLE设备会响应一个扫描响应(Scan Response),其中可能包含设备的名称、服务UUID等额外信息。
- 由于需要发送和接收数据,主动扫描的功耗相对较高,但能够获取更详细的设备信息。
应用场景:
- 适用于需要快速发现并连接BLE设备的场景,如蓝牙音箱、蓝牙耳机等外设的配对连接。
- 在需要获取设备额外信息的情况下,主动扫描是更合适的选择。
2.1.2. 被动扫描(Passive Scanning)
特点:
- 被动扫描模式下,扫描设备仅监听周围的BLE广播信号,不发送任何扫描请求。
- 因此,它只能接收到BLE设备广播的广告包(Advertising Packet),这些数据包中通常包含设备的MAC地址(但注意在BLE 5.0及以后版本中,出于隐私考虑,设备地址可能被随机化)、设备名称的一部分等信息。
- 被动扫描的功耗较低,因为不需要发送数据,但可能无法获取到设备的全部信息。
应用场景:
- 适用于低功耗要求的场景,如物联网设备(IoT)的监控和发现。
- 当仅需要快速检测周围BLE设备的存在,而不需要详细信息时,被动扫描是更优的选择。
2.1.3. 注意事项
- 在Android系统中,特别是在Android 6.0及以上版本,由于系统对后台应用进行了更严格的限制,BLE扫描在后台运行时可能会受到更多限制。
- 在熄屏状态下,Android系统可能会将BLE扫描限制为低功耗模式,即只能进行被动扫描。
- 实际开发中需要根据应用的实际需求和用户场景,选择合适的扫描模式。
2.2. 扫描流程的主要步骤
2.2.1. 获取蓝牙适配器
通过BluetoothManager获取BluetoothAdapter对象。使用BluetoothAdapter
的isEnabled()
方法来检查蓝牙是否已经开启。
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
// 蓝牙未开启
}
2.2.2. 开启蓝牙
如果蓝牙未开启,可以通过启动一个Intent
来请求用户启用蓝牙。这通常会导致系统显示一个对话框,询问用户是否允许启用蓝牙。
if (!bluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
其中,REQUEST_ENABLE_BT
是一个自定义的请求码,用于在onActivityResult()
方法中识别这个特定的请求。这样,当用户响应蓝牙启用请求后,应用就可以知道用户是否允许了蓝牙的启用。
2.2.3. 处理用户响应并开始扫描
重写onActivityResult()
方法来处理用户对蓝牙启用请求的响应。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == Activity.RESULT_OK) {
// 蓝牙已启用,继续执行BLE操作
} else {
// 用户拒绝了蓝牙的启用,处理这种情况
Toast.makeText(this, "蓝牙未开启,无法继续操作", Toast.LENGTH_SHORT).show();
}
}
super.onActivityResult(requestCode, resultCode, data);
}
如果用户允许了蓝牙的启用,可以继续执行BLE扫描和连接操作。使用BluetoothAdapter的getBluetoothLeScanner()方法获取BluetoothLeScanner对象,并调用其startScan()方法开始扫描BLE设备。可以设置扫描参数,如扫描模式(SCAN_MODE_LOW_LATENCY、SCAN_MODE_BALANCED、SCAN_MODE_LOW_POWER)和报告延迟等。
BluetoothLeScanner
类是用于执行BLE设备扫描的核心类,该类中的startScan
方法允许以更灵活和强大的方式开始BLE设备的扫描。可以指定扫描过滤器(ScanFilter
)、扫描设置(ScanSettings
)以及扫描回调(ScanCallback
),以便在发现设备时接收通知。
以下是startScan
方法的一些重载版本及其简要说明:
-
带有扫描回调的startScan(List<ScanFilter> filters, ScanSettings settings, ScanCallback callback):这是最常用的版本,它允许指定一个或多个扫描过滤器(用于匹配特定的设备广告数据),扫描设置(如扫描模式、报告延迟等),以及一个扫描回调(用于接收扫描结果)。 示例用法:
List<ScanFilter> filters = new ArrayList<>();
// 可以根据需要添加ScanFilter到列表中
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 设置扫描模式
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // 设置回调类型
.build();
ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
// 处理扫描结果
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
// 处理批量扫描结果(如果启用了批量扫描)
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
// 处理扫描失败的情况
}
};
bluetoothLeScanner.startScan(filters, settings, scanCallback);
-
带有PendingIntent的
startScan(List<ScanFilter> filters, ScanSettings settings, PendingIntent callbackIntent)
(不常用):
这个版本的startScan
方法通过PendingIntent
来接收扫描结果,这种方法允许应用在不直接运行的情况下(例如,当应用处于后台或被系统挂起时),通过广播接收器(Broadcast Receiver)或其他方式接收扫描到的BLE设备信息。然而,在大多数情况下,直接使用ScanCallback
会更简单、更直接。
下面是一个基本的示例:首先,需要创建一个 PendingIntent
,它指向一个能够接收扫描结果的广播接收器(Broadcast Receiver):
Intent intent = new Intent(context, YourBroadcastReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
然后,使用
startScan
方法,并传入扫描过滤器、扫描设置和上面创建的
PendingIntent
:
List<ScanFilter> filters = new ArrayList<>();
// 如果需要,可以向filters列表中添加ScanFilter
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
bluetoothLeScanner.startScan(filters, settings, pendingIntent);
在广播接收器中,需要处理接收到的扫描结果。但是,需要注意的是,BLE扫描的 PendingIntent
并不直接传递扫描结果作为Intent的额外数据(extras)。相反,当扫描到匹配的设备时,系统会发送一个带有特定动作(action)的广播,而广播接收器需要监听这个action,并可能需要通过其他方式(如查询最新的扫描结果)来获取具体的设备信息。
2.2.4. 处理扫描结果
通过BluetoothLeScanner.ScanCallback的onScanResult(int callbackType, ScanResult result)
回调中,可以处理扫描到的BLE设备信息。这个回调方法会在每次扫描到新的BLE设备或已扫描到的设备更新了其广播数据时被调用。以下是如何在onScanResult
回调中处理BLE设备信息的基本步骤:
ScanCallback scanCallback =
new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
if (callbackType != ScanSettings.CALLBACK_TYPE_ALL_MATCHES) {
// Should not happen.
Log.e(TAG, "LE Scan has already started");
return;
}
ScanRecord scanRecord = result.getScanRecord();
if (scanRecord == null) {
return;
}
if (serviceUuids != null) {
List<ParcelUuid> uuids = new ArrayList<ParcelUuid>();
for (UUID uuid : serviceUuids) {
uuids.add(new ParcelUuid(uuid));
}
List<ParcelUuid> scanServiceUuids = scanRecord.getServiceUuids();
if (scanServiceUuids == null
|| !scanServiceUuids.containsAll(uuids)) {
if (DBG) {
Log.d(TAG, "uuids does not match");
}
return;
}
}
callback.onLeScan(
result.getDevice(), result.getRssi(), scanRecord.getBytes());
}
};
2.2.5. 停止扫描
在适当的时候(比如找到了目标设备或者用户请求停止扫描时),调用BluetoothLeScanner
的stopScan()
方法来停止扫描。这有助于减少不必要的功耗,并避免在不再需要时继续接收扫描结果。
// 停止扫描
scanner.stopScan(scanCallback);
注意:在上面的stopScan()调用中,scanCallback是传递给startScan()方法的同一个ScanCallback实例。然而,从Android API 21(Lollipop)开始,stopScan()方法实际上并不要传递任何参数。如果没有引用到特定的ScanCallback实例,或者只是想停止所有扫描(无论是由哪个ScanCallback触发的),可以简单地调用无参数的stopScan()方法。
// 更简单的停止扫描方式(不需要传递ScanCallback)
scanner.stopScan();
实际开发中结合具体需求和Android API版本选择适当的方法。
三、配对BLE设备(可选)
BLE设备通常不需要像经典蓝牙那样进行配对,因为它们通过密钥交换来确保通信安全。但是,某些BLE设备可能要求配对或绑定操作,可以通过调用BluetoothDevice
对象的createBond()
方法来发起配对请求。这个过程对于需要更高安全性的通信场景尤为重要,因为配对过程中会生成并存储密钥,这些密钥在未来的连接中用于身份验证和加密通信,从而保护数据的机密性和完整性。
3.1. 配对过程
3.1.1. 发起配对请求
通过调用BluetoothDevice的createBond()方法,可以向远程蓝牙设备发起配对请求。这个方法会返回一个布尔值,但在Android中,由于配对是异步进行的,这个返回值通常不会被用来判断配对是否成功。相反,为了确定配对是否成功,需要注册一个BroadcastReceiver来监听BluetoothDevice.ACTION_BOND_STATE_CHANGED这一系统广播。当蓝牙设备的配对状态发生变化时(如从未配对变为正在配对,或从正在配对变为已配对或配对失败),系统会发送这个广播。
在BroadcastReceiver
的onReceive()
方法中,通过检查Intent
中的BluetoothDevice.EXTRA_DEVICE
和BluetoothDevice.EXTRA_BOND_STATE
来获取发生状态变化的蓝牙设备和其新的配对状态。配对状态可以是以下几种之一:
BluetoothDevice.BOND_NONE
:表示设备未配对。BluetoothDevice.BOND_BONDING
:表示设备正在配对过程中。BluetoothDevice.BOND_BONDED
:表示设备已经成功配对。
通过检查这些状态,你可以在UI中相应地更新状态,或者在配对成功后执行其他操作(如建立BLE连接)。
此外,值得注意的是,在某些情况下(如用户取消配对请求或设备不支持配对),配对可能会失败。在这些情况下,你也应该在UI中向用户显示适当的错误消息。
最后,别忘了在不再需要监听配对状态变化时注销BroadcastReceiver
,以避免内存泄漏或其他潜在问题。
3.1.2. 监听配对状态
为了获知配对的状态,需要注册一个BroadcastReceiver
来监听BluetoothDevice.EXTRA_BOND_STATE
和BluetoothDevice.EXTRA_DEVICE
这两个Intent extra来获取配对状态和相关的BluetoothDevice
对象。
3.1.3. 处理配对结果
- 如果配对成功(即状态变为
BOND_BONDED
),就可以安全地与该设备进行通信了。Android系统会自动保存配对信息,并在后续的连接尝试中自动使用这些信息进行身份验证。 - 如果配对失败(例如,用户拒绝了配对请求或设备不支持配对),应该在UI中向用户显示适当的错误消息,并可能提供重试配对的选项。
3.1.4. 更新UI或执行后续操作
根据配对状态,可以在UI中显示相应的消息,或者执行其他必要的操作(如尝试建立BLE连接)。
3.1.5. 注销BroadcastReceiver
当不再需要监听配对状态变化时(例如,当用户离开包含蓝牙功能的Activity时),应该注销BroadcastReceiver
以避免内存泄漏。
3.2. 代码示例
下面是一个简化的代码示例,展示了如何调用createBond()
方法并注册一个BroadcastReceiver
来监听配对状态变化:
// 假设已经有了BluetoothDevice对象 bluetoothDevice
// 调用createBond()方法(注意:这个方法不返回配对结果)
boolean result = bluetoothDevice.createBond();
// 注意:这里的result只是表示请求是否已经被系统接收,并不表示配对是否成功
// 注册BroadcastReceiver来监听配对状态变化
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(mBondStateChangeReceiver, filter);
// BroadcastReceiver的实现
private final BroadcastReceiver mBondStateChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == bluetoothDevice) {
final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
// 处理配对状态变化...
switch (state) {
case BluetoothDevice.BOND_NONE:
// 未配对
break;
case BluetoothDevice.BOND_BONDING:
// 正在配对
break;
case BluetoothDevice.BOND_BONDED:
// 已配对
break;
}
}
}
}
};
// 不要忘记在适当的时候注销BroadcastReceiver
unregisterReceiver(mBondStateChangeReceiver);
3.3. 注意事项
- 并非所有BLE设备都支持配对。一些设备可能只使用基于密钥的加密(如AES加密)来保护通信,而不需要传统的配对过程。
- 配对过程可能需要用户交互,特别是在Android 6.0(API级别23)及更高版本上,由于隐私和安全性的考虑,系统可能会限制对蓝牙扫描和连接行为的访问。
- 配对信息(包括密钥)是敏感信息,应该妥善保管,并在不再需要时及时删除。然而,在Android中,一旦配对成功,通常不需要手动删除配对信息,因为系统会自动管理这些信息。但是,用户可以在系统设置中手动取消配对。
- 在实际进行BLE开发时,查阅最新的Android文档和蓝牙规范,以了解最新的安全特性和最佳实践。
四、连接BLE设备
连接到BLE设备的GATT Server并与之交互的过程,通常包括连接设备、发现服务、读写数据等步骤。以下是一个简化的流程说明,以及如何在Android应用中使用这些步骤的示例代码。
4.1. 连接到GATT Server
首先,需要有一个BluetoothDevice
对象,通常是通过扫描BLE设备并选择一个来获得的。然后,可以使用connectGatt()
方法来尝试连接到该设备的GATT Server。
BluetoothGatt bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback);
onnectGatt
方法参数和返回值的说明:
-
Context context
:是一个上下文对象,用于应用级别的操作,如启动服务、发送广播或加载资源。在大多数情况下,可以使用当前的Activity或Application的上下文。 -
boolean autoConnect
:这个参数指定是否应该自动连接到BLE设备。如果设置为true
,则当BLE设备变为可连接状态时,系统会自动尝试连接。如果设置为false
,则系统将立即尝试连接,如果BLE设备当前不可连接,则连接将失败。 -
BluetoothGattCallback callback
:是一个回调接口,用于接收来自BLE设备的异步事件。这个接口包含多个回调方法,如onConnectionStateChange()
(当连接状态发生变化时调用)、onServicesDiscovered()
(当发现BLE设备的服务时调用)、onCharacteristicRead()
(当读取特性值时调用)等。需要在实现这个接口的类中定义这些方法,以处理BLE通信中的不同事件。
-
请注意,BLE通信是异步的,因此不能在调用
connectGatt()
之后立即执行特征读写等操作,而应该在onConnectionStateChange()
回调中等待连接成功后再执行这些操作。同样地,在onServicesDiscovered()
回调中,应该遍历发现的服务和特征,以便找到想要交互的特定特征。
4.2. 处理连接回调
在BluetoothGattCallback的onConnectionStateChange()方法中处理连接状态的改变。当连接状态变为BluetoothGatt.STATE_CONNECTED时,表示连接成功。
private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
// 连接成功,开始发现服务
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
// 连接丢失,处理断开连接的情况
// ...
}
}
// 其他回调方法,如onServicesDiscovered(), onCharacteristicRead()等
};
4.3. 发现服务
连接成功后,调用BluetoothGatt的discoverServices()方法来发现设备提供的所有服务。服务发现的结果将在onServicesDiscovered()
回调中返回。
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 服务发现成功,可以开始读取或写入数据
// 遍历gatt.getServices()获取服务列表
// ...
} else {
// 服务发现失败
// ...
}
}
4.4. 读写数据
通过服务中的特性(Characteristic)来读写数据。可以使用BluetoothGatt的readCharacteristic()或writeCharacteristic()方法来进行数据的读写操作。
// 假设你已经找到了一个BluetoothGattCharacteristic对象名为characteristic
// 读取数据
bluetoothGatt.readCharacteristic(characteristic);
// 写入数据
characteristic.setValue(data); // data是一个byte[],包含你想要写入的数据
bluetoothGatt.writeCharacteristic(characteristic);
// 注意:写操作可能需要在`onCharacteristicWrite()`回调中确认是否成功
五、断开连接
5.1. 调用disconnect()
方法
在不再需要BLE连接时,应调用BluetoothGatt的disconnect()方法来断开连接。
if (bluetoothGatt != null) {
bluetoothGatt.disconnect();
}
5.2. 设置标志以跟踪连接状态
同样地,由于断开连接是异步的,可能需要设置一个标志来跟踪连接状态,并在onConnectionStateChange()回调中处理断开连接后的逻辑。
private boolean isConnected = false;
// ...
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
isConnected = true;
// 连接成功,开始发现服务
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
isConnected = false;
// 连接已断开,清理资源
if (gatt != null) {
gatt.close();
bluetoothGatt = null; // 避免内存泄漏
}
// 处理断开连接后的逻辑
// ...
}
}
5.3. 释放BluetoothGatt对象
一旦连接状态变为STATE_DISCONNECTED
,并且确定不再需要BluetoothGatt
对象,就应该调用close()
方法来释放它。这有助于避免资源泄漏,特别是如果应用可能会频繁地连接和断开BLE设备。
在上面的onConnectionStateChange()
回调示例中,当连接状态变为STATE_DISCONNECTED
时,我们调用了gatt.close()
并将bluetoothGatt
设置为null
,以确保没有引用指向该对象,从而允许垃圾回收器回收它。
5.4. 清理其他资源
除了BluetoothGatt
对象外,如果应用还使用了其他与BLE相关的资源(如线程、定时器、监听器等),也应该在断开连接后适当地清理它们。
5.5. 注意事项
- 确保在调用
disconnect()
之前,BluetoothGatt
对象不是null
。- 调用
disconnect()
后,不要立即调用close()
,因为disconnect()
是异步的,并且close()
应该在连接状态确实变为STATE_DISCONNECTED
后调用。- 如果应用可能会频繁地连接和断开BLE设备,请确保你的逻辑能够处理这种情况,以避免资源耗尽或性能问题。
- 始终在UI线程之外执行BLE操作,以避免阻塞UI。如果你需要在UI中显示结果,请使用
Handler
或runOnUiThread()
等方法将结果传回UI线程。
以上即为Android BLE蓝牙的扫描、配对与连接流程的大致梳理。需要注意的是,不同设备和不同应用场景下,具体的实现细节可能会有所不同。