Android BLE的一些难点

近期在总结项目中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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值