Android BLE 扫描连接与收发消息实战

BLE 在 Android 里最常见的开发链路其实就三步:先扫到设备,再连上设备,最后通过 GATT 读写特征值收发数据。

单看 API 不算多,但这套流程有个特点,所有步骤几乎都是异步回调,而且顺序不能乱。你如果把扫描、连接、发现服务、发消息、收消息全揉在一起,代码很快就会变得很难维护。

这篇就只讲一条最常用的链路:

  • 扫描 BLE 设备
  • 连接目标设备
  • 发现服务和特征值
  • 写入消息
  • 接收通知消息

示例代码用 Kotlin 写,目标是先把整个流程跑通。

先加权限

如果你 target 的是 Android 12 及以上,BLE 相关权限一般至少要这些:

xml 复制代码
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

如果你还要兼容 Android 11 及以下,通常还会加:

xml 复制代码
<uses-permission
    android:name="android.permission.ACCESS_FINE_LOCATION"
    android:maxSdkVersion="30" />

代码里也得在运行时申请:

kotlin 复制代码
private val blePermissions = arrayOf(
    Manifest.permission.BLUETOOTH_SCAN,
    Manifest.permission.BLUETOOTH_CONNECT
)

这一步没搞定,后面很多 BLE 问题都没法排查。

先准备几个 UUID

BLE 通信最终靠的是 service 和 characteristic。

假设我们的设备有这样一组 UUID:

kotlin 复制代码
val SERVICE_UUID: UUID =
    UUID.fromString("0000FFF0-0000-1000-8000-00805F9B34FB")

val WRITE_UUID: UUID =
    UUID.fromString("0000FFF1-0000-1000-8000-00805F9B34FB")

val NOTIFY_UUID: UUID =
    UUID.fromString("0000FFF2-0000-1000-8000-00805F9B34FB")

val CCCD_UUID: UUID =
    UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")

实际开发里,这几个 UUID 要跟硬件协议保持一致。

扫描设备

Android 扫描 BLE 设备最常用的是 BluetoothLeScanner

先拿到蓝牙相关对象:

kotlin 复制代码
val bluetoothManager =
    context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager

val bluetoothAdapter = bluetoothManager.adapter
val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner

再定义扫描回调:

kotlin 复制代码
private val scanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        val device = result.device
        Log.d("BLE", "scan result: ${device.address} name=${device.name}")
    }

    override fun onScanFailed(errorCode: Int) {
        Log.e("BLE", "scan failed: $errorCode")
    }
}

开始扫描:

kotlin 复制代码
fun startScan() {
    val settings = ScanSettings.Builder()
        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
        .build()

    bluetoothLeScanner.startScan(null, settings, scanCallback)
}

停止扫描:

kotlin 复制代码
fun stopScan() {
    bluetoothLeScanner.stopScan(scanCallback)
}

如果你已经知道目标设备地址,可以在 onScanResult() 里匹配后停止扫描,然后发起连接:

kotlin 复制代码
private var isConnecting = false
private val targetAddress = "AA:BB:CC:DD:EE:FF"

override fun onScanResult(callbackType: Int, result: ScanResult) {
    val device = result.device
    if (device.address == targetAddress && !isConnecting) {
        isConnecting = true
        stopScan()
        connect(device)
    }
}

连接设备

BLE 连接入口是 connectGatt()

kotlin 复制代码
private var bluetoothGatt: BluetoothGatt? = null

fun connect(device: BluetoothDevice) {
    bluetoothGatt = device.connectGatt(context, false, gattCallback)
}

这里的 gattCallback 是核心。扫描、连接、发现服务、写入、通知,几乎都靠它串起来。

先看一个最基本的版本:

kotlin 复制代码
private val gattCallback = object : BluetoothGattCallback() {

    override fun onConnectionStateChange(
        gatt: BluetoothGatt,
        status: Int,
        newState: Int
    ) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            Log.d("BLE", "connected")
            gatt.discoverServices()
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            Log.d("BLE", "disconnected")
            bluetoothGatt?.close()
            bluetoothGatt = null
            isConnecting = false
        }
    }

    override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            Log.d("BLE", "services discovered")
            enableNotification(gatt)
        }
    }

    override fun onCharacteristicWrite(
        gatt: BluetoothGatt,
        characteristic: BluetoothGattCharacteristic,
        status: Int
    ) {
        Log.d("BLE", "write status=$status")
    }

    override fun onCharacteristicChanged(
        gatt: BluetoothGatt,
        characteristic: BluetoothGattCharacteristic,
        value: ByteArray
    ) {
        val text = value.toString(Charsets.UTF_8)
        Log.d("BLE", "received: $text")
    }
}

