Xamarin Android监听蓝牙时如何避免主线程阻塞并更新UI
问题根因
当前代码存在三个核心问题:
- 虽然已经将阻塞的
Accept()调用放到子线程执行,但子线程直接执行_data.Text = dat操作UI控件,违反Android UI单线程模型规则,轻则界面不刷新,重则直接抛出CalledFromWrongThreadException导致崩溃。 - 现有监听逻辑没有停止机制、没有异常捕获、没有资源释放逻辑,开关关闭后无法终止后台监听,连接断开后会出现蓝牙资源泄漏。
- 现有逻辑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
相关产品推荐
相关产品推荐

