Android-蓝牙ble(扫描篇)

本章关键:蓝牙ble蓝牙ble扫描ble广播包解析扫描框架优化

前言

前篇讲解了蓝牙ble的基本概念,主要分为:扫描发现蓝牙设备解析蓝牙广播包、建立GATT连接、mtu协商、打开服务、进行通信、断开连接。

本章主要介绍蓝牙ble扫描ble广播包解析Android蓝牙ble扫描框架优化

蓝牙频道

工作频道

蓝牙设备使用2.4 GHz ISM(工业,科学和医疗)频段进行操作。在这个频段内,蓝牙设备有79(经典蓝牙)个或40(低功耗)个频道(取决于蓝牙版本)可供使用。但是,对于蓝牙低功耗(Bluetooth Low Energy,BLE)设备,频道数量和用途有所不同。 对于BLE,频段被划分为40个频道,每个频道间隔2 MHz(这里涉及到一个频偏)。这40个频道被进一步划分为3个广播频道(也称为主要广播频道)和37个数据频道。

  1. 广播频道(Advertising Channels):BLE使用频道37(2402 MHz),38(2426 MHz)和39(2480 MHz)作为广播频道。这些频道被选择是因为它们在2.4 GHz频段中的干扰最小。在这些频道上,BLE设备可以广播其存在和提供的服务,以便其他设备可以发现并连接它。本章重点讨论的蓝牙扫描,主要就是广播频道。

  2. 数据频道(Data Channels):其他的37个频道(频道0至36)用于数据传输。一旦设备之间的连接建立,这些频道用于双向通信。为了避免干扰,BLE使用一种称为频率跳跃的技术,在这些频道之间快速切换。

频偏

频偏(Frequency Offset)是一个在无线通信中常见的概念,通常用于描述一个信号的实际频率与其理论频率之间的差异。这种差异可能是由于各种因素引起的,如晶振的不稳定性、环境因素(如温度、湿度)、设备的电源电压变化等。

我们知道,ble在频道中被划分为40个频道,每个频道的间隔为2 MHz。其中广播的频道为固定的3个。假设ble规范的广播频道其中之一为F 。但是实际设备发送出来的频率为F1 ,那么 |F1 - F| 的值就是所谓的频偏。在实际的使用当中,频偏越小,意味着设备的频率愈加稳定,那么设备能被发现的概率越高、效率越快。

蓝牙ble广播数据解析

要想和蓝牙设备进行通信,首先我们要先发现蓝牙设备,之所以能发现蓝牙设备,是因为蓝牙设备在向外发送蓝牙ble的广播包 。我们通过蓝牙ble的广播包的解析,能够获取到蓝牙ble设备的一些设备信息,包括mac地址(6字节)、蓝牙名称、蓝牙广播包服务、信号rssi 等等。这里,我们借用nRF connect(Nordic公司开发的蓝牙工具)来具体看下蓝牙广播包里的数据。

  • 红色框:扫描到的设备名称
  • 绿色框:扫描到的设备mac地址(通常是6个长度的字节)
  • 黄色框:代表设备到类型。Bluetooth Mesh是蓝牙SIG联盟推出的一种网状拓扑结构的一种设备,它基于ble,后面我会单独介绍、iBeacon是apple公司自己制定的一种信标的广播协议,后面也会单独介绍。这里大家只用知道他们是广播包里面代表的不同的设备类型即可。
  • 灰色框:代表设备的信号强度。这个信号强度可以理解成跟wifi一样,绝对值越小,信号强度越大。

点开其中的一个条目,可以看到更多信息,如下:

我们能看到:

  • Device type :LE only(蓝牙设备类型只支持 Bluetooth Low Energy(BLE)或 Bluetooth Smart 技术)
  • Advertising type:Legacy("Legacy" advertising type 是指传统的蓝牙设备广播方式。这种广播方式主要用于蓝牙 5.0 以前的设备,但在蓝牙 5.0 及以后的设备中仍然支持。在 Legacy 广播中,蓝牙设备会定期发送广播包,告知其存在并提供一些基本信息,例如设备名称和服务。这些广播包可以被任何在范围内的蓝牙设备接收,无需设备之间建立连接。5.0之后还引入了扩展广播包。这种广播方式在许多应用中都很有用,例如设备发现、配对和连接建立等。然而,与蓝牙 5.0 引入的扩展广播(Extended Advertising)相比,Legacy 广播的数据容量较小,广播间隔也较长。)
  • Flags :GeneralDiscoverable,BrEdrNotSupported(General Discoverable(一般可发现) : 这个标志表示设备处于一般可发现模式。在这种模式下,设备可以被任何其他 Bluetooth 设备发现,无论其他设备是否处于有限的发现模式。这种模式通常用于设备需要长时间被发现的情况。- BrEdrNotSupported(不支持经典蓝牙) : 这个标志表示设备不支持 Bluetooth 经典(BR/EDR)协议,只支持 Bluetooth 低功耗(LE)协议。这对于只需要 BLE 功能的设备来说是很有用的,因为它可以节省设备的功耗和成本。)
  • Incomplete List of 16-bit Service UUIDS:0x????(不完整的一个16 bit UUID,通常会是一个公司的标记)
  • Service Data:UUID:0x???????????(广播包里面的service数据)
  • Manufacture data(Bluetooth Core 4.1):Company:xxxxxxx(Manufacturer Specific Data(制造商特定数据)是一种在广播包中包含的特定数据类型,它允许制造商定义和包含专有信息,这些信息可以用于增强设备的功能或提供额外的服务信息。制造商特定数据的结构通常包括公司标识符(Company Identifier,CID),这是一个16位的数值,用于唯一标识制造商。紧接着是制造商自定义的数据,这些数据可以包含服务UUID、设备类型、版本信息等。在实际应用中,制造商特定数据可以根据不同的需求进行设计和扩展,以适应各种BLE应用场景)
  • Complete Local Name:蓝牙ble的名称

以上的数据均是通过蓝牙广播包里面的数据解析得到的。因此,我们需要了解一下蓝牙广播的数据格式。这里我借用鸿蒙官网对蓝牙广播包协议的介绍来对大家进行一个简单的说明。因为本文主要是对蓝牙ble的开发进行说明,所以蓝牙协议栈不是本次的重点,因此只会做一些必要的介绍。

蓝牙广播包总体结构

蓝牙设备发送的广播包和响应包的Payload均包含AdvA、Data两部分,如图1所示。

图1 广播数据包结构

  • AdvA: 表示广播方的地址,即蓝牙设备的MAC地址,长度为6字节。注意MAC地址必须使用Public MAC,不得使用随机MAC。如果采用随机MAC地址,将导致设备发现与实际配网的两个MAC地址不同。主控设备使用随机MAC地址在云端查询设备注册状态时,无法匹配到正确的结果。
  • AdvData: 表示数据包,可以为AdvData(广播数据)或ScanRspData(扫描响应数据)。每个数据包均由有效数据(significant)和无效数据(non-significant)两部分组成,长度固定为31字节。
    • 有效数据 :包含若干个广播数据单元(即AD Structure),AD Structure的结构=Length+AD Type+AD data
      • Length: 表示该AD Structure数据的总长度,即为AD Type与AD Data的长度和(即不含Length字段本身的1字节)。
      • AD Type: 表示该广播数据代表的含义,如设备名、UUID等。
      • AD Data: 表示具体的数据内容。
    • 无效数据: 当有效数据长度不满31字节时,其余的用0补全。这部分的数据是无效的。

蓝牙的名称(Complete Local Name)就是AD Structure的一种,他的type类型为0x09。关键的一些type数据类型如下所示: 以下是一些蓝牙低功耗 (Bluetooth Low Energy, BLE) 广播中的关键数据类型值(type):

  • 0x01:Flags
  • 0x02:Incomplete List of 16-bit Service Class UUIDs
  • 0x03:Complete List of 16-bit Service Class UUIDs
  • 0x04:Incomplete List of 32-bit Service Class UUIDs
  • 0x05:Complete List of 32-bit Service Class UUIDs
  • 0x06:Incomplete List of 128-bit Service Class UUIDs
  • 0x07:Complete List of 128-bit Service Class UUIDs
  • 0x08:Shortened Local Name
  • 0x09:Complete Local Name
  • 0x0A:Tx Power Level
  • 0x16:Service Data
  • 0x19:Appearance
  • 0x1B:LE Bluetooth Device Address
  • 0x1C:LE Role
  • 0x20:Service Data - 32-bit UUID
  • 0x21:Service Data - 128-bit UUID
  • 0xFF:Manufacturer Specific Data

