蓝牙性能优化:连接延迟优化、吞吐量测试、内存泄漏排查、线程模型优化
大家好,我是你们的老朋友。今天咱们聊聊蓝牙性能优化。说实话,这话题挺大的,但也是实际开发中最容易踩坑的地方。我见过太多项目,功能都跑通了,一上真机就卡顿、断连、内存暴涨。嗯,今天就把我这些年积累的经验掰开揉碎讲给你听。
一、连接延迟优化:从握手到数据流
连接延迟,说白了就是设备从发起连接到真正能传数据的时间。我刚开始做蓝牙时,总觉得这玩意儿是协议栈的事,跟我没关系。直到有一次,客户反馈说他们的智能门锁要等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,延迟瞬间降下来。
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 常见泄漏场景
- Gatt 回调未注销: 每次
connectGatt都会注册一个回调,如果close()时没清掉,回调会一直持有 Activity 引用。 - 广播接收器未解注册:
BluetoothAdapter.ACTION_STATE_CHANGED这类广播,注册了就得解。 - 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: 看内存堆栈,找
BluetoothGatt和BluetoothDevice的实例数。 - 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 引用,一样会泄漏。记得在
onDestroy时removeCallbacksAndMessages(null)。
知识体系总览
下面这张图是我自己总结的蓝牙性能优化框架,涵盖了今天讲的所有要点:
这张图把四个优化方向串起来了。你从任何一个模块切入都可以,但最终都要回到核心目标:让蓝牙又快又稳又省内存。
好了,今天的内容就到这里。性能优化是个持续的过程,没有银弹。多测、多分析、多总结,慢慢你就能找到感觉。下次见!