蓝牙性能优化:连接延迟优化、吞吐量测试、内存泄漏排查、线程模型优化

大家好,我是你们的老朋友。今天咱们聊聊蓝牙性能优化。说实话,这话题挺大的,但也是实际开发中最容易踩坑的地方。我见过太多项目,功能都跑通了,一上真机就卡顿、断连、内存暴涨。嗯,今天就把我这些年积累的经验掰开揉碎讲给你听。

一、连接延迟优化:从握手到数据流

连接延迟,说白了就是设备从发起连接到真正能传数据的时间。我刚开始做蓝牙时,总觉得这玩意儿是协议栈的事,跟我没关系。直到有一次,客户反馈说他们的智能门锁要等3秒才能开……嗯,从那以后我再也不敢小看连接延迟了。

1.1 扫描参数调优

连接的第一步是扫描。扫描参数直接影响发现设备的速度。我个人习惯这样配:

// 扫描间隔和窗口,单位是毫秒
ScanSettings settings = new ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)  // 低延迟模式
    .setReportDelay(0)                                 // 实时上报
    .setLegacy(false)                                  // 使用扩展扫描
    .build();

这里有个坑:SCAN_MODE_LOW_LATENCY 确实快,但功耗也高。如果你做的是穿戴设备,建议用 BALANCED 模式,省电优先。

1.2 连接参数协商

连接建立后,GAP 层会协商连接参数。关键参数有三个:

参数 说明 推荐值
Connection Interval 连接间隔,决定数据交互频率 7.5ms - 30ms
Slave Latency 从设备可跳过的连接事件数 0 - 4
Supervision Timeout 超时断开时间 2s - 6s

我曾经遇到过一个奇葩问题:连接间隔设成 50ms,结果每次发数据都要等 50ms 的轮询周期。你想想看,如果一次交互要 3-4 个连接事件,那延迟直接奔着 200ms 去了。后来我把间隔压到 15ms,延迟瞬间降下来。

小技巧: 连接参数不是设了就生效的。Android 端可以通过 requestConnectionPriority 请求高优先级,但最终决定权在远端设备。建议在连接建立后主动发起一次参数更新请求。

二、吞吐量测试:别被数字骗了

吞吐量,就是单位时间内能传多少数据。很多人测吞吐量时,直接在实验室里跑,结果一上线就崩。为什么?因为真实环境有干扰、有丢包、有重传。

2.1 测试方法

我推荐用 ATT MTU 配合 Data Length Extension 来测。先看代码:

// 请求更大的 MTU
BluetoothGatt gatt = device.connectGatt(context, false, callback);
gatt.requestMtu(512);  // 默认是 23,最大可以到 512

// 启用 Data Length Extension(BLE 5.0+)
gatt.setPreferredPhy(BluetoothDevice.PHY_LE_1M_MASK, 
                     BluetoothDevice.PHY_LE_1M_MASK, 
                     BluetoothDevice.PHY_OPTION_NO_PREFERRED);

测吞吐量时,我习惯用这样的脚本:

// 发送端:连续写数据
byte[] data = new byte[512];
for (int i = 0; i < 1000; i++) {
    gatt.writeCharacteristic(characteristic, data, 
                             BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
    // 注意:不要在这里 sleep,用回调控制流控
}
注意: 不要用 WRITE_TYPE_RESPONSE 测吞吐量!每次写都要等确认,吞吐量直接砍半。我见过有人用这个模式测出 20KB/s,还以为是设备性能问题……其实是用错了 API。

2.2 影响吞吐量的因素

  • MTU 大小: 越大越好,但受远端设备限制。建议先协商到 512。
  • 连接间隔: 间隔越小,吞吐量越高。但太小的间隔会增加功耗。
  • 射频环境: 2.4GHz 频段干扰严重。Wi-Fi、微波炉、USB 3.0 都会影响。
  • 协议栈实现: 不同厂商的芯片差异很大。高通和联发科的协议栈行为就不一样。

三、内存泄漏排查:那些年我追过的泄漏

内存泄漏是蓝牙开发的老大难问题。我印象最深的一次,是某个智能家居项目,设备连接 10 次后 App 直接 OOM 崩溃。查了三天,最后发现是 BluetoothGattCallback 没注销。

3.1 常见泄漏场景

  1. Gatt 回调未注销: 每次 connectGatt 都会注册一个回调,如果 close() 时没清掉,回调会一直持有 Activity 引用。
  2. 广播接收器未解注册: BluetoothAdapter.ACTION_STATE_CHANGED 这类广播,注册了就得解。
  3. Handler 持有 Activity: 内部类 Handler 隐式持有外部类引用,导致 Activity 无法回收。
// 错误写法:匿名内部类持有 Activity
BluetoothGattCallback callback = new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        // 这里隐式持有外部类引用
    }
};