这段代码把主链路串起来了:

  • 连上后发现服务
  • 服务发现后开启通知
  • 写入后收写回调
  • 设备主动推送时收通知回调

拿到 service 和 characteristic

服务发现以后,先把我们需要的特征值找出来:

kotlin 复制代码
private fun getWriteCharacteristic(gatt: BluetoothGatt): BluetoothGattCharacteristic? {
    val service = gatt.getService(SERVICE_UUID) ?: return null
    return service.getCharacteristic(WRITE_UUID)
}

private fun getNotifyCharacteristic(gatt: BluetoothGatt): BluetoothGattCharacteristic? {
    val service = gatt.getService(SERVICE_UUID) ?: return null
    return service.getCharacteristic(NOTIFY_UUID)
}

如果这一步拿不到,通常说明 UUID 配错了,或者设备根本没暴露这个服务。

开启通知收消息

BLE 收消息一般不是一直去轮询读,而是让设备主动通知。

先打开本地通知:

kotlin 复制代码
fun enableNotification(gatt: BluetoothGatt) {
    val characteristic = getNotifyCharacteristic(gatt) ?: return
    gatt.setCharacteristicNotification(characteristic, true)

    val descriptor = characteristic.getDescriptor(CCCD_UUID) ?: return
    descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
    gatt.writeDescriptor(descriptor)
}

这一步很多人容易漏掉 descriptor。

只调用 setCharacteristicNotification() 往往还不够,很多设备必须再写 CCCD 才会开始真正推送数据。

如果设备推消息,最终会走到:

kotlin 复制代码
override fun onCharacteristicChanged(
    gatt: BluetoothGatt,
    characteristic: BluetoothGattCharacteristic,
    value: ByteArray
) {
    val hex = value.joinToString(" ") { "%02X".format(it) }
    Log.d("BLE", "notify: $hex")
}

如果你设备发的是字符串,也可以直接转:

kotlin 复制代码
val text = value.toString(Charsets.UTF_8)

写入消息

如果要给 BLE 设备发消息,本质上就是往写特征值里写字节数组。

Android 13,也就是 API 33+,推荐用新的写法:

kotlin 复制代码
fun sendMessage(message: String) {
    val gatt = bluetoothGatt ?: return
    val characteristic = getWriteCharacteristic(gatt) ?: return

    val data = message.toByteArray(Charsets.UTF_8)

    val result = gatt.writeCharacteristic(
        characteristic,
        data,
        BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
    )

    Log.d("BLE", "write result=$result")
}

如果你发的是十六进制协议包,也可以直接写字节:

kotlin 复制代码
val packet = byteArrayOf(0xA5.toByte(), 0x01, 0x02, 0x03)
gatt.writeCharacteristic(
    characteristic,
    packet,
    BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
)

设备收到后,如果协议设计里会回应一包数据,那么这包回应通常会从 onCharacteristicChanged() 里回来。

一个简单的 BLE 管理类

把上面的逻辑收一下,最起码应该整理成一个 manager,而不是散在 Activity 里。

