Android蓝牙连接错误read failed处理及ANR避免
2025-03-19 18:11:51
蓝牙连接错误 "read failed, socket might closed or timeout, read ret: -1" 的正确处理
开发点对点蓝牙应用时,经常会遇到连接另一台设备上自身应用的情况。有时候,对方的应用程序实例可能尚未启动,需要捕获 connect()
抛出的 IOException
并在稍后重试(在独立线程中)。以下是一个代码片段例子:
BluetoothSocket clientSocket = getBluetoothDevice().createRfcommSocketToServiceRecord(MY_UUID);
while(!connected) {
try {
Log.d(APP_NAME, "About to connect");
// Connect to the remote server. If the server is not available
// an exception is thrown
clientSocket.connect();
connected = true;
Log.d(APP_NAME, "Connected");
} catch (IOException connectException) {
Log.d(APP_NAME, "Failed to connect: " + connectException.getMessage()); // 错误消息
try {
Thread.sleep(sleepTime);
} catch (InterruptedException ignored) {}
}
}
if (connected) {
...
}
报错消息是read failed, socket might closed or timeout, read ret: -1
,这个提示语法不对,但更重要的是,它完全没有说清楚真正的错误原因以及该如何应对。在 Android Studio 中运行应用程序时,Logcat 中可能会出现如下信息:
W A resource failed to call close.
我推测这些警告和上面的错误有关。官方文档并没有说明在这种错误发生后,BluetoothSocket
是否仍然可用。我目前的假设是可以用的,而且程序看上去也能正常运行 —— 当远端应用激活后,连接也能成功建立。 但或许应该关闭并重新创建 socket
?
虽然没有其他的错误信息,但应用 UI 最终会卡住,并出现 ANR 崩溃。我觉得还是和上述问题有联系。 请大伙帮忙看一看。
错误原因分析
出现 "read failed, socket might closed or timeout, read ret: -1" 错误的原因可能有多种, 通常可以归结为以下几类:
-
对端设备未准备好: 尝试连接的蓝牙设备上,对应的蓝牙服务(例如,RFCOMM 服务)尚未启动或正在启动中,导致连接无法立即建立。
-
蓝牙未开启或不可用: 本地设备或对端设备的蓝牙功能未启用,或者设备正处于飞行模式等状态,导致蓝牙功能不可用。
-
信号弱或干扰: 蓝牙设备之间的距离过远,或者周围存在强烈的电磁干扰,导致连接信号不稳定,连接尝试超时。
-
设备配对问题: 两个蓝牙设备之间没有正确配对,或者配对信息已过期或失效。
-
Socket 资源泄漏: 如果
BluetoothSocket
对象在使用后没有被正确关闭,可能会导致资源泄漏。Android 系统可能会尝试自动关闭这些未关闭的资源,从而在 Logcat 中出现 "A resource failed to call close." 警告。这可能会间接导致 UI 冻结和 ANR 崩溃。 -
UUID 不匹配: 客户端和服务端使用的 UUID 不一致,会导致连接失败。
-
并发问题 :在多线程环境中不正确地操作
BluetoothSocket
对象,导致出现并发的问题。
解决方案
针对上述问题,以下是几种解决方案和对应的实现步骤:
1. 重试连接 + 关闭 Socket
这种方法会在捕获 IOException
后,关闭当前的 clientSocket
,并创建一个新的 clientSocket
来进行重试,以此来处理 socket 出现问题,不能复用的场景。
-
原理: 发生连接错误后,可能出现 Socket 内部状态不一致。关闭并重新创建可以保证 Socket 处于干净、可用的状态。
-
代码示例:
BluetoothSocket clientSocket = null; // 初始化为 null
while(!connected) {
try {
Log.d(APP_NAME, "About to connect");
// 每次循环都创建一个新的 Socket
if (clientSocket == null) {
clientSocket = getBluetoothDevice().createRfcommSocketToServiceRecord(MY_UUID);
}
clientSocket.connect();
connected = true;
Log.d(APP_NAME, "Connected");
} catch (IOException connectException) {
Log.d(APP_NAME, "Failed to connect: " + connectException.getMessage());
// 关闭当前 Socket
if (clientSocket != null) {
try {
clientSocket.close();
} catch (IOException closeException) {
Log.e(APP_NAME, "Failed to close socket", closeException);
}
clientSocket = null; // 置为 null,以便下一次循环创建新的
}
try {
Thread.sleep(sleepTime);
} catch (InterruptedException ignored) {}
}
}
if (connected) {
...
}
- 安全建议: 在循环外初始化
clientSocket
为null
。关闭Socket
后也将其设置为null
, 这样下一次循环就能创建一个新的Socket
对象。 错误处理中也要关闭Socket
。
2. 检查蓝牙状态和配对
在尝试连接之前,应确保蓝牙已打开并处于可发现模式,并已完成正确的设备配对。
-
原理: 确保基本的蓝牙环境已就绪。
-
代码示例 (部分):
// 检查蓝牙是否开启
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
// 设备不支持蓝牙
Log.e(APP_NAME, "Device does not support Bluetooth");
return; // 退出
}
if (!bluetoothAdapter.isEnabled()) {
// 蓝牙未开启,请求用户开启蓝牙
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
return; // 退出当前流程,等待用户操作
}
// 检查是否已配对
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(deviceAddress);
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
// 未配对,开始配对
device.createBond();
return; // 退出,等待配对完成的广播
}
// 开始连接 (可以在另一个地方执行上面的连接代码)
- 安全建议: 处理
startActivityForResult
的结果,并根据用户的选择继续操作或给出提示。正确处理配对请求的结果 (使用BroadcastReceiver
监听BluetoothDevice.ACTION_BOND_STATE_CHANGED
广播)。
3. 指数退避重试
不要每次都立即重试,采用指数退避策略,逐步增加重试间隔,避免过于频繁的连接尝试导致系统资源耗尽。
-
原理: 减少对目标设备的连接频率,给目标设备和网络环境更多恢复时间。
-
代码示例:
long sleepTime = 1000; // 初始等待时间 (1 秒)
final long maxSleepTime = 30000; // 最大等待时间 (30 秒)
BluetoothSocket clientSocket = null;
while (!connected) {
try {
if (clientSocket == null) {
clientSocket = getBluetoothDevice().createRfcommSocketToServiceRecord(MY_UUID);
}
clientSocket.connect();
connected = true;
Log.d(APP_NAME, "Connected");
sleepTime = 1000;//重置等待的时间
} catch (IOException connectException) {
Log.d(APP_NAME, "Failed to connect: " + connectException.getMessage());
if (clientSocket != null) {
try {
clientSocket.close();
} catch (IOException closeException) {
Log.e(APP_NAME, "Failed to close socket", closeException);
}
clientSocket = null;
}
try {
Log.d(APP_NAME, "Waiting " + sleepTime + "ms before retrying");
Thread.sleep(sleepTime);
} catch (InterruptedException ignored) {}
// 指数退避: 等待时间翻倍,但不超过最大值
sleepTime = Math.min(sleepTime * 2, maxSleepTime);
}
}
if(connected){
...
}
4. 使用异步任务
将蓝牙连接操作放在后台线程中执行,防止阻塞主线程,避免 ANR。可以使用 AsyncTask
、Thread
+Handler
或 ExecutorService
。
-
原理: 耗时操作在后台执行, 不影响 UI 线程。
-
代码示例 (使用 ExecutorService):
private ExecutorService executor = Executors.newSingleThreadExecutor(); //单线程
// ...
executor.execute(new Runnable() {
@Override
public void run() {
// 这里执行上述的连接代码 (包含循环、重试等逻辑)
// ...
BluetoothSocket clientSocket = null;
while (!connected) { //注意这里的connected应该是volatile
try {
if (clientSocket == null) {
clientSocket = getBluetoothDevice().createRfcommSocketToServiceRecord(MY_UUID);
}
clientSocket.connect();
connected = true;
Log.d(APP_NAME, "Connected");
} catch (IOException connectException) {
Log.d(APP_NAME, "Failed to connect: "+ connectException.getMessage());
// 关闭当前 Socket
if(clientSocket !=null) {
try{
clientSocket.close();
}catch(IOException closeException){
Log.e(APP_NAME, "Failed to close socket",closeException);
}
clientSocket = null; //close 后设置为null.
}
try{
Thread.sleep(sleepTime); //注意捕获中断异常
}catch (InterruptedException ignored){}
}
if(connected){
//连接建立成功
// 使用 Handler 更新UI,发送连接成功的消息。
// 不能直接在子线程中操作 UI
handler.post(new Runnable(){
@Override
public void run() {
//处理连接后的UI更新等
}
});
}
}
});
- 进阶: 如果连接成功,
connected
变量应该是用volatile
修饰的,以确保可见性. - 如果你的Activity可能在这个后台任务还未完成前就被销毁,需要关闭
ExecutorService
.
@Override
protected void onDestroy(){
super.onDestroy();
executor.shutdownNow(); // 尝试立即停止
}
5. UUID 检查
-
原理: 确保客户端与服务器使用相同的 UUID,避免因 UUID 不匹配而无法连接。
-
操作步骤:
1.仔细检查客户端代码和服务器代码中的UUID定义。
2.确保UUID字符串完全相同。//在程序中使用同一个My_UUID对象. private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); //示例
通过采用以上几种解决方案,你应该可以更加稳定可靠地处理蓝牙连接,解决 "read failed, socket might closed or timeout, read ret: -1" 错误,并且避免 UI 冻结和 ANR。 记住要仔细处理每一个步骤, 特别是关闭资源和异步操作, 才能写出稳定高效的蓝牙应用。