啊,蓝牙……这种让我们又爱又恨的技术。它就像那个总是快要建立连接,但最终却没能成功的朋友。

多年来,Android开发者们与蓝牙之间一直保持着一种既戏剧性又常常充满挫折的“关系”。我们一直在努力应对它的各种怪癖,恳求它能够正常工作,也为那些莫名其妙的连接中断而暗自流泪。

但要是我告诉你,情况即将好转呢?如果我说随着Android 16的发布,蓝牙技术终于开始对我们“笑脸相迎”了,你们会相信吗?这不是梦,朋友们。这就是AOSP 16中的蓝牙扫描功能,它将为我们这些疲惫的开发者带来新的希望。

在这本手册中,我们将一起踏上一段探索之旅——深入了解AOSP 16中全新的蓝牙功能。我们会欢笑,也会流泪(但希望这次是喜极而泣),同时学会如何将这些新功能用于有益的目的。我们将探究被动扫描的神奇之处,了解导致蓝牙连接中断的各种原因,还会体验到无需繁琐操作就能获取服务UUID的便捷性。

读完这本手册后,你将能够:

  • 开发出效率极高的蓝牙扫描工具,几乎具备“预知未来”的能力。

  • 像经验丰富的侦探一样,排查各种连接问题。

  • 用你新掌握的蓝牙技术,让朋友和同事们刮目相看。

先决条件:

在开始学习之前,最好对Android开发及Kotlin有一定的了解。如果你曾经尝试过让两台设备进行通信,结果却差点把电脑扔出去的话,那你就已经具备足够的入门条件了。

那么,拿起你最喜欢的饮料,穿上你的“编程斗篷”,让我们一起迎接蓝牙技术的新时代吧!

目录

  1. Android中蓝牙技术的发展简史

  2. AOSP 16中的新功能:三大重要变化

  3. 深入探讨#1:被动扫描技术

  4. 了解BluetoothLeScanner的工作原理

  5. 动手实践:构建你的第一个被动扫描工具

  6. 深入探讨#2:蓝牙连接中断的原因

  7. 深入探讨#3:从广告中获取服务UUID的方法

  8. 高级主题:提升你的扫描技能

  9. 实际应用案例:蓝牙技术在现实世界中的用途

  10. API版本检测:如何避免应用程序崩溃

  11. 测试与调试:这个被忽视却很有趣的环节

  12. 性能优化与最佳实践:如何成为“合格的蓝牙使用者”

  13. 结论:未来的蓝牙技术将更加依赖被动扫描模式,而这并没有什么不好。

蓝牙的简史(或是我们如何学会不再担心电量问题并爱上无线电波)

经典蓝牙时代

最初,人们使用的是经典蓝牙。它就像一个吵闹、活跃的派对参与者——能够传输大量数据(比如将你最喜欢的音乐传送到扬声器上),但它的耗电量也非常大。对于流媒体音频播放来说,它确实非常适用;但对于那些数据量小、传输频率不高的场景而言?使用它就如同用消防水管来浇灌一盆盆栽植物——完全是一种过度行为,而且说实话,还会造成很多不必要的麻烦。

在这个时代,开发人员们整天都在与BluetoothAdapter、BluetoothDevice以及那个令人头疼的BluetoothSocket打交道。那是一段充满不确定性的时期:建立一个简单的连接有时甚至需要花费好几秒钟的时间……嗯,换句话说,你完全可以趁这个时间去喝杯咖啡。至于电量消耗问题?用户的手机电池电量会以惊人的速度下降。

蓝牙低功耗技术的诞生

后来,随着Android 4.3的发布,一个新的技术出现了:蓝牙低功耗技术。这与传统的蓝牙截然不同——它更加高效、更省电,专为短时间的数据传输而设计,就像品鉴优质葡萄酒一样,不会浪费过多的电量。

蓝牙低功耗技术真正成为了那个“酷炫的新星”。它为我们打开了一个充满可能性的新世界:心率监测器、智能手表,以及无数其他可以通过一枚纽扣电池运行数月的物联网设备。这一技术的出现确实改变了整个行业的发展格局。

然而,强大的功能往往也伴随着复杂的操作流程。我们必须学习GATT、GAP等一系列全新的技术规范,这就像是从编写简单的脚本突然转变为创作一部完整的歌剧一样——潜力巨大,但学习曲线也非常陡峭。

扫描技术带来的挑战

还有另一个问题,那就是设备扫描。在蓝牙低功耗技术的早期阶段,扫描过程仍然显得相当混乱且耗电量大。手机会不断地向周围发送信号“有没有设备在响应?”,然后等待回复。这种做法虽然有效,但依然会导致电量迅速消耗,尤其是当应用程序需要长时间进行扫描操作时。

这正是开发人员们面临的经典困境:你确实需要找到这些设备,但又不能因此让用户的手机在午饭时间之前就没电了。多年来,我们一直在努力平衡这一矛盾——一方面要满足设备搜索的需求,另一方面又要尽可能地节省电量。

AOSP 16正是在这样的背景下诞生的。这个版本迫切需要一种更高效的扫描方式,也急需一位能够解决这个问题的“英雄”。而这位英雄,朋友们,就是被动扫描技术。不过关于这一技术的更多细节,我们稍后还会详细讨论……

两次发布的故事

令人意想不到的是,Android决定在2025年发布两个重要的API版本。首先,在第二季度发布了Android 16——这个版本的代号是“巴克拉瓦”,因为谁会不喜欢美味的糕点呢?这是一个传统的、大规模发布的版本,其中包含了大家熟悉的各种功能变更,这些变化可能会让人感到欢喜,也可能会引起一些困扰。

然后在第四季度,又出现了另一个惊喜:一个次要的发布版本。在这个版本中,我们看到了许多新的蓝牙功能。这个版本的更新主要集中在新增特性和API上,并没有那些会破坏应用程序正常运行的重大改动。这就像是在你已经付完账单之后,还能免费得到一份甜点一样。

蓝牙领域的“三剑客”

那么,这个第四季度发布的版本为蓝牙技术带来了哪些新功能呢?很高兴你问了这个问题。它为我们带来了三个新的“英雄”,这些功能能够帮助我们解决在使用蓝牙时遇到的各种问题。我把它们称为……蓝牙领域的“三剑客”。

功能名称

主要内容

为什么你应该关注这个功能

被动扫描

这种模式允许应用程序在不需要主动发送信号的情况下,静静地搜索周围的蓝牙设备。

这样一来,你的应用程序就可以以更加安静、省电的方式运行了。

蓝牙连接中断的原因分析

终于,我们能够清楚地了解为什么蓝牙连接会突然断开了。

再也不用盲目猜测问题原因了,现在你可以真正地调试这些连接故障了。

从广告中获取设备信息

你现在可以直接从设备的广告信息中获取到其重要的配置参数。

这就像是蓝牙设备之间的“速配”过程,能让连接速度更快、效率更高。

这些可不是些微不足道的调整,它们实际上会显著改善我们开发和使用蓝牙应用程序的体验。看来Android团队确实听到了我们的呼声啊……(我知道,我也感到非常惊讶。)

在接下来的几节内容中,我们将详细了解每一个新功能。我们会深入研究相关代码,探讨这些功能的实际应用场景,并学习如何充分利用它们。那么,让我们先来认识第一位“三剑客”吧:那就是被称为“被动扫描”的这个功能。

深度解析#1:被动扫描

想象一下,你正在图书馆里寻找一位朋友,但不知道他们在哪里。这时你有两种选择:

  • 主动扫描:你站在图书馆的中间大声呼喊:“嘿,史蒂夫!你在吗?”这种方法虽然有效,但也会吵到别人,而且还会被图书管理员赶走……在这个比喻中,图书管理员就代表着用户的电池电量。

  • 被动扫描:你可以安静地在图书馆里走动,仔细聆听朋友特有的笑声。你不需要发出任何声音,只需要静静地等待。这种方法既隐蔽又高效,而且不会消耗你的“社交电量”或实际电池电量。

多年来,Android系统的蓝牙扫描功能一直像是在图书馆里大声喧哗的人。但有了AOSP 16,我们终于可以成为那些安静的倾听者了。这就是被动扫描的神奇之处。

主动扫描与被动扫描:技术层面的对比

在BLE技术领域,设备会发送一些被称为“广告信息”的小数据包。这些信息其实是在告诉其他设备:“嘿,我在这里,我的功能是这样的!”

  • 主动扫描:当手机进行主动扫描时,它会接收到这些广告信息,然后发送一个SCAN_REQ请求。这相当于在说:“请告诉我更多关于你的信息吧!”此时,外围设备会回复一个包含额外信息的SCAN_RSP响应。

  • 被动扫描:而在被动扫描模式下,手机只是接收这些广告信息,不会发送任何回应。它只会记录下最初收到的信息,然后继续执行其他操作。这种通信方式是单向的。

代码实现:如何成为蓝牙高手

那么,我们该如何实现这种被动扫描模式呢?其实方法非常简单。关键在于你在开始扫描时所使用的ScanSettings设置。

以前,你可能会这样编写代码:

val settings = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
    .build()

但现在,随着AOSP 16的发布,我们有了一个新的选项。要启用被动扫描模式,只需将scan类型设置为SCAN_TYPE_PASSIVE即可:

这才是真正的关键代码!
.setScanMode(ScanSettings.SCAN_TYPE_PASSIVE)

等等,这样似乎不太对吧。文档上明明写着SCAN_TYPEPASSIVE是一种扫描类型,而不是扫描模式……你说得对!实在是我的疏忽。正确的做法应该是将scan模式设置为SCAN_MODE_OPPORTUNISTIC。我们再试一次。

val settings = ScanSettings.Builder()
    这才是真正的关键步骤!
    .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) 这个选项最接近被动扫描模式
    .build()

等一下,这样似乎也不对。看来我搞混了……我们还是去查阅官方文档吧……啊,找到了!在Android 16 QPR2版本中,ScanSettings.Builder确实增加了一个新的方法。这个方法并不是setScanMode,而是一个全新的设置选项。