这些类型值定义了广播数据包中不同字段的含义和格式。我们上面通过nRF connect发现的蓝牙设备列表展示的数据,基本上都是跟上面定义的type相符合。我们实际开发当中,自己定义的应用层涉及到的广播数据,通常是放在0xff的Manufacturer Specific Data里面

Android API

开启ble扫描

Android系统提供为我们封装了相应的API来帮助我们去扫描蓝牙设备(不同的版本可能API使用推荐不同)。我们来看下如何使用:

获取BluetoothAdapter

kotlin 复制代码
object BluetoothUtils {
    private val context : Context by lazy { 
        BluetoothApplication.ctx
    }
    // 获取BluetoothManager实例
    private val bluetoothManager: BluetoothManager by lazy {
        context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    }
    // 获取BluetoothAdapter实例
    private val bluetoothAdapter : BluetoothAdapter by lazy { 
        bluetoothManager.adapter
    }
}

开启扫描

kotlin 复制代码
/**
 * 蓝牙扫描,这里先不做权限申请处理,突出扫描说明
 */
public fun startLeScan(){
    if (ActivityCompat.checkSelfPermission(
            context,
            Manifest.permission.BLUETOOTH_SCAN
        ) != PackageManager.PERMISSION_GRANTED
    ) {
        // 缺少扫描权限,权限的说明后面会单独展开
        return
    }
    bluetoothAdapter.bluetoothLeScanner.startScan(object : ScanCallback(){
        // 扫描结果回调
        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            // 注意,这里会回调多次
            // scanResult就是扫描到的蓝牙设备
        }
    })
}

这里我们看下ScanResult的组成部分

java 复制代码
package android.bluetooth.le;

public final class ScanResult implements Parcelable, Attributable {
    ...
    // Remote Bluetooth device.
    private BluetoothDevice mDevice;//蓝牙扫描实例,提供连接、断开、读写数据等API

    // Scan record, including advertising data and scan response data.
    @Nullable
    private ScanRecord mScanRecord;//蓝牙广播包内的数据
    // Received signal strength.
    private int mRssi;//信号量

    // Device timestamp when the result was last seen.
    private long mTimestampNanos;// 最后一次发现设备的时间戳
    ...
}

解析蓝牙广播包

我们来看下我们如何解析蓝牙的AD Structure的数据:

kotlin 复制代码
import java.nio.ByteBuffer
import java.nio.ByteOrder

/**
 * 获取匹配特定类型的字段的字节数组
 * 
 * @param scanRecord 扫描记录的字节数组,通过{@link ScanResult#scanRecord#bytes}获取
 * @param type 想要查找的类型
 * @return 匹配类型的第一个字段的字节数组,如果没有找到匹配的类型或遇到终止符,则返回 null
 */
fun getBytesForSpecificType(scanRecord: ByteArray, type: Int): ByteArray? {
    val buffer = ByteBuffer.wrap(scanRecord).order(ByteOrder.LITTLE_ENDIAN)
    while (buffer.remaining() > 0) {
        val length = buffer.get().toInt()
        if (length == 0) return null // 终止符

        val currentType = buffer.get().toInt()
        if (length < 1 || length > buffer.remaining()) return null // 非法长度,可能是错误的数据

        if (currentType == type) {
            val bytes = ByteArray(length - 1)
            buffer.get(bytes)
            return bytes
        } else {
            buffer.position(buffer.position() + length - 1) // 跳过这个字段
        }
    }

    return null // 没有找到匹配的类型
}

// 获取指定蓝牙名称
val bleCompleteLocalName = getBytesForSpecificType(scanResult.scanRecord.bytes,0x09)

蓝牙扫描策略

对于蓝牙扫描,android还为我们提供了丰富的扫描策略设置项供我们使用,下面我们来介绍一下蓝牙扫描的设置。