kotlin 复制代码
class BleManager(
    private val context: Context
) {
    private val bluetoothManager =
        context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager

    private val bluetoothAdapter = bluetoothManager.adapter
    private val scanner = bluetoothAdapter.bluetoothLeScanner

    private var bluetoothGatt: BluetoothGatt? = null
    private var isConnecting = false

    var onMessageReceived: ((ByteArray) -> Unit)? = null

    private val scanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            if (!isConnecting) {
                isConnecting = true
                stopScan()
                connect(result.device)
            }
        }
    }

    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(
            gatt: BluetoothGatt,
            status: Int,
            newState: Int
        ) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                gatt.discoverServices()
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                gatt.close()
                bluetoothGatt = null
                isConnecting = false
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                enableNotification(gatt)
            }
        }

        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            value: ByteArray
        ) {
            onMessageReceived?.invoke(value)
        }
    }

    fun startScan() {
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .build()
        scanner.startScan(null, settings, scanCallback)
    }

    fun stopScan() {
        scanner.stopScan(scanCallback)
    }

    private fun connect(device: BluetoothDevice) {
        bluetoothGatt = device.connectGatt(context, false, gattCallback)
    }

    private fun enableNotification(gatt: BluetoothGatt) {
        val service = gatt.getService(SERVICE_UUID) ?: return
        val characteristic = service.getCharacteristic(NOTIFY_UUID) ?: return
        gatt.setCharacteristicNotification(characteristic, true)

        val descriptor = characteristic.getDescriptor(CCCD_UUID) ?: return
        descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
        gatt.writeDescriptor(descriptor)
    }

    fun sendMessage(message: String) {
        val gatt = bluetoothGatt ?: return
        val service = gatt.getService(SERVICE_UUID) ?: return
        val characteristic = service.getCharacteristic(WRITE_UUID) ?: return

        gatt.writeCharacteristic(
            characteristic,
            message.toByteArray(Charsets.UTF_8),
            BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
        )
    }

    fun close() {
        bluetoothGatt?.disconnect()
        bluetoothGatt?.close()
        bluetoothGatt = null
        isConnecting = false
    }
}

这个版本不算完整工业级,但已经够你把 BLE 的扫描、连接、收发消息跑通。

Activity 里怎么用

页面层尽量只做调用,不要把 BLE 细节都塞进去。

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var bleManager: BleManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        bleManager = BleManager(this)

        bleManager.onMessageReceived = { bytes ->
            val text = bytes.toString(Charsets.UTF_8)
            runOnUiThread {
                Log.d("BLE", "ui received: $text")
            }
        }

        findViewById<Button>(R.id.btnScan).setOnClickListener {
            bleManager.startScan()
        }

        findViewById<Button>(R.id.btnSend).setOnClickListener {
            bleManager.sendMessage("hello ble")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        bleManager.close()
    }
}

这时候主流程就通了:

  • 点扫描
  • 扫到设备
  • 自动连接
  • 发现服务
  • 开启通知
  • 点发送按钮发消息
  • 设备回消息后在通知回调里收到

最后几个实战提醒

BLE 真写起来,最常见的问题一般集中在这几件事:

  • 权限没申请全
  • UUID 写错
  • 扫描时没停扫就连
  • 没写 CCCD
  • 写入太快,没做串行控制
  • 设备协议不是字符串,结果你按字符串解析

如果你后面要继续往下做,下一步通常就是补这些能力:

  • 扫描结果过滤和设备列表展示
  • 连接状态管理
  • 读写操作队列
  • 自动重连
  • 十六进制协议解析
  • Flow / StateFlow 包装 BLE 状态

先把扫描、连接、收发消息这条链跑顺,再去做这些高级能力会轻松很多。

相关推荐
fly spider4 小时前
MySQL索引篇
android·数据库·mysql
xinhuanjieyi5 小时前
php setplayersjson实现类型转换和文件锁定机制应对高并发
android·开发语言·php
533_5 小时前
[vxe-table] 表头:点击出现输入框
android·java·javascript
邹阿涛涛涛涛涛涛5 小时前
Jetpack Compose Modifier 深度解析:从链式调用到 Modifier.Node
android
jinanwuhuaguo6 小时前
OpenClaw 2026年4月升级大系深度解读剖析:从“架构重塑”到“信任内建”的范式跃迁
android·开发语言·人工智能·架构·kotlin·openclaw
huhy~6 小时前
基于Ubuntu 24.04 LTS 搭建OpenStack F 版
android·ubuntu·openstack
2401_885885047 小时前
视频短信接口接入麻不麻烦?API调用说明
android·音视频
lI-_-Il7 小时前
喜马拉雅 v9.4.56.3:移动端全站音频资源畅听版
android·音视频
我命由我123458 小时前
Android Jetpack Compose - SearchBar(搜索栏)、Tab(标签页)、时间选择器、TooltipBox(工具提示)
android·java·java-ee·kotlin·android studio·android jetpack·android-studio