让我们把这个问题彻底弄清楚吧。以下是正确启用被动扫描的方法:

// 此方法在Android 16 QPR2及更高版本中可用
val settings = ScanSettings.Builder()
    // 这一行代码才是关键所在,我保证!
    .setScanType(ScanSettings.SCAN_TYPE_PASSIVE) 
    .build()

就这样,通过这一行代码,你的应用程序就能从一个耗电量大、噪音大的“蓝牙设备”,变成一个安静高效、低功耗的“蓝牙工具”。你的用户一定会为此感到庆幸的。

当然,这也意味着一些妥协。由于你没有发送SCAN_REQ请求,因此也就无法接收到SCAN_RSP中包含的额外数据。但对于很多应用场景来说,最初的广告信息就已经足够了,而节省下来的电量绝对值得这个代价。

现在我们已经掌握了被动扫描的技术,接下来就让我们来了解BluetoothLeScanner本身吧。

了解BluetoothLeScanner(我们的核心工具)

要想真正掌握蓝牙扫描技术,首先就必须了解我们的主要武器——BluetoothLeScanner。可以把它想象成《捉鬼敢死队》里使用的那种PKE检测仪。它就是我们用来探测周围环境中那些“看不见的能量”(在我们的例子中,就是BLE广告信号)的工具。那么,这个捉鬼神器究竟是如何工作的呢?

其架构原理:揭秘内部机制

从宏观角度来看,整个过程其实相当简单。你的应用程序决定要寻找一些BLE设备,然后创建一个BluetoothLeScanner实例,并让它去执行扫描任务。

在底层,实际上发生了很多复杂的交互。BluetoothLeScanner会与Android的蓝牙堆栈进行通信(这个堆栈被命名为“Fluoride”,听起来像是牙医会引以为豪的名字)。随后,蓝牙堆栈会与设备的蓝牙控制器进行交互——而这个控制器才是负责实际发送和接收无线电波的硬件设备。由此可见,“实际情况比看起来要复杂得多”。

相关术语解释:GATT、GAP等

当你开始研究BLE技术时,会遇到很多缩写词。别担心,它们并没有你想象中那么难理解。其中最需要了解的两个术语就是GAP和GATT。

  • GAP(通用访问配置文件):这个协议规定了设备之间如何发现彼此并建立连接。可以把GAP想象成夜店里的门卫,它负责决定哪些设备可以互相通信。GAP还管理广告信息的发送以及扫描过程——也就是你的应用程序如何接收这些广告信号。我们的BluetoothLeScanner在GAP机制中扮演着关键角色。

  • GATT(通用属性配置文件):一旦两个设备建立了连接,GATT就会开始发挥作用。它规定了设备之间如何交换数据。可以把GATT想象成夜店里发生的实际对话过程,其中涉及的服务、特性以及描述符等概念都非常重要。例如,某个设备可能拥有“心率服务”,而这个服务中又包含“心率测量特性”。你的应用程序可以通过读取或写入这些特性来获取所需的数据。

就扫描功能而言,我们实际上处于GAP这个框架所构建的世界之中。我们就像站在俱乐部外的人,时刻留意那些有趣的广告信息。

扫描生命周期:一部三幕剧

蓝牙扫描的过程其实是一部简单而精妙的“戏剧”。

  • 第一幕:准备工作。你的应用程序会决定开始进行扫描。它会获取BluetoothLeScanner对象,创建一组ScanFilters(用于仅搜索特定的设备),以及ScanSettings(用于定义扫描方式,比如我们新推出的被动扫描模式),同时还会定义一个ScanCallback。

  • 第二幕:实际扫描过程。你的应用程序会调用startScan()方法。此时蓝牙模块会开始工作,搜寻符合设定条件的设备信息。一旦找到符合条件的设备,它就会通过ScanCallback中的onScanResult()方法将结果反馈给应用程序。

  • 第三幕:扫描结束。当你的应用程序完成扫描任务时(或者更重要的是,当你找到了想要找到的设备时),它会调用stopScan()方法。此时蓝牙模块会关闭,一切恢复平静。需要注意的是,完成扫描后务必及时停止相关操作——否则那些未经授权的扫描行为就会成为用户抱怨“电池一小时就耗尽了”的主要原因。

简而言之,BluetoothLeScanner就是我们进入BLE设备发现世界的入口。它功能强大且结构复杂,但随着Android版本的不断更新,它的性能也在不断提升、效率也在不断提高。现在我们已经了解了这个工具的用途,那么就让我们动手实践吧,制作出我们的第一个被动扫描器吧!

动手实践:制作你的第一个被动扫描器

理论固然重要,但说实话,我们都是开发者。我们是通过实际操作来学习的(或者通过复制Stack Overflow上的代码来学习的)。现在是时候卷起袖子,打开Android Studio,开始动手实践了。我们将创建一个简单的应用程序,利用刚刚学到的被动扫描技术来查找附近的BLE设备。

第一步:权限申请

在编写任何Kotlin代码之前,我们必须先获得Android系统的授权许可。这个过程虽然有些繁琐,但却十分重要。对于蓝牙扫描功能而言,这些权限要求在这些年里发生了一些变化。

首先,打开你的AndroidManifest.xml文件,然后添加以下内容:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

<!-- 适用于Android 12(API 31)及更高版本 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

<!-- 对于较低版本的Android系统,还需要位置权限 -->
<!-- 如果你的应用程序还支持旧版本的设备,可能仍然需要这个权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
uses-permission android:name="android.permissionACCESS_COARSE_LOCATION" />>

从我们上面列出的权限设置中,你可以实时看到Android蓝牙权限模型的发展变化。

前两项权限:BLUETOOTHBLUETOOTH_ADMIN,属于旧有的权限设置。它们从Android早期就存在了,用于提供基本的蓝牙功能以及设备发现功能。而BLUETOOTH_SCAN这一权限则是从Android 12(API 31)开始引入的,它体现了谷歌在隐私保护方面的新理念。

没错,你的理解是正确的。在Android 12之前,谷歌认为搜索蓝牙设备实际上就等同于获取用户的精确位置信息。这种做法看似合理:因为如果能够识别出附近的蓝牙设备,就可以通过三角定位来确定用户的位置。但仅仅为了寻找一副耳机就要求用户提供位置信息,确实显得有些不合理,这也导致用户会怀疑这类应用的真实目的。

幸运的是,从Android 12开始,谷歌引入了BLUETOOTH_SCAN权限设置,这一设计要合理得多。现在,应用程序可以在不需要获取用户位置信息的情况下扫描蓝牙设备,从用户的角度来看,这种做法显然更加合理。虽然你仍然需要在运行时请求这个权限,但至少不必再向用户解释为什么一个简单的设备查找应用会需要知道他们的居住地址。

不过,请注意最后两项与位置信息相关的权限设置——这些其实是旧系统的遗留物。如果你正在开发需要支持Android 11及更低版本设备的应用程序,为了保证兼容性,你仍然需要在应用的清单文件中保留这些权限设置。而在现代设备上,仅使用BLUETOOTH_SCAN权限就已经足够了。

步骤2:代码的实现

好了,现在让我们进入有趣的部分吧。下面将详细介绍如何在你的Activity或Fragment中实现被动蓝牙设备扫描功能。

获取扫描器对象

首先,我们需要获取一个BluetoothLeScanner实例。

private val bluetoothAdapter: BluetoothAdapter? by lazy {
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    bluetoothManager.adapter
}

private val bleScanner: BluetoothLeScanner? by lazy {
    bluetoothAdapter?.bluetoothLeScanner
}

让我们来分析一下上面的代码。这里使用了Kotlin的lazy延迟初始化机制,简单来说就是“直到真正需要这个对象时才创建它”。这种做法非常合理,因为获取蓝牙适配器涉及系统调用,如果根本不会使用这个对象,就没有必要进行这些操作。

首先,我们需要从系统服务中获取BluetoothManager。可以将BluetoothManager视为设备上所有蓝牙相关功能的管控者。通过这个管理器,我们可以得到代表设备蓝牙硬件的BluetoothAdapter。需要注意的是,我们将它声明为可为空类型(BluetoothAdapter?),因为事实上,并非所有的Android设备都配备了蓝牙功能。有些平板电脑或较为少见的设备可能没有蓝牙硬件,因此我们必须考虑到这种可能性。

一旦获得了BluetoothAdapter,我们就可以向它请求获取BluetoothLeScanner。这个对象才是我们用来执行扫描操作的实际工具。同样,这里我们也使用了安全调用操作符(?.),因为如果BluetoothAdapter为null(即设备没有蓝牙硬件),那么我们就肯定无法从中获取到BluetoothLeScanner。这种防御性的编程方式虽然看起来有些过于谨慎,但实际上正是它让应用程序能够在遇到异常情况时依然能够正常运行,而不会出现莫名其妙崩溃的情况。

定义回调函数

这才是真正关键的部分。ScanCallback是一个用于接收扫描结果的对象。我们需要重写两个方法:`onScanResult`和`onScanFailed`。

private val scanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        找到设备了!
        “result”对象中包含了设备的地址、RSSI值以及广告数据。
        Log.d("BleScanner", "找到的设备是:${result.rssi}")
    }

    override fun onScanFailed(errorCode: Int) {
        或者可能是出现了严重的错误。
        Log.e("BleScanner", “扫描失败,错误代码为:$errorCode")
    }
}

上面定义的ScanCallback实际上就是你的应用程序在蓝牙世界中的“耳朵”。当扫描器找到设备时,它并不会将信息存储在某个地方,而是会通过这个回调对象主动将结果传递给你的应用程序。这就是典型的事件驱动编程方式,也正是Android能够确保应用程序在不阻塞主线程的情况下依然保持响应性的原因。

