9、蓝牙状态监听与广播:系统蓝牙广播与动态注册

蓝牙开发中,有一件事你躲不开——监听状态变化。

蓝牙开关了没?设备连上了没?断开了没?这些信息,系统都会通过广播告诉你。说白了,广播就是Android系统跟你的App之间的一条消息通道。今天我们就来聊聊这条通道怎么用,以及Android 14之后它变得有多“难走”。

9.1 系统蓝牙广播:那些你必须知道的Action

Android系统定义了一系列蓝牙相关的广播Action。我个人习惯把它们分成三类:

  • 本地蓝牙状态:蓝牙开关、飞行模式等
  • 连接状态:ACL连接、配对、绑定等
  • 设备发现:扫描开始、结束、找到设备等

下面这张图,是我整理的核心广播Action关系图。你想想看,整个蓝牙状态监听体系,其实就是围绕这几个Action展开的。

Android 蓝牙核心广播 Action 体系 蓝牙广播中心 本地蓝牙状态 ACTION_STATE_CHANGED EXTRA_STATE / EXTRA_PREVIOUS_STATE ACL 连接状态 ACTION_ACL_CONNECTED ACTION_ACL_DISCONNECTED ACTION_ACL_DISCONNECT_REQUESTED 配对 & 发现 ACTION_BOND_STATE_CHANGED ACTION_FOUND ACTION_DISCOVERY_STARTED/FINISHED 注:ACL = Asynchronous Connection-Less,即蓝牙底层异步无连接链路

9.2 ACTION_STATE_CHANGED:蓝牙开关状态

这个广播,是监听蓝牙开关最直接的方式。当用户打开或关闭蓝牙时,系统会发送这个广播。

广播里带两个关键Extra:

Extra 名称 类型 说明
EXTRA_STATE int 当前蓝牙状态
EXTRA_PREVIOUS_STATE int 之前的状态

状态值有四个:STATE_OFFSTATE_TURNING_ONSTATE_ONSTATE_TURNING_OFF

经验之谈: 我在项目中遇到过一个问题——很多新手只监听 STATE_ON 和 STATE_OFF,忽略了 TURNING_ON 和 TURNING_OFF。结果呢?蓝牙还没完全打开就开始做连接操作,必然失败。记住,蓝牙从 OFF 到 ON 中间有个 TURNING_ON 状态,这时候 BluetoothAdapter 虽然不为 null,但底层还没准备好。

9.3 ACTION_ACL_CONNECTED:设备连接与断开

ACL 是蓝牙底层的链路层连接。说白了,两个蓝牙设备之间建立了一条数据通道,就叫 ACL 连接。

这三个广播你要记住:

  • ACTION_ACL_CONNECTED:ACL 链路建立
  • ACTION_ACL_DISCONNECT_REQUESTED:系统请求断开 ACL 链路
  • ACTION_ACL_DISCONNECTED:ACL 链路已断开

每个广播都携带一个 BluetoothDevice 对象,通过 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) 获取。

避坑指南: 我曾经以为 ACTION_ACL_CONNECTED 就代表设备已经配对成功。其实不是。ACL 连接只是链路层通了,配对(Bonding)是更高层的安全机制。有些设备 ACL 连上了但没配对,这种情况在低安全需求的场景下是允许的。

9.4 动态注册 BroadcastReceiver

Android 推荐使用动态注册,而不是在 AndroidManifest.xml 里静态注册。原因很简单——动态注册可以控制生命周期,避免不必要的广播接收。

代码示例:

// 1. 定义 BroadcastReceiver
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action == null) return;

        switch (action) {
            case BluetoothAdapter.ACTION_STATE_CHANGED:
                int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 
                                                BluetoothAdapter.ERROR);
                int prevState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, 
                                                    BluetoothAdapter.ERROR);
                handleStateChange(prevState, state);
                break;

            case BluetoothDevice.ACTION_ACL_CONNECTED:
                BluetoothDevice device = intent.getParcelableExtra(
                    BluetoothDevice.EXTRA_DEVICE);
                Log.d("BluetoothDemo", "设备已连接: " + device.getName());
                break;

            case BluetoothDevice.ACTION_ACL_DISCONNECTED:
                BluetoothDevice disconnectedDevice = intent.getParcelableExtra(
                    BluetoothDevice.EXTRA_DEVICE);
                Log.d("BluetoothDemo", "设备已断开: " + disconnectedDevice.getName());
                break;
        }
    }
};

// 2. 注册(通常在 onStart 或 onResume 中)
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
registerReceiver(bluetoothReceiver, filter, 
                 Context.RECEIVER_EXPORTED); // Android 14+ 必须指定

// 3. 注销(通常在 onStop 或 onPause 中)
unregisterReceiver(bluetoothReceiver);

注意: 上面的 RECEIVER_EXPORTED 是 Android 14 新增的要求。如果你不指定,系统会抛异常。具体来说,如果你的 receiver 需要接收来自系统或其他应用的广播,必须显式声明导出。

9.5 Android 14+ 的广播限制

Android 14(API 34)对广播做了更严格的限制。我总结了几条关键变化:

  • 动态注册必须指定导出标志Context.RECEIVER_EXPORTEDContext.RECEIVER_NOT_EXPORTED
  • 静态注册受限:大部分蓝牙广播不再支持静态注册,必须动态注册
  • 后台启动限制:App 在后台时,收到的广播不能直接启动 Activity

为什么会这样?说白了,Google 在努力限制 App 在后台“偷偷”做事情。蓝牙广播以前是很多流氓 App 唤醒自己、启动服务的“后门”。现在这个后门被堵上了。

我建议的做法:

  1. onStart 中注册,onStop 中注销
  2. 使用 RECEIVER_EXPORTED 明确声明
  3. 如果只是 App 内部使用,用 RECEIVER_NOT_EXPORTED 更安全

个人经验: 我记得有一次升级 targetSdk 到 34,App 突然收不到蓝牙广播了。查了半天,发现是忘了加 RECEIVER_EXPORTED 标志。嗯,这个坑我踩过,希望你别踩。

9.6 实战建议:一个完整的监听流程

最后,我给出一个实际项目中常用的监听流程:

  1. App 启动 → 检查蓝牙是否开启
  2. 动态注册 BroadcastReceiver,监听 STATE_CHANGED 和 ACL 广播
  3. 蓝牙关闭时 → 提示用户开启,或自动请求开启
  4. 蓝牙开启后 → 开始扫描或连接已知设备
  5. 设备连接 → 更新 UI,开始数据传输
  6. 设备断开 → 清理资源,尝试重连(如果需要)
  7. App 退出 → 注销 BroadcastReceiver

这个流程看起来简单,但每一步都有细节。比如自动重连,你不能一断开就立刻重连,得加个延迟和重试次数限制。不然设备反复断开重连,用户体验极差。

好了,关于蓝牙状态监听和广播,今天就聊到这里。记住一句话:广播是桥梁,但别让它成为你 App 的“后门”。


公众号:蓝海资料掘金营,微信deep3321