杰理 SDK 文档中心
首页
首页
  • 项目概览

    • 仓库简介与示例构成
    • 支持的平台与协议
  • 快速开始

    • 导入工程与编译运行
    • 蓝牙权限配置
  • 应用架构

    • 工程结构与模块分层
    • 核心类与回调接口
  • 核心功能

    • 蓝牙设备扫描
    • ATT 设备连接与断开管理
    • 数据收发与通知回调
  • 配置与调试

    • 协议配置常量
    • 日志系统与调试指南
  • 界面与交互

    • 设备扫描与连接界面
    • 设备详情与设置界面

蓝牙设备扫描

本文档介绍 JieLi Android BT Demo(ATTConnect 模块)中的 BLE 设备扫描能力,涵盖扫描入口 BleManager、扫描配置、超时机制、结果回调体系 IBtScanCallback 以及扫描结果数据模型 ScanDeviceInfo 的完整实现。

Purpose and Scope

本页聚焦于**蓝牙设备扫描(Discovery/Scan)**这一核心能力,内容范围包括:

  • 扫描管理器 BleManager 的初始化(单例模式)与扫描环境检查;
  • startLeScan() / stopLeScan() 的完整控制流,以及 Android 5.0+(BluetoothLeScanner)与旧版 API(startLeScan)的双路径兼容实现;
  • 扫描参数(ScanSettings、ScanFilter)、超时自动停止机制与消息处理;
  • 扫描结果回调接口 IBtScanCallback 与回调管理器 BleEventCallbackManager 的协作方式;
  • 扫描结果数据模型 ScanDeviceInfo 的字段与 Parcelable 序列化。

以下主题属于相邻目录页的内容,本页不展开:

  • 扫描到设备后的 BLE 连接流程(connectBleDevice、GATT 回调、MTU 协商)——参见"设备连接"页面;
  • 连接后的数据读写(SendBleDataThread、OnWriteDataCallback)——参见"数据通信"页面。

Overview

在 ATTConnect 中,扫描是发现周边 BLE 设备的唯一入口,也是连接流程的前置步骤。BleManager 作为全局单例统一封装了系统蓝牙 API,对外提供:

  • 一键扫描:startLeScan(timeout) 自动处理权限检查、环境检查、扫描参数构建与超时调度;
  • 状态查询:isBleScanning() 随时可查询扫描状态;
  • 事件通知:通过注册 BleEventCallback,业务层可收到扫描开始/结束、设备发现、适配器开关、扫描失败等事件。

设计上,BleManager 采用单例 + 回调分发模式:所有系统级蓝牙回调(ScanCallback / LeScanCallback / BluetoothGattCallback)集中在管理器内部处理,再通过 mCallbackManager 分发到多个业务回调订阅者。这样既避免了业务层直接接触系统 API 的复杂度,也让扫描、连接、数据读写共用一个管理器,保证状态一致(例如连接前必须停止扫描)。

Architecture

flowchart TD
    subgraph sg_App["应用层 (ATTConnect)"]
        UI["UI / 业务代码"]
        EventCallback["BleEventCallback<br/>(implements IBtScanCallback)"]
    end

    subgraph sg_BleManager["BleManager (单例)"]
        Instance["BleManager.getInstance()"]
        ScanEntry["startLeScan(timeout)"]
        StopEntry["stopLeScan()"]
        ScanEnv["checkScanEnv() + 权限检查"]
        ScanSettings["ScanSettings 构建<br/>LOW_LATENCY / AGGRESSIVE"]
        CallbackMgr["BleEventCallbackManager"]
        Handler["Handler<br/>MSG_SCAN_BLE_TIMEOUT"]
        Devices["mDiscoveredBleDevices<br/>设备去重缓存"]
    end

    subgraph sg_Android["Android 系统蓝牙栈"]
        Adapter["BluetoothAdapter"]
        Scanner["BluetoothLeScanner<br/>(API 21+)"]
        Legacy["startLeScan 旧版回调<br/>(API < 21)"]
    end

    subgraph sg_Model["数据模型"]
        ScanInfo["ScanDeviceInfo<br/>(Parcelable)"]
    end

    UI --> Instance
    UI -->|"registerBleEventCallback"| EventCallback
    EventCallback --> CallbackMgr
    Instance --> ScanEntry
    ScanEntry --> ScanEnv
    ScanEnv -->|"通过"| ScanSettings
    ScanSettings --> Scanner
    Scanner -->|"ScanCallback"| CallbackMgr
    Adapter --> Legacy
    Legacy -->|"LeScanCallback"| CallbackMgr
    ScanEntry -->|"启动超时"| Handler
    Handler -->|"超时自动停止"| StopEntry
    CallbackMgr -->|"onDiscoveryDevice"| ScanInfo
    CallbackMgr -->|"onDiscoveryState / onDiscoveryFail"| EventCallback
    Devices --> CallbackMgr

