Android低功耗蓝牙踩坑填坑记录

Android 4.3(API Level 18)开始引入Bluetooth Low Energy(BLE,低功耗蓝牙)的核心功能并提供了相应的 API, 应用程序可以通过这些 API 执行扫描蓝牙设备、查询 services、读写设备的 characteristics(属性特征)等操作。

关于BLE的基础介绍和API使用说明,可以参照其他文章(比如这篇《Android BLE 蓝牙开发入门》就写的不错),本文主要关注和记录开发过程中遇到的坑和解决办法。

关于操作线程

扫描、连接、读写等全部蓝牙操作都应该在同一个线程中执行,否则在不同手机上可能会出现各种奇奇怪怪的错误。如果业务操作不频繁,没有要求同时操作多个蓝牙设备的话,可以简单一点处理,把操作都放在UI线程中执行,但更优的方法是创建一个新线程,所有操作都在这个线程中执行。

关于蓝牙状态

扫描蓝牙设备前需要做几步常规操作:

  1. 检查设备是否支持蓝牙
val manager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val adapter = manager.adapter
if(adapter == null){
    toast("设备不支持蓝牙")
}
  1. 6.0以上系统,需要请求 ACCESS_COARSE_LOCATION 权限才能扫描附近的蓝牙设备
ActivityCompat.requestPermissions(
    this,
    arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
    REQUEST_PERMISSION_LOCATION
)
  1. 6.0以上系统,需要打开定位服务才能扫描到蓝牙设备,部分手机还只有开启高精度定位才能扫描,可以用文字提醒用户去开启
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivityForResult(intent, REQUEST_LOCATION)
  1. 打开蓝牙开关
val manager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val adapter = manager.adapter
if(!adapter.enable()){
    val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
    startActivityForResult(intent, REQUEST_BLUETOOTH)
}

关于扫描方法

系统提供了两个扫描蓝牙设备的,分别是BluetoothAdapter和BluetoothLeScanner,其中BluetoothAdapter又提供了两个搜索蓝牙设备的方法startDiscovery、startLeScan,它们的区别如下:

方法 API 搜索范围 效率 其他特点
BluetoothAdapter.startDiscovery 18 经典蓝牙和低功耗蓝牙 搜索低功耗蓝牙的效率很低
BluetoothAdapter.startLeScan 18 低功耗蓝牙
BluetoothLeScanner 21 低功耗蓝牙 接口更灵活

对于BLE设备,如果API支持,最好使用BluetoothLeScanner,否则,应该使用效率更高的startLeScan方法。

val bluetoothManager = mContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val adapter = bluetoothManager.adapter
// 方法一
adapter.startDiscovery()
// 方法二
adapter.startLeScan(mLeScanCallback)
// 方法三
adapter.bluetoothLeScanner.startScan(scanCallback);

关于资源消耗

蓝牙搜索非常消耗电量,在搜索到目标设备时,应该马上停止搜索操作,可以通过设置一个定时器来定时停止扫描操作。

val overtime = 10000; //10秒超时
Flowable.timer(overtime, TimeUnit.MILLISECONDS, Schedulers.newThread())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe({
        stopScan()
    }, Throwable::printStackTrace)

关于连接设备

连接设备时,系统会返回一个操作对象Gatt,这个Gatt有几个比较坑的点:

  1. 连接设备时要设置一个BluetoothGattCallback的回调,只有关闭Gatt才能同时关闭回调,如果只是断开了Gatt连接而没有关闭,后续连接上同一个设备时会把全部Callback都回调一次
  2. Gatt提供了重连功能,但部分手机上重连后会发现不到service
  3. 系统支持的连接句柄数是有限的,当达到上限后无法再建立新的连接,所以Gatt在不用时要及时关闭

结合上面三点,最好的使用方式是断开连接后马上关闭Gatt,重新连接时不要使用重连功能,而是创建一个新的Gatt对象。

// 连接设备
mGatt = device.connectGatt(mContext, false, mConnectGattCallbackImpl)

......

// 断开连接
mGatt.disconnect()
mGatt.close()

关于发现服务

部分手机上 discoverServices 的操作很慢,甚至一直不回调 onServicesDiscovered 方法,原因可能是connect interval太大。根据从网上找到的资料,有的手机会主动向设备发起更改connect interval,而有的手机却不会,这样的话connect interval相差就会很大,有的手机是7ms,有的手机是默认的50ms,所以发现service都要8s,甚至20s。
对此,我们有两个解决方法:

  1. 建立连接后,手动修改连接间隔
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    mGatt!!.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
}
  1. 对 discoverServices 方法进行超时检查,一段时间内没有回调 onServicesDiscovered 的话,就重新执行发现服务的操作

实际上,以上两个解决方法是不冲突的,可以同时使用。

关于Notify开关

Gatt提供了开关 Notify 的方法,但对于部分蓝牙设备来说并不足够,我们还必须对Descriptor写入一个数值才能真正完成开关 Notify 的操作,同时要注意,等到onDescriptorWrite被回调才能执行下一个操作。

    private val CHARACTERISTIC_UPDATE_NOTIFICATION_DESCRIPTOR_UUID =
        UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
//设置Char的notify开关
var result = mGatt?.setCharacteristicNotification(char, enable)!!
if (result) {
    val descriptor = char.getDescriptor(CHARACTERISTIC_UPDATE_NOTIFICATION_DESCRIPTOR_UUID)
    descriptor?.value = if (enable)
        BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
    else
        BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
    result = result && mGatt?.writeDescriptor(descriptor)!!
}

你可能感兴趣的:(Android低功耗蓝牙踩坑填坑记录)