第4章 Android中蓝牙API的基本使用与生命周期

各位同学好,我是老张。这一章我们聊聊蓝牙API的基础用法和生命周期管理。很多泄漏问题,说白了就是生命周期没管好。Flow没取消、连接没断开、资源没释放——根源都在这里。

Android蓝牙API从4.2开始就比较稳定了。核心就那几个类:BluetoothAdapter、BluetoothDevice、BluetoothSocket(经典蓝牙)、BluetoothGatt(BLE)。别看类少,用起来坑不少。我曾经在一个IoT项目里,就因为Gatt连接没在onDestroy里断开,导致Activity泄漏,最后手机蓝牙服务都崩溃了。

4.1 蓝牙基础操作流程

先捋一遍经典蓝牙(RFCOMM)的基本调用顺序。我们做蓝牙通信,通常要经过这几步:

  1. 获取BluetoothAdapter(系统蓝牙管理器)
  2. 检查蓝牙是否开启,没开就请求打开
  3. 开始扫描设备,得到BluetoothDevice
  4. 用设备创建BluetoothSocket,发起连接
  5. 连接成功后,通过输入输出流通信
  6. 通信结束,关闭socket和流

BLE稍微不同:用BluetoothLeScanner扫描,连接后通过BluetoothGatt回调获取服务和特征值。但生命周期管理的本质一样——必须成对出现:创建与销毁、打开与关闭、连接与断开。

4.2 蓝牙API的生命周期关键点

我个人的习惯是:把蓝牙操作绑定到组件(Activity/Fragment)的特定生命周期回调中。比如扫描放在onResume里开始,onPause里停止。连接操作在后台线程做,但断开清理一定要在onDestroy或onStop里执行干净。

生命周期阶段应执行的操作常见错误
onCreate / onStart初始化Adapter,注册广播接收器重复注册未注销的广播
onResume开始扫描(如果需要)扫描未设定超时,一直扫耗电
onPause停止扫描忘记停止,导致系统资源占用
onDestroy关闭所有socket、Gatt连接,清理回调只断开没关闭引用,造成泄漏

嗯,这里要注意一点:蓝牙连接是异步的。你调用connect()或connectGatt()后,结果通过回调返回。如果组件已经销毁,回调回来再操作UI或资源就会NullPointer或者泄漏。我遇到过最典型的场景:Activity已经finish了,蓝牙回调里还在更新进度条,直接就崩了。

推荐做法:使用LifecycleObserver或协程的lifecycleScope来管理蓝牙连接的生命周期。让协程随着LifecycleOwner自动取消,这样Flow也好、回调也好,都不会有泄漏。

4.3 一个典型的安全连接代码片段

下面这段代码展示了我常用的生命周期管理模式——用ReactiveX或协程包装蓝牙操作,但这里我先用手动管理的方式让你看清本质:

class BleConnection(private val lifecycle: Lifecycle) {
    private var bluetoothGatt: BluetoothGatt? = null
    private var isDestroyed = false

    fun connect(device: BluetoothDevice) {
        if (isDestroyed) return
        bluetoothGatt?.let { return } // 防止重复连接
        bluetoothGatt = device.connectGatt(context, false, object : BluetoothGattCallback() {
            override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
                if (isDestroyed) {
                    gatt.close()
                    return
                }
                // 处理连接状态
            }
        })
        // 注册生命周期观察者
        lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onDestroy(owner: LifecycleOwner) {
                bluetoothGatt?.let {
                    it.disconnect()
                    it.close()
                    bluetoothGatt = null
                }
                isDestroyed = true
            }
        })
    }
}

看见没?isDestroyed标志位配合LifecycleObserver,确保无论回调何时抵达,都不会操作已销毁的组件。这是最基础的防守,但很多同学连这个都没做。你想想看,如果没加这个标志,Flow还在发射数据,Activity已经销毁,那蓝牙相关的资源就会一直持有Context,导致泄漏。

4.4 蓝牙生命周期的核心原则

讲这么多,其实可以归纳成一条原则:

蓝牙资源(Socket/Gatt/扫描回调)的创建和释放,必须严格配对,生命周期结束时必须显式清理。不要依赖GC,不要指望系统帮你回收。

为什么?因为蓝牙服务是系统级进程,你的App一旦请求了蓝牙连接,系统会分配底层资源。如果App进程不主动释放,系统会一直保持连接直到超时(几分钟甚至更久)。在这期间,你的Activity/Fragment虽然销毁了,但持有的BluetoothGatt对象还在,它引用了Context,Context又引用了整个Activity——泄漏就这么来的。

4.5 生命周期与蓝牙扫描的特殊性

蓝牙扫描使用的是BluetoothLeScanner.startScan()。这个API必须在onPause时停止,否则系统会持续给你返回扫描结果。我曾经有个项目,扫描没在onPause停止,结果用户切到后台后,扫描回调一直触发,IO线程池全被占满了,最后ANR。更糟的是,扫描回调里持有Activity强引用,Activity无法回收,导致大内存泄漏。

扫描类型停止方式生命周期最佳挂接点
经典蓝牙扫描(discovery)cancelDiscovery()onPause
BLE扫描(LeScanner)stopScan(callback)onPause
连续扫描(带filter)同stopScanonPause + 超时机制(比如30秒自动停止)

所以我的习惯是:写一个ScannerManager,内部用Lifecycle监听,自动在onPause时停止扫描。这样永远不怕忘记调stop方法。

特别注意:某些情况下,onPause不一定跟着onDestroy(比如按Home键只会pause)。所以扫描应该在onPause停止,但Gatt连接应该在onDestroy才关闭。别混了。

4.6 用一个图来总结蓝牙生命周期

下面这张SVG图我画了一下,把蓝牙操作和Android组件生命周期绑定在了一起。灰色虚线是组件生命周期,彩色框是蓝牙API调用,箭头表示时序依赖。你对照着图,再看前面的代码,就会清晰很多。

蓝牙操作与组件生命周期绑定 onCreate onResume onPause onDestroy 获取BluetoothAdapter 开始扫描 停止扫描 关闭Socket/Gatt 核心原则:蓝牙操作必须与组件生命周期成对出现,防止资源泄漏

这张图里,左边是Activity/Fragment的生命周期,右边是蓝牙操作。箭头表示“在该生命周期里应该执行的操作”。特别注意onDestroy对应的红色框——关闭连接和释放资源,很多人就是在这里遗漏了。

4.7 关于Flow和协程的提前思考

虽然这一章还没讲到Flow,但你可以先想想:如果你用Flow来发射蓝牙扫描结果,而这个Flow订阅了onResume开始,onPause取消,那它就不会泄漏。反之,如果你在onCreate里启动一个flow,但忘记在onDestroy取消,那么就算Activity销毁了,Flow仍在后台收集蓝牙回调数据——因为回调持有Activity引用,导致内存泄漏。

下一章我们会深入分析Flow+蓝牙的坑,但今天你得先掌握:无论用回调、RxJava、协程还是Flow,蓝牙资源的生命周期管理原则不变——该开的时候开,该关的时候关,不拖泥带水。

我的一个小技巧:在项目中建一个BluetoothLifecycleHelper类,内部使用DefaultLifecycleObserver接口。所有蓝牙操作都通过这个helper来管理。这样团队里每个人都遵循同一套生命周期逻辑,不容易出错。

好了,这一章就到这里。记住一句话:蓝牙的生命周期就是你App的生命周期的影子。影子的长度,由你决定。