架构说明:

  • BleManager 是扫描能力的唯一入口(单例),持有 BluetoothAdapter 与 BluetoothLeScanner(API 21+)两个系统句柄,以及扫描状态标志 isBleScanning 与已发现设备缓存 mDiscoveredBleDevices;
  • BleEventCallbackManager 负责将系统回调(扫描结果、扫描失败、状态变化)广播给所有注册的 BleEventCallback,业务层实现 IBtScanCallback 接口即可订阅;
  • ScanDeviceInfo 是扫描结果的载体,携带 BluetoothDevice、RSSI、原始广播数据(rawData)与连接状态,并通过 equals/hashCode 以设备为维度去重;
  • 扫描超时由内部 Handler 的 MSG_SCAN_BLE_TIMEOUT 消息驱动,超时后自动调用 stopLeScan(),避免扫描永久运行消耗电量。

核心实现解析

BleManager 单例与初始化

BleManager 采用**双重检查锁(Double-Checked Locking)**的单例模式,通过 getInstance() 获取唯一实例。构造函数中完成两件关键初始化:

  1. 获取系统 BluetoothAdapter(BluetoothAdapter.getDefaultAdapter());
  2. 在 Android 5.0(LOLLIPOP)及以上版本,通过适配器获取 BluetoothLeScanner——这是新扫描 API 的入口。
private BleManager(Context context) {
    if (null == context) {
        throw new RuntimeException("Context can not be null.");
    }
    mContext = context;
    mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (Build.VERSION.SDK_INT >= LOLLIPOP && mBluetoothAdapter != null) {
        mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
    }
    registerReceiver();
}

public static BleManager getInstance() {
    if (instance == null) {
        synchronized (BleManager.class) {
            if (instance == null) {
                instance = new BleManager(MyApplication.Companion.getApplication());
                JL_Log.w(TAG, "init", "instance : " + instance);
            }
        }
    }
    return instance;
}

Source: BleManager.java

设计意图:将 BluetoothLeScanner 的获取放在构造阶段而非每次扫描时获取,避免重复查询系统服务;同时 getInstance() 使用应用级 Context(MyApplication.Companion.getApplication()),防止 Activity 上下文泄漏。

扫描入口:startLeScan(timeout)

startLeScan(long timeout) 是扫描能力的核心方法,其控制流可以拆解为四个阶段:

阶段一:环境与权限检查

@SuppressLint("MissingPermission")
public boolean startLeScan(long timeout) {
    if (!checkScanEnv("startLeScan")) return false;
    if (!PermissionUtil.INSTANCE.isHasLocationPermission(mContext)) {
        JL_Log.w(TAG, "startLeScan", "Missing location permissions.");
        return false;
    }
    if (timeout <= 0) timeout = SCAN_BLE_TIMEOUT;

Source: BleManager.java

  • checkScanEnv() 负责校验蓝牙适配器是否存在、是否持有蓝牙扫描权限(hasBluetoothScanPermission);
  • 位置权限是 Android 上扫描 BLE 广播的传统前提(Android 12 之前定位权限决定能否拿到广播结果),缺失时直接返回 false 并记录警告日志;
  • timeout <= 0 时回退到默认超时常量 SCAN_BLE_TIMEOUT。

阶段二:重复扫描处理(幂等保护)

    if (isBleScanning()) {
        JL_Log.i(TAG, "startLeScan", "BLE is searching.");
        if (mBluetoothLeScanner != null && Build.VERSION.SDK_INT >= LOLLIPOP) {
            mBluetoothLeScanner.flushPendingScanResults(mScanCallback);
        }
        mDiscoveredBleDevices.clear();
        mHandler.removeMessages(MSG_SCAN_BLE_TIMEOUT);
        mHandler.sendEmptyMessageDelayed(MSG_SCAN_BLE_TIMEOUT, timeout);
        isBleScanning(true);
        syncSystemBleDevice();
        return true;
    }

Source: BleManager.java

若扫描已在运行,不会重复调用系统 startScan(系统层会报错),而是:

  • 调用 flushPendingScanResults() 让系统立即回调已缓存的扫描结果;
  • 清空设备缓存并重置超时计时(先移除旧消息再发送新消息),相当于"续扫";
  • 重新同步系统已绑定设备。这保证了重复调用是幂等的。

阶段三:双路径启动(版本兼容)

    boolean ret;
    if (Build.VERSION.SDK_INT >= LOLLIPOP && mBluetoothLeScanner != null) {
        ScanSettings scanSettings;
        int scanMode = ScanSettings.SCAN_MODE_LOW_LATENCY; //修改搜索BLE模式 -- 均衡模式
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            ScanSettings.Builder builder = new ScanSettings.Builder()
                    .setScanMode(scanMode)
                    .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
                    .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                builder.setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED);
            }
            scanSettings = builder.build();
        } else {
            scanSettings = new ScanSettings.Builder()
                    .setScanMode(scanMode)
                    .build();
        }
        List<ScanFilter> filters = new ArrayList<>();
        mBluetoothLeScanner.startScan(filters, scanSettings, mScanCallback);
        ret = true;
    } else {
        ret = mBluetoothAdapter.startLeScan(mLeScanCallback);
    }