kotlin 复制代码
        // 获取BluetoothManager实例
        val bluetoothManager: BluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        // 获取BluetoothAdapter实例
        val bluetoothAdapter : BluetoothAdapter = bluetoothManager.adapter
        // 设置扫描策略
        val scanSettings : ScanSettings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
            .setLegacy(true)
            .setMatchMode(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
            .setReportDelay(3000)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
            .build()
        // 设置过滤条件
        val scanFilterList = arrayListOf<ScanFilter>()
        val scanFilter = ScanFilter.Builder()
            .setServiceUuid(ParcelUuid(UUID.randomUUID()))// 替换程自己的UUID
            .build()
        scanFilterList.add(scanFilter)
        bluetoothAdapter.bluetoothLeScanner.startScan(scanFilterList,scanSettings,object : ScanCallback(){
            // 扫描结果回调
            override fun onScanResult(callbackType: Int, result: ScanResult?) {
                // 注意,这里会回调多次
                // scanResult就是扫描到的蓝牙设备
            }

            override fun onBatchScanResults(results: MutableList<ScanResult>?) {
                super.onBatchScanResults(results)
                // 当
            }
        })

蓝牙扫描设置

android提供的蓝牙扫描设置,主要是被封装在一个ScanSetting里面,我们来了解一下这个扫描设置。

java 复制代码
/*
 * Copyright (C) 2014 The Android Open Source Project
 * ...
 */

package android.bluetooth.le;

public final class ScanSettings implements Parcelable {
    // Bluetooth LE scan mode.
    private int mScanMode;

    // Bluetooth LE scan callback type.
    private int mCallbackType; 

    // Bluetooth LE scan result type.
    private int mScanResultType;

    // Time of delay for reporting the scan result.
    private long mReportDelayMillis;

    private int mMatchMode;

    private int mNumOfMatchesPerFilter;

    // Include only legacy advertising results.
    private boolean mLegacy;

    private int mPhy;
}

Bluetooth LE scan mode

蓝牙扫描模式 在 Android 的 ScanSettings 类中,有三种扫描模式(Scan Mode)可供选择,分别是:

  1. SCAN_MODE_LOW_POWER:这种模式优化了低功耗的使用,适合后台扫描。这是一种低延迟、低功耗的扫描方式,可以在不大幅度影响设备电池寿命的情况下进行。

  2. SCAN_MODE_BALANCED:这种模式在扫描延迟和功耗之间找到了一个平衡点。在这种模式下,扫描操作会更频繁一些,因此可能会更快地发现附近的设备,但这也会导致电池消耗增加。

  3. SCAN_MODE_LOW_LATENCY:这种模式优化了低延迟扫描,适合需要快速发现设备的场景。这种模式下,扫描操作会非常频繁,可以尽快发现附近的设备,但同时也会大幅度增加电池消耗。

你可以根据应用的需求和设备的电池状况来选择最合适的扫描模式。例如,如果你的应用需要在后台持续扫描设备,那么你可能会选择 SCAN_MODE_LOW_POWER 来降低电池消耗。如果你的应用需要快速发现并连接到附近的设备,那么你可能会选择 SCAN_MODE_LOW_LATENCY,尽管这会增加电池消耗。

Bluetooth LE Callback Type

在 Android 的 ScanSettings 类中,有四种回调类型(Callback Type)可供选择,分别是:

  1. CALLBACK_TYPE_ALL_MATCHES:这种类型的回调将为所有发现的广播回调,包括先前已经匹配过的设备。这是默认的回调类型。

  2. CALLBACK_TYPE_FIRST_MATCH:这种类型的回调只会在发现的设备第一次匹配到扫描过滤器时触发。这可以用于只关心第一次发现设备的场景。

  3. CALLBACK_TYPE_MATCH_LOST:这种类型的回调在之前匹配过的设备不再被发现时触发。这可以用于跟踪设备的连接状态,例如,当设备离开范围或关闭时。

  4. CALLBACK_TYPE_SINGLE_MATCH:这种类型的回调在发现的设备匹配到扫描过滤器时只会触发一次,不会重复回调。

你可以根据你的应用需求选择合适的回调类型。例如,如果你的应用需要跟踪设备的连接状态,你可能会选择 CALLBACK_TYPE_MATCH_LOST。如果你的应用只关心第一次发现设备,你可能会选择 CALLBACK_TYPE_FIRST_MATCH

Bluetooth LE scan result type.

在 Android 的 ScanSettings 类中,Bluetooth LE 扫描结果类型有三种可供选择,分别是:

  1. SCAN_RESULT_TYPE_FULL:该类型表示扫描将返回全部的扫描结果,包括完整的广播数据。如果你需要获取设备的全部广播数据,可以选择这种类型。

  2. SCAN_RESULT_TYPE_ABBREVIATED:该类型表示扫描将返回缩略的扫描结果,只包含设备的地址和 RSSI。这种类型可以在需要节省电量的情况下使用。

  3. SCAN_RESULT_TYPE_TRUNCATED:该类型表示扫描将返回截断的扫描结果,只包含设备的地址、RSSI 和一个部分的广播数据。这种类型可以在需要获取部分广播数据,且需要节省电量的情况下使用。

你可以根据你的应用需求选择合适的扫描结果类型。例如,如果你的应用需要获取设备的全部广播数据,你可能会选择 SCAN_RESULT_TYPE_FULL。如果你的应用只需要获取设备的地址和 RSSI,且需要节省电量,你可能会选择 SCAN_RESULT_TYPE_ABBREVIATEDSCAN_RESULT_TYPE_TRUNCATED

Time of delay for reporting the scan result.

"Time of delay for reporting the scan result" 是指扫描结果报告的延迟时间。这是在设置 Bluetooth LE 扫描参数时的一个选项。

这个设置主要影响的是扫描结果的报告频率。具体来说,如果设置了一个较长的延迟时间,那么扫描结果将会在这个延迟时间结束后一次性报告,而不是在每次扫描到一个新设备时立即报告。这种方式可以有效地减少应用的 CPU 使用率,因为应用不需要频繁地处理扫描结果。这对于需要长时间运行扫描的应用来说是非常有用的。

然而,这也意味着应用将不能立即获取到扫描结果。如果应用需要实时处理扫描结果,那么这个设置可能就不合适了。所以在设置这个参数时,需要根据应用的需求进行权衡。

如果你设置了批处理扫描结果的延迟时间(即 "Time of delay for reporting the scan result"),那么扫描结果将会在这个延迟时间结束后一次性通过 onBatchScanResult 方法返回,而不是在每次扫描到一个新设备时立即通过 onScanResult 方法返回

Bluetooth Le Match Mode

在 Android 的蓝牙低功耗(Bluetooth Low Energy,BLE)扫描中,mMatchMode 是一个参数,用于控制扫描过程中设备匹配的模式。具体来说,它有两个选项:

  1. ScanSettings.MATCH_MODE_AGGRESSIVE:这是一种积极的匹配模式,扫描过程会尽可能地返回所有匹配的结果。这种模式可能会消耗更多的系统资源。

  2. ScanSettings.MATCH_MODE_STICKY:这是一种更保守的匹配模式,只有在设备被多次扫描到并且满足一定的信号强度要求时,才会返回匹配结果。这种模式可以减少偶然的匹配,并可能减少系统资源的消耗。

在设置这个参数时,需要根据应用的需求和设备的资源状况进行权衡。例如,如果应用需要立即发现所有的设备,那么可以选择积极模式;如果应用只需要发现稳定的设备并且需要节省资源,那么可以选择保守模式。

Bluetooth Le NumOfMatchesPerFilte

在 Android 的蓝牙低功耗(Bluetooth Low Energy,BLE)扫描中,mNumOfMatchesPerFilter 是一个参数,用于控制每个过滤器返回的匹配设备的数量。具体来说,它有三个选项:

  1. ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT:这个设置意味着,对于每个过滤器,只返回一次匹配的广播包。也就是说,如果一个设备发出多个广播,那么只有第一个广播会被返回。

  2. ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT:这个设置意味着,对于每个过滤器,只返回少量的匹配广播。具体的数量由系统决定,但肯定会多于一次。

  3. ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT:这个设置意味着,对于每个过滤器,返回所有匹配的广播。这个设置可能会消耗更多的系统资源。

在设置这个参数时,需要根据应用的需求和设备的资源状况进行权衡。例如,如果应用只需要知道设备是否存在,那么可以选择 MATCH_NUM_ONE_ADVERTISEMENT;如果应用需要获取设备广播的更多信息,那么可以选择 MATCH_NUM_FEW_ADVERTISEMENTMATCH_NUM_MAX_ADVERTISEMENT

Include only legacy advertising results.

在 Android 的蓝牙低功耗(Bluetooth Low Energy,BLE)扫描中,mLegacy用来设置是否只扫描低功耗的蓝牙设备类型,默认为true。

Bluetooth le Phy

这个用于设置扫描过程中使用的物理层。

  • setPhy 方法仅在 ScanSettings.Builder#setLegacy 被设置为 false 时使用。这意味着,只有在进行非遗留扫描(即使用蓝牙5.0或更高版本的特性)时,才能设置物理层。

  • 你可以通过调用 android.bluetooth.BluetoothAdapter#isLeCodedPhySupported 方法来检查设备是否支持 LE Coded PHY。如果设备不支持你选择的 PHY,那么扫描将无法启动。

  • phy 参数可以是以下几种值之一:

    • BluetoothDevice#PHY_LE_1M:使用 1M PHY 进行扫描。
    • BluetoothDevice#PHY_LE_CODED:使用 Coded PHY 进行扫描。
    • ScanSettings#PHY_LE_ALL_SUPPORTED:使用所有支持的 PHY 进行扫描。

这个方法允许你根据需要选择最适合你的应用的物理层,以实现更高的数据传输速率或更远的传输距离。

蓝牙扫描过滤

对于蓝牙扫描,android还为我们提供了扫描过滤设置项供我们使用,下面我们来介绍一下蓝牙扫描过滤的设置。

在 Android 的 BLE 扫描中,ScanFilter 需要以列表(List)的形式提供,因为这样可以允许应用程序一次性定义多个过滤条件。每个 ScanFilter 对象代表一组特定的过滤条件。这样,只要扫描到的设备满足列表中任何一个 ScanFilter 的条件,就会被报告给扫描回调。

例如,如果你的应用程序需要连接到多种不同类型的 BLE 设备,每种设备都有自己独特的服务 UUID,那么你就可以为每种设备创建一个 ScanFilter,然后将这些 ScanFilter 放入列表中。这样,只要扫描到的设备满足任何一个 ScanFilter 的条件,就会被报告给扫描回调。

使用 ScanFilter 列表可以让你的扫描过程更加灵活和高效。

由于ScanFilter通常是匹配的方式,这里不再一一介绍了,大家可以查看官方的android文档。一下是ScanFilter的过滤的主要匹配项,其中,uuid是使用的较多的。

less 复制代码
private ScanFilter(String name, String deviceAddress, ParcelUuid uuid, ParcelUuid uuidMask,
        ParcelUuid solicitationUuid, ParcelUuid solicitationUuidMask,
        ParcelUuid serviceDataUuid, byte[] serviceData, byte[] serviceDataMask,
        int manufacturerId, byte[] manufacturerData, byte[] manufacturerDataMask,
        @AddressType int addressType, @Nullable byte[] irk, int advertisingDataType,
        @Nullable byte[] advertisingData, @Nullable byte[] advertisingDataMask,
        @Nullable TransportBlockFilter transportBlockFilter) {

停止扫描

scss 复制代码
// 停止扫描
bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback)

Android扫描策略优化

本来这一章节只是简单的介绍一下蓝牙广播包以及扫描API的使用。不过还是想着结合android系统对蓝牙扫描的设计,来介绍一下android app上对蓝牙扫描的优化建议。

android的蓝牙扫描是一种系统服务,类似wifi。我们通过系统提供的API进行扫描,实际上就是提交了一个扫描任务给到系统底层,系统按照我们提交的扫描设置,返回我们的扫描结果。我们发现,其实蓝牙的扫描里面,是没有超时参数,除非你调用停止扫描,否则他会一直按照扫描当中的策略进行。

在 Android 中,频繁地开启和停止蓝牙扫描可能会导致以下问题:

  1. 影响性能:蓝牙扫描是一项耗电的操作。频繁地开启和停止扫描可能会导致设备电池电量快速消耗。
  2. 影响设备连接:频繁地开启和停止扫描可能会导致蓝牙堆栈不稳定,从而影响设备的连接性能和稳定性。
  3. 可能被系统限制:从 Android 7.0(API 级别 24)开始,Android 对后台应用进行了限制,如果应用在后台频繁扫描,可能会被系统限制。

因此,最好的做法是尽量减少扫描的频率,并在不需要扫描时立即停止扫描。此外,如果可能,应使用低功耗扫描模式,并在扫描过程中使用过滤器来减少不必要的结果。

我们先根据自己的应用的蓝牙场景,确定大多数场景下适用的蓝牙扫描策略,将该策略作为一个基础策略。然后所有外部的扫描均作为一个扫描请求的任务进入队列即可。因此我们可以将系统的扫描方法使用门面模式+构建者模式+生产者-消费者的模式,来进行android的蓝牙扫描封装。

定义对外接口

kotlin 复制代码
interface IScanLeDo{
    // 添加扫描任务
    fun addScanLeRequest(scanLeRequest: ScanLeRequest)
    // 移除扫描任务
    fun removeScanLeRequest(scanLeRequest: ScanLeRequest)
}

定义扫描任务

kotlin 复制代码
// 定义扫描请求任务
data class ScanLeRequest(
    val scanFilterList: List<ScanFilter>?,
    val scanSettings: ScanSettings?,
    val timeout : Long,
    val scanCallback: ScanCallback
){
    // 根据timeout计算超时的时刻
    val timeoutMillis : Long
        get() = System.currentTimeMillis() + timeout
}

初始化扫描管理器

kotlin 复制代码
object BleScanRequestManager : IScanLeDo{
    private val MESSAGE_WHAT_START_SCAN = 1
    private val MESSAGE_WHAT_STOP_SCAN = 2

    private val scanLeRequestMap : MutableMap<ScanLeRequest,ScanCallback> = ConcurrentHashMap<ScanLeRequest,ScanCallback>()
    private var scanTimeMillis : Long = 0
    private val scanning : AtomicBoolean = AtomicBoolean(false)
    private val context : Context by lazy {
        TestApplication.context
    }

    // 获取BluetoothManager实例
    private val bluetoothManager: BluetoothManager by lazy { context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager }
    // 获取BluetoothAdapter实例
    private val bluetoothAdapter : BluetoothAdapter by lazy { bluetoothManager.adapter }
    // 单独使用一个HandlerThread去进行扫描,异步进行
    private val bleHandlerThread : HandlerThread = HandlerThread("Ble-Scanner")
    // 使用Handler进行控制蓝牙的开启扫描和停止扫描
    private val bleHandler : Handler
    // 定义全局的扫描返回callback进行代理,利用时间戳去判断是否需要回调分发
    private val scanCallback: ScanCallback = object : ScanCallback(){
        override fun onBatchScanResults(results: MutableList<ScanResult>?) {
            super.onBatchScanResults(results)
            scanLeRequestMap.forEach{scanLeRequest->
                // 这里简单使用时间去进行判断
                if (scanLeRequest.key.timeoutMillis >= System.currentTimeMillis()){
                    // todo 其余的过滤判断
                    scanLeRequest.key.scanCallback.onBatchScanResults(results)
                }
            }
        }

        override fun onScanFailed(errorCode: Int) {
            super.onScanFailed(errorCode)
            scanLeRequestMap.forEach{scanLeRequest->
                // 这里简单使用时间去进行判断
                if (scanLeRequest.key.timeoutMillis >= System.currentTimeMillis()){
                    // todo 其余的过滤判断
                    scanLeRequest.key.scanCallback.onScanFailed(errorCode)
                }
            }
        }

        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            super.onScanResult(callbackType, result)
            scanLeRequestMap.forEach{scanLeRequest->
                // 这里简单使用时间去进行判断
                if (scanLeRequest.key.timeoutMillis >= System.currentTimeMillis()){
                    // todo 其余的过滤判断
                    scanLeRequest.key.scanCallback.onScanResult(callbackType,result)
                }
            }
        }
    }
    
    init {
        bleHandlerThread.start()
        bleHandler = Handler(bleHandlerThread.looper){
             when(it.what){
                MESSAGE_WHAT_START_SCAN->{
                    // start scan
                    startRealScan()
                    true
                }
                MESSAGE_WHAT_STOP_SCAN->{
                    // stop scan
                    stopRealScan()
                    true
                }
                else -> false
             }
        }
    }


    override fun addScanLeRequest(scanLeRequest: ScanLeRequest){
        if (scanLeRequest.timeout > 0){
            scanLeRequestMap[scanLeRequest] = scanLeRequest.scanCallback
            if (scanLeRequestMap.isEmpty() && !scanning.get()){
                // 如果当前没有扫描任务且当前蓝牙并没有进行扫描,则开启扫描
                sendStartScanMessage()
            }else{
                // 否则仅更新停止扫描的时间
                sendStopScanMessage()
            }
            // 如果新的扫描时间大于当前设置的扫描时间,则使用新的扫描时间
            scanTimeMillis = (if (scanLeRequest.timeoutMillis > scanTimeMillis) scanLeRequest.timeoutMillis else scanTimeMillis)
        }
    }

    override fun removeScanLeRequest(scanLeRequest: ScanLeRequest) {
        scanLeRequestMap.remove(scanLeRequest)
    }
    // 开始扫描
    private fun sendStartScanMessage(){
        if (scanning.compareAndSet(false,true)) {
            bleHandler.removeMessages(MESSAGE_WHAT_START_SCAN)
            bleHandler.sendEmptyMessage(MESSAGE_WHAT_START_SCAN)
        }
        sendStopScanMessage()
    }

    // 更新停止扫描时间
    private fun sendStopScanMessage(){
        bleHandler.removeMessages(MESSAGE_WHAT_STOP_SCAN)
        bleHandler.sendEmptyMessageAtTime(MESSAGE_WHAT_STOP_SCAN,scanTimeMillis)
    }

    private fun startRealScan(){
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.BLUETOOTH_SCAN
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // 权限处理
            return
        }
        // 设置扫描策略
        val scanSettings : ScanSettings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
            .setLegacy(true)
            .setMatchMode(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
            .setReportDelay(3000)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
            .build()
        // 设置过滤条件
        val scanFilterList = arrayListOf<ScanFilter>()
        val scanFilter = ScanFilter.Builder()
            .setServiceUuid(ParcelUuid(UUID.randomUUID()))// 替换程自己的UUID
            .build()
        scanFilterList.add(scanFilter)
        bluetoothAdapter.bluetoothLeScanner.startScan(scanFilterList,scanSettings, scanCallback)
    }

    private fun stopRealScan(){
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.BLUETOOTH_SCAN
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // 权限处理
            return
        }
        bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback)
    }
}

