近期在总结项目中BLE相关的操作,在review代码的时候,暴露出了挺多的问题,在这里总结比较有参考价值的点; 需要注意的是,这不是入门指导,请确定你真的弄过BLE的相关API。
BluetoothGattCallback的线程问题
BluetoothGattCallback gattCallBack=new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState== BluetoothProfile.STATE_CONNECTED){
gatt.discoverServices();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
//代码块1
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
}
复制代码
BluetoothGattCallback
可以说是最基础的API。不管你要读写还是订阅,都绕不过这个回调。
上面是常用的写法,大部分人都会直接在onConnectionStateChange
中调用discoverServices()
,但是更为难堪的是:上例的==代码块1==中,也有很大部分的人直接/间接 调用上readCharacteristic()
亦或是writeCharacteristic()
。
那么,我为什么觉得这让人难堪,出于IPC的原因,整个gattcallback的回调其实是跑在binder线程。所以如果你在onServicesDiscovered内执行了耗时操作,是的,你的UI线程确实不会阻塞,但是,这个binder会被阻塞,从而导致onCharacteristicRead
,onCharacteristicWrite
,onCharacteristicChanged
这三个数据回调在等待你的操作完成,如果你调用readCharacteristic
后得不到onCharacteristicRead
的反馈,不妨排查下。其实对于普通场景,也不应该如此苛刻要求?但是这里的异步,让大多数人模糊了线程的概念。
read/write Characteristic的反馈
我们先来看一下官方的注释
/**
* Reads the requested characteristic from the associated remote device.
*
* <p>This is an asynchronous operation. The result of the read operation
* is reported by the {@link BluetoothGattCallback#onCharacteristicRead}
* callback.
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* @param characteristic Characteristic to read from the remote device
* @return true, if the read operation was initiated successfully
*/
复制代码
如果你把弄过API,很明显,你知道readCharacteristic的结果是在上面说到的gattcallback#onCharacteristicRead这里异步回调。@return true, if the read operation was initiated successfully
这里的return false有挺多情况,但是最为常见的就是断连和连续读写导致的。
public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
//省略了一段
synchronized(mDeviceBusy) {
if (mDeviceBusy) return false;
mDeviceBusy = true;
}
try {
mService.readCharacteristic(mClientIf, device.getAddress(),
characteristic.getInstanceId(), AUTHENTICATION_NONE);
} catch (RemoteException e) {
mDeviceBusy = false;
return false;
}
return true;
}
复制代码
mDeviceBusy
这个标志位决定了你两次读写之间的间隔(通常在25ms左右),BLE的读写,必须等待上一次读写完成,就是等onCharacteristicRead
/onCharacteristicWrite
回调。
那么为了做到短时连续读写,项目中的老旧代码是这样写的:
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
/**
*一系列的标志位判定,进度标识
*/
while(running){
gatt.readCharacteristic(characteristic);
}
}
复制代码
就这样,形成了难以控制的连续回调,而且上面也说过,这样的代码还会导致阻塞。就读这个操作而言,可能还不显得丑陋,但是对于多数物联网蓝牙设备,OTA蓝牙升级是必须的,升级就是把固件文件分块写到设备中,你就得在onCharacteristicWrite中自己做标记和进度控制,让这种异步
混乱了你的流程。
我想要的API
在对代码进行重构的期间,一点点跟完旧的代码,我不禁被这混乱且‘到处跑’的流程搞懵比。对于部分问题,其实都可以归结为线程概念的模糊,混乱的回调地狱。不可否认,因为IPC和不阻塞Ui线程的原因,android官方的api好像没什么过错,但是对于我这个项目而言,我觉得很不OK。所以优先要做的就是梳理逻辑和解决掉这个回调地狱。
我不需要异步,我只想面条式编码
只要你能明确你的工作线程,我觉得抛弃官方原有的异步才是最好的解决办法。
就拿OTA升级这种连续写入的场景而言:writeCharacteristic
->等待->onCharacteristicWrite
->循环写入下一块数据
我们可以通过锁的控制,把异步包裹成阻塞式同步,实现这样的流程:
while(running && otaBin.nextBlock()){
byte[] block = otaBin.get();
xxx.write(block);
}
复制代码
这样子的代码,就很适合阅读(手动滑稽)
为了达到这样的流程,我给出下面的参考:(ps:读写共用同一个锁)
public void write(byte[] datas){
if (mc==null){return;}
ioLock.lock();
mc.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
mc.setValue(datas);
mGatt.writeCharacteristic(mc);
}
//然后在gattcallback里
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//一些解析操作?
ioLock.unlock();
}
复制代码
关于锁
根据上面的分析,很明显的,我需要的是不可重入
的锁。
我这里给出两种方式:
//CAS 自旋的方式
public class CASSpinLock {
private AtomicInteger status = new AtomicInteger(0);
public void lock() {
while (!status.compareAndSet(0, 1)) {
Thread.yield();
}
}
public void unlock() {
status.compareAndSet(1, 0);
}
}
//线程休眠唤醒的方式
public class SpinLock {
private ArrayBlockingQueue queue = new ArrayBlockingQueue(1);
private Object signObj=new Object();
public boolean lock(){
boolean result=false;
try {
result = queue.offer(signObj,10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
return result;
}
}
public void unlock(){
try {
if (queue.size()==0){
return;
}
queue.poll(10, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
这两种方式,CAS会导致cpu占用的提升,线程休眠机制需要频繁唤醒,休眠线程,开销暂不知。我是建议不要用CAS方式啦,毕竟蓝牙就很耗电了。
最后给出实例代码 点我去github