Source: BleManager.java

  • API 21+ 路径:使用 BluetoothLeScanner.startScan(filters, scanSettings, mScanCallback)。filters 为空列表,即不过滤任何广播,扫描全部周边 BLE 设备;
  • 旧版路径(API < 21 或 scanner 为空):回退到 BluetoothAdapter.startLeScan(mLeScanCallback),结果通过 LeScanCallback 回调;
  • 两种路径共用一个状态机:isBleScanning(ret) 会把扫描状态广播给业务层。

阶段四:启动成功后的收尾

    JL_Log.i(TAG, "startLeScan", BluetoothUtil.formatString("%s. timeout : %d.", ret, timeout));
    isBleScanning(ret);
    if (ret) {
        mDiscoveredBleDevices.clear();
        mHandler.removeMessages(MSG_SCAN_BLE_TIMEOUT);
        mHandler.sendEmptyMessageDelayed(MSG_SCAN_BLE_TIMEOUT, timeout);
        syncSystemBleDevice();
    }
    return ret;
}

Source: BleManager.java

启动成功后清空历史设备缓存(避免与上一次扫描结果混淆)、调度超时消息,并调用 syncSystemBleDevice() 将系统已绑定/已连接的设备补充进发现列表——这保证了"已配对设备即使不在广播范围内也能出现在列表里"。

停止扫描:stopLeScan()

