Android 蓝牙开发做久了,你会发现一个扎心的事实:系统版本越新,API 越“漂亮”,但老设备上的坑永远不会自己消失。项目里只要写了一句startLeScan,低版本手机直接就给你脸色看;好不容易把 Android 13 的权限适配好了,结果一台 Android 6.0 的老平板连设备都搜不到。这个标题“android 蓝牙连接-兼容旧版本”说白了就是一句话:在 2026 年做蓝牙功能,怎么才能让 Android 4.4 到 Android 14 的机器都能跑得通,同时不把自己折腾疯。
这篇文章我会拿实际项目里的适配经历来讲,覆盖经典蓝牙(Classic Bluetooth)和低功耗蓝牙(BLE)两条线,重点讲版本差异、权限适配、UUID 服务发现、扫描接口兼容这些让人头大的点。内容比较适合正在做蓝牙相关功能的 Android 开发,或者自己折腾蓝牙模块、HC-05、ESP32 这类硬件的朋友。读完你至少能把 minSdkVersion 设到 21 甚至 19,并且知道每个版本上哪些 API 能碰、哪些一定要绕开。
1. 核心思路拆解:兼容旧版本到底在兼容什么?
1.1 一个目标,三条主线
做蓝牙兼容,很多人一上来就盯着 API 名字看,哪个方法废弃了就换哪个,结果越改越乱。我自己的经验是,先把“兼容”拆成三个维度,再一个一个解决。
第一个维度是系统 API 的版本差异。Android 4.3 开始提供 BLE 支持,4.4 开始能作为外围设备,5.0 引入了全新的扫描和连接接口,6.0 加了运行时权限,8.0 对后台扫描做了限制,12.0 开始把蓝牙权限拆成了细粒度权限。每个大版本都是一道坎,而这些坎并不会因为你只适配新版本而消失。项目里只要还有一台 Android 6.0 的测试机,这些问题就必须面对。
第二个维度是厂商定制系统的行为差异。同样是 Android 9.0,小米、华为、OPPO、三星对后台权限、自启动、省电策略的处理差异,足以让同一条代码在不同手机上产生完全不同的表现。这个问题我在第 4 章会详细展开,这里先记住一个结论:在国产 ROM 上,系统的“省电管理”比你的代码更容易杀掉蓝牙连接。
第三个维度是蓝牙协议本身的版本差异。同一条设备链路上,手机端的蓝牙协议栈(Bluedroid、Fluoride、各种厂商自研栈)老版本和新版本在处理配对、重连、加密时的行为并不一致。比如某些老协议栈对128-bit UUID的过滤方式就是有 bug,导致新手机找不到老设备上的服务——这不是你代码的问题,但你得在业务层把这个坑填上。
三条线互相交织,这就是为什么网上那些“把 targetSdk 升到 33 然后加三个权限就好了”的教程根本不够用。真正的兼容适配,是面向版本、面向厂商、面向协议栈的组合工程。
1.2 先看懂系统版本和蓝牙 API 的关系
我给项目里做过一张表,后来发现这张表可以当团队入职培训材料用。核心不是背 API,而是知道每个版本的“分界线”在哪。
| Android 版本 | 蓝牙相关关键变化 | 对兼容性的实际影响 |
|---|---|---|
| 4.3 (API 18) | 首次支持 BLE 中心设备模式 | BLE 基础能力下限,startLeScan可用 |
| 4.4 (API 19) | 支持 BLE 外围设备模式 | 可以做 Peripheral,但 API 极其原始 |
| 5.0 (API 21) | 引入BluetoothLeScanner、ScanFilter、ScanSettings | 新扫描体系开始,替代startLeScan |
| 6.0 (API 23) | 运行时权限模型,需要申请定位权限才能蓝牙扫描 | 动态权限成为必须,很多老代码在这里崩 |
| 7.0 (API 24) | 对 BLE 连接数量做了限制 | 多连接设备的噩梦开始 |
| 8.0 (API 26) | 后台扫描 30 分钟限制 | 后台蓝牙功能进入“灰色地带” |
| 10.0 (API 29) | 定位权限继续保留在蓝牙扫描链路中 | 依然要申请定位权限,很多人想不明白为什么 |
| 12.0 (API 31) | BLUETOOTH_SCAN/BLUETOOTH_CONNECT/BLUETOOTH_ADVERTISE新权限 | 蓝牙权限首次单独从定位权限中拆出 |
| 13.0 (API 33) | 新权限体系逐步强制化 | targetSdk 33+ 必须适配新权限,否则直接崩 |
这张表里最容易搞混的是 Android 12 到 13 这一段。Android 12 其实已经引入了新的蓝牙权限,但如果你的targetSdk还在 30 及以下,系统会帮你走旧逻辑,不会强制要新权限。一旦升到 31,BLUETOOTH和BLUETOOTH_ADMIN在蓝牙相关的 API 上基本就失效了,必须用新的三个权限。到 Android 13 之后更是如此,所以我的建议是:不管你的项目目标版本是多少,新权限声明先加上,运行时权限兼容层也直接按旧新两套写。不写等出问题再补,通常已经上线上事故了。
2. 权限适配:从“一键授权”到“三分天下”
2.1 Android 6.0 动态权限是所有兼容性问题的分水岭
Android 6.0(API 23)之前,蓝牙权限只要在AndroidManifest.xml里声明就行,安装时用户不授权也能装,系统直接给。6.0 之后运行时权限模型出现,危险权限必须在运行时弹窗申请。而蓝牙恰恰有一个特别容易让人误解的地方:蓝牙扫描需要定位权限。
为什么蓝牙要定位权限?因为蓝牙扫描能拿到周边设备的信号强度(RSSI),理论上可以通过三角定位推断用户位置,所以 Google 把蓝牙扫描归到了定位相关的权限之下。这个设计从 6.0 一直到 Android 11 都是这样。实际开发中你必须在onRequestPermissionsResult里同时处理好定位权限的授予结果,否则用户拒绝了定位权限,蓝牙功能就静默失效。
代码上要注意targetSdk >= 23时,Manifest.permission.ACCESS_FINE_LOCATION是必需项,只写ACCESS_COARSE_LOCATION在部分机型上会导致扫描永远没结果。我踩过最离谱的一个坑是某款 Android 7.0 的三星平板上,COARSE和FINE同时声明、只申请COARSE,扫描结果里所有 BLE 设备的 RSSI 全是 0,换真机测试没事,一上平板就露馅。后来老老实实把FINE加上,问题才消失。
2.2 Android 12+ 蓝牙专属权限的出现
Android 12(API 31)之后,蓝牙权限正式“三分天下”:负责扫描的BLUETOOTH_SCAN、负责连接的BLUETOOTH_CONNECT、负责广播的BLUETOOTH_ADVERTISE。这三个都是运行时权限,也都需要用户在设置里手动授权。它们和定位权限不再是绑定关系,扫描 BLE 设备时,如果你不需要知道设备的位置信息、不做基于位置的过滤,理论上可以不再申请定位权限——但前提是你的targetSdk已经到 31 以上并且完全走新接口。
为了兼容旧版本,我一般用这样一个权限集合常量:
val REQUIRED_BLUETOOTH_PERMISSIONS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_ADVERTISE, // 如果没有广播功能可以不申请 Manifest.permission.ACCESS_FINE_LOCATION // 部分扫描场景仍需定位兜底 ) } else { arrayOf( Manifest.permission.ACCESS_FINE_LOCATION ) }这段代码看着简单,但里面有一个隐藏的深坑:Android 12 系统上,如果你的targetSdk是 30 或更低,系统会弹一个“旧的蓝牙权限申请”兼容弹窗,此时你 Manifest 里声明的还是旧的BLUETOOTH/BLUETOOTH_ADMIN,不会触发新权限弹窗。这意味着同一个 APK 在不同系统上的权限弹窗次数和逻辑都不一样。我建议直接把清单文件里新旧权限全部声明,不要吝啬:
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />neverForLocation这个 flag 是 Android 12 引入的,加上它之后系统知道你拿扫描结果不是为了定位,可以简化定位权限的申请。但需要注意,这个 flag 对某些国产 ROM 不一定生效,该申请定位权限还是申请,别省这一步。
2.3 兼容写法的统一封装
因为权限逻辑分散在多个版本分支里,我建议封装成一个工具方法,业务层只关心回调结果。下面是一个简化版,实际项目里可以再加超时逻辑和取消逻辑:
object BluetoothPermissionHelper { private const val REQ_BLUETOOTH_PERMISSION = 1001 fun ensurePermissions(activity: Activity, callback: (Boolean) -> Unit) { val deniedList = REQUIRED_BLUETOOTH_PERMISSIONS.filter { ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED } if (deniedList.isEmpty()) { callback(true) return } // 这里建议把请求逻辑挂到 Activity 的 onRequestPermissionsResult 里统一处理 ActivityCompat.requestPermissions( activity, deniedList.toTypedArray(), REQ_BLUETOOTH_PERMISSION ) } }这里有个经验点:不要把权限请求逻辑写在 Fragment 里然后只用getActivity()强转,因为我遇到过多次 Activity 已经销毁但权限回调还在的情况,导致弱引用崩溃。权限回调尽量写到基类或者独立的 Lifecycle 感知组件里,避免在业务页面上堆权限代码。
3. 经典蓝牙连接兼容性实战
3.1 打开蓝牙和发现设备的版本差异“陷阱”
经典蓝牙的连接链路,从打开蓝牙到搜索设备,每一步都有版本坑。先说打开蓝牙这段。老代码最经典的写法是:
if (!mBluetoothAdapter.isEnabled()) { mBluetoothAdapter.enable(); }这是 Android 4.x 时代最常见的代码,但enable()是一个异步且隐藏的接口,在 Android 5.0 之后,系统会强制要求应用通过ACTION_REQUEST_ENABLE来请求用户打开蓝牙,直接调用enable()在很多 6.0+ 机型上不仅无效,还会被系统判定为“非法后台操作”。正确做法是用startActivityForResult弹系统对话框,不过这个方法也要处理 Android 11 之后包可见性变化对resolveActivity的影响。我建议统一这样写:
fun requestEnableBluetooth(activity: Activity) { val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) try { activity.startActivityForResult(intent, REQ_ENABLE_BT) } catch (e: ActivityNotFoundException) { // 有些极简 ROM 没有这个 Activity,走 enable() 兜底 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { bluetoothAdapter.enable() } } }设备发现这块,老的startDiscovery()在 Android 6.0 之后也要配合定位权限,否则毫无反应。另外一个大坑是Android 8.0(API 26)开始,后台应用不能执行蓝牙扫描,这里的“后台”定义是:没有前台 Activity、没有前台 Service、没有其他可见的 app 组件。如果你的应用打算在后台定期扫描设备,必须启动一个前台服务,否则就算你的逻辑没问题,系统也会直接杀死扫描。
3.2 配对流程的版本差异
经典蓝牙配对(Bonding)是另一个版本分裂严重的区域。Android 4.4 到 6.0,createBond()只要调用成功,系统会弹出一个 PIN 码输入框,由用户手动输入确认。从 Android 6.0 开始,部分 OEM 机型把配对弹窗做成了自动确认模式,输入相同 PIN 码的逻辑经常失效。而 Android 10 及之后的机型,很多已经默认使用 Secure Simple Pairing(SSP),要求“确认配对码”而不是“输入 PIN 码”模式。
所以你在兼容旧版本时,至少要在业务层做两套逻辑:老设备(比如 HC-05/HC-06 模块)走 PIN 码输入,新设备走确认弹窗。很多模块资料上写“默认 PIN 1234”,但你用新系统手机去连,弹窗可能根本没有 PIN 输入框,而是显示“是否与设备配对?”的确认框。遇到这种场景,不用慌,点确认就行,底层已经完成了 PIN 交换。
对于主动发起配对,Android 8.0(API 26)之前可以通过反射调用createBond()成功触发配对。8.0 之后有部分厂商设备必须调用系统的ACTION_PAIRING_REQUEST才会响应对码。这里我个人的建议是:不要试图完全控制配对流程,官方 API 的配对交互在不同厂商手里行为无法预测,尽量把用户引导到系统配对界面,保证兼容性优先。
3.3 UUID 服务发现:连接失败最常见的坑
连接经典蓝牙设备时,建立 Socket 需要拿到一个 UUID。Android 上常见的 SPP UUID 是:
00001101-0000-1000-8000-00805F9B34FB这个 UUID 代表串口服务。问题在于,很多老设备、尤其是淘宝买的 HC-05/HC-06 蓝牙模块,它们的 SDP 记录里根本没有标准 SPP UUID,而是随意写了一个自定义 UUID。Android 系统在createRfcommSocketToServiceRecord(uuid)时,如果远端设备的 SDP 记录里没有匹配的 UUID,就会直接抛Service discovery failed异常。这种问题在新手机上更容易出现,因为新的蓝牙协议栈对 SDP 记录合法性检查更严格。
我遇到过一台 Android 6.0 手机上能连上的 HC-05 模块,换到 Android 12 手机上就报Service discovery failed。最后解决的办法是使用反射方式创建 RFCOMM 通道,绕开 SDP 查询:
@Suppress("DEPRECATION") fun createRfcommSocketFallback(device: BluetoothDevice): BluetoothSocket? { return try { val method = device.javaClass.getMethod("createRfcommSocket", Int::class.javaPrimitiveType) method.invoke(device, 1) as BluetoothSocket } catch (e: Exception) { null } }这种方法在 Android 4.x 到 Android 13 之间都有效,但不能保证所有 ROM 都支持。我建议把它作为Service discovery failed异常后的兜底方案,而不是默认方案,因为反射调用的通道号(channel 1)并不一定对应你设备上实际可用的 RFCOMM 服务通道。还有一点:fetchUuidsWithSdp()这个老 API 在 Android 12 之后已经基本废了,因为它需要BLUETOOTH_CONNECT权限,而且在部分新机型上返回的 UUID 列表为空,不要依赖它。
3.4 老版本系统上的 getDefaultAdapter 兼容写法
BluetoothAdapter.getDefaultAdapter()从 Android 4.x 用到 Android 13 都没被标记废弃,但它内部的行为在新系统上有变化。比如在 Android 13 上,如果应用没有BLUETOOTH_CONNECT权限,调用这个方法不会抛异常,但拿到 adapter 之后你做任何 connect 操作都会抛SecurityException。所以不要用“是否非空”来判断权限状态,要用上一章那种显式的checkSelfPermission判断。
另外说一个冷门但实际踩过的坑:部分 Android 5.0/5.1 的 ROM 上,getDefaultAdapter()可能返回 null,原因是蓝牙服务没有顺利启动,而系统并没有把这个当严重故障处理。遇到这种场景,不能直接崩,应该提示用户重启蓝牙或重启系统,而不是代码里继续空指针调用下去。
4. BLE 蓝牙在旧版本上的适配要点
4.1 minSdk 怎么选,决定你少踩多少坑
BLE 从 Android 4.3(API 18)开始支持,但 API 18 和 API 21 的 BLE 接口差距大到像两个完全不同的功能。4.3 到 4.4 时代只有startLeScan/stopLeScan,而且扫描结果回调是散装的,每次只回调一个设备,没有扫描过滤器,也不能设置扫描模式。Android 5.0 之后才有BluetoothLeScanner、ScanFilter、ScanSettings这套完整体系。
我的建议是,如果产品没有特别极端的低版本诉求,minSdk 最少定到 21(Android 5.0)。原因是 21 之后的 BLE API 才具备基本可用的扫描、连接、过滤能力,定到 18 的话,你将不得不在startLeScan和startScan之间维护两套代码,并且 4.x 系统上 BLE 的稳定性本身就很差,很容易掉线,调试成本远高于收益。当然,如果老板确实要求覆盖 Android 4.4 的存量用户,那就准备好两套扫描逻辑,这个跑不掉。
4.2 扫描接口的差异:startLeScan 与 startScan 的统一封装
扫描接口是所有 BLE 兼容问题的重灾区。Android 5.0 之前的startLeScan(callback)没有过滤功能,只能扫到所有设备,而且回调的BluetoothDevice对象上getName()经常返回 null(因为广播包里的名称在扫描响应包里)。Android 5.0 之后的startScan(filters, settings, callback)功能强得多,但低版本手机上你却没法用。
我习惯做一个扫描器的封装层,对外暴露统一接口,内部判断版本走不同实现:
class BleScanner(context: Context) { private val adapter: BluetoothAdapter? by lazy { val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager manager.adapter } private val leScanner: BluetoothLeScanner? by lazy { adapter?.bluetoothLeScanner } private val callback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { // 处理 result.device, result.rssi, result.scanRecord } } @Suppress("DEPRECATION") fun startScan() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val settings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() leScanner?.startScan(null, settings, callback) } else { adapter?.startLeScan { device, rssi, scanRecord -> // 模拟 ScanResult 结构,统一回调给上层 onLegacyLeScan(device, rssi) } } } }封装时注意一个细节:BluetoothLeScanner.startScan的扫描回调结果一般一次能拿到多个设备,而startLeScan一次只给一个。如果要保证上层逻辑一致,可以先收集、定时批量回调,而不必追求每条回调都对齐。这个设计在数据展示上会省很多事。
4.3 Android 8.0 的后台扫描限制与保活策略
Android 8.0(API 26)之后,后台扫描 30 分钟会被系统静默停止,这个机制不管你的 targetSdk 是多少都生效。如果你的产品需要长时间在后台维持扫描,比如蓝牙门禁、蓝牙手环自动重连这类场景,唯一稳妥的方案是启动一个前台服务,通过startForegroundService把扫描任务放到前台服务里,这样系统才允许你持续扫描。
前台服务在 Android 8.0 上要立刻调用startForeground(),Android 12 之后还需要声明前台服务类型(比如connectedDevice),否则会抛ForegroundServiceStartNotAllowedException。这里就体现兼容旧版本的琐碎程度:一套前台服务逻辑,至少要适配 8.0 的前台服务限制、10.0 的后台启动限制、12.0 的前台服务类型这三次变化。每次升级系统版本,都要回去翻一遍前台服务的写法。
4.4 连接不稳定与 MTU 协商的老问题
BLE 连接稳定性和系统版本的关系非常明显。Android 4.x 时代的 BLE 连接经常出现偶发断连,官方也承认早期实现有稳定性问题,后来在 5.0、6.0 上逐步修复。真到了 Android 8.0 之后,系统级的连接管理已经比较稳定,但仍要面对国产 ROM 的后台清理策略,比如华为的“启动管理”、小米的“神隐模式”,会直接杀掉应用的蓝牙连接。针对这种问题,除了引导用户把 App 加入电池优化白名单,没有特别好的代码级解决方式。
MTU 协商也值得单独说一下。Android 5.0(API 21)引入了requestMtu()方法,但部分 Android 4.x 设备完全没有这个方法,只能用默认 23 字节 MTU 通信。如果你的协议和硬件端约定的是用大包传输,比如一次传 512 字节,那么在低版本设备上就必须做分包发送,否则硬件端接收会异常。分包要考虑 MTU 值动态获取,不能写死 20 字节/包,因为有些设备协商后能到 512 甚至更多。
分包实现时,最保险的做法是:
- 连接成功后请求 MTU;
- 拿不到 MTU 值就默认 20 字节每包;
- 发送端按 MTU 值做切片,接收端做粘包组装;
- 协议头里带上总长度或者包序号,方便对端拼装。
这套逻辑在 4.x 和 13+ 上都验证过,属于“不优雅但稳定”的方案。
5. 常见问题与排查技巧实录
这部分我整理成速查表,基本上都是我在实际调试过程中碰到的,比网上那些“重启手机试试”的建议要靠谱得多。
| 现象 | 可能原因 | 排查思路与解决方式 |
|---|---|---|
| 低版本手机扫描不到任何 BLE 设备 | 没有定位权限、蓝牙未开启、扫描回调被系统静默 | 先检查权限,再检查蓝牙开关,最后确认是否在后台执行扫描 |
| 高版本手机连不上 HC-05/HC-06 老模块 | SDP 服务发现失败,老模块 UUID 不标准 | 使用反射createRfcommSocket兜底,或引导用户先配对再连接 |
| 新系统扫码后 RSSI 全部为 0 | 定位权限缺失或neverForLocation影响 | 补申请ACCESS_FINE_LOCATION,确认 Manifest 权限没写错 |
| BLE 连接成功后偶尔掉线,重连不上 | 后台清理、扫描并发冲突、MTU 协商异常 | 使用前台服务保活,重连前断开旧 Gatt,避免重复连接同地址 |
| Android 12 上安装后闪退 | targetSdk 31+ 但没适配新蓝牙权限 | 在代码里动态申请BLUETOOTH_SCAN/BLUETOOTH_CONNECT |
调用fetchUuidsWithSdp()返回空列表 | 新系统协议栈限制 SDP 查询 | 改用广播接收器等待ACTION_UUID,或直接反射建 RFCOMM 通道 |
| 搜索到了设备但无法配对 | 配对方式不匹配,老模块要求 PIN 码输入 | 主动触发ACTION_PAIRING_REQUEST,引导系统弹 PIN 码框 |
| 手环/门禁设备在 Android 9 后台收不到广播 | Android 8.0 后台扫描限制 | 启动前台服务,并且保持前台通知可见,否则进程会被回收 |
低版本手机上getName()返回 null | 扫描广播包中不含设备名称,名称在扫描响应里 | 不用依赖名称,用 MAC 地址做业务标记,或等待 Gatt 连接后再次获取名称 |
排查蓝牙问题,我自己的习惯是:优先看底层日志而不是直接改代码。Android 系统蓝牙相关的日志标签有BluetoothAdapter、BluetoothGatt、BluetoothHci、bt_btif等,通过adb logcat -s BluetoothAdapter:V BluetoothGatt:V BluetoothHci:V抓取可以看到连接状态、GATT 回调、错误码等关键信息。很多“为什么连不上”的问题,日志里早就给出了答案,比如133错误码表示连接被远端设备关闭,257表示 GATT 内部错误。
另外说一下 A2DP 切 SCO 的问题。很多做蓝牙耳机的开发者会碰到:通话时要从音乐模式切到 SCO 模式,但老系统切不过去。这是因为 Android 上AudioManager.startBluetoothSco()在不同 Android 版本有不同的调用时机要求,Android 8.0 之后必须在ACTION_AUDIO_BECOMING_NOISY之后等一段时间再调用,调用太早会失败。解决办法是先注册ScoAudioStateListener,监听连接成功后再启动音频流,而不是无脑startBluetoothSco()加startBluetoothSco()。
6. 实测经验与后续扩展建议
我最后分享两个实测经验,这也是我在团队里反复强调的。
第一个是测试设备矩阵不要偷懒。如果你的蓝牙项目真正要覆盖旧版本,一个最基本的内测设备矩阵至少包含:Android 6.0 的低端机(很多 App 蓝牙连不上都是这类机器)、Android 8.0 的小米/华为(后台限制最严)、Android 11 的原生机(验证权限模型)、Android 13 的 Pixel 或者主流新机(验证 targetSdk 升级适配)。没有条件买这么多实体机,也可以用云真机平台,但要注意云真机的蓝牙硬件和本地真实手机差异较大,发现的版本问题种类也有限,该买的老旧机器还是得买。
第二个是版本分支代码要控制在最小范围。我之前见过一个项目,为了适配 Android 4.4,在业务逻辑里大量分散if (Build.VERSION.SDK_INT >= 21)判断,把代码搞得一团糟,后来根本没人敢动那部分。合理做法是像我在扫描器封装里做的那样,把版本差异集中到一个类、一个方法里,上层业务代码永远只和统一接口打交道。这样即使将来 Android 14、15 又带来一波新的 API 变更,你要改的也只是底层封装这一个文件,而不需要把整个项目翻一遍。
继续扩展的话,可以从这几个方向往下做:一是把经典蓝牙和 BLE 的封装统一起来,对外暴露一套“连接设备–发现服务–收发数据”的高级 API,让上层业务不再感知底层是走 RFCOMM 还是 GATT;二是加入自动化兼容性测试,在设备矩阵上跑同一套蓝牙冒烟脚本,把每一次系统升级后的回归验证变成例行流程;三是把日志和状态机沉淀下来,做成线上问题的排查后台,这样以后线上反馈“蓝牙连不上”,你能立刻看到是哪个环节、哪个错误码、哪个系统版本,而不是靠用户截图在那猜。
蓝牙兼容这条路注定没有终点,系统版本每年都在变,厂商定制永远在给你“惊喜”,但把这些规律摸透之后,你至少可以从容应对,而不是每次升级都像拆盲盒。希望这篇文章里的细节能帮你在做 Android 蓝牙适配的时候少熬夜。