You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Xamarin Android监听蓝牙时如何避免主线程阻塞并更新UI

问题根因

当前代码存在三个核心问题:

  1. 虽然已经将阻塞的Accept()调用放到子线程执行,但子线程直接执行_data.Text = dat操作UI控件,违反Android UI单线程模型规则,轻则界面不刷新,重则直接抛出CalledFromWrongThreadException导致崩溃。
  2. 现有监听逻辑没有停止机制、没有异常捕获、没有资源释放逻辑,开关关闭后无法终止后台监听,连接断开后会出现蓝牙资源泄漏。
  3. 现有逻辑Accept到一个连接后就会结束监听流程,无法持续接收后续设备的连接请求。
修改方案

1. 重构SocketListener监听类

不要在工作线程中持有TextView等UI控件的强引用,改为通过回调的方式向外传递接收到的消息和连接状态,同时添加停止监听方法、异常捕获和资源释放逻辑:

public class SocketListener : Thread
{
    private BluetoothServerSocket _serverSocket;
    private Action<string> _onMessageReceived;
    private Action<BluetoothSocket> _onDeviceConnected;
    private bool _isListening = false;

    // 构造函数传入回调,不直接持有UI控件引用,避免内存泄漏
    public SocketListener(BluetoothServerSocket socket, Action<string> onMessageReceived, Action<BluetoothSocket> onDeviceConnected = null)
    {
        _serverSocket = socket;
        _onMessageReceived = onMessageReceived;
        _onDeviceConnected = onDeviceConnected;
    }

    // 供外部调用的停止监听方法
    public void StopListening()
    {
        _isListening = false;
        try
        {
            _serverSocket?.Close();
        }
        catch { }
    }

    public override void Run()
    {
        _isListening = true;
        BluetoothSocket clientSocket = null;
        Stream inputStream = null;
        StreamReader reader = null;
        try
        {
            // 阻塞等待客户端连接
            clientSocket = _serverSocket.Accept();
            // 触发设备连接成功回调
            _onDeviceConnected?.Invoke(clientSocket);

            inputStream = clientSocket.InputStream;
            reader = new StreamReader(inputStream);
            string receivedContent = "";
            string line;

            // 持续读取客户端发送的消息
            while (_isListening && (line = reader.ReadLine()) != null)
            {
                receivedContent += line;
                // 收到消息后触发回调,实时推送接收到的内容
                _onMessageReceived?.Invoke(receivedContent);
            }
        }
        catch (Java.IO.IOException ex)
        {
            // 主动调用Close停止监听时会触发该异常,属于正常流程无需额外处理
            Log.Debug("BluetoothListener", "Listening stopped: " + ex.Message);
        }
        catch (Exception ex)
        {
            Log.Error("BluetoothListener", "Listen error: " + ex.Message);
        }
        finally
        {
            // 统一释放所有占用资源
            reader?.Dispose();
            inputStream?.Dispose();
            clientSocket?.Close();
            _serverSocket?.Close();
            _isListening = false;
        }
    }
}

2. 修改Activity层的开关逻辑

在Activity中声明监听线程的全局引用,方便开关关闭时终止监听;收到子线程的消息回调时,通过RunOnUiThread将UI更新操作调度到主线程执行,避免子线程直接操作控件:

// 在Activity类内部声明全局变量,不要定义在点击事件内部
private SocketListener _bluetoothListener;
// 如果你还没有初始化_serverSocket,需要通过BluetoothAdapter的ListenUsingRfcommWithServiceRecord方法创建
// 示例:_serverSocket = _adapter.ListenUsingRfcommWithServiceRecord("BluetoothService", Java.Util.UUID.FromString("替换为你自己的自定义UUID"));
private BluetoothServerSocket _serverSocket;

// Switch点击事件逻辑
server_switch.Click += (o, e) =>
{
    Switch myswitch = o as Switch;
    if (myswitch.Checked)
    {
        Log.Debug("Check Status Enabled", myswitch.Checked.ToString());
        // 蓝牙可见性配置逻辑和原有逻辑保持一致
        switch (_adapter.ScanMode)
        {
            case ScanMode.Connectable:
                MakeVisibleFor300Seconds();
                break;
            case ScanMode.None:
                MakeVisibleFor300Seconds();
                break;
            case ScanMode.ConnectableDiscoverable:
                Log.Debug("Adapter Mode",
                    "Connectable Discoverable:" +
                    (_adapter.ScanMode == ScanMode.ConnectableDiscoverable).ToString());
                break;
        }

        // 初始化监听线程,传入消息接收回调
        _bluetoothListener = new SocketListener(_serverSocket, (receivedText) =>
        {
            // 所有UI操作必须通过RunOnUiThread切到主线程执行
            RunOnUiThread(() =>
            {
                display_data.Text = receivedText;
            });
        });
        _bluetoothListener.Start();
    }
    else
    {
        Log.Debug("Check Status Disabled", myswitch.Checked.ToString());
        // 关闭开关时主动停止监听,释放资源
        _bluetoothListener?.StopListening();
        _bluetoothListener = null;
    }
};

3. 注意事项

  • Android的UI控件只能在主线程操作,Xamarin.Android中除了RunOnUiThread,也可以用Handler.Post、SynchronizationContext.Post实现线程切换,RunOnUiThread是最简便的实现方式。
  • 蓝牙的阻塞调用(Accept()、流读取)必须支持主动取消,不要让工作线程长期在后台空跑,在Activity销毁、开关关闭时必须主动关闭Socket释放资源。
  • 如果需要支持多设备同时连接,可在Accept到一个客户端连接后,新开独立工作线程处理该客户端的消息读写,主监听线程立刻回到Accept状态等待下一个连接。
  • 记得在AndroidManifest.xml中声明蓝牙相关权限,Android 12及以上版本需要额外申请BLUETOOTH_CONNECT、BLUETOOTH_SCAN运行时权限,否则会抛出权限异常。

内容的提问来源于stack exchange,提问作者TechGeek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 22:01:24