本文转自冼东芝的文章Android BLE4.+ 蓝牙开发国产手机兼容性解决方案。
如有版权问题,请私信,谢谢。
转载请注明出处。https://blog.csdn.net/u014418171/article/details/81219297
算是做了n年的智能穿戴BLE开发了, 首先对国内的安卓开发者提醒下 , BLE开发是真的很坑, 特别是安卓, ios端也坑, 但没安卓坑 因为国产有很多手机 各种奇葩兼容都有,
其实这些方案我很早就写到云笔记里了,一直没公开, 这里的解决方案大部分都是网上搜不到 或者网上搜到类似的问题, 但回复基本上是回答[无法解决] 或 [重启手机解决]等 没意义的解决办法,让我很无语…
以下内容可能涉及到各种系统类源码
你可以通过这里阅读 https://www.androidos.net.cn/
废话不多说, 希望对你们有用
一.刷新蓝牙app的状态
问题描述:
某些手机用久了会出现扫描不到任何设备的bug,
此时是因为手机误认为本app不是[ble类] app , f**k!!!!! 还有这种操作???
但值得注意的是, 这只是一种原因,[ 扫描不到任何设备的bug] 有很多种原因, 详情请看第3点
解决方案:
[目前网上没有与我类似的解决办法, 所以具体副作用自测]
参考IBluetoothManager.aidl
系统源码
出现该问题时于是通过查看系统源码找到isBleAppPresent
方法 ,反射调用其后居然返回false ,
换了一台能正常使用的手机 调用该方法 返回true 因此证实了这个问题,
然后发现系统有私有的updateBleAppCount
方法, 可以刷新ble类app的状态,反射调用之…
因此解决了 [偶尔ble设备扫描不出来]的bug),
通过传入你的app包名 以 刷新 蓝牙app的错误状态
public static void refreshBleAppFromSystem(Context context, String packageName) {
//6.0以上才有该功能,不是6.0以上就算了
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
return;
}
if (!adapter.isEnabled()) {
return;
}
try {
Object mIBluetoothManager = getIBluetoothManager(adapter);
Method isBleAppPresentM = mIBluetoothManager.getClass().getDeclaredMethod("isBleAppPresent");
isBleAppPresentM.setAccessible(true);
boolean isBleAppPresent = (Boolean) isBleAppPresentM.invoke(mIBluetoothManager);
if (isBleAppPresent) {
return;
}
Field mIBinder = BluetoothAdapter.class.getDeclaredField("mToken");
mIBinder.setAccessible(true);
Object mToken = mIBinder.get(adapter);
//刷新偶尔系统无故把app视为非 BLE应用 的错误标识 导致无法扫描设备
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//8.0+ (部分手机是7.1.2 也是如此)
Method updateBleAppCount = mIBluetoothManager.getClass().getDeclaredMethod("updateBleAppCount", IBinder.class, boolean.class, String.class);
updateBleAppCount.setAccessible(true);
//关一下 再开
updateBleAppCount.invoke(mIBluetoothManager, mToken, false, packageName);
updateBleAppCount.invoke(mIBluetoothManager, mToken, true, packageName);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
//6.0~7.1.1
Method updateBleAppCount = mIBluetoothManager.getClass().getDeclaredMethod("updateBleAppCount", IBinder.class, boolean.class);
updateBleAppCount.setAccessible(true);
//关一下 再开
updateBleAppCount.invoke(mIBluetoothManager, mToken, false);
updateBleAppCount.invoke(mIBluetoothManager, mToken, true);
} catch (NoSuchMethodException e) {
//8.0+ (部分手机是7.1.2 也是如此)
try {
Method updateBleAppCount = mIBluetoothManager.getClass().getDeclaredMethod("updateBleAppCount", IBinder.class, boolean.class, String.class);
updateBleAppCount.setAccessible(true);
//关一下 再开
updateBleAppCount.invoke(mIBluetoothManager, mToken, false, packageName);
updateBleAppCount.invoke(mIBluetoothManager, mToken, true, packageName);
} catch (NoSuchMethodException e1) {
e1.printStackTrace();
}
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
二.明明gatt.disconnect() 断开蓝牙了,甚至关闭了手机的蓝牙,甚至飞行模式了, 设备仍然在 [已连接] 状态!!! 设备离手机远了才断开,说明,这压根就没断开啊!
看到标题知道国产手机奇葩了吧? 而且我相信不少人都遇到这个问题, 这个问题经常出现在华为>小米>魅族>(VIVO|OPPO) 上。
问题描述
首先导致这个原因可能是:
- 操作:
gatt.disconnect/connect
断开,连接,断开,连接,断开… 反复重试n次, 有一定的几率导致系统残留了该gatt的引用, app这边没有拿到这个引用 (app操作蓝牙api是通过 remote aidl 操作远程的 系统service ), 系统蓝牙app 也没有了这个引用 ,于是即使你 直接关闭手机蓝牙, 也没有断开连接… 有些手机直到开启飞行模式 才会断开, 而有些手机 即使开启飞行模式也不会断开! 这得看这些手机 的 飞行模式 的实现代码的区别了,暂时没去研究. - 猜测连接设备后 被系统杀掉/ 或手动杀掉 也会导致这种情况
解决方案
1.首先你可以获取真正的连接状态:
[目前网上没有与我类似的解决办法, 所以具体副作用自测]
参看IBluetooth.aidl
系统源码
出现这种假断开问题时, 笔者曾经尝试 各种gatt.getConnectionState()
,BluetoothManager.getConnectionState
都是 给我们开发者返回 已断开!, 但实际上没有断开, 经过一番研究后 发现 判断内部连接状态可以通过另一个办法 而不通过 gatt,
则BluetoothDevice
类 内部的isConnected()
方法
这个方法被标记为@SystemApi
和@hide
, 不能直接使用.
并且在低版本的手机上没有, 查看了源码isConnected
是由IBluetooth.getConnectionState()
实现的, 低版本有getConnectionState
:
@SystemApi
public boolean isConnected() {
final IBluetooth service = sService;
if (service == null) {
// BT is not enabled, we cannot be connected.
return false;
}
try {
return service.getConnectionState(this) != CONNECTION_STATE_DISCONNECTED;
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
}
}
于是我们可以通过反射
BluetoothDevice.isConnected/ IBluetooth .getConnectionState
实现内部连接状态的判断
高低版本兼容的代码如下:
public static final int CONNECTION_STATE_DISCONNECTED = 0;
public static final int CONNECTION_STATE_CONNECTED = 1;
public static final int CONNECTION_STATE_UN_SUPPORT = -1;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@SuppressLint("PrivateApi")
public static int getInternalConnectionState(String mac) {
//该功能是在21 (5.1.0)以上才支持, 5.0 以及以下 都 不支持
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return CONNECTION_STATE_UN_SUPPORT;
}
if(Build.MANUFACTURER.equalsIgnoreCase("OPPO")){//OPPO勿使用这种办法判断, OPPO无解
return CONNECTION_STATE_UN_SUPPORT;
}
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
BluetoothDevice remoteDevice = adapter.getRemoteDevice(mac);
Object mIBluetooth = null;
try {
Field sService = BluetoothDevice.class.getDeclaredField("sService");
sService.setAccessible(true);
mIBluetooth = sService.get(null);
} catch (Exception e) {
return CONNECTION_STATE_UN_SUPPORT;
}
if (mIBluetooth == null) return CONNECTION_STATE_UN_SUPPORT;
boolean isConnected;
try {
Method isConnectedMethod = BluetoothDevice.class.getDeclaredMethod("isConnected");
isConnectedMethod.setAccessible(true);
isConnected = (Boolean) isConnectedMethod.invoke(remoteDevice);
isConnectedMethod.setAccessible(false);
} catch (Exception e) {
//如果找不到,说明不兼容isConnected, 尝试去使用getConnectionState 判断
try {
Method getConnectionState = mIBluetooth.getClass().getDeclaredMethod("getConnectionState", BluetoothDevice.class);
getConnectionState.setAccessible(true);
int state = (Integer) getConnectionState.invoke(mIBluetooth, remoteDevice);
getConnectionState.setAccessible(false);
isConnected = state == CONNECTION_STATE_CONNECTED;
} catch (Exception e1) {
return CONNECTION_STATE_UN_SUPPORT;
}
}
return isConnected ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED;
}
2.尝试断开
[目前网上没有与我类似的解决办法, 所以具体副作用自测]
参考AdapterService.java
系统源码
仍然是从IBluetooth
入手, 因为 应用层 能拿到的东西不多
研究源码发现 有两个函数可以尝试让 ble 服务关闭和启动,分别是onLeServiceUp / onBrEdrDown
示例:
@RequiresApi(api = Build.VERSION_CODES.M)
public static void setLeServiceEnable(boolean isEnable) {
Object mIBluetooth;
try {
Field sService = BluetoothDevice.class.getDeclaredField("sService");
sService.setAccessible(true);
mIBluetooth = sService.get(null);
} catch (Exception e) {
return;
}
if (mIBluetooth == null) return;
try {
if (isEnable) {
Method onLeServiceUp = mIBluetooth.getClass().getDeclaredMethod("onLeServiceUp");
onLeServiceUp.setAccessible(true);
onLeServiceUp.invoke(mIBluetooth);
} else {
Method onLeServiceUp = mIBluetooth.getClass().getDeclaredMethod("onBrEdrDown");
onLeServiceUp.setAccessible(true);
onLeServiceUp.invoke(mIBluetooth);
}
} catch (Exception e) {
e.printStackTrace();
}
}
但该方法可能在某些手机上仍然无效,原因是 很多国产手机都重新修改了 蓝牙底层相关代码, 为了所谓的省电, 所以单靠看原生系统的源码可能是无意义的
之后琢磨出另一个解决办法 那就是…. 尝试连接~然后断开!
方法很简单,直接通过gatt.connectGatt()
等待连接成功后disconnect
一次, 此时设备终于断开了!
原因可能是connect
后 刷新了残留的gatt引用 于是app又重新拿到了最新的引用, 此时可以操作设备断开了
...
gatt.connectGatt();
...
onConnectionStateChange(final BluetoothGatt gatt, final int status, int newState){
if (newState == BluetoothProfile.STATE_CONNECTED){
gatt.disconnect();
}
}
不过你要注意下不要和你的正常连接逻辑冲突
以上操作,手机显示的蓝牙图标一直是关闭的, 你可能想问我 : 那手机蓝牙关了 怎么反射让他显示开… 这个你只能问这个手机的相关工程师为啥这么脑残了… 无解, 我们只考虑app问题,系统脑残管不了
三.多次打开app/退出app/后台被杀等, 导致扫描不到设备,并返回ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED 错误!
问题描述
但值得注意的是, 这只是第二种原因,[ 扫描不到任何设备的bug]还有其他原因, 详情请看第4点
扫描周围的BLE设备时某些手机会遇到 GATT_Register: cant Register GATT client, MAX client reached!
或者回调中的 onScanFailed 返回了 errorCode =2 则: ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED
具体表现则为 明明周围有很多设备,但是扫描不到任何东西
查到
1.https://blog.csdn.net/chy555chy/article/details/53788748
2.https://stackoverflow.com/questions/27516399/solution-for-ble-scans-scan-failed-application-registration-fail
3.http://detercode121.blogspot.com/2012/04/bluetooth-lowenergy-solution-for-ble.html
等等 没有一个是正常的解决办法,上面这些修复方案是用代码实现关闭蓝牙然后重新打开蓝牙来释放 可是 国产的手机会弹出蓝牙授权的 比如我们要后台扫描重连设备时遇到这种情况 难道要弹出授权让用户确定? 那还要后台重连功能干啥…
而且,有些手机即使关闭蓝牙再打开 也无法释放,有些手机关闭蓝牙后 再打开会卡死系统, 导致蓝牙图标一直卡在那很久 才打开了蓝牙…
解决方案
[目前网上没有与我类似的解决办法, 所以具体副作用自测]
参考IBluetoothGatt.aidl
参考BluetoothLeScanner.java
参考ScanManager.java
参考GattService.java
问题就在于 一些手机在startScan
扫描的过程中还没来得及stopScan
,就被系统强制杀掉了, 导致mClientIf未被正常释放,实例和相关蓝牙对象已被残留到系统蓝牙服务中,
打开app后又重新初始化ScanCallback
多次被注册,导致每次的扫描mClientIf的值都在递增, 于是mClientIf的值
在增加到一定程度时(最大mClientIf数量视国产系统而定 不做深究),onScanFailed
返回了errorCode =2
至今网上无任何正常的解决办法
于是 我查看了系统源码 发现关键位置BluetoothLeScanner
类下的 BleScanCallbackWrapper#startRegistration()
扫描是通过registerClient
传入 mClientIf 来实现的,
在stopScan
时调用了iGatt.stopScan()
和iGatt.unregisterClient()
进行解除注册. 了解该原理后 我们就可以反射调用这个方法 , 至于解除mClientIf哪个值 需要你自己做存储记录
这里我写的是解除全部客户端 mClientIf的范围是 0~40
问题至此完美解决 这可能是目前全网唯一不用关闭/开启蓝牙就能完美解决该问题的方案
public static boolean releaseAllScanClient() {
try {
Object mIBluetoothManager = getIBluetoothManager(BluetoothAdapter.getDefaultAdapter());
if (mIBluetoothManager == null) return false;
Object iGatt = getIBluetoothGatt(mIBluetoothManager);
if (iGatt == null) return false;
Method unregisterClient = getDeclaredMethod(iGatt, "unregisterClient", int.class);
Method stopScan;
int type;
try {
type = 0;
stopScan = getDeclaredMethod(iGatt, "stopScan", int.class, boolean.class);
} catch (Exception e) {
type = 1;
stopScan = getDeclaredMethod(iGatt, "stopScan", int.class);
}
for (int mClientIf = 0; mClientIf <= 40; mClientIf++) {
if (type == 0) {
try {
stopScan.invoke(iGatt, mClientIf, false);
} catch (Exception ignored) {
}
}
if (type == 1) {
try {
stopScan.invoke(iGatt, mClientIf);
} catch (Exception ignored) {
}
}
try {
unregisterClient.invoke(iGatt, mClientIf);
} catch (Exception ignored) {
}
}
stopScan.setAccessible(false);
unregisterClient.setAccessible(false);
BLESupport.getDeclaredMethod(iGatt, "unregAll").invoke(iGatt);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
其中如果你想获得 mClientIf 的值,方便研究该问题 可以尝参考以下代码
其中参数ScanCallback
类 是安卓6.0扫描回调类
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static boolean isScanClientInitialize(ScanCallback callback) {
try {
Field mLeScanClientsField = getDeclaredField(BluetoothLeScanner.class, "mLeScanClients");
// HashMap<ScanCallback, BleScanCallbackWrapper>()
HashMap callbackList = (HashMap) mLeScanClientsField.get(BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner());
int size = callbackList == null ? 0 : callbackList.size();
if (size > 0) {
Iterator iterator = callbackList.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
Object key = entry.getKey();
Object val = entry.getValue();
if (val != null && key != null && key == callback) {
int mClientIf = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Field mScannerIdField = getDeclaredField(val, "mScannerId");
mClientIf = mScannerIdField.getInt(val);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Field mClientIfField = getDeclaredField(val, "mClientIf");
mClientIf = mClientIfField.getInt(val);
}
System.out.println("mClientIf=" + mClientIf);
return true;
}
}
} else {
if (callback != null) {
return false;
}
}
} catch (Exception ignored) {
}
return true;
}
四、扫描不到设备
前面说了好几个扫描不到设备的原因, 这里还有呢…
1.未开启位置访问权限Manifest.permission.ACCESS_COARSE_LOCATION
如果你是6.0系统 则需要申请该权限 才能扫描设备, 检查和申请网上有 这里不重复说了
2.检查GPS的 LOCATION_MODE
是否开启,否则在OPPO/VIVO等手机 无法扫描设备。
//代码 反编译 nrfconnect 参考得来:
public static boolean hasLocationEnablePermission(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
int locationMode = Settings.Secure.LOCATION_MODE_OFF;
try {
locationMode = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE);
} catch (Exception ignored) {
}
if (locationMode != Settings.Secure.LOCATION_MODE_OFF) {
return true;
}
return false;
}
//没有权限则跳转到 gps界面授权
if(!hasLocationEnablePermission(this)){
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
3.安卓7.0不允许在30s内连续扫描5次,否则无法扫描到任何设备,只能重启app, 你可以写一个算法 比如每次先延时30/5=6秒 才开始扫描, 以防止用户一直点扫描按钮 , 或者使用动态计算 以减少用户等待时间
五、其他注意点
1.为了加快连接设备的速度 , 你可以不扫描设备直接通过mac地址连接 ,使用 gatt.connectGatt ,但有时候连接不上是因为 设备信息可能变化了, 但系统缓存没变,所以一直连接不上, 即使连接上了 马上返回各种-133 -192等错误, 解决办法是 你需要 重新扫描这个mac一下,找到了mac ,再连接
2.扫描设备的时候 要切记, 扫描到了后 先停止扫描, 过1秒左右 再连接, 避免扫描的时候连接, 导致连接过程中 缓存再次被刷新
3.无任何原因, app扫描不到该设备,但能搜索到其他设备, 而另一个手机却都能搜索到, 试试下载 nrf的 nrfconnect 去搜索测试(同一台手机), 若 nrfconnect 能搜索到 则是app代码问题, 否则检测 设备蓝牙晶振频率 是否不支持该手机的蓝牙频率发现范围! 联系相关开发人员解决
4.距离防丢功能, 通过rssi可以拿到设备距离手机的信号值来判断 设备是否远离手机 触发防丢警报, 但rssi信号 受各种环境因素影响, 所以有点坑 , 建议做延迟处理 ,意思是达到防丢rssi值时 延时n秒 才警报, 若在n秒内 又恢复, 说明只是信号突然弱了一下. 无需 警报. 还有就是因素太多了 ,和手机的蓝牙模块有关, 和设备的蓝牙天线有关, 功率有关等, 建议在app内添加 一个用户可以设定的rssi防丢范围, 因为程序没法精准计算
5.扫描蓝牙设备callback回调时 建议丢到另外一个线程用队列去处理, 不要在扫描回调里处理耗时逻辑., 同理 在onCharacteristicChanged 中接收设备notify通知返回的数据时, 不要在此方法内进行耗时处理, 否则大量数据过来时会100%丢包!!! 解决办法和前面的一样. (话说某BLE开源框架就有这个问题,还好我用我自己写的)
6.同步大量数据时, 某些手机完美正常, 某些手机出现丢包严重,建议修改连接间隔 同步前 使用gatt.requestConnectionPriority(CONNECTION_PRIORITY_HIGH) , 同步完成后 恢复原来的连接间隔CONNECTION_PRIORITY_BALANCED
7.对设备进行OTA升级后,直接使用mac来连接, 连接不上, 原因是系统缓存没刷新, 你需要扫描后停止扫描再连接
8.连接之前建议先把 gatt.close一下
9.使用gatt.discoverServices()发现服务之前,建议先 sleep 500 毫秒, 因为刚刚连接上, 系统有些东西需要刷新,同理,遇到任何问题 延时一下看看能否解决, 因为有些系统的蓝牙很慢很卡,甚至手动关闭蓝牙 都卡死在那 ,偶尔还死机重启了…
六、补充
贴出一些上面缺失的函数,因为方便和减少代码重复量, 所以上面没贴
@SuppressLint("PrivateApi")
public static Object getIBluetoothGatt(Object mIBluetoothManager) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method getBluetoothGatt = getDeclaredMethod(mIBluetoothManager, "getBluetoothGatt");
return getBluetoothGatt.invoke(mIBluetoothManager);
}
@SuppressLint("PrivateApi")
public static Object getIBluetoothManager(BluetoothAdapter adapter) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Method getBluetoothManager = getDeclaredMethod(BluetoothAdapter.class, "getBluetoothManager");
return getBluetoothManager.invoke(adapter);
}
public static Field getDeclaredField(Class<?> clazz, String name) throws NoSuchFieldException {
Field declaredField = clazz.getDeclaredField(name);
declaredField.setAccessible(true);
return declaredField;
}
public static Method getDeclaredMethod(Class<?> clazz, String name, Class<?>... parameterTypes) throws NoSuchMethodException {
Method declaredMethod = clazz.getDeclaredMethod(name, parameterTypes);
declaredMethod.setAccessible(true);
return declaredMethod;
}
public static Field getDeclaredField(Object obj, String name) throws NoSuchFieldException {
Field declaredField = obj.getClass().getDeclaredField(name);
declaredField.setAccessible(true);
return declaredField;
}
public static Method getDeclaredMethod(Object obj, String name, Class<?>... parameterTypes) throws NoSuchMethodException {
Method declaredMethod = obj.getClass().getDeclaredMethod(name, parameterTypes);
declaredMethod.setAccessible(true);
return declaredMethod;
}
你可能还想问, 还有呢 还有最重要的 连接时总是返回 -133 -86 -192 这些怎么办啊 怎么解决啊
我只想和你说, 别抱着希望了 你可能在网上看到很多解决办法 但最终你使用了解决代码, 仍然无法解决…
放弃吧, 换一种思路, 遇到这种错误, 直接断开+sleep+扫描+重连, , 若检测到无数次返回这种错误,没一次连接成功的情况 记录下次数, 达到一定数量时 提示让用户关闭/开启飞行模式 然后重试吧. 这种因素很多 有手机蓝牙辣鸡的,有代码有问题的比如不扫描就连接, 有蓝牙设备有问题的 各种因素都有.
七、调试
调试设备的工具 有 ios的lightblue,
安卓的推荐nrf芯片公司开发的 nrf connect 调试工具]
我也写了两个小应用,有兴趣可以下来看看
BLE调试器
https://www.coolapk.com/apk/com.toshiba.ble
BLE指令协议窃取工具(需要xposed), 可以窃取手机上的某app和其对应的ble设备 正在进行的数据通讯, 你可以理解为蓝牙协议抓包 (仅供学习用途)
https://www.coolapk.com/apk/com.tos.bledetector
拓展阅读
如果您有更好的建议欢迎评论分享,如有错误,请批评指正,谢谢。
网友评论