onScanResult方法会在扫描器检测到符合您设定条件的设备时被调用(如果您没有使用任何过滤条件,那么就会扫描所有设备)。result参数中包含了大量有用的信息:其中包含BluetoothDevice对象(该对象记录了设备的MAC地址和名称)、RSSI值(接收信号强度指示器——数值越高,表示设备距离越近),以及设备正在发送的原始广告数据。

在上面这个简单的例子中,我们只是记录了设备的MAC地址和RSSI值,但在实际的应用程序中,您可能想要更新用户界面、将设备添加到列表中,或者尝试建立连接。

callbackType参数可以告诉您为什么会触发这次回调。它可以是CALLBACK_TYPE_ALL_MATCHES(默认值,表示“我们找到的所有设备”),CALLBACK_TYPE_FIRSTMATCH(表示“第一次检测到这个设备”),或者CALLBACK_TYPE_MATCH_LOST(表示“已经有一段时间没有检测到这个设备了,很可能它已经离开了附近区域”)。我们会在后面的高级内容中进一步详细讲解这些类型。

onScanFailed方法则是我们都不希望被调用的,但绝对必须妥善处理的。当扫描过程中出现严重错误时,就会触发这个方法。例如,蓝牙适配器可能在扫描过程中被关闭了,或者您的应用程序没有足够的权限来执行扫描操作,又或者蓝牙控制器出现了故障。errorCode参数会告诉您具体发生了什么问题,因此您应该始终记录这些错误信息,并采取适当的处理措施——比如向用户显示提示信息,或者在一段时间后重新尝试进行扫描。

配置扫描设置

接下来,我们需要创建ScanSettings对象。通过这个对象,我们可以告诉Android系统我们希望以节能的方式运行扫描功能。

val scanSettings = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) 这样就能节省电池电量了
    .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
    .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
    .setNumOfMatches(ScanSettingsMATCH_NUM_ONE_ADVERTISEMENT) 每个广告信息只报告一次
    .setReportDelay(0L) 开始与停止扫描

最后,我们需要一些函数来启动和停止扫描操作。请记住:一定要随时停止扫描!被遗忘的扫描任务会成为消耗电池的“害兽”。

private fun startBleScan() {
    别忘了先请求权限!
    if (bleScanner != null) {
        你可以在这里添加ScanFilters来搜索特定的设备
        val scan Filters: List = listOf() 
        bleScanner.startScan(scanFilters, scanSettings, scanCallback)
        Log.d("BleScanner", “扫描开始。”)
    } else {
        Log.e("BleScanner", “蓝牙设备不可用。”)
    }
}

private fun stopBleScan() {
    if (bleScanner != null) {
        bleScanner.stopScan(scanCallback)
        Log.d("BleScanner", “扫描停止。”)
    }
}

上面这两个函数实际上就是用于控制你的蓝牙扫描器的开关,尽管它们的功能非常简单,但作用却极为重要。让我们来详细分析一下每个函数的具体工作原理。
startBleScan()函数中,我们首先会检查bleScanner是否为null。这是一个必要的安全措施:如果设备没有蓝牙硬件,或者蓝牙功能被关闭,那么bleScanner就会是null,而如果我们尝试对一个null对象调用方法,应用程序很可能会崩溃。如果扫描器存在,我们就会使用三个参数来调用startScan()函数:一组ScanFilter对象、我们精心配置的ScanSettings,以及之前定义好的ScanCallback
在我们的示例中,scanFilters列表目前是空的,这意味着系统会“搜索所有蓝牙设备”。在现实世界的应用程序中,你通常会添加一些过滤条件来缩小搜索范围。例如,如果你正在开发一个只与心率监测器配合使用的应用,你可以创建一个过滤器,只让那些广播Heart Rate Service UUID的设备被检测到。这样做对于提升应用程序的性能和延长电池续航时间来说非常重要:既然你只关心健身追踪设备,为什么还要让应用程序去响应那些随机出现的蓝牙牙刷设备呢?
startScan()函数被调用时,扫描过程就会开始。从这一刻起,蓝牙模块会处于主动监听状态,等待设备发送广播信息,而你的scanCallback也会开始接收搜索结果。需要注意的是,这是一个异步操作:你的代码不会在这里阻塞等待结果,而是会继续执行其他任务,而结果会在准备好后被通过回调函数传回来。
现在我们来谈谈stopBleScan()函数。这可能是你编写的所有蓝牙相关代码中最重要的一个函数了。当你调用stopScan()并传入相应的回调函数时,你实际上是在告诉蓝牙模块:“好了,我们的扫描任务已经完成了,你可以重新进入休眠状态了。”这样就能立即停止扫描过程,并释放相关资源。
需要重点理解的是,如果你不调用这个函数,扫描操作就会无限期地持续下去,从而像吸血鬼一样耗尽用户的电池电量。这就是为什么我们如此强调要使用stopBleScan()的原因:忘记调用这个函数是导致蓝牙应用程序出现电池消耗问题的最常见原因之一。
需要注意的是,在stopScan()函数中,我们传递的仍然是之前在startScan()函数中使用的那个scanCallback对象。正是通过这个对象,Android系统才能知道应该停止哪一次扫描操作。理论上来说,你也可以让多个不同的扫描任务使用不同的回调函数来处理结果,但这样做通常并不可取。因此,请务必确保使用相同的回调函数引用来停止你之前启动的扫描操作。

整体总结

下面是一个完整的示例代码,你可以将其添加到任何Activity中。不过别忘了处理运行时权限相关的问题哦!

// 在你的Activity类中

class MainActivity : AppCompatActivity() {

    // ... (上面提到的适配器和扫描器的相关代码)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ... 进行UI布局设置……

        // 示例:点击按钮开始扫描
        // 在调用此方法之前,必须先请求权限!
            startBleScan()
        }

        // 示例:点击另一个按钮停止扫描
        // ... (上面提到的scanCallback、startBleScan、stopBleScan方法)

    override fun onPausesuper.onPause()
        // 当Activity不可见时,必须停止扫描操作。
        stopBleScan()
    }
}

上面这个完整的示例展示了在真实的应用程序中,所有这些组件是如何协同工作的。这是一个功能简单但能够正常运行的蓝牙扫描器。让我们来重点介绍一下这里使用的一些重要设计模式。

首先,请注意我们是如何通过按钮点击将扫描流程与用户的操作联系起来的。这是一种常见的设计模式:用户可以明确地启动或停止扫描,这样他们就可以控制应用程序何时使用蓝牙功能。这种设计既有利于提升用户体验,也有助于延长电池寿命,因为只有当用户需要时,扫描功能才会被激活。

但真正关键的部分在于对onPause()方法的重写。这是一个非常重要的安全机制。当你的应用程序进入后台状态时(比如用户按下了主屏幕按钮,或者切换到了其他应用程序),onPause()方法会被调用,此时扫描功能会立即停止。这一点非常重要,因为如果用户看不到你的应用程序,他们自然也不需要扫描结果,因此没有必要让电池继续消耗电量。这种设计确保了即使用户忘记了点击“停止”按钮,扫描功能也不会在后台持续运行。

你可能会问:“那onResume()方法呢?当用户返回到应用程序时,我们不应该重新启动扫描功能吗?”这其实是一个设计上的选择。在某些应用程序中,你可能希望在onResume()方法中自动重新开始扫描;而在其他应用程序中,你可能希望让用户再次明确地点击“开始”按钮。这取决于你的具体应用场景。对于那些用户正在主动进行搜索的设备查找类应用程序来说,自动恢复扫描功能是合理的;而对于那些在后台运行的监控类应用程序而言,用户可能需要更加明确的控制权。

在这个示例中,我们没有涉及到运行时权限处理的问题。还记得我们在应用程序的清单文件中声明的那些权限吗?在Android 6.0及更高版本系统中,你不能仅仅在清单文件中声明这些权限,而必须在运行时向用户请求这些权限。在调用startBleScan()方法之前,你应该先检查自己是否拥有必要的权限;如果没有,就需要使用ActivityCompat.requestPermissions()方法来请求这些权限。如果你在没有相应权限的情况下尝试启动扫描功能,系统会默默地失败(或者根据Android版本的不同,会给出相应的提示),而你会因此感到困惑,不明白为什么什么都不会发生。

就这样!你刚刚成功开发出了自己的第一个基于AOSP 16平台的被动式蓝牙扫描器。这个扫描器的体积很小,运行效率极高,它会在后台安静地监听BLE广告信号,并通过回调机制将检测结果传递给应用程序;在不需要扫描时,它会优雅地停止自身运行。

现在,让我们来讨论下一个话题:当出现问题时应该如何处理。是时候谈谈“蓝牙连接中断”的原因了……

深入探讨#2:蓝牙连接中断的原因

啊,蓝牙连接……这是一种美好而神圣的东西。它就像是数字世界中的“友谊手链”——当你将手机与耳机连接在一起时,你就建立了一种长期且值得信赖的关联。这两者会共享密钥,会互相识别,并且会承诺自动连接,从而让你免去了每次都需要手动配对的麻烦。这真是一种美好的“浪漫关系”啊。

直到那种状态不再存在为止。

突然有一天,它们就会……彼此忘记。连接消失了,信任也被打破了。而你的应用程序则还留在那里,试图扮演“调解人”的角色,却完全不知道出了什么问题。你被彻底抛弃了。直到现在,Android系统也毫无帮助——你只会收到一条通知,告知连接状态已变为“BOND_NONE”,但除此之外没有任何解释,也没有任何结论,只有那种代表连接失败后的冰冷沉默。

终于,有了些许交代!

不过,Android团队的开发者们显然也经历过一些艰难的分手经历,因为在AOSP 16版本中,他们为我们提供了这样的“交代”。他们新增了`BluetoothDevice.EXTRA_BOND_LOSS_reason`这个字段。这个字段会随`ACTION_BOND_STATE_CHANGED`广播消息一起被发送过来,用来说明连接失败的具体原因。这简直就像收到了一条能解释分手真相的信息一样!

