如何使用 BluetoothAdapter 和 BluetoothGatt 实现 BLE 设备连接?
解读
国内面试中,BLE 连接是 Android 端 IoT/健康/车载岗位的高频考点。面试官不仅想知道“能不能连”,更关注“连得稳、断得优雅、权限合规、功耗可控”。因此,回答要覆盖:运行时权限、扫描过滤、连接参数、主线程与 Binder 线程隔离、异常重试、12/19 字节 MTU 协商、国产 ROM 后台限制、以及 Google 从 2021 年起强制启用的 BLUETOOTH_CONNECT 权限适配。回答顺序遵循“权限→扫描→连接→发现服务→读写→断开”完整闭环,并给出可直接落地的代码骨架。
知识点
- 权限模型:Android 12+ 的 BLUETOOTH_CONNECT、BLUETOOTH_SCAN、BLUETOOTH_ADVERTISE 为 runtime permission;旧版本用 LOCATION 权限。国产 ROM(小米、华为)在后台扫描时需额外“定位权限+GPS 开关”。
- 扫描优化:ScanFilter 指定 ServiceUuid 减少 CPU 唤醒;ScanSettings.SCAN_MODE_LOW_LATENCY 仅前台使用,后台必须 LOW_POWER 且加 10 min 轮询,否则被系统 kill。
- 连接与并发:BluetoothDevice.connectGatt() 返回 BluetoothGatt,必须在主线程调用;并发连接数 ≤7(Bluetooth 芯片固件限制)。国产 ROM 对后台应用限制 1 个。
- Gatt 回调线程:onConnectionStateChange 在 Binder 线程,不能直接在里做耗时操作;需 Handler 切换到主线程再操作 UI。
- MTU 与 PHY:requestMtu(247) 在 onServicesDiscovered 后发起;国内手环普遍只支持 23→185,失败需 fallback。requestConnectionPriority(CONNECTION_PRIORITY_HIGH) 可缩短连接间隔到 7.5 ms,但耗电+30%。
- 断开与重连:gatt.disconnect() 与 close() 必须成对出现,否则 BluetoothService 句柄泄漏导致 133 错误。国产 ROM 后台断开 5 min 后自动清理缓存,需刷新 BluetoothAdapter 再扫。
- 省电与后台:WorkManager + 前台服务提升进程优先级;扫描时使用 PendingIntent 扫描替代长驻 Service,适配 Android 12 后台启动限制。
- 安全合规:配对阶段如果设备使用 Just-Works,需弹窗提醒用户“明文传输风险”;若设备支持 LESC,主动启用 BluetoothDevice.createBond() 触发配对,避免明文泄露数据。
答案
以下代码演示“最小可运行 + 国内 ROM 兼容”的 BLE 连接流程,可直接在 Android 12 真机通过。
class BleConnector(private val context: Application) {
private val mainHandler = Handler(Looper.getMainLooper())
private var bluetoothGatt: BluetoothGatt? = null
private val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val adapter: BluetoothAdapter
get() = bluetoothManager.adapter
/* ---------- 权限 ---------- */
private val permissions12 = arrayOf(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
)
private val permissionsLegacy = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
fun checkPermission(activity: Activity, block: () -> Unit) {
val target = if (Build.VERSION.SDK_INT >= 31) permissions12 else permissionsLegacy
val lacked = target.filter {
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
}
if (lacked.isEmpty()) block()
else ActivityCompat.requestPermissions(activity, lacked.toTypedArray(), 1001)
}
/* ---------- 扫描 ---------- */
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val name = result.device.name ?: return
if (name.startsWith("MyDevice")) {
adapter.bluetoothLeScanner?.stopScan(this)
connect(result.device)
}
}
}
fun startScan() {
val filter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000fff0-0000-1000-8000-00805f9b34fb"))
.build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
adapter.bluetoothLeScanner?.startScan(listOf(filter), settings, scanCallback)
}
/* ---------- 连接 ---------- */
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
mainHandler.post { gatt.discoverServices() }
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
mainHandler.post { close() }
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt.requestMtu(247) // 国产手环常见 185
val service = gatt.getService(UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"))
val char = service?.getCharacteristic(UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb"))
char?.let { enableNotification(gatt, it) }
}
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) Log.d("BLE", "MTU=$mtu")
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val data = characteristic.value
Log.d("BLE", "Notify data=${data.toHexString()}")
}
}
private fun connect(device: BluetoothDevice) {
bluetoothGatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
}
private fun enableNotification(gatt: BluetoothGatt, char: BluetoothGattCharacteristic) {
gatt.setCharacteristicNotification(char, true)
val descriptor = char.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
/* ---------- 写数据 ---------- */
fun write(cmd: ByteArray) {
bluetoothGatt?.let { gatt ->
val service = gatt.getService(UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"))
val char = service?.getCharacteristic(UUID.fromString("0000fff2-0000-1000-8000-00805f9b34fb"))
char?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
char?.value = cmd
gatt.writeCharacteristic(char)
}
}
/* ---------- 断开 ---------- */
fun close() {
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
bluetoothGatt = null
}
}
使用方式(在 Activity 内):
val connector = BleConnector(application)
connector.checkPermission(this) {
connector.startScan()
}
要点回顾:
- 权限申请放在最前,Android 12 与旧版本分支处理。
- 扫描过滤指定 ServiceUuid,减少系统唤醒。
- connectGatt 第三个参数传 TRANSPORT_LE,避免国产双模芯片回退到 BR/EDR。
- onConnectionStateChange 的 status 非 0 时记录错误码,133 表示缓存失效,需刷新 BluetoothAdapter。
- disconnect() 与 close() 必须成对,防止 fd 泄漏。
拓展思考
- 断线重连策略:使用指数退避(1 s、2 s、4 s… 到 64 s),并在国产 ROM 后台限制场景下结合 WorkManager 拉起前台服务,保证重连进程存活。
- 高并发场景:车载蓝牙钥匙需同时连接 4 把钥匙,需自行维护“连接池”队列,限制并发 ≤7,超出则排队等待 onDisconnected 回调后再补位。
- 安全加固:对写特征值使用 AES-CCM 加密,密钥通过 LESC 配对阶段协商;在 TEE 中存储 LTK,防止国产 ROM 被 root 后 dump 出密钥。
- 功耗压测:使用 Battery Historian 观察扫描占空比,目标后台扫描电流 < 1 mA;若超过,改用 PendingIntent 扫描 + 10 min 间隔,符合国内终端厂商功耗白名单。
- 折叠屏适配:折叠展开导致 Activity 重建,需在 ViewModel 中持有 BleConnector,并通过 rememberSaveable 保存 MacAddress,重建后自动 reconnectGatt 复用缓存,避免用户重新扫描。