Android BLE设备蓝牙通信框架BluetoothKit
BluetoothKit是一款功能强大的Android蓝牙通信框架,支持低功耗蓝牙设备的连接通信、蓝牙广播扫描及Beacon解析。
关于该项目的详细文档请关注:https://github.com/dingjikerbo/BluetoothKit
对于刚接触Android蓝牙开发的初学者来说,会经常遇到一些奇怪的坑,我也是一路走过来的,将我遇到的一些坑总结了一下,这些坑在这个项目中都修复了,所以大家不必再费时费力去重复踩一遍。这个项目目前正在不断更新,有什么更好的建议大家可以随时提出来。
蓝牙扫描相关的坑
一、startDiscovery在大多数手机上是可以同时发现经典蓝牙和Ble的,但是startDiscovery的回调无法返回Ble的广播,所以无法通过广播识别设备,且startDiscovery扫描Ble的效率比StartLeScan低很多。所以在实际应用中,还是StartDiscovery和StartLeScan分开扫,前者扫经典蓝牙,后者扫低功耗蓝牙。
二、startLeScan() 的时候,在onLeScan() 中不能做耗时操作,特别是周围的BLE设备多的时候,容易导致底层堵塞。如果有耗时操作请丢到子线程中去处理。
三、有的手机会过滤设备广播,一次扫描过程中只发一次request,如果没有收到response就再也收不到该设备广播回调了,解决的办法是多扫几次。
蓝牙连接相关的坑
一、对蓝牙设备的操作不能并行,只能串行,即每次都要在收到上一个操作的回调后才能继续下一个操作。但是断开连接例外,断开连接要马上closeGatt,不用等任务队列中的其他操作了。而且要给所有正在执行或者准备执行的任务都cancel。
二、有时候蓝牙协议栈出现异常可能收不到回调,所以我们要对每个操作做超时检查,否则后面的所有操作都被阻塞了。
三、对于超时的任务,最好closeGatt,下次重新连接的时候重开一个gatt。
四、蓝牙连接可能不稳定,最好支持失败自动重试机制,尤其是连接和发现服务,因为80%的问题都发生在建立连接和发现服务的时候,而且这一块也是最耗时的。
五、当连接建立后,可以由设备端发起更改连接间隔,这样能加快后续发现服务以及数据读写的速度。有的手机discover service很慢,原因是connect interval太大了,有的手机会主动向设备发起更改connect interval,而有的手机却不会。这样的话connect interval相差就会很大,实践中发现有的手机是7ms,有的手机是默认的50ms,所以发现service都要8s,甚至20s的都很寻常,这对用户来说是无法忍受的。所以比较好的办法是设备主动发起更改connect interval
六、同一个设备的所有操作最好都放在同一个线程串行执行,最好不要放在UI线程,虽然这些操作都是异步的,理论上来说不会耗时,但是由于涉及到跨进程,还是有可能出现ANR。另外不建议每个设备都开一个线程,设备多了会费内存也会降低性能。较好的做法是开一个线程,所有设备的操作都在该线程中发起,虽然占用同一个线程,但是每个设备各自维护自己的任务队列。
七、蓝牙操作都是异步的,回调通常都在binder线程里执行,因为这是跨进程回调回来的。一定要注意到这一点,否则会出现一些奇怪的问题。比如writeCharacter在工作线程,但是onCharacterWrite是在binder线程,回调里如果涉及到任务队列的调度一定要post回工作线程中处理,否则会出现多线程造成的数据不一致问题。
八、接着第七条,在回调中post回工作线程处理时要注意不要带句柄,而是要带数据。比如对于onCharacteristicWrite,不要带BluetoothGattCharacteristic,而是要带其中的value。否则会漏掉一些数据,且最后可能收到一组重复的数据。同样的问题在notify上也有。
九、当设备固件升级后,profile可能发生了变化,然而下次discover service的时候还是返回的旧的缓存,这样读写character可能会失败。解决办法是固件升级后,下次连接时刷新一下该设备的缓存。当然,重启蓝牙也会刷新缓存,不过会影响到所有设备。另外有时候discoverService服务发现的不全,或者根本发现不了服务,也可以考虑清除一下缓存。
十、有的手机上discoverService可能会回调不止一次onServiceDiscover,这个要注意防御。
十一、service不要缓存,虽然uuid什么的可能都没变,但是这些service都会和gatt关联的,如果gatt变了,那service就报废了,对这些service和character做任何读写操作都会出错。所以建议每次连接上时都去discover service,不要缓存。
十二、固件升级通常是写设备,为加快写速度,可以在write character时指定no response标志,实践发现速度可以提升2~3倍。不过要注意的是即便带了no response标志,也不代表这种写操作是没有回调的,我们仍然要遵循收到上一次写回调后才能进行下一次写操作。提升写速度的手段还有更改MTU和更改连接间隔,不过更改MTU硬件不一定支持,得尝试几次。
十三、写的时候不要指定writeType,不指定writeType不代表writeType就是WRITE_TYPE_DEFAULT,事实上系统会自动根据property来决定writeType,如果带PROPERTY_WRITE_NO_RESPONSE属性,则会自动选择WRITE_TYPE_NO_RESPONSE,否则才会选择WRITE_TYPE_DEFAULT。
十四、打开/关闭character的notify,必须等收到onDescriptorWrite回调之后才算结束,才能开始下一个任务。
十五、设备的gatt在不用时要及时关闭,系统支持的连接句柄数是有限的,当达到上限后无法再建立新的连接了。
十六、当连接断开后要调closeGatt释放资源,不用调disconnect,也不要下次复用之前的gatt来reconnect,因为有的手机上重连可能会存在问题,比如重连后死活发现不了service。这种情况下,最好只要断开连接就close gatt,下次连接时打开全新的gatt,这样就可以发现service了。
使用
使用起来非常简单,添加一行依赖,然后创建一个全局单例BluetoothClient,详细接口调用方法参考上面的链接。
1、在build.gradle中添加依赖
compile 'com.inuker.bluetooth:library:1.3.8'
2、创建一个BluetoothClient,最好做成单例全局使用
BluetoothClient mClient = new BluetoothClient(context);
使用到的一些技术
**一、实现了一个
完整的跨进程异步任务队列,支持任务超时、出错重试及防御队列溢出**
二、拦截并Hook系统层蓝牙Binder,实现对所有蓝牙设备通信的监控,当同时连接设备数过多时会自动断掉活跃度最低的设备
三、整个框架封装在一个service中,可灵活指定service所在进程。
四、屏蔽了接口异步回调可能持有调用端Activity引用导致的内存泄露
五、利用动态代理自动将所有操作封闭在工作线程,所以整个框架无锁