现在,当蓝牙连接被断开时,你就可以得到一个具体的原因代码。可以把这些原因代码看作是蓝牙连接失败的“借口”,只不过它们是针对蓝牙连接的罢了:

表示连接失败原因是LE加密失败。

表示连接失败原因是LE配对失败。

原因代码(示例说明)

实际含义

BOND_LOSS_REASON_BREDR_AUTH_FAILURE

表示连接失败的原因是BREDR认证失败。

BOND_LOSS_reason_BREDR_INCOMING_PAIRING

表示连接失败原因是BREDR配对失败。

BOND_LOSS_REASON_LE_ENCRYPT_FAILURE

BOND_LOSS_reason_LE_INCOMING_PAIRING

代码:扮演侦探的角色

那么,我们该如何获取这些有用的信息呢?我们需要创建一个`BroadcastReceiver`来监听蓝牙连接状态的变化。

// 创建一个 BroadcastReceiver来监听蓝牙连接状态的变化
private val bondStateReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
            val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
            val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
            val previousBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDeviceERROR)

            // 检查连接状态是否从“已连接”变为“未连接”
            if (bondState == BluetoothDevice.BOND_NONE && previousBondState == BluetoothDevice.BOND_BONDED) {
                Log.d("BondBreakup", “我们被// 现在,让我们找出原因吧……
                val reason = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_LOSS_reason, -1)

                when (reason) {
                    // 注意:具体的常量值可以在Android SDK中找到
                    BluetoothDevice.BOND_LOSS_reason_REMOTE_DEVICE_REMOVED -> {
                        Log.d("BondBreakup", “原因:远程设备主动断开了连接。”)
                        // 可以向用户显示一条提示信息:“您的耳机似乎已经‘忘记’您了,请尝试重新配对。”
                    }
                    // ……处理其他原因……
                    else -> {
                        Log.d("BondBreakup", “原因:情况比较复杂(未知原因代码:$reason)”)
                    }
                }
            }
        }
    }
}

// 在你的Activity或Service中,注册这个接收器
override fun onResume() {
    super.onResume()
    val filter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
    registerReceiver(bondStateReceiver, filter)
}

override fun onPause() {
    super.onPause()
    // 别忘了注销这个接收器!
    unregisterReceiver(bondStateReceiver)
}

上述代码实现了一个用于检测蓝牙连接状态变化的系统,其功能其实比看上去要复杂得多。让我们来了解一下这种广播接收器模式的工作原理,以及它为何如此强大。

首先,我们创建一个BroadcastReceiver,这是Android提供的一种让应用程序能够监听系统级事件的方式。可以将其理解为订阅一种通知服务:每当Android系统中发生某些重要变化时(比如蓝牙连接状态发生变化),系统就会向所有已注册的接收器发送“意图”信息,而我们的接收器就是其中之一。
onReceive()方法中,我们首先会检查传入的意图所表示的动作是否为ACTION_BOND_STATE_CHANGED。这一点非常重要,因为广播接收器可能会接收到多种类型的意图信息,而我们只关心蓝牙连接状态的变化。确认这是正确的事件类型后,我们会使用getParcelableExtra()getIntExtra()方法从意图中提取相关数据。
device对象可以告诉我们这个事件是关于哪台蓝牙设备的。毕竟,我们可能同时与多台设备保持连接(比如耳机、智能手表或汽车),因此我们需要知道到底是哪一台设备与我们的连接中断了。bondState字段表示当前的连接状态,而previousBondState则记录了状态变化之前的情况。
关键逻辑体现在这个条件判断语句中:if (bondState == BluetoothDevice.BOND_NONE && previousBondState == BluetoothDevice.BOND_BONDED)。这条语句专门用于检测“已连接”状态向“未连接”状态的转变,这实际上就代表了蓝牙连接的断开。我们并不关心连接建立的过程本身,而只关注现有连接何时被中断。
一旦检测到连接中断,我们就会从意图中提取EXTRA_BOND_LOSS_reason字段。这个功能是AOSP 16版本新增的,它能够让我们准确了解连接中断的具体原因——是远程设备主动断开了连接,还是用户手动忘记了该设备,又或者是认证失败导致了连接中断?每种原因代码都对应着不同的情况,我们可以根据这些信息采取相应的处理措施。
在上面的示例中,我们使用了条件语句来针对不同的原因代码进行相应处理。对于BOND_LOSS_reason_BREDR_INCOMING_PAIRING这种原因,我们知道是另一台设备主动提出了断开连接,因此可以显示一条提示信息,比如“您的耳机似乎忘记了您,请尝试重新配对”。对于其他原因,则需要添加更多的分支代码来进行专门处理。
最后请注意代码中的生命周期管理机制。我们在onResume()方法中注册接收器,在onPause()方法中取消注册它。这一点非常重要:如果忘记取消注册广播接收器,即使Activity已经被销毁,它仍然会继续接收广播信息,从而导致内存泄漏或应用程序崩溃。通过在onResume()中注册、在onPause()中取消注册,我们可以确保只有当Activity处于可见且活跃状态时,才会监听蓝牙连接状态的变化。

这对调试过程以及用户体验而言,无疑是一个巨大的进步。以前我们只能向用户简单地说“连接失败”,但现在可以根据导致连接中断的具体原因,为他们提供有针对性的建议。这就好比是一位乐于助人、知识渊博的关系顾问,而不是一个只能耸耸肩说“我不知道发生了什么”的茫然旁观者。

既然我们已经谈过了分手带来的情感问题,接下来就让我们来聊聊一些轻松有趣的话题吧:蓝牙设备的快速配对功能。

深度探讨 #3:广告中的服务UUID

让我们来看看如何为你的应用程序找到合适的合作伙伴吧。在BLE技术领域,并非所有设备都是相同的。心率监测仪与智能灯泡之间的区别显而易见,那么你的应用程序该如何判断自己正在与哪种类型的设备进行通信呢?答案就是服务UUID。

什么是服务UUID?

服务UUID(通用唯一标识符)就像是设备的“职业名称”。它是一个由128位二进制数字组成的唯一编码,用来表明该设备提供的是“心率监测服务”还是“电池管理服务”。这一信息对于确定设备的功能而言至关重要。

传统方法:繁琐的初次匹配过程

传统上,要了解一个设备的具体功能需要经历一系列复杂的步骤。这就好比参加一场三道菜的正式晚餐约会,只是为了弄清楚对方的职业而已。具体的操作流程如下:

  1. 扫描:找到目标设备。

  2. 连接:建立连接(这个过程既耗时又耗电)。

  3. 查询服务功能:向设备询问“你主要提供什么服务?”,然后等待它列出所有可用的服务选项。

  4. 验证:检查列出的服务中是否包含你感兴趣的功能。

  5. 断开连接:如果目标设备不符合要求,就必须断开连接并继续寻找其他设备。真是浪费时间和精力啊!

这种做法效率极低,尤其是在一个充满数十个BLE设备的拥挤环境中,而你却只在寻找某一种特定类型的设备时。

新方法:便捷的标识方式

如果派对上的每个人都能佩戴上写有自己职业名称的标签该多好啊?正是AOSP 16版本为我们在BluetoothDevice.EXTRA_UUID_LE中提供了这样的便利。许多蓝牙设备已经主动在广告数据包中包含了它们的主要服务UUID,这相当于它们在向整个房间大声宣告:“我是心率监测仪!”

在AOSP 16之前,要从广告数据包中提取这些信息需要通过手动解析扫描记录中的原始字节数组来完成。虽然这种方法可行,但编写这样的代码往往意味着只需要编写一次,然后祈祷它能正常工作,之后就再也不用去修改它了。

现在,Android已经为我们完成了这项繁琐的工作!系统会自动解析广告数据,如果其中包含了任何服务UUID,就会将这些信息直接通过`ScanResult`传递给我们。

代码实现:读取设备名称标签

这一新功能使得我们的`ScanCallback`更加强大。现在,我们在发现某个设备后,就可以立即检查它的类型,而无需进行任何连接操作。