// 正确写法:使用静态内部类 + 弱引用
private static class MyGattCallback extends BluetoothGattCallback {
    private WeakReference<MainActivity> activityRef;
    
    MyGattCallback(MainActivity activity) {
        activityRef = new WeakReference<>(activity);
    }
}

3.2 排查工具

我常用的工具链:

  • Android Studio Profiler: 看内存堆栈,找 BluetoothGattBluetoothDevice 的实例数。
  • LeakCanary: 自动检测泄漏,省心省力。
  • dumpsys: adb shell dumpsys meminfo 看进程内存分布。
避坑指南: 我曾经在 onDestroy 里只调了 disconnect() 没调 close()。结果 BluetoothGatt 对象一直活着,每次重连都 new 一个新的,旧的也不释放。嗯,从那以后我写了个工具类,统一管理 Gatt 的生命周期。

四、线程模型优化:别在主线程搞事情

蓝牙操作是异步的,但很多人习惯在主线程里直接调 connectGatt。你想想看,如果网络不好,连接超时 30 秒,主线程就卡 30 秒。用户不骂你才怪。

4.1 推荐的线程模型

我一般这样设计:

public class BluetoothManager {
    private final HandlerThread workerThread;
    private final Handler workerHandler;
    private final Handler mainHandler;
    
    public BluetoothManager() {
        workerThread = new HandlerThread("ble-worker");
        workerThread.start();
        workerHandler = new Handler(workerThread.getLooper());
        mainHandler = new Handler(Looper.getMainLooper());
    }
    
    public void connect(BluetoothDevice device) {
        workerHandler.post(() -> {
            BluetoothGatt gatt = device.connectGatt(context, false, callback);
            // 所有 Gatt 操作都在工作线程
        });
    }
    
    // 回调结果切回主线程
    private final BluetoothGattCallback callback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            mainHandler.post(() -> {
                // 更新 UI
            });
        }
    };
}

4.2 线程安全注意事项

  • Gatt 操作是串行的: 同一个 BluetoothGatt 对象不能并发操作。我见过有人用线程池并发写,结果数据乱序。
  • 回调线程: Android 的 BLE 回调默认在 binder 线程池里跑,不是主线程。所以回调里不能直接操作 UI。
  • Handler 泄漏: 工作线程的 Handler 如果持有 Activity 引用,一样会泄漏。记得在 onDestroyremoveCallbacksAndMessages(null)
个人经验: 我习惯把蓝牙操作封装成一个 Service,所有操作通过 Intent 或 Binder 通信。这样生命周期清晰,也方便做重连逻辑。Service 里再开一个 HandlerThread,完美。

知识体系总览

下面这张图是我自己总结的蓝牙性能优化框架,涵盖了今天讲的所有要点:

蓝牙性能优化知识体系 连接延迟优化 • 扫描参数调优 • 连接间隔协商 • 从设备延迟控制 • 超时时间设置 吞吐量测试 • MTU 协商 • DLE 扩展 • 写类型选择 • 射频干扰分析 内存泄漏排查 • Gatt 回调泄漏 • 广播未解注册 • Handler 泄漏 • Profiler 分析 线程模型优化 • HandlerThread • 串行操作保证 • 回调线程切换 • Service 封装 蓝牙性能 优化核心 工具链:Android Profiler | LeakCanary | dumpsys | hcidump | Wireshark

这张图把四个优化方向串起来了。你从任何一个模块切入都可以,但最终都要回到核心目标:让蓝牙又快又稳又省内存。

好了,今天的内容就到这里。性能优化是个持续的过程,没有银弹。多测、多分析、多总结,慢慢你就能找到感觉。下次见!

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