深入解析:Android BLE 蓝牙扫描完全指南:使用 RxAndroidBle框架

news/2025/9/20 22:53:00/文章来源:https://www.cnblogs.com/yfceshi/p/19103010

前言

在现代移动应用开发中,蓝牙低功耗(BLE)技术广泛应用于物联网设备、健康监测、智能家居等领域。本文将带你从零开始,完整实现一个 Android BLE 扫描功能。

️ 1. 环境配置

添加依赖

在 app/build.gradle 中添加:

dependencies {
// RxAndroidBle 核心库
implementation "com.polidea.rxandroidble3:rxandroidble:1.17.2"
// RxJava 3
implementation "io.reactivex.rxjava3:rxjava:3.1.6"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
// 可选:Lifecycle 用于更好的生命周期管理
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
}

权限配置

在 AndroidManifest.xml 中添加:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Android 12+ 需要以下权限 --><uses-permission android:name="android.permission.BLUETOOTH_SCAN" /><uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /><!-- 如果需要定位功能 --><uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

2. 权限工具类

// PermissionUtils.kt
object PermissionUtils {
/**
* 检查蓝牙权限
*/
@SuppressLint("InlinedApi")
fun checkBluetoothPermissions(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
} else {
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 获取需要的权限数组
*/
@SuppressLint("InlinedApi")
fun getRequiredPermissions(): Array<String>{return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {arrayOf(Manifest.permission.BLUETOOTH_SCAN,Manifest.permission.BLUETOOTH_CONNECT)} else {arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)}}/*** 请求权限*/fun requestBluetoothPermissions(activity: Activity, requestCode: Int) {val permissions = getRequiredPermissions()ActivityCompat.requestPermissions(activity, permissions, requestCode)}}

3. 蓝牙扫描工具类

// BleScanner.kt
class BleScanner
(private val context: Context) {
private val rxBleClient: RxBleClient by lazy { RxBleClient.create(context)
}
private var scanDisposable: Disposable? = null
private val scanResultsSubject = PublishSubject.create<ScanResult>()private val scannedDevices = mutableMapOf<String, ScanResult>()private var isScanning = false// 扫描状态回调var onScanStateChanged: ((Boolean) -> Unit)? = nullvar onDeviceFound: ((ScanResult) -> Unit)? = nullvar onScanError: ((Throwable) -> Unit)? = null/*** 开始扫描*/@SuppressLint("MissingPermission")fun startScan(scanMode: Int = ScanSettings.SCAN_MODE_LOW_LATENCY) {if (!PermissionUtils.checkBluetoothPermissions(context)) {onScanError?.invoke(SecurityException("缺少蓝牙扫描权限"))return}stopScan()val scanSettings = ScanSettings.Builder().setScanMode(scanMode).setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES).build()isScanning = trueonScanStateChanged?.invoke(true)scanDisposable = rxBleClient.scanBleDevices(scanSettings).subscribe({ scanResult ->handleScanResult(scanResult)},{ error ->handleScanError(error)})}/*** 处理扫描结果*/private fun handleScanResult(scanResult: ScanResult) {val macAddress = scanResult.bleDevice.macAddressscannedDevices[macAddress] = scanResult// 通知监听器onDeviceFound?.invoke(scanResult)scanResultsSubject.onNext(scanResult)}/*** 处理扫描错误*/private fun handleScanError(error: Throwable) {isScanning = falseonScanStateChanged?.invoke(false)onScanError?.invoke(error)scanResultsSubject.onError(error)}/*** 停止扫描*/fun stopScan() {scanDisposable?.dispose()scanDisposable = nullisScanning = falseonScanStateChanged?.invoke(false)}/*** 获取扫描结果的Observable*/fun getScanResultsObservable(): Observable<ScanResult>{return scanResultsSubject}/*** 获取所有扫描到的设备*/fun getAllDevices(): List<ScanResult>{return scannedDevices.values.toList()}/*** 根据条件过滤设备*/fun filterDevices(name: String? = null,minRssi: Int? = null,maxRssi: Int? = null): List<ScanResult>{return scannedDevices.values.filter { device ->(name == null || device.bleDevice.name?.contains(name, true) == true) &&(minRssi == null || device.rssi >= minRssi) &&(maxRssi == null || device.rssi <= maxRssi)}}/*** 清空扫描结果*/fun clearResults() {scannedDevices.clear()}/*** 检查是否正在扫描*/fun isScanning(): Boolean = isScanning/*** 获取设备数量*/fun getDeviceCount(): Int = scannedDevices.size/*** 释放资源*/fun release() {stopScan()scanResultsSubject.onComplete()}}

4. 设备信息数据类

// BleDeviceInfo.kt
data class BleDeviceInfo
(
val name: String,
val macAddress: String,
val rssi: Int,
val scanRecord: ByteArray?,
val timestamp: Long = System.currentTimeMillis()
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BleDeviceInfo
return macAddress == other.macAddress
}
override fun hashCode(): Int {
return macAddress.hashCode()
}
}

