ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity与Android蓝牙开发全流程:从AAR插件封装到跨平台通信实战

2026/8/10 15:57:58 拓冰建站 浏览量
Unity与Android蓝牙开发全流程:从AAR插件封装到跨平台通信实战 1. 项目概述为什么Unity与Android蓝牙开发是个“技术缝合怪”如果你是一个Unity开发者想在移动端实现一个蓝牙功能比如连接一个心率带、控制一个智能小车或者做一个多人联机的蓝牙小游戏你大概率会一头撞上“Unity与Android原生交互”这堵墙。Unity本身并不直接提供完整的、稳定的、跨平台的蓝牙API尤其是在Android这个碎片化极其严重的生态里。所以这个项目的核心本质上是一个“技术缝合”过程你需要用C#在Unity里写游戏逻辑用Java/Kotlin在Android Studio里写蓝牙通信的原生代码然后再用一套“桥接”机制让两者能互相喊话。这个过程从接口设计到广播接收每一步都充满了“坑”。今天我就以一个实际做过的智能硬件控制项目为例把从零到一的全流程包括那些官方文档不会告诉你的“暗坑”彻底拆解清楚。2. 核心思路与架构选型为什么选Android Java Bridge面对Unity调用Android蓝牙的需求通常有几种路径Unity的AndroidJavaClass/AndroidJavaObject直接调用、封装成Android AAR插件、或者使用第三方跨平台插件如Unity Bluetooth LE。我们的选择是基于Android Java Bridge (AJB) 封装AAR插件。这是最主流、最可控的方案。为什么这么选直接调用AndroidJavaClass太“裸奔”虽然Unity提供了AndroidJavaClass和AndroidJavaObject来直接调用Java代码但这意味着你的Unity脚本里会充斥着androidJavaClass.CallStatic这样的代码。蓝牙操作涉及生命周期、权限、回调逻辑复杂全部用这种字符串方法名调用代码难以维护、调试困难且极易因Android版本差异崩溃。第三方插件可能“水土不服”像一些开源的Unity蓝牙LE插件它们试图提供统一的C# API。但问题在于Android蓝牙的细节太多了不同手机厂商小米、华为、三星对蓝牙协议栈的实现有差异第三方插件很难覆盖所有边缘情况。当遇到特定手机连接不稳定、服务发现不全等问题时你几乎无法调试和修改插件底层代码。AAR插件是平衡点将所有Android蓝牙操作初始化、扫描、连接、读写、广播监听封装在一个独立的Android Library工程中编译成.aar文件。在Unity中你只需要导入这个AAR然后通过一个设计良好的C#接口层来调用。这样做的好处是职责分离Android原生代码由熟悉Android开发的同事维护Unity端只关注业务逻辑。便于调试Android端的代码可以用Android Studio进行完整的调试和日志输出。灵活可控遇到特定机型问题可以直接修改Android原生代码重新打包AAR无需大动Unity工程。复用性高封装好的AAR可以用于公司其他Unity项目。整体架构图文字描述Unity (C# Layer) | |--- C# Wrapper Interface (定义连接、扫描、发送数据等方法) | |--- AndroidJavaProxy / UnitySendMessage (用于接收来自Android的回调) | Android AAR Plugin (Java Layer) |--- BluetoothManager (单例管理蓝牙适配器、生命周期) |--- GattCallback (处理蓝牙连接、服务发现、数据读写回调) |--- ScanCallback (处理蓝牙设备扫描结果) |--- BroadcastReceiver (监听蓝牙状态、配对状态等系统广播) | Android System Bluetooth Stack这个架构的核心在于双向通信Unity调用Java方法执行操作Java通过回调接口利用UnityPlayer.currentActivity和UnitySendMessage将结果和事件通知回Unity。3. Android端接口设计与实现详解Android端是整个系统的基石设计良好的接口是成功的一半。我们的目标是设计一个高内聚、低耦合、生命周期感知的蓝牙管理器。3.1 核心类设计BluetoothManager我们创建一个BluetoothManager类采用单例模式因为它需要全局管理蓝牙适配器和各种回调。// 示例代码结构 public class BluetoothManager { private static BluetoothManager instance; private final BluetoothAdapter bluetoothAdapter; private final Context appContext; private BluetoothGatt activeGatt; // 当前连接的Gatt private ScanCallback scanCallback; private GattCallback gattCallback; // 内部自定义的GattCallback private BluetoothManager(Context context) { this.appContext context.getApplicationContext(); // 使用Application Context避免内存泄漏 this.bluetoothAdapter BluetoothAdapter.getDefaultAdapter(); this.gattCallback new GattCallback(); // 初始化回调 } public static synchronized BluetoothManager getInstance(Context context) { if (instance null) { instance new BluetoothManager(context); } return instance; } }关键设计点应用上下文Application Context务必使用context.getApplicationContext()因为Activity的Context在Activity销毁时可能导致内存泄漏而蓝牙操作是长生命周期的。单例与生命周期虽然用了单例但要小心处理。在App退出或Unity Activity销毁时需要提供release()或disconnectAll()方法主动关闭Gatt连接和停止扫描否则会引起资源泄露和后续连接失败。3.2 权限申请与动态检查从Android 6.0 (API 23) 开始蓝牙扫描需要精确位置权限ACCESS_FINE_LOCATION因为蓝牙信标可以被用于位置追踪。这是一个大坑很多新手会在这里卡住。在AndroidManifest.xml中声明uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / !-- 关键Android 6.0 扫描必备 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / !-- 对于Android 12 (API 31)还需要新的蓝牙权限 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT /在代码中动态申请和检查针对不同API级别public boolean checkAndRequestPermissions(Activity unityActivity) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { // Android 12 if (unityActivity.checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) ! PackageManager.PERMISSION_GRANTED || unityActivity.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) ! PackageManager.PERMISSION_GRANTED) { String[] permissions new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT}; unityActivity.requestPermissions(permissions, REQUEST_CODE_BT_PERMISSION); return false; } } else if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { // Android 6.0 - 11 if (unityActivity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { unityActivity.requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_BT_PERMISSION); return false; } } // Android 5.0及以下或权限已授予 return true; }实操心得务必在Unity的启动Activity通常是UnityPlayerActivity中处理权限回调。你需要重写onRequestPermissionsResult方法并将授权结果通过UnitySendMessage通知回Unity。否则用户点了“允许”你的Unity代码也不知道。3.3 蓝牙扫描Scan接口设计扫描是发现设备的第一步。Android 5.0引入了新的BluetoothLeScannerAPI比旧的startLeScan更强大。public void startScan(ScanCallback callback) { if (bluetoothAdapter null || !bluetoothAdapter.isEnabled()) { sendMessageToUnity(OnBluetoothError, Bluetooth not enabled); return; } BluetoothLeScanner scanner bluetoothAdapter.getBluetoothLeScanner(); if (scanner null) { sendMessageToUnity(OnBluetoothError, BLE scanner not available); return; } ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 低延迟扫描更快发现设备 .build(); ListScanFilter filters new ArrayList(); // 可以添加过滤器例如只扫描特定名称或服务UUID的设备 // filters.add(new ScanFilter.Builder().setDeviceName(MyDevice).build()); this.scanCallback callback; scanner.startScan(filters, settings, scanCallback); } public void stopScan() { if (bluetoothAdapter ! null scanCallback ! null) { BluetoothLeScanner scanner bluetoothAdapter.getBluetoothLeScanner(); if (scanner ! null) { scanner.stopScan(scanCallback); } scanCallback null; } }关键参数解析SCAN_MODE_LOW_LATENCY平衡了发现速度和功耗适合前台实时扫描。SCAN_MODE_BALANCED和SCAN_MODE_LOW_POWER则更省电但发现慢。ScanFilter非常有用如果你知道目标设备的名称或服务UUID强烈建议加上过滤器。这能极大减少回调次数节省电量并避免在Unity端处理大量无关设备数据。如何将扫描结果传回Unity在自定义的ScanCallback中每当发现设备就将其关键信息名称、地址、信号强度RSSI拼接成一个字符串如JSON格式通过UnitySendMessage发送。private ScanCallback scanCallback new ScanCallback() { Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); BluetoothDevice device result.getDevice(); String deviceName device.getName(); String deviceAddress device.getAddress(); int rssi result.getRssi(); // 封装成JSON字符串方便Unity解析 String jsonResult String.format({\name\:\%s\,\address\:\%s\,\rssi\:%d}, deviceName ! null ? deviceName : Unknown, deviceAddress, rssi); // 调用Unity中GameObject上的方法 UnityPlayer.UnitySendMessage(BluetoothBridge, OnDeviceFound, jsonResult); } };踩坑记录UnitySendMessage是主线程回调。蓝牙扫描回调onScanResult可能发生在后台线程。如果直接在onScanResult中调用UnitySendMessage在部分机型或高频率扫描时可能导致崩溃。稳妥的做法是用Handler或runOnUiThread将其抛到主线程执行。4. Unity端C#封装与桥接Android端准备好了现在需要在Unity里创建一个优雅的C#层来调用它。目标是让游戏逻辑工程师像调用普通C#库一样使用蓝牙功能完全屏蔽JNI细节。4.1 创建C#服务接口首先定义一个接口明确蓝牙模块需要提供哪些功能。public interface IBluetoothService { void Initialize(); void RequestPermissions(); void StartScan(); void StopScan(); void ConnectToDevice(string deviceAddress); void Disconnect(); void WriteCharacteristic(string serviceUuid, string characteristicUuid, byte[] data); event Actionstring OnDeviceFound; // 发现设备事件 event Actionstring OnConnected; // 连接成功事件 event Actionstring OnDisconnected; // 断开连接事件 event Actionstring, byte[] OnDataReceived; // 收到数据事件 }4.2 实现Android桥接类然后实现一个针对Android平台的类内部使用AndroidJavaClass和AndroidJavaObject与我们的AAR插件交互。public class AndroidBluetoothService : IBluetoothService { private AndroidJavaObject bluetoothManager; private AndroidJavaObject unityActivity; public event Actionstring OnDeviceFound; public event Actionstring OnConnected; // ... 其他事件 public AndroidBluetoothService() { // 获取当前的Unity Player Activity AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer); unityActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity); // 实例化我们Java端的BluetoothManager AndroidJavaClass managerClass new AndroidJavaClass(com.yourcompany.bluetooth.BluetoothManager); bluetoothManager managerClass.CallStaticAndroidJavaObject(getInstance, unityActivity); } public void StartScan() { if (bluetoothManager ! null) { bluetoothManager.Call(startScan); } } // ... 其他方法实现 }4.3 接收来自Android的回调UnitySendMessage的映射这是关键一步。在Unity场景中我们需要创建一个名为BluetoothBridge的GameObject名字与Java端UnitySendMessage的第一个参数对应并挂载一个脚本用于接收Java发来的消息。public class BluetoothMessageReceiver : MonoBehaviour { // 持有AndroidBluetoothService实例 public AndroidBluetoothService bluetoothService; void Start() { DontDestroyOnLoad(gameObject); // 保证这个对象在场景切换时不销毁 } // 这个方法名必须与Java端UnitySendMessage的第二个参数完全一致 public void OnDeviceFound(string deviceJson) { // 解析JSON触发事件 Debug.Log($Found device: {deviceJson}); bluetoothService?.OnDeviceFound?.Invoke(deviceJson); } public void OnBluetoothConnected(string deviceAddress) { Debug.Log($Connected to: {deviceAddress}); bluetoothService?.OnConnected?.Invoke(deviceAddress); } public void OnCharacteristicChanged(string data) { // data可能包含UUID和Base64编码的数据如 uuid|base64Data string[] parts data.Split(|); if (parts.Length 2) { string uuid parts[0]; byte[] rawData Convert.FromBase64String(parts[1]); bluetoothService?.OnDataReceived?.Invoke(uuid, rawData); } } }工作流程串联Unity脚本调用AndroidBluetoothService.StartScan()。C#桥接类通过JNI调用Java的BluetoothManager.startScan()。Java开始扫描在ScanCallback.onScanResult中通过UnitySendMessage(BluetoothBridge, OnDeviceFound, json)发送消息。Unity场景中的BluetoothBridgeGameObject上的BluetoothMessageReceiver.OnDeviceFound方法被调用。该方法解析数据并触发C#层的OnDeviceFound事件。游戏逻辑代码订阅了这个事件从而获取到设备信息并更新UI。核心技巧为了处理复杂的回调数据如多个特征值通知我通常设计一个简单的协议。例如Java端将回调类型、UUID、数据用特殊分隔符如|或$$拼接成一个字符串Unity端再按规则拆分。对于二进制数据先转换成Base64字符串再传输。5. 蓝牙连接GATT与数据通信实战扫描到设备后最复杂的部分来了——建立GATT连接并进行数据读写。这里涉及到大量的异步回调。5.1 连接与服务发现在Java端的BluetoothManager中实现连接方法public void connectToDevice(String deviceAddress) { BluetoothDevice device bluetoothAdapter.getRemoteDevice(deviceAddress); if (device null) { sendMessageToUnity(OnBluetoothError, Device not found: deviceAddress); return; } // 关键第二个参数autoConnect设为false。设为true会进行后台重连行为不可控且容易出问题。 activeGatt device.connectGatt(appContext, false, gattCallback); }在自定义的GattCallback继承自BluetoothGattCallback中你需要重写一系列方法private class GattCallback extends BluetoothGattCallback { Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { super.onConnectionStateChange(gatt, status, newState); if (newState BluetoothProfile.STATE_CONNECTED) { // 连接成功开始发现服务 gatt.discoverServices(); sendMessageToUnity(OnBluetoothConnected, gatt.getDevice().getAddress()); } else if (newState BluetoothProfile.STATE_DISCONNECTED) { // 连接断开 sendMessageToUnity(OnBluetoothDisconnected, gatt.getDevice().getAddress()); if (activeGatt ! null) { activeGatt.close(); activeGatt null; } } } Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); if (status BluetoothGatt.GATT_SUCCESS) { // 服务发现成功可以将服务列表传回Unity ListBluetoothGattService services gatt.getServices(); // 将服务信息UUID等封装成JSON传回Unity sendServiceListToUnity(services); } } Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); // 这是订阅Notify的特征值发生变化时的回调是接收数据的主要入口 byte[] data characteristic.getValue(); String charUuid characteristic.getUuid().toString(); sendCharacteristicDataToUnity(charUuid, data); // 将UUID和数据传回Unity } // 还需要重写 onCharacteristicRead, onCharacteristicWrite, onDescriptorWrite 等 }5.2 数据读写与“订阅-通知”机制蓝牙低功耗BLE通信的核心是特征值Characteristic。操作分为读、写、通知Notify/指示Indicate。写数据向设备发送指令。public boolean writeCharacteristic(String serviceUuid, String charUuid, byte[] data) { if (activeGatt null) return false; BluetoothGattService service activeGatt.getService(UUID.fromString(serviceUuid)); if (service null) return false; BluetoothGattCharacteristic characteristic service.getCharacteristic(UUID.fromString(charUuid)); if (characteristic null) return false; characteristic.setValue(data); // 第二个参数决定是否需要响应Write Type。true为WRITE_TYPE_DEFAULT需要响应false为WRITE_TYPE_NO_RESPONSE不需要响应更快但不可靠。 boolean success activeGatt.writeCharacteristic(characteristic); return success; // 注意这里返回的只是写入请求是否成功发送实际写入结果在onCharacteristicWrite回调中 }重要区别WRITE_TYPE_NO_RESPONSE速度更快因为不等待设备回复确认包。适合发送频繁且允许丢失的数据如传感器实时流。但对于关键指令务必使用WRITE_TYPE_DEFAULT以确保送达。读数据从设备读取当前值。调用activeGatt.readCharacteristic(characteristic)结果在onCharacteristicRead回调中返回。订阅通知Notify这是设备主动向手机推送数据的标准方式。你需要先找到支持Notify或Indicate的特征值然后向其客户端配置描述符CCCD写入BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE来启用通知。public boolean enableNotification(String serviceUuid, String charUuid, boolean enable) { // ... 获取service和characteristic boolean setNotifySuccess activeGatt.setCharacteristicNotification(characteristic, enable); if (!setNotifySuccess) return false; BluetoothGattDescriptor descriptor characteristic.getDescriptor(UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG)); if (descriptor null) return false; byte[] value enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; descriptor.setValue(value); return activeGatt.writeDescriptor(descriptor); // 写入描述符以启用/禁用通知 }启用后设备发送的新数据就会触发onCharacteristicChanged回调。5.3 连接稳定性与超时处理蓝牙连接不稳定是常态。必须处理超时和自动重连。连接超时在调用connectGatt时启动一个定时器如Handler.postDelayed10-15秒后如果状态仍未变为STATE_CONNECTED则判定为超时调用gatt.disconnect()并通知Unity。自动重连不建议在connectGatt中设置autoConnecttrue因为它行为诡异。更好的做法是在onConnectionStateChange收到STATE_DISCONNECTED时如果不是主动断开则延迟几秒后尝试重连。注意重连次数限制避免死循环。连接状态维护维护一个清晰的内部状态机如IDLE,CONNECTING,CONNECTED,DISCONNECTING避免重复操作。6. 系统广播Broadcast处理监听蓝牙全局状态除了设备通信我们还需要监听系统级的蓝牙事件例如蓝牙开关状态、设备配对状态变化。这需要通过注册BroadcastReceiver来实现。在BluetoothManager的初始化中注册广播接收器private void registerBluetoothBroadcastReceiver() { IntentFilter filter new IntentFilter(); filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); // 蓝牙开关状态 filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); // 设备物理连接 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); // 设备物理断开 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); // 配对状态变化 appContext.registerReceiver(bluetoothBroadcastReceiver, filter); } private final BroadcastReceiver bluetoothBroadcastReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { int state intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); if (state BluetoothAdapter.STATE_OFF) { // 蓝牙被关闭清理所有连接和扫描 disconnectAll(); stopScan(); sendMessageToUnity(OnBluetoothStateChanged, OFF); } else if (state BluetoothAdapter.STATE_ON) { sendMessageToUnity(OnBluetoothStateChanged, ON); } } else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) { // 系统通知设备断开即使我们的Gatt回调没收到 BluetoothDevice device intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (device ! null activeGatt ! null device.getAddress().equals(activeGatt.getDevice().getAddress())) { // 触发我们的断开逻辑 onGattDisconnected(); } } } };注意事项广播接收器一定要在适当的时候注销如在BluetoothManager的release方法中否则会造成内存泄漏。ACTION_ACL_DISCONNECTED广播非常有用有时Android的Gatt回调会漏掉断开事件但这个广播很可靠。7. 常见问题、性能优化与避坑指南这是多年踩坑经验的精华请务必仔细阅读。7.1 连接失败与“133”错误码在onConnectionStateChange中status参数非0即表示错误。常见的status133或0x85是“GATT ERROR”的泛指。可能原因系统蓝牙资源耗尽这是最常见原因。Android系统对同时活跃的Gatt连接数有限制通常4-7个。务必在断开连接onConnectionStateChange收到STATE_DISCONNECTED后调用gatt.close()释放资源。不调用close()这个连接句柄会一直被占用直到系统GC回收可能很久很快资源就会耗尽。设备端拒绝连接检查设备是否处于可连接状态或者是否已被其他主机连接。距离或干扰确保设备在有效范围内避开强干扰源。解决方案实现严格的连接池管理。每次连接前检查是否有闲置连接断开后立即close()。7.2 服务发现discoverServices失败或不全有时onServicesDiscovered回调的status是成功的但获取的服务列表是空的或不完整。时机问题不要在onConnectionStateChange中一收到STATE_CONNECTED就立刻调用discoverServices。等待100-200毫秒。设备兼容性问题部分低功耗蓝牙芯片或旧版本系统有bug。可以尝试在连接后先调用一次gatt.readRemoteRssi()一个轻量级操作来“激活”通道然后再发现服务。这是一个偏方但对某些设备有效。重试机制如果第一次发现服务失败或不全可以延迟1秒后再次调用discoverServices。7.3 数据吞吐量瓶颈与MTU协商BLE单次数据传输有大小限制默认是23字节减去3字节开销实际20字节。传输大量数据如图片、长指令会非常慢。MTU协商在连接后可以请求更大的MTU最大传输单元。调用activeGatt.requestMtu(512)。结果在onMtuChanged回调中。成功的话单包可以传输几百字节极大提升速度。数据分包如果MTU协商失败或仍不够用必须在应用层实现分包和组包协议。例如发送端将大数据拆成固定大小的包加上序号和校验接收端按序号重组。7.4 多线程与主线程安全如前所述几乎所有蓝牙回调扫描、连接、数据读写都发生在非主线程Binder线程。而Unity的UnitySendMessage和很多UI操作必须在主线程进行。统一使用Handler在BluetoothManager初始化时获取主线程的Handlernew Handler(Looper.getMainLooper())。在所有回调中如果需要与Unity通信或更新UI状态都通过这个Handler.post(Runnable)来执行。Java层状态同步对activeGatt、scanCallback等共享变量的访问要考虑线程安全必要时使用synchronized关键字。7.5 功耗与后台运行长时间扫描或保持连接非常耗电。扫描策略前台应用使用SCAN_MODE_LOW_LATENCY。如果应用退到后台应立即停止扫描或切换到SCAN_MODE_LOW_POWER/SCAN_MODE_BALANCED。连接心跳如果设备需要保持长连接可以设计一个简单的心跳包如每分钟一次而不是持续的高频通信。在Android端可以考虑使用Foreground Service来维持连接但需要向用户显示一个持续的通知。7.6 厂商定制系统MIUI, EMUI等的适配国产定制ROM是“重灾区”。后台限制MIUI、EMUI等有严格的省电策略会杀死后台服务。你的Unity游戏如果切到后台蓝牙连接很可能被系统强制断开。需要在App设置里引导用户手动授予“自启动”、“后台弹出界面”、“省电策略无限制”等权限每个ROM设置路径不同非常头疼。权限弹窗部分系统在调用BluetoothAdapter.getDefaultAdapter()时就会弹出定位权限请求而不是在你调用startScan时。这会导致逻辑顺序错乱。要做好兼容在初始化时就检查权限。7.7 Unity端的生命周期对齐Unity的Application生命周期和Android的Activity生命周期并不完全同步。OnApplicationPause当Unity应用进入后台OnApplicationPause(true)你应该通知Android端停止扫描、断开连接或进入低功耗模式。当回到前台OnApplicationPause(false)再重新初始化。Native Code Reload在Unity Editor中播放停止时或者某些脚本重载情况下原生的JNI引用可能会失效。确保你的C#桥接类在OnDestroy中有清理逻辑并能重新初始化。8. 调试与日志排查实战高效的调试是开发进度的保障。Android Studio Logcat这是最强大的工具。在Java代码中大量使用Log.d(TAG, message)。过滤标签TAG查看你的插件日志。重点关注错误码、回调顺序、线程信息。Android Device Monitor已弃用但旧版本仍可用或Profiler查看实时的蓝牙HCI日志需要启用开发者选项中的“蓝牙HCI信息收集日志”。这能看到最底层的蓝牙指令和事件对排查协议级问题至关重要。Unity Console ADB Logcat在Unity C#端除了Debug.Log也可以将关键信息通过UnitySendMessage发回或者直接调用Android的Log类输出到Logcat方便在Unity编辑器中同时查看。常用ADB命令adb logcat -s Unity只看Unity输出的日志。adb logcat -s YourBluetoothTag只看你插件的日志。adb shell dumpsys bluetooth_manager查看系统蓝牙服务的详细状态包括已连接设备、Gatt客户端列表等。第三方蓝牙调试App如nRF Connect、LightBlue。它们可以扫描、连接、读写特征值是验证设备本身是否正常工作的利器。先用它们测试你的蓝牙设备如果它们能正常工作而你的App不行问题就出在你的代码上。整个流程走下来你会发现Unity与Android蓝牙开发就像在两根摇晃的钢丝之间搭桥需要同时处理好Unity的游戏循环、Android的原生API、多线程通信、系统广播、厂商适配等一系列问题。但一旦这座桥搭稳了它就能为你的游戏或应用打开一扇通往丰富硬件交互的大门。记住耐心、细致的日志和对蓝牙协议栈的深入理解是解决所有诡异问题的终极武器。