@SuppressLint("MissingPermission")
public boolean stopLeScan() {
    if (!checkScanEnv("stopLeScan")) return false;
    if (!isBleScanning()) return false;
    try {
        if (Build.VERSION.SDK_INT >= LOLLIPOP && mBluetoothLeScanner != null) {
            mBluetoothLeScanner.stopScan(mScanCallback);
        } else {
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    mHandler.removeMessages(MSG_SCAN_BLE_TIMEOUT);
    mHandler.removeMessages(MSG_SCAN_HID_DEVICE);
    isBleScanning(false);
    return true;
}

Source: BleManager.java

  • 与启动对称地走双路径停止;try/catch 包裹系统调用,防止系统内部异常(如蓝牙关闭瞬间)导致崩溃;
  • 同时移除 MSG_SCAN_BLE_TIMEOUT 与 MSG_SCAN_HID_DEVICE 两个消息——后者表明 HID 设备发现逻辑也挂在扫描生命周期上;
  • 通过 isBleScanning(false) 广播"扫描结束"状态。

扫描状态管理

扫描状态由布尔字段 isBleScanning 维护,其 setter 是唯一的"状态广播点":

public boolean isBleScanning() {
    return isBleScanning;
}

private void isBleScanning(boolean isScanning) {
    isBleScanning = isScanning;
    mCallbackManager.onDiscoveryState(isScanning);
}

Source: BleManager.java 与 BleManager.java

任何扫描状态的翻转(开始/结束)都会触发 onDiscoveryState 回调,业务层可据此刷新 UI 上的扫描指示器。

回调接口 IBtScanCallback

业务层通过实现 IBtScanCallback 接口订阅扫描事件。接口定义了四类回调:

public interface IBtScanCallback {
    /** 蓝牙适配器开关回调 */
    void onAdapterChange(boolean bEnabled);

    /** 搜索蓝牙设备的状态回调 */
    void onDiscoveryState(boolean bStart);

    /** 发现蓝牙设备的回调 */
    void onDiscoveryDevice(ScanDeviceInfo scanDeviceInfo);

    /** 搜索失败回调 */
    void onDiscoveryFail(int code, String message);
}

Source: IBtScanCallback.java

回调触发时机用途
onAdapterChange(boolean)蓝牙适配器开关状态变化(由 registerReceiver() 监听的系统广播驱动)提示用户开启蓝牙
onDiscoveryState(boolean)扫描开始/结束(isBleScanning(boolean) 每次翻转时)UI 扫描动画/按钮状态
onDiscoveryDevice(ScanDeviceInfo)每个新发现的 BLE 设备刷新设备列表
onDiscoveryFail(int, String)扫描失败(如系统返回 SCAN_FAILED_* 错误码)错误提示与重试

扫描结果模型 ScanDeviceInfo

ScanDeviceInfo 实现了 Parcelable,可在组件间安全传递,核心字段包括:

public class ScanDeviceInfo implements Parcelable {
    /** 蓝牙设备 */
    private final BluetoothDevice device;
    /** 信号强度 */
    private int rssi;
    /** 是否允许连接(默认允许连接,特殊情况不允许) */
    private boolean isEnableConnect = true;
    /** 原始数据 */
    private byte[] rawData;
    /** 连接状态 */
    private int connection = BluetoothProfile.STATE_DISCONNECTED;
    ...
}

Source: ScanDeviceInfo.java

  • device 是最终不可变字段,作为设备唯一标识;
  • equals()/hashCode() 仅以 device 为维度比较——这意味着同一个 MAC 地址的设备只会出现在列表一次,新的广播结果会更新既有条目(RSSI/rawData)而非新增条目;
  • toString() 通过 CHexConver.byte2HexStr(rawData) 以十六进制字符串打印广播原始数据,便于日志排查;
  • isEnableConnect 默认 true,供上层业务按需标记"不可连接"设备(例如白名单外设备)。

核心流程:一次完整的扫描生命周期

sequenceDiagram
    participant UI as 业务代码 (UI)
    participant BM as BleManager (单例)
    participant ENV as checkScanEnv / 权限检查
    participant SYS as 系统蓝牙栈 (BluetoothLeScanner)
    participant CB as BleEventCallbackManager
    participant DEV as ScanDeviceInfo

    UI->>BM: startLeScan(10000)
    BM->>ENV: 检查适配器 / 扫描权限 / 定位权限
    ENV-->>BM: 通过
    BM->>BM: 已在扫描? 是则续扫 (flush + 重置超时)
    BM->>SYS: startScan([], ScanSettings(LOW_LATENCY), mScanCallback)
    SYS-->>BM: 启动成功
    BM->>CB: onDiscoveryState(true)
    CB-->>UI: 扫描开始 (更新UI)
    BM->>BM: sendEmptyMessageDelayed(MSG_SCAN_BLE_TIMEOUT, 10000)

    loop 每次收到广播
        SYS->>BM: onScanResult(callbackType, result)
        BM->>DEV: 构建 ScanDeviceInfo (device/rssi/rawData)
        BM->>BM: 按 device 去重后加入 mDiscoveredBleDevices
        BM->>CB: onDiscoveryDevice(scanDeviceInfo)
        CB-->>UI: 刷新设备列表
    end

    Note over BM: 10 秒后 Handler 触发 MSG_SCAN_BLE_TIMEOUT
    BM->>BM: stopLeScan() 自动停止
    BM->>SYS: stopScan(mScanCallback)
    BM->>CB: onDiscoveryState(false)
    CB-->>UI: 扫描结束 (更新UI)

流程说明:

  1. 业务层调用 startLeScan(timeout),管理器依次完成环境检查(适配器非空、扫描权限、定位权限);
  2. 若扫描已在进行,则走"续扫"路径:flushPendingScanResults() 立即取回系统缓存的广播,并重置超时计时器;
  3. 首次启动时,按 API 版本选择 BluetoothLeScanner.startScan(API 21+)或 startLeScan(旧版),随后广播 onDiscoveryState(true) 并调度超时消息;
  4. 扫描期间,每个广播回调都被转换为 ScanDeviceInfo,按设备去重后通过 onDiscoveryDevice 分发;
  5. 超时消息触发自动停止:移除超时与 HID 相关消息、调用 stopScan、广播 onDiscoveryState(false)。整个生命周期无需业务层额外干预。

使用示例

示例一:注册扫描回调并启动扫描

业务层实现 IBtScanCallback(或继承 BleEventCallback)注册到 BleManager,随后调用 startLeScan 开始扫描。扫描结果在 onDiscoveryDevice 中刷新列表:

// 注册回调(BleEventCallback 实现了 IBtScanCallback,可只重写关心的回调)
mCallback = new BleEventCallback() {
    @Override
    public void onDiscoveryState(boolean bStart) {
        // 更新扫描动画/按钮
    }

    @Override
    public void onDiscoveryDevice(ScanDeviceInfo scanDeviceInfo) {
        // 将 scanDeviceInfo 加入列表适配器并 notifyDataSetChanged()
    }

    @Override
    public void onDiscoveryFail(int code, String message) {
        // 提示用户扫描失败
    }
};
BleManager.getInstance().registerBleEventCallback(mCallback);

// 开始扫描,10 秒超时
boolean ret = BleManager.getInstance().startLeScan(10000);

Source(注册/注销入口与扫描调用): BleManager.java

示例二:读取扫描结果数据

onDiscoveryDevice 收到的 ScanDeviceInfo 可直接读取设备、信号强度与原始广播数据,用于列表展示或过滤:

public ScanDeviceInfo setRssi(int rssi) {
    this.rssi = rssi;
    return this;
}

public boolean isEnableConnect() {
    return isEnableConnect;
}

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    ScanDeviceInfo that = (ScanDeviceInfo) o;
    return Objects.equals(device, that.device);
}

@Override
public int hashCode() {
    return Objects.hashCode(device);
}

Source: ScanDeviceInfo.java

setRssi 返回 this 支持链式调用;equals/hashCode 仅基于 device,保证同一设备在集合中的唯一性。

示例三:停止扫描与销毁清理

扫描结束或页面销毁时必须显式停止扫描,避免系统资源占用:

public void destroy() {
    JL_Log.w(TAG, "destroy", "instance : " + instance);
    unregisterReceiver();
    stopConnectTimeout();
    clearConnectedBleDevices();

    if (isBleScanning()) stopLeScan();
    isBleScanning(false);
    mDiscoveredBleDevices.clear();

    mCallbackManager.release();
    mHandler.removeCallbacksAndMessages(null);
    instance = null;
}

Source: BleManager.java

destroy() 是资源回收的总入口:停止扫描、清空设备缓存、释放回调、移除所有 Handler 消息并将单例置空。注意它在 isBleScanning() 为真时才调用 stopLeScan(),且之后显式将状态置为 false——这是为了防止广播风暴或重复停止。

另外,connectBleDevice 在建立连接前也会先 stopLeScan()(见 BleManager.java),因为 Android 系统不允许扫描与连接并发使用同一 GATT 回调通道,先停扫再连接是保证连接稳定性的关键步骤。

配置选项

扫描行为的配置集中在 BleManager 内部,业务层通过方法参数控制:

配置项类型默认值说明
SCAN_BLE_TIMEOUTlong常量(本页所读片段未展示具体数值)默认扫描超时时间(毫秒),timeout <= 0 时回退到该值
startLeScan(timeout) 参数longSCAN_BLE_TIMEOUT单次扫描超时;超时后 Handler 自动触发 stopLeScan()
ScanSettings.SCAN_MODE_LOW_LATENCYint固定值低延迟扫描模式,最快回报广播,但更耗电(代码注释标注为"均衡模式")
ScanSettings.MATCH_MODE_AGGRESSIVEintAPI 23+激进匹配模式,尽量回报更多广告包(厂商私有广播通常仅周期性出现一次)
ScanSettings.MATCH_NUM_MAX_ADVERTISEMENTintAPI 23+单次匹配回报的广告包数量上限设为最大
ScanSettings.PHY_LE_ALL_SUPPORTEDintAPI 26+同时扫描 1M/2M/编码 PHY,兼容更广的设备
ScanFilter 列表List空列表不设过滤条件,扫描全部 BLE 设备

设计意图:LOW_LATENCY + AGGRESSIVE + MAX_ADVERTISEMENT 的组合是面向厂商私有广播协议(如杰理芯片设备的上报广播)的典型配置——这类设备广播周期短、载荷大,只有激进的扫描策略才能稳定捕获;代价是功耗上升,因此必须配合超时自动停止机制。

API 参考

BleManager.getInstance(): BleManager

获取全局唯一实例(双重检查锁单例)。首次调用时使用应用级 Context 构造,初始化 BluetoothAdapter 与 BluetoothLeScanner。

  • 返回:BleManager 实例
  • 抛出:RuntimeException(Context 为 null 时,见构造函数)

Source: BleManager.java

startLeScan(timeout: long): boolean

启动 BLE 扫描。

  • 参数:timeout — 扫描超时毫秒数;<= 0 时使用默认值 SCAN_BLE_TIMEOUT
  • 返回:true 表示扫描已启动(或已在扫描并续扫);false 表示环境/权限检查失败
  • 失败原因:适配器为 null、缺少蓝牙扫描权限、缺少定位权限(分别记录对应警告日志)
  • 副作用:清空设备缓存、调度超时消息、同步系统已绑定设备、广播 onDiscoveryState(true)

Source: BleManager.java

stopLeScan(): boolean

停止 BLE 扫描。

  • 返回:true 表示已停止;false 表示环境检查失败或本就未在扫描
  • 副作用:移除 MSG_SCAN_BLE_TIMEOUT 与 MSG_SCAN_HID_DEVICE 消息、广播 onDiscoveryState(false)

Source: BleManager.java

isBleScanning(): boolean

查询当前是否正在扫描。

Source: BleManager.java

registerBleEventCallback(callback: BleEventCallback) / unregisterBleEventCallback(callback: BleEventCallback)

注册/注销扫描事件订阅者。内部委托给 BleEventCallbackManager,支持多订阅者。

Source: BleManager.java

destroy()

释放全部扫描/连接资源:停止扫描、清空缓存、释放回调管理器、移除 Handler 消息、单例置空。应用退出时调用。

Source: BleManager.java

IBtScanCallback 接口方法

方法签名参数触发条件
onAdapterChange(boolean bEnabled)适配器开关状态系统蓝牙开关广播
onDiscoveryState(boolean bStart)扫描是否进行中isBleScanning() 状态翻转
onDiscoveryDevice(ScanDeviceInfo scanDeviceInfo)扫描结果对象每个新设备广播
onDiscoveryFail(int code, String message)错误码与描述系统扫描失败回调

Source: IBtScanCallback.java

ScanDeviceInfo 关键方法

  • getDevice(): BluetoothDevice — 设备对象(不可变)
  • getRssi(): int / setRssi(int): ScanDeviceInfo — 信号强度(链式)
  • isEnableConnect(): boolean / setEnableConnect(boolean): ScanDeviceInfo — 是否允许连接
  • getRawData(): byte[] / setRawData(byte[]): ScanDeviceInfo — 广播原始数据
  • getConnection(): int / setConnection(int): ScanDeviceInfo — 连接状态(BluetoothProfile 常量)
  • equals/hashCode — 以 device 为唯一性维度
  • 实现 Parcelable(CREATOR 工厂),支持跨组件传递

Source: ScanDeviceInfo.java

失败模式、边界情况与并发

环境与权限失败

场景表现处理
设备不支持蓝牙(mBluetoothAdapter == null)checkScanEnv 记录警告 "No support for Bluetooth function."startLeScan / stopLeScan 直接返回 false
缺少扫描权限(hasBluetoothScanPermission)记录 "Missing permission to search Bluetooth."返回 false,由业务层引导用户授权
缺少定位权限startLeScan 记录 "Missing location permissions."返回 false;这是 Android 12 之前获取广播结果的前提
蓝牙未开启扫描可被系统拒绝或返回空结果通过 onAdapterChange(false) 通知 UI 引导开启

扫描期间调用连接

connectBleDevice 内部会先检查 isBleScanning() 并调用 stopLeScan()(见 BleManager.java)。这是有意的串行化设计:Android BLE 栈在同一时刻处理扫描与连接会产生竞争,先停扫再连接可避免 GATT 回调与扫描回调交错导致的异常。

重复调用 startLeScan(幂等续扫)

再次调用时不会重复 startScan,而是走续扫分支:flushPendingScanResults() 拉取缓存、清空列表、重置超时。这使 UI 层的"下拉刷新/重扫"操作可以安全地重复触发。

并发与状态一致性

  • 所有扫描状态翻转都经由 isBleScanning(boolean) 单一入口广播,避免状态与回调不一致;
  • 单例通过 synchronized(BleManager.class) 保护创建,但扫描启动本身不持锁——Android 系统回调运行在 Binder 线程,业务层应只读 ScanDeviceInfo 并尽快拷贝数据到 UI 线程;
  • 超时消息用 removeMessages + sendEmptyMessageDelayed 模式管理,续扫/停止时都会先移除旧消息,杜绝"已停止扫描仍收到超时回调"的竞态;
  • destroy() 在 isBleScanning() 为真时先停扫再清空缓存,并 removeCallbacksAndMessages(null) 清空所有待处理消息,保证单例重建时无残留状态。

系统扫描失败

API 21+ 的 ScanCallback.onScanFailed 会携带错误码(如 SCAN_FAILED_ALREADY_STARTED、SCAN_FAILED_APPLICATION_REGISTRATION_FAILED、SCAN_FAILED_FEATURE_UNSUPPORTED)。管理器将这些错误转换为 onDiscoveryFail(code, message) 通知业务层,业务层应据此提示用户并给出重试路径。

性能与运维注意事项

  • 功耗:SCAN_MODE_LOW_LATENCY 回报快但耗电高;必须依赖超时自动停止机制,业务层无需(也不应)依赖用户手动停止;
  • 回调频率:MATCH_MODE_AGGRESSIVE + MATCH_NUM_MAX_ADVERTISEMENT 下同一设备会高频回报广告包。ScanDeviceInfo.equals 以 device 为维度去重,mDiscoveredBleDevices 是去重缓存——UI 层应复用该缓存而非每次全量刷新列表;
  • 线程模型:系统扫描回调在 Binder 线程触发,ScanDeviceInfo 的 Parcelable 支持安全传递,但列表适配器的 notifyDataSetChanged() 必须切回主线程;
  • 日志:扫描关键路径均使用 JL_Log(startLeScan / stopLeScan / 状态翻转)记录,ScanDeviceInfo.toString() 以十六进制输出 rawData,线上排查广播协议问题时可直接从日志定位;
  • Android 12+ 权限变化:从 Android 12 开始,扫描需要 BLUETOOTH_SCAN 运行时权限(hasBluetoothScanPermission 已覆盖);Android 13+ 不再要求定位权限即可扫描,但本实现的定位检查仍然保留,向下兼容旧版本。

扩展点

  • 自定义扫描过滤:startLeScan 内部构建了空 ScanFilter 列表(扫描全部设备)。如需按 Service UUID/厂商数据过滤,可在 mBluetoothLeScanner.startScan(filters, ...) 处传入定制 ScanFilter 列表——当前实现刻意保持全量扫描,把过滤职责留给业务层;
  • 多订阅者回调:通过 registerBleEventCallback(BleEventCallback) 可注册多个订阅者,BleEventCallbackManager 统一分发。业务层可选择实现完整 IBtScanCallback,或继承 BleEventCallback 只重写关心的回调(如只关心 onDiscoveryDevice);
  • 扫描超时策略:timeout 参数由调用方决定,UI 层可结合业务场景(首次搜索给更长超时、刷新给更短超时)动态传入;
  • 设备可连接性标记:ScanDeviceInfo.isEnableConnect 供业务层标记"可见但不可连"的设备(如白名单过滤结果),展示层据此禁用连接按钮。

相关链接

  • 扫描到的设备如何建立连接(connectBleDevice、GATT 回调、MTU 协商)——参见"设备连接"目录页
  • 连接后的数据读写(SendBleDataThread、OnWriteDataCallback)——参见"数据通信"目录页
  • 回调分发实现:BleEventCallbackManager.java
  • 扫描入口与连接管理:BleManager.java
  • 扫描回调接口:IBtScanCallback.java
  • 扫描结果模型:ScanDeviceInfo.java
  • 蓝牙权限工具:PermissionUtil(startLeScan 中定位权限检查处)
Next
ATT 设备连接与断开管理