Android BLE 扫描完整实战

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_SCANBLUETOOTH_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 提供两种方式:

  1. 旧 APIBluetoothAdapter.startLeScan)→ 已废弃,不推荐。

  2. 新 APIBluetoothLeScanner.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) }
    }
}

六、常见问题

  1. 为什么扫描不到?

    • 未开启蓝牙

    • 未申请运行时权限

    • 设备未开启广播(有些血压计需要按下测量按钮)

    • 模拟器不支持 BLE(必须在真机上测试)

  2. nRF Connect 可以扫描到,但我的代码不行?

    • 说明设备确实在广播

    • 检查是否加了错误的 UUID 过滤

    • 建议先不加过滤,直接扫描全部设备

  3. 在机模拟器上跑不通?

    • 模拟器 不支持 BLE,那是硬件支持

七、总结

  • Android BLE 扫描依赖权限、硬件和设备广播状态

  • 建议先用 nRF Connect 测试设备广播

  • 开发时:

    • 权限要全

    • 蓝牙要开

    • 设备要处于广播/测量模式

  • 不要依赖模拟器,必须真机调试!

相关推荐
TeleostNaCl8 小时前
如何安装 Google 通用的驱动以便使用 ADB 和 Fastboot 调试(Bootloader)设备
android·经验分享·adb·android studio·android-studio·android runtime
fatiaozhang95278 小时前
中国移动浪潮云电脑CD1000-系统全分区备份包-可瑞芯微工具刷机-可救砖
android·网络·电脑·电视盒子·刷机固件·机顶盒刷机
2501_915918419 小时前
iOS 开发全流程实战 基于 uni-app 的 iOS 应用开发、打包、测试与上架流程详解
android·ios·小程序·https·uni-app·iphone·webview
lichong9519 小时前
【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之dist打包发布在Android工程asserts里
android·vue.js·iphone
Android出海10 小时前
Android 15重磅升级:16KB内存页机制详解与适配指南
android·人工智能·新媒体运营·产品运营·内容运营
一只修仙的猿10 小时前
毕业三年后,我离职了
android·面试
编程乐学10 小时前
安卓非原创--基于Android Studio 实现的新闻App
android·ide·android studio·移动端开发·安卓大作业·新闻app
雅雅姐11 小时前
Android14 init.rc中on boot阶段操作4
android
fatiaozhang952712 小时前
中国移动中兴云电脑W132D-RK3528-2+32G-刷机固件包(非原机制作)
android·xml·电脑·电视盒子·刷机固件·机顶盒刷机