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

发布于:2025-09-15 ⋅ 阅读:(20) ⋅ 点赞:(0)

📋 前言

在现代移动应用开发中,蓝牙低功耗(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)? = null
    var onDeviceFound: ((ScanResult) -> Unit)? = null
    var 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 = true
        onScanStateChanged?.invoke(true)

        scanDisposable = rxBleClient.scanBleDevices(scanSettings)
            .subscribe(
                { scanResult ->
                    handleScanResult(scanResult)
                },
                { error ->
                    handleScanError(error)
                }
            )
    }

    /**
     * 处理扫描结果
     */
    private fun handleScanResult(scanResult: ScanResult) {
        val macAddress = scanResult.bleDevice.macAddress
        scannedDevices[macAddress] = scanResult
        
        // 通知监听器
        onDeviceFound?.invoke(scanResult)
        scanResultsSubject.onNext(scanResult)
    }

    /**
     * 处理扫描错误
     */
    private fun handleScanError(error: Throwable) {
        isScanning = false
        onScanStateChanged?.invoke(false)
        onScanError?.invoke(error)
        scanResultsSubject.onError(error)
    }

    /**
     * 停止扫描
     */
    fun stopScan() {
        scanDisposable?.dispose()
        scanDisposable = null
        isScanning = false
        onScanStateChanged?.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.name
            txtMac.text = device.macAddress
            txtRssi.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.size

    fun addOrUpdateDevice(device: BleDeviceInfo) {
        val existingIndex = devices.indexOfFirst { it.macAddress == device.macAddress }
        if (existingIndex != -1) {
            devices[existingIndex] = device
            notifyItemChanged(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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center_vertical">

        <Button
            android:id="@+id/btnStartScan"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="开始扫描"
            android:layout_marginEnd="8dp"/>

        <Button
            android: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"/>

        <Button
            android: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">

    <TextView
        android:id="@+id/txtDeviceName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="bold"/>

    <TextView
        android:id="@+id/txtDeviceMac"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:layout_marginTop="4dp"/>

    <TextView
        android: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> = _scanState
    
    init {
        bleScanner.onScanStateChanged = { isScanning ->
            _scanState.postValue(isScanning)
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        bleScanner.release()
    }
}

🎯 总结

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

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

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

优点:

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

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