5. RecyclerView 适配器

// DeviceAdapter.kt
class DeviceAdapter
(
private val onDeviceClick: (BleDeviceInfo) -> Unit
) : RecyclerView.Adapter<DeviceAdapter.DeviceViewHolder>() {private val devices = mutableListOf<BleDeviceInfo>()inner class DeviceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {private val txtName: TextView = itemView.findViewById(R.id.txtDeviceName)private val txtMac: TextView = itemView.findViewById(R.id.txtDeviceMac)private val txtRssi: TextView = itemView.findViewById(R.id.txtDeviceRssi)fun bind(device: BleDeviceInfo) {txtName.text = device.nametxtMac.text = device.macAddresstxtRssi.text = "信号: ${device.rssi}dBm"itemView.setOnClickListener {onDeviceClick(device)}}}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceViewHolder {val view = LayoutInflater.from(parent.context).inflate(R.layout.item_device, parent, false)return DeviceViewHolder(view)}override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {holder.bind(devices[position])}override fun getItemCount(): Int = devices.sizefun addOrUpdateDevice(device: BleDeviceInfo) {val existingIndex = devices.indexOfFirst { it.macAddress == device.macAddress}if (existingIndex != -1) {devices[existingIndex] = devicenotifyItemChanged(existingIndex)} else {devices.add(device)notifyItemInserted(devices.size - 1)}}fun clear() {devices.clear()notifyDataSetChanged()}fun getDevices(): List<BleDeviceInfo>= devices.toList()}

6. Activity/Fragment 实现

// MainActivity.kt
class MainActivity :
AppCompatActivity() {
companion object {
private const val REQUEST_BLE_PERMISSIONS = 1001
}
private lateinit var bleScanner: BleScanner
private lateinit var deviceAdapter: DeviceAdapter
private var resultsDisposable: Disposable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initViews()
initBleScanner()
checkPermissions()
}
private fun initViews() {
deviceAdapter = DeviceAdapter { device ->
onDeviceSelected(device)
}
recyclerViewDevices.apply {
adapter = deviceAdapter
layoutManager = LinearLayoutManager(this@MainActivity)
addItemDecoration(DividerItemDecoration(this@MainActivity, DividerItemDecoration.VERTICAL))
}
btnStartScan.setOnClickListener {
startScanning()
}
btnStopScan.setOnClickListener {
stopScanning()
}
btnClear.setOnClickListener {
clearResults()
}
}
private fun initBleScanner() {
bleScanner = BleScanner(this)
bleScanner.onScanStateChanged = { isScanning ->
updateScanUI(isScanning)
}
bleScanner.onDeviceFound = { scanResult ->
val deviceInfo = convertToDeviceInfo(scanResult)
runOnUiThread {
deviceAdapter.addOrUpdateDevice(deviceInfo)
updateDeviceCount()
}
}
bleScanner.onScanError = { error ->
runOnUiThread {
showError("扫描错误: ${error.message
}")
}
}
}
private fun convertToDeviceInfo(scanResult: ScanResult): BleDeviceInfo {
return BleDeviceInfo(
name = scanResult.bleDevice.name ?: "未知设备",
macAddress = scanResult.bleDevice.macAddress,
rssi = scanResult.rssi,
scanRecord = scanResult.scanRecord?.bytes
)
}
private fun checkPermissions() {
if (!PermissionUtils.checkBluetoothPermissions(this)) {
PermissionUtils.requestBluetoothPermissions(this, REQUEST_BLE_PERMISSIONS)
}
}
@SuppressLint("MissingPermission")
private fun startScanning() {
if (!PermissionUtils.checkBluetoothPermissions(this)) {
PermissionUtils.requestBluetoothPermissions(this, REQUEST_BLE_PERMISSIONS)
return
}
try {
bleScanner.startScan()
observeScanResults()
} catch (e: Exception) {
showError("启动扫描失败: ${e.message
}")
}
}
private fun observeScanResults() {
resultsDisposable = bleScanner.getScanResultsObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ scanResult ->
val deviceInfo = convertToDeviceInfo(scanResult)
deviceAdapter.addOrUpdateDevice(deviceInfo)
updateDeviceCount()
}, { error ->
showError("扫描错误: ${error.message
}")
})
}
private fun stopScanning() {
bleScanner.stopScan()
resultsDisposable?.dispose()
}
private fun clearResults() {
bleScanner.clearResults()
deviceAdapter.clear()
updateDeviceCount()
}
private fun onDeviceSelected(device: BleDeviceInfo) {
AlertDialog.Builder(this)
.setTitle("选择设备")
.setMessage("设备: ${device.name
}\nMAC: ${device.macAddress
}\n信号强度: ${device.rssi
}dBm")
.setPositiveButton("连接") { _, _ ->
connectToDevice(device.macAddress)
}
.setNegativeButton("取消", null)
.show()
}
private fun connectToDevice(macAddress: String) {
// 这里实现设备连接逻辑
Toast.makeText(this, "连接设备: $macAddress", Toast.LENGTH_SHORT).show()
}
private fun updateScanUI(isScanning: Boolean) {
runOnUiThread {
btnStartScan.isEnabled = !isScanning
btnStopScan.isEnabled = isScanning
progressBar.visibility = if (isScanning) View.VISIBLE else View.GONE
txtStatus.text = if (isScanning) "扫描中..." else "扫描已停止"
}
}
private fun updateDeviceCount() {
txtDeviceCount.text = "设备数量: ${deviceAdapter.itemCount
}"
}
private fun showError(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
updateScanUI(false)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<
out String>
,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_BLE_PERMISSIONS) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED
}) {
startScanning()
} else {
showError("需要蓝牙权限才能扫描")
}
}
}
override fun onDestroy() {
super.onDestroy()
bleScanner.release()
resultsDisposable?.dispose()
}
}

