返回

Android蓝牙连接错误read failed处理及ANR避免

Android

蓝牙连接错误 "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" 错误的原因可能有多种, 通常可以归结为以下几类:

  1. 对端设备未准备好: 尝试连接的蓝牙设备上,对应的蓝牙服务(例如,RFCOMM 服务)尚未启动或正在启动中,导致连接无法立即建立。

  2. 蓝牙未开启或不可用: 本地设备或对端设备的蓝牙功能未启用,或者设备正处于飞行模式等状态,导致蓝牙功能不可用。

  3. 信号弱或干扰: 蓝牙设备之间的距离过远,或者周围存在强烈的电磁干扰,导致连接信号不稳定,连接尝试超时。

  4. 设备配对问题: 两个蓝牙设备之间没有正确配对,或者配对信息已过期或失效。

  5. Socket 资源泄漏: 如果 BluetoothSocket 对象在使用后没有被正确关闭,可能会导致资源泄漏。Android 系统可能会尝试自动关闭这些未关闭的资源,从而在 Logcat 中出现 "A resource failed to call close." 警告。这可能会间接导致 UI 冻结和 ANR 崩溃。

  6. UUID 不匹配: 客户端和服务端使用的 UUID 不一致,会导致连接失败。

  7. 并发问题 :在多线程环境中不正确地操作 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) {
          ...
}
  • 安全建议: 在循环外初始化 clientSocketnull。关闭 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。可以使用 AsyncTaskThread+HandlerExecutorService

  • 原理: 耗时操作在后台执行, 不影响 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。 记住要仔细处理每一个步骤, 特别是关闭资源和异步操作, 才能写出稳定高效的蓝牙应用。