上面的写法只是去表达我对蓝牙扫描的一种策略,实际当中还要根据业务具体需要进行封装。

总结

本章我们介绍了蓝牙开发中的蓝牙ble广播包数据包的格式数据包的解析 以及蓝牙扫描策略 ,下一篇我们开始了解蓝牙的连接相关的知识

相关推荐
2401_8979078628 分钟前
10天学会flutter DAY2 玩转dart 类
android·flutter
m0_748233641 小时前
【PHP】部署和发布PHP网站到IIS服务器
android·服务器·php
Yeats_Liao2 小时前
Spring 定时任务:@Scheduled 注解四大参数解析
android·java·spring
雾里看山4 小时前
【MySQL】 库的操作
android·数据库·笔记·mysql
水瓶丫头站住12 小时前
安卓APP如何适配不同的手机分辨率
android·智能手机
xvch13 小时前
Kotlin 2.1.0 入门教程(五)
android·kotlin
xvch17 小时前
Kotlin 2.1.0 入门教程(七)
android·kotlin
望风的懒蜗牛17 小时前
编译Android平台使用的FFmpeg库
android
浩宇软件开发17 小时前
Android开发,待办事项提醒App的设计与实现(个人中心页)
android·android studio·android开发
ac-er888818 小时前
Yii框架中的多语言支持:如何实现国际化
android·开发语言·php