private val scanCallback = object : ScanCallback() {
    override onScanResult(callbackType: Int, result: ScanResult) {
        Log.d("BleSpeedDating", "发现设备:)

        // 我们来读取这个设备的名称标签吧!
        val serviceUuids = result.scanRecord?.serviceUuids
        if (serviceUuids.isEmpty()) {
            Log.d("BleSpeedDating", "这个设备很特殊,广告数据中没有任何服务UUID。)
            return
        }

        // 定义我们要查找的UUID(例如标准的心率监测服务UUID)
        val heartRateServiceUuid = ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")

        if (serviceUuids.contains(heartRateServiceUuid)) {
            Log.d("BleSpeedDating", "找到了!这是一个心率监测设备,我们可以进行连接了!")
            // 既然确定了这是正确的设备,就可以继续进行连接操作了。
            stopBleScan() // 我们已经找到了所需的信息。
            // connectToDevice(result.device)
        } "BleSpeedDating", "不匹配,继续查找其他设备。)
        }
    }

    ... onScanFailed ...
}

上述代码展示了如何直接从广告数据中读取服务UUID,这一功能极大地改善了设备识别的效率。让我们详细分析一下其中的具体原理以及为什么这一改进如此重要。

当我们的回调函数接收到扫描结果时,`result`对象中会包含一个`scanRecord`属性。实际上,这个扫描记录就是BLE设备发送到空气中的原始广告数据包。

在AOSP 16版本之前,如果你想从这些数据中提取服务UUID,就必须手动解析字节数组,了解BLE广告的格式,处理不同的数据类型,并且还要担心会不会出现数值错误。这种代码往往只用一次之后就再也不会被修改了,因为人们害怕再次遇到问题。

现在,随着AOSP 16的改进,Android已经为我们完成了所有这些繁琐的解析工作。我们只需调用`result.scanRecord?.serviceUuids`,就能得到一份整齐清晰的`ParcelUuid`对象列表。在这里,安全调用操作符`?.`非常重要,因为并不是所有的设备都会在扫描结果中包含服务UUID信息,我们需要妥善处理这种情况。

在获取到服务UUID之后,我们会检查这个列表是否为空。有些设备的广告信息中并不包含服务UUID,可能是它们使用了自定义的格式,也可能是配置有误。如果没有找到任何UUID,我们就会记录一条日志然后立即结束操作——如果我们无法确定该设备的具体功能,继续扫描也没有意义。

接下来,我们需要定义自己要查找的UUID。在这个例子中,我们要寻找心率监测设备,因此使用的是标准的心率服务UUID:`0000180D-0000-1000-8000-00805F9B34FB`。这个UUID是由蓝牙技术联盟定义的,任何符合该标准的心率监测设备都会在广告中发布这个UUID。你可以在蓝牙规范中找到所有标准服务UUID的列表,如果你正在开发自己的BLE外围设备,也可以使用自定义的UUID。

真正的魔法发生在`if (serviceUuids.contains(heartRateServiceUuid))`这一判断语句中。在这里,我们就像是在“快速约会”一样:检查设备的“身份标识”,看它是否与我们要找的设备相匹配。

如果匹配成功,我们就找到了目标设备!我们可以立即停止扫描(既然已经找到了需要的设备,再继续扫描也没意义了),然后尝试连接该设备。因为我们已经确定这个设备就是心率监测器,所以就不会浪费时间和电量去连接那些根本不符合要求的设备了。

如果UUID不匹配,我们就会记录“未找到匹配设备”,然后继续下一个设备的扫描流程。当找到下一个设备时,这个回调过程会再次被执行,我们会重复这一系列操作,直到找到我们需要的心率监测设备,或者用户主动停止扫描为止。

与旧的方法相比,这种处理方式在性能上有了巨大的提升。以前,我们必须连接每一个找到的设备,进行服务发现操作(这需要与设备进行多次来回通信),然后检查这些设备是否提供了我们需要的服务,如果没有,就断开连接。每次连接尝试都会消耗时间和电量,还会产生不必要的无线信号干扰。

而现在,我们可以在扫描阶段就快速筛选并识别目标设备。再也不用像以前那样,误将一个智能灯泡当成健身追踪器来连接了,现在我们可以进行高效、有针对性的连接操作。

这对于那些需要在海量无关设备中寻找特定类型传感器或外设的应用程序来说尤其有用。想象一下,你身处一家医院,那里有数百台支持蓝牙技术的医疗设备;或者你在一个智能家庭里,家里安装了数十个传感器和执行器。如果能够根据设备的广告信息立即识别出目标设备,那么这样的应用程序就会显得反应迅速、专业可靠,而那些运行缓慢、不可靠的应用程序则完全无法与之相提并论。

到目前为止,我们已经掌握了蓝牙技术中的三项关键要素:为了提高电池使用效率而采用的被动扫描机制、为便于调试而设计的连接中断处理方式,以及通过广告信息获取设备的服务UUID以实现快速识别目标设备的功能。不过我们的探索还没有结束,现在该深入研究那些更为高级的扫描技术了。

高级主题:过滤、分批处理及其他优化技巧

好了,你已经掌握了基础知识。你可以使用被动扫描方式来获取设备信息,可以妥善处理连接中断的问题,也可以像专业人士一样快速识别目标设备。你已经不再是蓝牙技术的初学者了,是时候成为高手了。

让我们一起来学习那些高级的过滤、分批处理等优化技巧,让你的应用程序真正成为节省电池资源的“冠军”吧。

硬件过滤:你的个人助手

想象一下,如果你是一位名人,并且雇了一名个人助理,你肯定不希望每个想要签名的人都来打扰你。因此,你可以给助理列出一份名单:“只有当我代理或我的母亲出现时才通知我。”这样,助理就会只在名单上的这些人出现时才会来找你。

硬件过滤的功能正是如此。你的应用程序代码不需要为每一个被检测到的蓝牙设备而启动,而是可以将过滤逻辑交给蓝牙控制器来处理——这个功能从Android 6.0版本就开始提供了,但现在它的作用比以往任何时候都更为重要。

为什么这这么有用呢?因为这样你的应用程序代码就可以保持“睡眠”状态。主处理器不需要在每次有蓝牙设备发送广告信息时都被唤醒,而效率更高的蓝牙控制器会负责过滤这些信息,只有当控制器找到符合你要求的设备时,主处理器才会被激活。

代码实现:构建你的VIP列表

你可以使用`ScanFilter`来实现这一功能。可以根据设备的名称、MAC地址,或者它所发布的服务UUID来进行过滤。

“我们只需要在看到心率监测设备时才被唤醒即可。”
val heartRateServiceUuid = ParcelUuid.fromString(""0000180D-0000-1000-8000-00805F9B34FB")

val filter = ScanFilter.Builder()
    .setServiceUuid(heartRateServiceUuid)
    .build()

val scanFilters: List = listOf(filter)

“现在,在开始扫描时,只需传入这个列表即可。”
bleScanner.startScan(scan Filters, scanSettings, scanCallback)

上述代码展示了如何创建一种硬件级别的过滤器,这种过滤器能够显著提升电池续航时间和应用程序的性能。让我们深入了解一下其中的具体原理,以及为什么这种技术如此强大。

首先,我们需要定义我们感兴趣的服务UUID——在这个例子中,就是标准的心率服务UUID。这与我们在上一个示例中使用的UUID是相同的,但这次的使用方式截然不同。之前我们是在收到扫描结果后,在应用程序代码中检查这个UUID;而现在,我们是让蓝牙硬件本身只报告那些符合这个UUID的设备。

ScanFilter.Builder()是我们构建这种过滤器的工具。它采用了构建器模式,这意味着我们可以将多个方法串联起来,从而精确地配置我们想要筛选的条件。在这个例子中,我们调用了setServiceUuid(heartRateServiceUuid),这样过滤器就会只匹配那些广播了这一特定服务的设备。

不过,这个构建器还提供了许多其他可用的选项:

  • setDeviceName() —— 匹配名称为“我的心率监测仪”等特定名称的设备

  • setDeviceAddress() —— 根据设备的MAC地址来匹配特定的设备(如果你已经与某台设备配对过,想要再次找到它的话,这个选项非常有用)

  • setManufacturerData() —— 根据设备广告中包含的制造商特定信息来匹配设备

  • setServiceData() —— 根据设备广告中包含的服务数据来匹配设备

你甚至可以在一个过滤器中同时结合多种筛选条件。例如,你可以创建一个过滤器,既要求设备具有特定的服务UUID,又要求其制造商ID也是特定的。过滤条件越具体,出现误判的情况就会越少。

构建完过滤器后,我们会创建一个包含这些过滤器的列表。为什么要使用列表呢?因为你可以同时使用多个过滤器,而只要设备满足列表中任意一个过滤器的条件,它就会被识别出来。比如,你可以为心率监测仪创建一个过滤器,为血压监测仪创建另一个过滤器,那么扫描结果就会显示那些符合其中任意一种设备的列表。这就是“或”逻辑的体现:设备并不需要同时满足所有过滤器的条件,只要满足其中一个即可。

最后,我们会将这个过滤器列表,以及我们的扫描设置和回调函数一起传递给startScan()方法。这时真正的魔法就发生了:当你提供了这些过滤器后,Android并不会在应用程序代码中再进行筛选操作,而是将这些过滤器的指令直接发送给蓝牙控制器的硬件层面。也就是说,筛选过程是在最低层进行的,在你的应用程序得到任何通知之前就已经完成了。

这就是为什么这种技术如此强大的原因:如果没有使用过滤器,每当蓝牙设备接收到任何一台设备的广告信息时,它都必须要唤醒你的应用程序进程,然后将扫描结果传递给应用程序代码,由代码来决定是否需要处理这条信息。而每一次这样的操作都会消耗电池电量和处理器资源。

通过使用硬件过滤器,蓝牙控制器会自动忽略所有不符合您设定条件的设备,因此您的应用程序以及主处理器都会保持休眠状态。只有当检测到心率监测设备时,硬件才会唤醒应用程序并传递相应的检测结果。这就好比在俱乐部里安排了专人把关,只允许VIP名单上的客人进入,其他人都被拒之门外,而您甚至都不会知道他们的存在。

通过使用ScanFilter,您可以告诉硬件:“除非检测到心率监测设备,否则不要唤醒我。”这种设置是实现背景扫描时最有效的节能方式。结合被动扫描和批量报告功能,您就可以构建出一个能够连续运行数小时甚至数天而几乎不消耗电池电量的蓝牙扫描系统。专业级应用程序正是通过这种方式来长期监控设备,同时也不会对电池寿命造成负面影响。

批量扫描:每日报告功能

再回到我们之前用的那个例子吧。有时候,当妈妈出现时,您并不需要立刻被打扰;您更希望在一天结束时收到一份总结报告:“今天,你的妈妈来了两次,你的经纪人也打来过一次电话。”这就是批量扫描的功能。

蓝牙控制器可以不实时地将扫描结果传递给应用程序,而是将它们收集起来后再一次性发送。这一功能同样非常省电——您的应用程序可以在很长一段时间里保持休眠状态,然后被唤醒后一次性处理所有检测结果,之后再重新进入休眠模式。

您可以通过在ScanSettings中调用setReportDelay()方法来启用这一功能。

val scanSettings = ScanSettings.Builder()
    // ... 其他设置 ...
    // 每5秒(5000毫秒)发送一次结果
    .setReportDelay(5000)
    .build()

当您设置了报告延迟后,原本用于接收扫描结果的onScanResult回调方法会被替换为onBatchScanResults方法,后者会返回一个List类型的对象。

private object : ScanCallback() {
    override onBatchScanResults(results: ListScanResult) {
        Log.d("BatchScanner", “这是您的每日报告!共检测到for (result // 处理每一条检测结果
        }
    }

    // ... onScanFailed ...
}

上面介绍的批量扫描机制是Android蓝牙功能中最为被忽视的节能措施之一。了解其工作原理能够帮助你的应用程序显著提升电池使用效率。接下来,让我们详细分析这一机制的具体运作方式,以及何时应该使用它。

当你在代码中设置5000毫秒(即5秒钟)的报告延迟时,实际上就在改变扫描流程的工作方式。蓝牙控制器不会再在每次检测到设备时就立即唤醒你的应用程序,而是会像一位勤勉的助手一样,将这些检测结果暂时存储在内部缓冲区中。在这段时间内,你的应用程序完全处于休眠状态——既不会浪费CPU资源,也不会因为进程切换或系统唤醒而消耗电池电量。

5秒钟的延迟时间结束后,蓝牙控制器会将所有收集到的扫描结果一次性传递给你的`onBatchScanResults()`回调函数。这就是节能效果的产生之处:如果检测到了50个设备,系统只需唤醒应用程序一次,就能将这50个设备的扫描结果全部传给应用程序。这样,应用程序就可以高效地处理这些数据——比如更新用户界面、记录数据或检查特定设备的信息——之后再重新进入休眠状态,等待下一批数据的到来。

`onBatchScanResults()`函数中的`results`参数是一个`List`类型,列表中的每个`ScanResult`代表在批量扫描期间检测到的一个设备信号。需要注意的是,如果同一个设备在延迟时间内多次发送信号,那么你在这一批次中就会收到该设备的多个扫描结果。这个列表并不会自动去重,如果你需要去除重复数据,就需要自己进行处理。

在上面的示例中,我们只是简单地记录了检测到的设备数量,然后遍历每一个扫描结果。而在实际的应用程序中,你可以进行更复杂的处理。例如,可以根据设备的MAC地址创建一个映射关系,统计每个设备发送信号的次数;计算平均RSSI值来估算设备距离;或者过滤数据,只处理符合特定条件的设备。

**警告:** 批量扫描虽然是一个非常强大的工具,但并不适用于所有情况。如果你需要立即对设备的存在做出反应(比如在开发“寻找我的钥匙”这样的应用程序时),报告延迟反而会带来不便。用户不可能愿意等待5秒钟才能看到结果,他们需要的是即时反馈。在这种情况下,应该将`setReportDelay(0)`设置为0,以实现实时响应。

然而,在需要进行长期监控或数据收集的场景中,批量扫描绝对是节省电池电量的最佳选择。以下是一些典型的应用场景:

– **背景监控**:你的应用程序每隔一分钟检查一次用户的手表是否仍在信号范围内,但不需要每秒钟都进行更新。
– **环境监测**:你正在从建筑物内的温度传感器中收集数据,只需要每30秒更新一次仪表盘即可。
– **信标数据分析**:你根据手机发出的BLE信号来统计有多少人经过某个零售地点,并且每隔10秒汇总一次这些数据。

报告延迟的最佳时间间隔取决于你的具体使用场景。如果延迟太短(比如1秒),那么你几乎无法获得任何实际好处,因为设备仍然会频繁触发扫描操作;而如果延迟过长(比如60秒),应用程序可能会显得反应迟缓,甚至会错过一些需要及时处理的事件。对于大多数后台监控任务来说,5到30秒之间的延迟范围是比较合适的。

还需要注意的一点是:批量扫描也存在一定的局限性。蓝牙控制器用于存储扫描结果的缓冲区容量是有限的。如果你设置了过长的延迟时间,并且所处的环境中存在数百个蓝牙设备,那么在延迟时间结束之前,缓冲区就可能已经被填满,从而导致最旧的数据被丢弃。Android系统在发生这种情况时并不会发出警告,因此如果你发现数据丢失了,建议考虑缩短报告延迟时间,或者使用更严格的过滤条件来减少需要收集的数据量。

OnFound/OnLost:设备存在的检测机制

从Android 8.0版本开始,蓝牙设备的扫描功能得到了进一步的优化。你现在可以要求硬件在检测到设备出现时通知你,同时在设备消失时也发出提示。这是通过在`ScanSettings`中设置`CALLBACK_TYPE_FIRST_MATCH`和`CALLBACK_TYPEMATCH_LOST`这两个标志来实现的。

val scanSettings = ScanSettings.Builder()
    .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH or ScanSettings.CALLBACK_TYPE_MATCH_LOST)
    .build()

在`ScanCallback`方法中,`onScanResult`函数中的`callbackType`参数会告诉你具体发生了什么。

override fun onScanResult(callbackType: Int, result: ScanResult) {
    when (callbackType) {
        ScanSettings.CALLBACK_TYPE_FIRST_MATCH -> {
            Log.d("PresenceDetector", "设备找到了!)
        }
        ScanSettings.CALLBACK_TYPEMATCH_LOST -> {
            Log.d("PresenceDetector", "设备消失了!)
    }
}

上述这种设备存在状态的检测机制,实际上代表了我们对蓝牙扫描功能认知上的一个根本性转变。过去我们总是将扫描过程视为一连串的实时数据反馈,而现在则更注重那些特定的事件——比如“某个设备出现了”或者“某个设备消失了”。接下来让我们深入了解一下这一机制的工作原理,以及它为何如此强大。

当你使用位运算符`or`来设置回调类型时,你实际上是在告诉蓝牙硬件要持续跟踪设备的存在状态。代码`CALLBACK_TYPE_FIRST_MATCH or CALLBACK_TYPEMATCH_LOST`同时包含了这两个标志,这意味着你既希望在设备首次出现时收到通知,也希望在设备消失时得到提醒。如果你只关注其中一种类型的事件,也可以单独使用这两个标志;但只有同时使用它们,才能真正实现对设备存在状态的全面监控。

让我们来了解一下“首次匹配”和“匹配失败”这两个概念到底意味着什么。当蓝牙控制器第一次检测到某个设备符合你的筛选条件时,它会触发CALLBACK_TYPE_FIRST_MATCH事件。这与CALLBACK_TYPE_ALLMATCHES不同——后者会在设备每次进行广播时都被触发。由于某些设备可能会每秒钟多次发送广播信号,因此这两种机制之间的区别相当明显。FIRST_MATCH机制意味着只有当设备真正进入你的扫描范围时,你才会收到一次通知,而不会在设备持续进行广播时不断收到通知。

CALLBACK_TYPEMATCH_LOST事件则更加有趣。蓝牙控制器会记录它最后一次与每个设备建立通信的时间。如果某个设备停止了广播信号(因为它已经移出了扫描范围、被关闭了,或者电池耗尽了),控制器就会察觉到这一变化,并触发MATCH_LOST事件。这个过程是自动完成的——你无需手动记录时间戳,也无需在应用程序中实现超时逻辑,硬件会为你完成这一切。

但那么,硬件是如何判断某个设备“已经消失”呢?它是通过内部计时机制来实现的。如果控制器在一段时间内没有收到某个设备的任何信号(通常这个时间间隔为几秒钟,不过具体时长取决于设备的实现方式,且这一信息不会被暴露给应用程序),它就会认为该设备已经消失。因此,在设备实际离开扫描范围与你收到MATCH_LOST通知之间会存在一定的延迟,但这种延迟对于用于检测设备是否存在的情况来说通常是完全可以接受的。

在上面的代码示例中,我们使用了when语句来处理不同类型的回调事件。当我们收到FIRST_MATCH信号时,我们就知道该设备刚刚进入了我们的扫描范围,因此我们会记录“找到它们了!”这样的信息。这种机制非常适合用于触发某些动作——比如当手机靠近时解锁门,或者当检测到健身追踪器时开始同步数据。

而当我们收到MATCH_LOST信号时,我们就知道该设备已经离开了我们的扫描范围或停止了广播信号,因此我们会记录“它们不见了!”这样的信息。这种机制非常适合用于触发清理操作——比如当手机离开时锁门,或者当追踪器断开连接时停止数据同步。

对于需要检测设备是否存在的应用场景来说,这种机制简直非常有用。你的智能锁在范围内吗?你的健身追踪器还连接着吗?用户的手机在附近吗?现在你可以通过硬件层面的判断来得出这些答案,并且可以在设备状态发生变化时立即做出反应,而无需不断地进行轮询操作,也不需要在应用程序代码中维护复杂的状态机。

下面是一个具体的例子,展示了如何在智能家居应用中使用这种机制:

private val presenceCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        when (callbackType) {
            ScanSettings.CALLBACK_TYPE_FIRST_MATCH -> {
                // 用户的手机被检测到了——他们在家!
                Log.d("SmartHome", "欢迎回家!正在解锁门并打开灯光。")
                unlockFrontDoor()
                turnOnLights()
                adjustThermostat(COMFORTABLE_TEMP)
            }
            ScanSettings.CALLBACK_TYPE_MATCH_LOST -> {
                // 用户的手机不在附近了——他们离开了!
                Log.d("SmartHome", “再见!正在锁门并进入节能模式。”)
                lockFrontDoor()
                turnOffLights()
                adjustThermostat(ENERGY_SAVING_TEMP)
                armSecuritySystem()
            }
        }
    }

    override fun onScanFailed(errorCode: Int) {
        Log.e("SmartHome", “检测设备是否存在失败:错误代码为 $errorCode”)
    }
}

有一个重要的需要注意的事项:FIRST_MATCHMATCH_LOSTCALLBACK_TYPE_ALLMATCHES是互斥的。如果你将它们与ALL_MATCHES一起使用,应用程序的行为将会变得不可预测,并且会因设备的不同而有所差异。因此,要么选择ALL MATCHES以实现连续性的检测功能,要么选择FIRSTMATCH/MATCH_LOST来进行设备的存在检测——切勿同时使用这两种方式。

另外,请记住,当与硬件过滤功能结合使用时,存在检测的效果会最佳。如果你不使用任何过滤条件来扫描所有设备,控制器将不得不跟踪范围内每一个BLE设备的状态,这可能会导致其内部跟踪表负担过重。因此,在使用存在检测功能时,务必使用ScanFilter来限定你需要关注的设备范围。

通过结合这些先进的技术——硬件过滤、批量扫描以及存在检测功能——你可以开发出极其复杂且能效极高的蓝牙应用程序。此时,你已不再只是一个普通的开发者,而是一位真正的“蓝牙大师”:你能创造出能够感知周围环境、及时响应各种变化、同时还能有效节省电池电量的应用程序。

现在,让我们来看看在现实世界中,这些技术可以应用于哪些场景吧。

实际应用案例:蓝牙技术的现实用途

好了,我们已经掌握了许多非常实用的新技巧,此时我们基本上已经可以说是蓝牙领域的专家了。但是,如果这些技术不能被用来创造有用的应用程序,那它们又有什么意义呢?让我们来探讨一些具体的场景,看看AOSP 16中的新功能是如何让一个普通的应用程序变得出色的。

1. “寻找我的所有物品”应用程序

我们都遇到过这种情况:上班要迟到了,结果钥匙却“消失”在了某个地方。这时,BLE追踪器就派上了用场。

  • 传统方法:

    应用程序会不断进行主动扫描,在你慌忙寻找钥匙的过程中消耗大量电池电量。它会尝试连接家中的所有追踪设备,才能确定哪一个是你要找的钥匙。

  • AOSP 16的新方法:

    应用程序会在后台进行被动扫描,并使用硬件过滤功能来锁定目标追踪设备的特定Service UUID。这样几乎不会对电池造成任何影响。当你打开应用程序寻找钥匙时,它已经提前检测到了钥匙的位置——因为它一直在默默地监听周围设备。你按下“查找”按钮,应用程序就会立即连接上目标设备,然后你就会很快找到钥匙了。如果连接失败,系统还会告诉你追踪设备的电池是否已经耗尽,这样你就不会浪费时间去寻找一个已经没电的设备了。

2. 智能超市

想象一下,如果有一种应用程序能够在你经过商店里的某件商品时自动为你提供优惠券,那该有多方便啊。这正是近距离营销的理想境界……不过,历史上这种设想总是因为电池耗电问题而无法实现。

  • 传统方式:应用程序需要不断扫描这些信标,这样一来,当用户走到结账处时,手机电量早就耗尽了。

  • AOSP 16的解决方案:超市在每个货架区域都放置了BLE信标。应用程序会采用被动式的批量扫描方式:每隔一分钟左右就会唤醒一次,获取所有检测到的信标信息,然后再次进入睡眠状态。当它发现用户在某个货架区停留了五分钟时,就会利用广告中包含的服务UUID来识别相应的信标,并向用户发送奥利奥饼干的优惠券。这种扫描方式具有针对性,效率很高,而且能在用户完成结账之前避免耗尽手机电量。

3. 过度依赖智能设备的智能家居

智能家居就应该真正具备“智能”功能——它应该能够感知你的回家或离开时间,能在你进门时自动锁门、开灯。

  • 传统方式:用户必须依靠GPS(这种技术的耗电量非常大)或Wi-Fi连接来获取信息,但这些方式往往并不可靠。虽然也可以使用BLE技术,但持续不断的扫描会严重消耗电池电量。

  • AOSP 16的解决方案:你的手机就是关键。智能家居的中心控制设备会持续进行低功耗的被动扫描。当它检测到你的手机发出的BLE信号时,就知道你已经回家了。但如果你只是从房子附近经过呢?这时,“OnFound/OnLost”功能就派上用场了:该系统可以配置为只有在连续检测到你的设备一分钟之后才会启动“欢迎回家”流程;而如果五分钟内都没有检测到你的设备,就会触发“再见”流程。这种更加智能、可靠的检测机制让智能家居真正体现了“智能”的特点。

4. 企业资产追踪系统

在大型医院或仓库中,如何跟踪那些价值昂贵且需要移动的设备(比如静脉输液泵或叉车)是一个巨大的挑战。BLE标签为解决这一问题提供了有效方案。

  • 传统方式:员工们必须手持平板电脑进行主动扫描才能完成库存统计工作。这种做法既繁琐又效率低下。

  • AOSP 16的解决方案:在整个建筑内安装一系列固定的BLE网关。每个网关都是一台简单的设备(比如树莓派),会持续进行被动扫描。它们会收集所有资产标签发出的信号数据,并将这些信息发送到中央服务器。这样一来,管理员就可以实时了解到第34号静脉输液泵目前位于201房间,而第3号叉车则停在装卸区。完全不需要人工进行扫描了——这种低成本、低功耗的实时定位系统,正是得益于被动扫描技术的高效性。

这些只不过是其中的一些例子而已。从健身追踪设备到工业传感器,AOSP 16中新增的蓝牙功能为开发各种应用程序打开了无限可能,这些应用程序不仅功能强大,而且能够有效节约用户的电池电量。现在,让我们来讨论一下如何确保我们新开发的应用程序能够在所有设备上正常运行,而不仅仅是那些新型设备上。

API版本检测:如何避免应用程序崩溃

你使用AOSP 16第四季度发布的所有新功能,开发出了一款性能优秀但耗电量较小的应用程序。现在你已经准备好将其发布出去,然后成为百万富翁,去私人岛屿上退休了……然而,这时却收到了一份错误报告:你的应用程序在全新的Android 16设备上竟然会崩溃。这是怎么回事呢?!

欢迎来到API版本检测的奇妙世界。由于Android的新发布计划,这一功能如今变得比以往任何时候都更加重要——同时也稍微复杂了一些。

问题所在:两款不同的Android 16设备

正如我们之前讨论过的,2025年共有两次Android 16的版本更新:

  • 第二季度发布版:这是主要的版本更新,我们可以将其称为API级别36.0。

  • 第四季度发布版:这是一个包含次要功能的更新版本,我们的新蓝牙功能就是在这个版本中加入的。我们可以将其称为API级别36.1。

我们新的被动扫描API函数`setScanType()`仅存在于API级别36.1及更高版本中。如果你在运行初始第二季度发布版(即API级别36.0)的设备上尝试调用这个函数,你的应用程序就会因为“NoSuchMethodError”而崩溃。这就好比你去请求一个昨晚才被添加到菜单中的选项——应用程序根本不知道这个选项的存在,因此会陷入混乱并导致崩溃。

老办法:SDK_INT

多年来,我们用来检测API版本的工具一直是`Build.VERSION.SDK_INT`。这种方法简单且有效。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    // 使用Android 12(S)及以上版本中的API功能
}

但是,`SDK_INT`只能识别主要的版本号。对于Android 16的第二季度和第四季度版本来说,`SDK_INT`都会显示为36。它无法区分这两个版本之间的细微差别。这就好比你去问一个人他们的年龄,而他们只告诉你“三十多岁”一样——这样的回答显然不够具体。

新的解决方案:SDK_INT_FULL

为了解决这个问题,Android团队为我们提供了一个更加精确的工具:`Build.VERSION.SDK_INT FULL`。这个常量能够同时识别主要的版本号和次要的版本号。与之配套的,我们还有一组新的版本代码:`Build.VERSION_CODESFULL`。

因此,要想安全地使用我们新的被动扫描API,我们就需要进行更加具体的检测:

// 先构建我们的ScanSettings对象
val scanSettingsBuilder = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)

// 现在,我们需要检查该设备是否支持被动扫描模式
if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES FULL.BAKLAVA_1) {
    Log.d("ApiCheck", "该设备支持被动扫描模式。将使用新功能。)
    // 这是第四季度发布版本中的新API功能(API级别36.1)
    scanSettingsBuilder.setScanType(ScanSettings.SCAN_TYPE_PASSIVE)
} else {
    Log.d("ApiCheck", "该设备不支持被动扫描模式。将继续使用主动扫描模式。)
    // 对于没有新API功能的设备,就使用默认的主动扫描模式
    // 这种情况下我们什么都不需要做,因为主动扫描模式是默认设置
}

val scanSettings = scanSettingsBuilder.build()

优雅降级:有风格地出错的艺术

这就引出了一个至关重要的概念:优雅降级。这意味着,即使应用程序无法使用最新的功能,它仍然应该在旧设备上正常运行,而且应该能够以一种优雅的方式实现降级。

在上面的例子中,如果 setScanType 方法不可用,我们就干脆不调用它。应用程序会默认使用普通的扫描方式。虽然这种方式在电池消耗方面效果不佳,但应用依然可以正常运行。这样,使用旧设备的用户也能获得可用的应用程序,而使用新设备的用户则能享受到更优化的使用体验。这样一来,大家都能受益。

以下是一个表格,可以帮助你记住在什么情况下应该使用哪种检查方式:

如果你正在使用来自……的 API……

请使用这种检查方式……

重大版本的 Android 发布版(例如 Android 16 Q2)

if (SDK_INT >= VERSION_CODES.BAKLAVA)

小型更新版本,但会删除某些功能(例如 Android 16 Q4)

if (SDK_INT_FULL >= VERSION_CODES FULL.BAKLAVA_1)

掌握这些新的 API 检查方法是非常必要的。这是编写既创新又稳定的现代 Android 应用程序的关键。既然我们已经知道了如何构建一个可靠的应用程序,接下来就让我们来讨论一下,当应用程序不可避免地出现故障时,该如何修复它们吧。

测试与调试:那些没人愿意提及的有趣环节

在软件开发领域,有两条普遍存在的规律:

  • 在我的设备上,程序可以正常运行;

  • 但在现场演示时,程序却总会以最糟糕的方式出故障。

尤其是蓝牙功能的开发,似乎特别符合后一条规律。蓝牙技术是一种变化无常、难以预测的技术,它似乎对开发者有着某种“个人恩怨”。

那么,我们该如何应对呢?答案就是采取扎实的测试与调试策略。虽然这个过程并不光彩夺目,但却是保持理智的唯一途径。

模拟器:一个充满想象力的工具

Android Studio 的模拟器是一个非常出色的工具。它运行速度快,使用起来也很方便,而且能够模拟各种类型的设备。那么对于蓝牙功能来说呢?模拟器也……还算能提供一些帮助。模拟器确实支持蓝牙功能,你可以启用这个选项,这样你的应用程序就会认为自己拥有一个蓝牙适配器。这对于测试用户界面以及确保应用程序在尝试获取 BluetoothLeScanner 时不会崩溃来说,非常有用。

但问题在于:模拟器并不是真实的设备。它实际上无法与现实环境中的无线电波进行交互。因此,你不能用模拟器来查找现实生活中的 BLE 耳机。对于这类任务,你还是需要回到现实世界中去操作。

现实世界:漏洞存在的真正场所

没有任何东西能够替代在真实的物理设备上进行测试。每个手机制造商都会使用自己特有的蓝牙技术栈、独特的天线设计,以及各种会给你带来麻烦的设置。在谷歌 Pixel 上运行得非常顺利的扫描功能,在其他品牌的设备上可能会彻底失败。唯一确定的方法就是进行实际测试。

你的测试工具应包括以下内容:

  • 多种类型的手机:不同品牌、不同版本的Android系统。种类越多越好。

  • 多种BLE外围设备:不要只使用一种类型的设备进行测试,应该准备几种不同的信标、传感器或可穿戴设备。你会惊讶地发现它们的行为存在很大差异。

常见错误:那些经常出现的问题

当扫描失败时,系统会给出相应的错误代码。以下是一些最常见的错误原因:

可能是你操作过于急躁,导致多次调用startScan()而没有及时调用stopScan()。

你的应用程序设置存在根本性问题。

这个错误信息比较模糊,通常意味着你的权限设置有问题,或者系统暂时出现了故障。可以尝试重启蓝牙功能。

SCAN_FAILED_INTERNAL_ERROR

蓝牙系统的内部组件出现了故障。

这是一个典型的“问题不在你这边,而在设备本身”类型的错误。除了稍后重新尝试外,你也无能为力。

SCAN_FAILED_FEATUREUnsupported

你试图使用硬件不支持的功能。

如果你的设备不支持批量扫描功能,请检查是否使用了正确版本的API。

错误代码

问题所在

解决方法

SCAN_FAILED_ALREADY_STARTED

你试图启动一个已经正在运行的扫描任务。

SCAN_FAILED_APPLICATION_REGISTRATION_FAILED

调试工具:你的“捉鬼工具包”

当出现问题时,你需要合适的工具来了解蓝牙系统中发生的情况。

  • logcat:这是你的得力助手。请尽可能多地记录日志信息——在开始扫描、停止扫描、找到设备或扫描失败时都要进行记录。为你的应用程序设置相应的过滤条件,这样就能从海量数据中筛选出有用的信息。

  • Android的蓝牙HCI监听日志:这是蓝牙调试领域的“圣杯”。这个开发者选项会记录所有进出你设备的蓝牙数据包。虽然这些日志非常详细,但也会让人感到有些难以理解,但它确实是查明问题真相的关键。你可以使用Wireshark等工具来查看这些原始日志文件,从而了解手机与BLE设备之间的通信细节。这就像是在监听无线电波中的信号一样。

  • nRF Connect for Mobile:这是Nordic Semiconductor公司提供的免费应用程序,对于任何从事BLE开发的开发者来说都至关重要。它可以帮助你扫描设备、查看设备的广告信息、建立连接以及探索它们的GATT服务。如果你的应用程序无法找到目标设备,首先应该用nRF Connect试一试。如果它也无法成功连接,那么问题很可能出在外围设备上,而不是你的应用程序本身。

测试和调试蓝牙功能就像是一场马拉松,而不是短跑——这需要耐心、系统化的方法,以及一定的自嘲幽默感。不过,只要使用正确的工具和技术,就能顺利完成这项工作。

现在,让我们来探讨一下如何确保我们的应用程序在性能方面也能表现得“良好”。

性能与最佳实践:如何成为优秀的蓝牙设备

编写能够正常运行的代码是一回事,而编写高效、不会让用户想要把手机扔到墙上的代码则是另一回事。在蓝牙技术的应用中,要确保设备“表现良好”,关键因素就是电池消耗。

蓝牙模块确实是一种功能强大的硬件,但它也非常“耗电”。只要它处于激活状态,就会不断消耗电量。我们的任务就是确保它只在真正必要的时候才会消耗电力。以下是一些使用蓝牙技术时需要遵守的黄金法则。

1. 如果没有必要,就不要进行扫描

这听起来似乎很显而易见,但实际上却是人们最常犯的错误。在开始扫描之前,先问问自己:“我现在真的需要这样做吗?”如果用户当前并不在需要扫描结果的界面中,那就不要进行扫描;如果应用程序正在后台运行,就更应该格外注意了——因为在后台进行的扫描会严重消耗电池电量,而Android系统也严格限制了这种行为。

2. 立即停止扫描

这一点非常重要,所以我必须再强调一遍:一旦完成扫描操作,就必须立即停止它。继续让扫描功能处于运行状态,就相当于让电池“漏水”一样——电量会不断被消耗,直到电池完全耗尽。最佳的做法是将扫描操作的生命周期与应用程序的用户界面生命周期紧密绑定在一起。


override fun onPause() {
super.onPause()
stopBleScan()
}

override fun onResume() {
super.onResume()
startBleScan()
}

一旦找到了你想要查找的设备,就应该立即停止扫描操作——没有必要继续浪费电量了。

3. 选择合适的扫描模式

“ScanSettings”功能提供了多种不同的扫描模式,请根据实际需求谨慎选择合适的模式。

  • SCAN_MODE_LOW_POWER: 这是您的默认模式,也是日常使用中最为常见的模式。该模式会以一定的间隔进行扫描,从而在扫描速度与电池续航时间之间取得平衡。适用于大多数需要实时检测设备存在的场景。

  • SCAN_MODE_BALANCED: 这是一种折中的选择。该模式的扫描频率高于LOW_POWER模式。

  • SCAN_MODE_LOW_LATENCY: 当您“需要立即找到目标设备”时,可以使用这种模式。该模式会持续进行扫描,因此能够最快地找到目标设备,但也会导致电池电量迅速耗尽。仅适用于那些需要快速完成检测的任务。

  • SCAN_MODE_OPPORTUNISTIC: 这是一种完全被动的扫描模式——您的应用程序不会主动触发扫描操作,只有当其他应用程序正在执行扫描时,才能获得扫描结果。这种模式不会消耗额外的电池电量,但无法保证一定能找到目标设备。适用于那些不需要实时检测、属于后台更新的场景。

当然,如果你使用的是AOSP 16 QPR2或更高版本,那么在不需要扫描响应数据时,就应该使用setScanType(SCAN_TYPE_PASSIVE)这个方法。这是提高能效的最佳手段。

4. 使用硬件过滤和批量处理技术

我们在高级用法章节中已经介绍过这一点,但这一最佳实践仍然值得再次强调。如果你需要扫描特定的设备,可以使用ScanFilter;如果进行长时间扫描,可以使用setReportDelay()方法来批量处理扫描结果。这两种技术都能将计算任务交给能效更高的蓝牙控制器去完成,从而使你的应用程序代码得以进入休眠状态,这才是节省电池电量最有效的方法。

5. 注意内存使用

你的应用程序接收到的每一个ScanResult对象都会占用内存。如果你身处一个有数百个BLE设备的密集区域,而且没有使用过滤机制,那么你的应用程序很快就会因为内存不足而无法正常运行。这就是为什么过滤功能如此重要的原因之一——只有获取你真正需要的扫描结果,才能避免浪费内存。

遵循这些规则,你就可以开发出既功能强大又不会过度消耗用户设备资源的蓝牙应用程序。这样,你就真正成为了蓝牙技术的专家了。现在,让我们总结一下今天的内容,并展望未来吧。

结论:未来的蓝牙技术将是被动式的——而这并没有什么不妥

我们确实经历了一段漫长的学习过程,不是吗?我们回顾了经典蓝牙技术的早期阶段,见证了BLE技术的复兴,也走进了AOSP 16这个充满新机遇的时代。我们学会了如何利用被动扫描功能来悄无声息地收集数据,分析了设备连接失败的原因,还掌握了如何根据广告中的服务UUID快速选择目标设备。

如果说从这一切中可以得出一个最重要的结论的话,那就是:Android平台上的蓝牙技术将会变得更智能、更高效,使用起来也会更加顺畅。Android团队显然非常关注开发者的需求,为我们提供了构建更优秀、更省电的应用程序所需的各种工具。被动扫描功能的引入不仅仅是一项新的功能,它代表了一种理念的转变——有时候,最好的沟通方式就是倾听。

作为开发者,这些新工具让我们能够超越那些简单的“连接并传输数据”这类应用场景,去开发出那些能够实时感知周围环境、却不会让用户的手机变得笨重的复杂应用程序。实现一个真正智能、无缝连接的未来世界的目标,又向前迈进了一步——而这一切都离不开这些高能效技术的支持。

那么,接下来会发生什么呢?蓝牙技术总是在不断发展中。不久之后,我们将会看到Bluetooth 5.4版本的出现,它将支持Auracast功能、网状网络连接,以及更加精确的位置定位技术。可以肯定的是,未来的蓝牙工具会变得越来越先进,而相关的挑战也会变得更加有趣味性。

此刻,不妨花一点时间来欣赏我们所取得的进步。下次当你启动蓝牙扫描功能时,如果一切都能顺利进行,那就请花点时间为那些为这一功能的实现付出辛勤努力的工程师们表示感谢吧。而当你的应用程序中的电池电量图表呈现出一条平坦、整齐的线条,而不是那条令人胆战心惊的“斜坡”时,也请稍微认可一下被动扫描技术所带来的便利吧。

虽然蓝牙技术可能永远无法被完全“驯服”,但有了AOSP 16,我们终于得到了更强大的工具来操控它。现在,让我们继续创造更多精彩的事物吧……不过,请务必记得,在完成扫描任务后及时停止相关操作哦。

Comments are closed.