7. 布局文件

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:padding="16dp">
<LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:gravity="center_vertical">
<Buttonandroid:id="@+id/btnStartScan"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="开始扫描"android:layout_marginEnd="8dp"/>
<Buttonandroid:id="@+id/btnStopScan"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="停止扫描"android:layout_marginEnd="8dp"android:enabled="false"/>
<Buttonandroid:id="@+id/btnClear"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="清空结果"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="16dp">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone"/>
<TextView
android:id="@+id/txtStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="准备扫描"
android:layout_marginStart="8dp"
android:textSize="16sp"/>
<TextView
android:id="@+id/txtDeviceCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设备数量: 0"
android:textSize="14sp"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewDevices"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="16dp"/>
</LinearLayout>

item_device.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:padding="16dp">
<TextViewandroid:id="@+id/txtDeviceName"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="18sp"android:textStyle="bold"/>
<TextViewandroid:id="@+id/txtDeviceMac"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="14sp"android:layout_marginTop="4dp"/>
<TextViewandroid:id="@+id/txtDeviceRssi"android:layout_width="match_parent"android:layout_height="wrap_content"android:textSize="14sp"android:layout_marginTop="4dp"/>
</LinearLayout>

8. 使用技巧和最佳实践

扫描模式选择

// 低功耗模式(省电,但扫描间隔长)
ScanSettings.SCAN_MODE_LOW_POWER
// 平衡模式
ScanSettings.SCAN_MODE_BALANCED
// 低延迟模式(快速响应,但耗电)
ScanSettings.SCAN_MODE_LOW_LATENCY
// 机会主义模式(最低功耗)
ScanSettings.SCAN_MODE_OPPORTUNISTIC

错误处理

// 添加重试机制
fun startScanWithRetry(maxRetries: Int = 3) {
var retryCount = 0
bleScanner.getScanResultsObservable()
.retryWhen { errors ->
errors.flatMap { error ->
if (++retryCount < maxRetries) {
Observable.timer(2, TimeUnit.SECONDS)
} else {
Observable.error(error)
}
}
}
.subscribe(...)
}

生命周期管理

// 在 ViewModel 中管理
class BleViewModel
(application: Application) : AndroidViewModel(application) {
private val bleScanner = BleScanner(application)
// 使用 LiveData 暴露状态
private val _scanState = MutableLiveData<Boolean>()val scanState: LiveData<Boolean>= _scanStateinit {bleScanner.onScanStateChanged = { isScanning ->_scanState.postValue(isScanning)}}override fun onCleared() {super.onCleared()bleScanner.release()}}

总结

本文完整介绍了如何使用 RxAndroidBle 实现 Android BLE 扫描功能,包括:

  1. 环境配置和依赖添加
  2. 权限管理和检查
  3. 核心扫描功能实现
  4. UI 界面和列表展示
  5. 错误处理和最佳实践

这个实现提供了完整的蓝牙扫描解决方案,可以直接用于生产环境,也可以根据具体需求进行扩展和定制。

优点:

· 响应式编程,代码简洁
· 完整的错误处理
· 自动设备去重
· 灵活的过滤功能
· 良好的生命周期管理

希望这篇指南对你的博客写作有所帮助!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/908602.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot 2.5.0 集成 Elasticsearch 7.12.0 实现 CRUD 完整指南(Windows 环境) - 教程

Spring Boot 2.5.0 集成 Elasticsearch 7.12.0 实现 CRUD 完整指南(Windows 环境) - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !import…

TypeScript - typeof 搭配 as const 技巧总结

这是一种 TypeScript 的高级类型技巧,用于从值推导出类型,实现类型和值的完美同步。 基本语法 const values = ["A", "B", "C"] as const; type ValueType = typeof values[number]; …

CentOS 8.5.2.111部署Zabbix6.0 手把手、保姆级

CentOS 8.5.2.111部署Zabbix6.0 手把手、保姆级CentOS 8.5.2.111部署Zabbix6.0 手把手、保姆级 前提、设置网络Ip地址等 cd /etc/sysconfig cd network-scripts/ ls vim ifcfg-enp0s3 systemctl restart NetworkManage…

[Linux/Docker] BusyBox : 开源、轻量级的Unix工具集

0 序 Docker时代,软件程序的最小化、轻量化部署趋势BusyBox 现在越来越流行,特别是在 Docker 用户中,许多 Docker 镜像使用 BusyBox 为你提供最小镜像。BusyBox := 原 Linux 发行版预装的 GNU Coreutils 在 Docker …

Part03 数据结构 - 教程

Part03 数据结构 - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", &qu…

图解3:幂等使用场景

幂等,API接口和MQ消费重复

推荐一款数据库安全产品:全知科技知形-数据库风险监测系统的价值解析

推荐一款数据库安全产品:全知科技知形-数据库风险监测系统的价值解析在当下数字经济快速发展的浪潮中,数据已被视为企业最核心的生产要素。无论是金融、医疗,还是互联网与制造业,数据库都是数据存储与流转的“中枢…

变量,常量,作用域

变量JAVA是一种强类型语言,每个都必须声明其类型。JAVA变量是程序中最基本的存储单位,其要素包括变量名,变量类型和作用域。 type varName [=value] [{,varName[=value]}]; //数据类型 变量名 = 值; 可以使用逗号隔…

wireshark 进行snmp 协议加密报文解密查看

转发请注明出处:在环境上进行对数通设备进行 snmp 采集数据,在现网运行环境中运行时,会偶尔出现异常,于是,采用tcpdump抓包,tcpdump 抓包得报文用wireshark打开之后,查询上报设备上报得数据层data格式如下:由于…

linux kernel synchronization 2

Per CPU VariablesA CPU should not access the elements of the array corresponding to other CPU. 每个CPU拥有该变量的独立副本 无需加锁 - 由于每个CPU只操作自己的副本,因此读写自己的副本时不会产生竞争条件 缓…

MySQL高阶查询语句与视图实战指南 - 指南

MySQL高阶查询语句与视图实战指南 - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "M…

订单未支付多种方案

1、微服务常用MQ 2、单体情况下常用定时任务

耳鸣针灸学位

耳鸣针灸学位足驷马 中九里 灵骨 叉三 行间 肾关 太溪 牵引针患侧 听宫或者耳门

Twincat 中如何将位变量链接到字节

最近在测试一个EtherCAT IO模块, 参考视频Ethercat总线快速入门教程——1-2TwinCAT基本操作_哔哩哔哩_bilibili 我手里是欧辰的一个模块,它的输入输出都是字节形式的因此小改了下PLC程序 1. 在DUTs中新建了一个结构体…

不管不管,就要你的特殊对待(权限)

特殊权限,文件特殊属性除rwx(读写执行)三种文件权限外,还有哪些权限呢? 一.SUID 1.是什么? “以文件所有者的身份运行程序”。主要作用于可执行文件。 当一个可执行文件设置了 SUID 位时,任何用户在执行该文件时,…

202003_攻防世界_功夫再高也怕菜刀

流量分析,文件分离,WebShellTags:流量分析,文件分离,WebShell 0x00. 题目 附件路径:https://pan.baidu.com/s/1GyH7kitkMYywGC9YJeQLJA?pwd=Zmxh#list/path=/CTF附件 附件名称:202003_攻防世界_功夫再高也怕菜刀.zi…

工业软件:重塑协同流程、降低制造成本的关键器具

工业软件:重塑协同流程、降低制造成本的关键器具pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas",…

实用指南:【2025最新版】PCL点云处理算法汇总(C++长期更新版)

实用指南:【2025最新版】PCL点云处理算法汇总(C++长期更新版)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "C…