Android BLE 扫描完整实战(含完整代码示例)
一、前言
在 Android 上开发蓝牙(特别是低功耗蓝牙 BLE)应用时,最常见的问题就是 ------ 为什么我扫描不到设备? 。
本文将从 原理、权限、扫描逻辑、完整代码 四个方面,带你实现一个 BLE 扫描 + 设备列表显示 的 Demo,并分享常见坑。
二、BLE 扫描原理
-
经典蓝牙(BR/EDR):主要用于音频传输、文件传输。
-
低功耗蓝牙(BLE):适用于健康医疗设备(如血压计、心率带)、传感器等场景。
BLE 设备会周期性地发送 广播包(Advertisement),其中可能包含:
-
设备名称
-
Service UUID(设备服务)
-
RSSI 信号强度
Android 的 BLE 扫描逻辑就是 监听周围的广播包。
三、权限要求
Android 版本 | 必须权限 |
---|---|
6 ~ 11 | ACCESS_FINE_LOCATION |
12+ | BLUETOOTH_SCAN 、BLUETOOTH_CONNECT (可选 ACCESS_FINE_LOCATION ) |
另外需要在 运行时动态申请权限。
Manifest 配置:
Kotlin
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!-- Android 6 ~ 11 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- Android 12+ --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
运行时请求权限:
Kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
),
100
)
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
101
)
}
四、扫描逻辑
Android 提供两种方式:
-
旧 API (
BluetoothAdapter.startLeScan
)→ 已废弃,不推荐。 -
新 API (
BluetoothLeScanner.startScan
)→ 支持过滤和高效扫描,推荐。
过滤血压服务 UUID(0x1810):
Kotlin
val filter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("00001810-0000-1000-8000-00805f9b34fb"))
.build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
五、完整代码示例(Kotlin + Jetpack Compose)
Kotlin
package com.example.bletest
import android.Manifest
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.pm.PackageManager
import android.os.*
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import java.util.*
class MainActivity : ComponentActivity() {
private val bluetoothAdapter: BluetoothAdapter by lazy {
val manager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
manager.adapter
}
private val scanner: BluetoothLeScanner by lazy {
bluetoothAdapter.bluetoothLeScanner
}
private var scanCallback: ScanCallback? = null
private val devices = mutableStateListOf<BluetoothDevice>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 动态权限
requestBlePermissions()
setContent {
Scaffold(
topBar = {
TopAppBar(title = { Text("BLE 扫描 Demo") })
}
) { padding ->
Column(Modifier.padding(padding).fillMaxSize()) {
Button(
onClick = { startBleScan() },
modifier = Modifier.padding(16.dp)
) {
Text("开始扫描")
}
LazyColumn {
items(devices) { device ->
Card(
Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable {
connectDevice(device)
}
) {
Column(Modifier.padding(12.dp)) {
Text(device.name ?: "未知设备")
Text(device.address)
}
}
}
}
}
}
}
}
private fun requestBlePermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
),
100
)
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
101
)
}
}
private fun startBleScan() {
devices.clear()
val filter = ScanFilter.Builder()
//.setServiceUuid(ParcelUuid.fromString("00001810-0000-1000-8000-00805f9b34fb"))
.build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
if (!devices.any { it.address == device.address }) {
devices.add(device)
}
}
}
scanner.startScan(listOf(filter), settings, scanCallback!!)
}
private fun connectDevice(device: BluetoothDevice) {
device.connectGatt(this, false, object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
val services = gatt.services
services.forEach {
println("服务: ${it.uuid}")
}
}
})
}
override fun onDestroy() {
super.onDestroy()
scanCallback?.let { scanner.stopScan(it) }
}
}
六、常见问题
-
为什么扫描不到?
-
未开启蓝牙
-
未申请运行时权限
-
设备未开启广播(有些血压计需要按下测量按钮)
-
模拟器不支持 BLE(必须在真机上测试)
-
-
nRF Connect 可以扫描到,但我的代码不行?
-
说明设备确实在广播
-
检查是否加了错误的 UUID 过滤
-
建议先不加过滤,直接扫描全部设备
-
-
在机模拟器上跑不通?
- 模拟器 不支持 BLE,那是硬件支持
七、总结
-
Android BLE 扫描依赖权限、硬件和设备广播状态
-
建议先用 nRF Connect 测试设备广播
-
开发时:
-
权限要全
-
蓝牙要开
-
设备要处于广播/测量模式
-
-
不要依赖模拟器,必须真机调试!