Android 4.3(API Level 18)开始引入Bluetooth Low Energy(BLE,低功耗蓝牙)的核心功能并提供了相应的 API, 应用程序可以通过这些 API 执行扫描蓝牙设备、查询 services、读写设备的 characteristics(属性特征)等操作。
关于BLE的基础介绍和API使用说明,可以参照其他文章(比如这篇《Android BLE 蓝牙开发入门》就写的不错),本文主要关注和记录开发过程中遇到的坑和解决办法。
关于操作线程
扫描、连接、读写等全部蓝牙操作都应该在同一个线程中执行,否则在不同手机上可能会出现各种奇奇怪怪的错误。如果业务操作不频繁,没有要求同时操作多个蓝牙设备的话,可以简单一点处理,把操作都放在UI线程中执行,但更优的方法是创建一个新线程,所有操作都在这个线程中执行。
关于蓝牙状态
扫描蓝牙设备前需要做几步常规操作:
- 检查设备是否支持蓝牙
val manager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val adapter = manager.adapter
if(adapter == null){
toast("设备不支持蓝牙")
}
- 6.0以上系统,需要请求 ACCESS_COARSE_LOCATION 权限才能扫描附近的蓝牙设备
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
REQUEST_PERMISSION_LOCATION
)
- 6.0以上系统,需要打开定位服务才能扫描到蓝牙设备,部分手机还只有开启高精度定位才能扫描,可以用文字提醒用户去开启
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivityForResult(intent, REQUEST_LOCATION)
- 打开蓝牙开关
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有几个比较坑的点:
- 连接设备时要设置一个BluetoothGattCallback的回调,只有关闭Gatt才能同时关闭回调,如果只是断开了Gatt连接而没有关闭,后续连接上同一个设备时会把全部Callback都回调一次
- Gatt提供了重连功能,但部分手机上重连后会发现不到service
- 系统支持的连接句柄数是有限的,当达到上限后无法再建立新的连接,所以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。
对此,我们有两个解决方法:
- 建立连接后,手动修改连接间隔
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mGatt!!.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
}
- 对 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)!!
}