资讯动态

Android BLE官方Demo深度解析:GATT通信底层原理与调试实战

发布时间:2026/10/17 8:16:08 来源:尧图企业网站定制
简介本资源是基于Android官方蓝牙低功耗BLE开发指南实现的完整Demo工程面向Android应用开发者及嵌入式通信初学者聚焦BLE核心流程实践设备扫描scan、连接建立与断开connection/disconnect、服务发现discover service及UUID交互等关键能力。压缩包共70个文件涵盖26个Class类文件含BLE状态管理与回调逻辑、15个XML布局与配置资源、9个PNG图标素材、7个Java源码文件主Activity与BluetoothAdapter操作封装以及APK可运行包、JAR依赖库和项目构建配置文件project.properties、.classpath等整体仅1.34MB轻量易导入。已有525人学习下载代码结构清晰、模块职责分明完整复现了Android 4.0 BLE典型通信链路附带可直接调试的APK与详细Manifest权限声明是理解Android BLE协议栈集成与调试的优质入门参考。1. Android 蓝牙BLE官方demo不是“跑通就行”的玩具而是理解GATT通信底层逻辑的最小可信沙盒你手头有一块新模组用nRF Connect扫到了设备但App连上后读不到特征值、写不进去指令、notify一开就断连——别急着骂硬件或换SDK。我去年在某跨平台系统里调通一个医疗传感器时卡在“发现服务后UUID匹配失败”整整三天最后回退到Android官方demo里单步跟BluetoothGattCallback的onServicesDiscovered()回调才意识到问题根本不在你的代码而在你没真正看懂BluetoothGattService是如何被discoverServices()异步加载、又如何被getCharacteristic(UUID)按引用查找的。这个demo不是教学示例它是Android蓝牙协议栈与Linux内核BlueZ层之间那层薄薄胶水的透明切片。它覆盖从startScan()触发底层HCI命令到onConnectionStateChange()解析LE连接事件码再到onCharacteristicWrite()返回BluetoothGatt.GATT_SUCCESS的完整链路。适合所有正在调试BLE外设交互、需要确认是协议理解偏差还是实现Bug的嵌入式开发者、Android应用工程师和IoT固件联调人员——尤其当你面对的是非标准GATT服务、自定义128位UUID、或需要手动处理MTU协商时它比任何第三方库都更接近真相。2. 从源码结构到核心流程为什么必须亲手编译并打断点看BluetoothLeService2.1 源码包的真实组成与关键文件定位非GitHub镜像是AOSP分支直出官方demo实际指代的是Android Open Source Project中platform/development/samples/BluetoothLeGatt路径下的完整工程。它不是单个Activity而是一个包含三层结构的可运行模块app/src/main/java/com/example/android/bluetoothlegatt/主业务逻辑含DeviceScanActivity扫描页、DeviceControlActivity控制页、BluetoothLeService核心服务类app/src/main/java/com/example/android/bluetoothlegatt/BluetoothLeService.java真正的灵魂文件封装了BluetoothAdapter、BluetoothGatt、BluetoothGattCallback三者协作模型app/src/main/res/values/strings.xml硬编码了测试用的SAMPLE_SERVICE_UUID0000ffe0-0000-1000-8000-00805f9b34fb和SAMPLE_CHAR_UUID0000ffe1-0000-1000-8000-00805f9b34fb这是你后续替换自己设备UUID的起点提示不要直接下载ZIP包。务必通过repo init -u https://android.googlesource.com/platform/manifest -b android-13.0.0_r1同步对应Android版本的AOSP再进入development/samples/BluetoothLeGatt目录。原因不同Android版本对BluetoothGatt.refresh()的支持差异极大如Android 12已废弃该方法用错分支会导致onServicesDiscovered()永远不回调。2.2BluetoothLeService的四大生命周期钩子与状态机设计该服务类本质是一个状态驱动的BLE客户端引擎。其核心不是“连接”而是连接态下的服务发现与特征操作。我们拆解它的四个关键回调入口// BluetoothLeService.java 片段 private final BluetoothGattCallback mGattCallback new BluetoothGattCallback() { Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { // status0 表示操作成功newState2 表示已连接BluetoothProfile.STATE_CONNECTED // ⚠️ 注意此处status≠0不一定是失败可能是BUSY需重试 if (newState BluetoothProfile.STATE_CONNECTED) { broadcastUpdate(ACTION_GATT_CONNECTED); Log.i(TAG, Connected to GATT server.); // 连接成功后必须主动调用discoverServices() mBluetoothGatt.discoverServices(); } } Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { // status0 才代表服务发现完成很多开发者忽略此判断直接getServices() if (status BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); // 此处才是安全获取服务的时机 ListBluetoothGattService services gatt.getServices(); for (BluetoothGattService service : services) { Log.d(TAG, Found service: service.getUuid()); } } } Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { // 读取完成后触发status0表示成功 if (status BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } } Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { // 写入响应status0才表示外设已接收 if (status BluetoothGatt.GATT_SUCCESS) { Log.d(TAG, Write success for characteristic.getUuid()); } } };参数说明与逻辑要点status参数是操作结果码不是连接状态码。onConnectionStateChange()中的status是HCI命令执行结果如0x00success0x08connection timeout而onServicesDiscovered()中的status是GATT协议层返回码0x00success0x87insufficient authentication。二者语义完全不同混用必翻车。discoverServices()是异步阻塞操作调用后立即返回但后续所有特征操作read/write/notify必须等onServicesDiscovered()回调后才能执行。否则gatt.getService(uuid)返回null。broadcastUpdate()是本地广播机制用于通知UI层状态变更。实际项目中建议替换为LiveData或Flow避免内存泄漏。2.3 扫描启动与过滤策略为什么你的设备总“扫不到”DeviceScanActivity中的扫描逻辑看似简单实则暗藏玄机。关键在于ScanFilter的构建方式// DeviceScanActivity.java 片段 private void scanLeDevice(final boolean enable) { if (enable) { // 构建扫描过滤器只扫特定MAC地址或特定Service UUID ScanFilter.Builder filterBuilder new ScanFilter.Builder(); // 方式1按设备MAC地址过滤需提前知道 // filterBuilder.setDeviceAddress(AA:BB:CC:DD:EE:FF); // 方式2按广播包中的Service UUID过滤推荐无需MAC ParcelUuid parcelUuid ParcelUuid.fromString(0000ffe0-0000-1000-8000-00805f9b34fb); filterBuilder.setServiceUuid(parcelUuid); ListScanFilter filters Arrays.asList(filterBuilder.build()); // 扫描设置设置扫描窗口和间隔单位ms ScanSettings.Builder settingsBuilder new ScanSettings.Builder(); settingsBuilder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY); // 高频扫描 // ⚠️ 关键Android 8.0要求必须设置callbackType settingsBuilder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES); mBluetoothLeScanner.startScan(filters, settingsBuilder.build(), mScanCallback); } else { mBluetoothLeScanner.stopScan(mScanCallback); } }参数说明与逻辑要点setServiceUuid()过滤的是广播包Advertising Data中声明的Service UUID不是GATT服务端的UUID。这意味着你的外设必须在广播包的AD Type0x03Complete List of 16-bit Service Class UUIDs或0x06Incomplete List...字段中携带该UUID否则永远匹配不到。SCAN_MODE_LOW_LATENCY10ms窗口/10ms间隔耗电极高仅用于调试。量产应改为SCAN_MODE_BALANCED20ms/5000ms。CALLBACK_TYPE_ALL_MATCHES是Android 8.0强制要求否则mScanCallback不触发。旧版CALLBACK_TYPE_FIRST_MATCH已被废弃。2.4 连接建立与超时控制connectGatt()的三个参数陷阱连接动作发生在DeviceControlActivity.connect()中调用链为mBluetoothDevice.connectGatt(this, false, mGattCallback)。这三个参数决定连接行为参数取值含义实战建议contextthisContext用于绑定服务必须用Activity context不能用Application context否则onConnectionStateChange()不回调autoConnectfalse是否自动重连false表示立即连接适合已知设备true表示后台持续尝试适合信标类设备callbackmGattCallback回调实例必须是同一个实例若每次新建callback对象旧回调将丢失导致状态监听失效注意connectGatt()返回BluetoothGatt对象但此时连接尚未建立。onConnectionStateChange()回调才是连接成功的唯一信标。切勿在connectGatt()后立即调用discoverServices()。3. BLE建立连接与服务发现从HCI日志反推onServicesDiscovered()不触发的根因3.1 连接建立阶段的HCI命令流与典型失败点当调用connectGatt()后Android系统会向蓝牙控制器Controller下发一系列HCI命令。我们可通过adb logcat -d | grep -i hci\|bt抓取关键日志D bt_btif: btif_gattc_upstreams_evt: Event 1002 (GATT_CONNECTION_STATUS_EVT) D bt_btif: btif_gattc_conn_cb: conn_id1, connected1, reason0x0000 D bt_btif: btif_gattc_upstreams_evt: Event 1004 (GATT_GET SERVICES EVT) D bt_btif: btif_gattc_upstreams_evt: Event 1005 (GATT_GET CHARACTERISTICS EVT) ... E bt_btif: btif_gattc_upstreams_evt: Event 1004 failed, status0x87日志解读reason0x0000HCI连接成功0x0000successEvent 1004GATT Discover All Services命令下发status0x87GATT_INSUFFICIENT_AUTHENTICATION即外设要求配对但未配对常见失败码对照表HCI StatusGATT Status含义解决方案0x00000x0000成功继续下一步0x08—Connection Timeout检查外设是否处于可连接模式信号强度是否≥-70dBm0x3e—Connection Failed to be Established外设拒绝连接如已满连接数重启外设—0x87Insufficient Authentication主动调用createBond()配对或在外设端关闭配对要求3.2 服务发现失败的三大物理层原因与验证法onServicesDiscovered()不回调90%的情况并非代码错误而是物理层或协议层异常。我们用最原始的方法验证验证步骤1用nRF Connect确认外设GATT结构打开nRF Connect扫描并连接目标设备查看左侧Services列表确认0000ffe0-...服务是否存在点击该服务查看Characteristics列表确认0000ffe1-...特征是否存在若nRF Connect也看不到服务 → 外设固件未正确初始化GATT数据库检查sd_ble_gatts_service_add()调用验证步骤2抓取Android蓝牙HCI Snoop Log# 开启HCI日志需root或userdebug版本 adb shell setprop bluetooth.hci.snoop_log.enabled true adb reboot # 复现问题后拉取日志 adb pull /sdcard/btsnoop_hci.log用Wireshark打开btsnoop_hci.log过滤att协议观察是否有ATT Handle Value Notification包notify开启成功是否有ATT Read By Group Type Request服务发现请求对应的Response是否返回Attribute Not Found0x0a验证步骤3强制刷新GATT缓存仅Android 10以下// 在onServicesDiscovered()失败后尝试 if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { try { Method refresh BluetoothGatt.class.getMethod(refresh); refresh.invoke(mBluetoothGatt); } catch (Exception e) { Log.e(TAG, Refresh failed, e); } }注意Android 11已移除此方法强行反射会抛NoSuchMethodException。3.3 UUID匹配的精确性要求16位UUID与128位UUID的转换陷阱官方demo中硬编码的SAMPLE_SERVICE_UUID 0000ffe0-0000-1000-8000-00805f9b34fb是标准128位UUID。但你的外设可能只广播16位UUID如0xffe0。此时必须做显式转换// 将16位UUID转为128位标准格式Bluetooth Base UUID public static String uuid16To128(int uuid16) { return String.format(0000%04x-0000-1000-8000-00805f9b34fb, uuid16); } // 使用 String serviceUuidStr uuid16To128(0xffe0); // → 0000ffe0-0000-1000-8000-00805f9b34fb ParcelUuid parcelUuid ParcelUuid.fromString(serviceUuidStr); BluetoothGattService service mBluetoothGatt.getService(parcelUuid.getUuid());关键点Android的BluetoothGatt.getService(UUID)内部使用equals()比较要求UUID对象完全一致直接传入ffe0字符串会创建UUID.fromString(ffe0)其值为00000000-0000-0000-0000-000000000000必然匹配失败必须用ParcelUuid.fromString()或UUID.fromString()传入完整128位字符串4. 常见问题排查五个血泪经验总结的“必现翻车点”4.1 现象onServicesDiscovered()永远不回调Logcat显示D BluetoothLeService: Connecting to GATT server...后静默原因外设在GATT服务发现阶段返回了GATT_ERROR0x80但Android系统未向上层抛出该错误码导致回调丢失解决在onConnectionStateChange()中连接成功后添加超时保护机制private Handler mHandler new Handler(Looper.getMainLooper()); private Runnable mDiscoveryTimeout () - { Log.e(TAG, Service discovery timeout after 10s); broadcastUpdate(ACTION_GATT_DISCONNECTED); close(); // 主动断开 }; // 在discoverServices()后启动 mHandler.postDelayed(mDiscoveryTimeout, 10_000); // 在onServicesDiscovered()中移除 mHandler.removeCallbacks(mDiscoveryTimeout);4.2 现象onCharacteristicWrite()回调status0x80GATT_ERROR但外设实际已接收数据原因外设固件在sd_ble_gatts_value_set()后未调用sd_ble_gatts_hvx()发送Write Response导致Android等待超时解决检查外设固件中Characteristic的Property是否设置了BLE_GATT_CHAR_PROPERTIES_WRITE_WITHOUT_RESPONSE。若需响应必须设为WRITE并实现BLE_GATTS_EVT_WRITE事件处理。4.3 现象扫描时能发现设备但connectGatt()后onConnectionStateChange()回调status0x08timeout原因Android系统默认MTU为23字节而某些外设要求MTU≥50连接握手阶段因MTU协商失败导致超时解决连接成功后立即请求MTUAndroid 6.0Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState BluetoothProfile.STATE_CONNECTED) { gatt.requestMtu(50); // 请求50字节MTU } } Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { if (status BluetoothGatt.GATT_SUCCESS) { Log.d(TAG, MTU changed to mtu); } }4.4 现象同一台手机反复连接同一设备第二次开始onServicesDiscovered()不触发原因Android系统缓存了GATT服务结构后续连接直接复用缓存跳过服务发现流程解决每次连接前清除缓存仅调试用// Android 10以下可用反射 if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { try { Method refresh BluetoothGatt.class.getMethod(refresh); refresh.invoke(mBluetoothGatt); } catch (Exception ignored) {} } // Android 10必须断连后重新调用connectGatt() mBluetoothGatt.close(); mBluetoothGatt device.connectGatt(this, false, mGattCallback);4.5 现象onCharacteristicChanged()Notify回调收到数据但getValue()返回空字节数组原因未在onServicesDiscovered()后正确启用Notify缺少setCharacteristicNotification()和writeDescriptor()两步解决启用Notify的完整流程// 1. 先设置客户端特征描述符为ENABLED BluetoothGattDescriptor descriptor characteristic.getDescriptor( UUID.fromString(00002902-0000-1000-8000-00805f9b34fb) // CCCD ); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); // 2. 再启用通知 mBluetoothGatt.setCharacteristicNotification(characteristic, true);注意writeDescriptor()是异步操作必须等onDescriptorWrite()回调成功后再认为Notify已启用。5. 特征读写与Notify实战用BluetoothLeService封装一个可复用的GATT操作工具类5.1 构建类型安全的GATT操作队列避免回调地狱官方demo的回调嵌套connect→discover→getCharacteristic→read难以维护。我们将其重构为链式调用// GattOperation.java public class GattOperation { private final BluetoothGatt mGatt; private final UUID mServiceUuid; private final UUID mCharUuid; private final byte[] mWriteValue; public GattOperation(BluetoothGatt gatt, UUID serviceUuid, UUID charUuid) { this.mGatt gatt; this.mServiceUuid serviceUuid; this.mCharUuid charUuid; this.mWriteValue null; } public GattOperation write(byte[] value) { this.mWriteValue value; return this; } public void execute(GattCallback callback) { BluetoothGattService service mGatt.getService(mServiceUuid); if (service null) { callback.onError(Service not found: mServiceUuid); return; } BluetoothGattCharacteristic characteristic service.getCharacteristic(mCharUuid); if (characteristic null) { callback.onError(Characteristic not found: mCharUuid); return; } if (mWriteValue ! null) { characteristic.setValue(mWriteValue); characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); mGatt.writeCharacteristic(characteristic); } else { mGatt.readCharacteristic(characteristic); } } public interface GattCallback { void onSuccess(BluetoothGattCharacteristic characteristic); void onError(String error); } }使用示例// 在DeviceControlActivity中 new GattOperation(mBluetoothGatt, UUID.fromString(0000ffe0-0000-1000-8000-00805f9b34fb), UUID.fromString(0000ffe1-0000-1000-8000-00805f9b34fb)) .write(CMD_ON.getBytes()) .execute(new GattOperation.GattCallback() { Override public void onSuccess(BluetoothGattCharacteristic characteristic) { Log.d(TAG, Write success); } Override public void onError(String error) { Log.e(TAG, error); } });5.2 Notify数据解析的边界处理如何应对分包与粘包BLE Notify数据长度受MTU限制默认20字节当外设发送长数据如固件升级包时会被自动分包。官方demo未处理此场景导致数据错乱// 在BluetoothLeService的onCharacteristicChanged()中增强 Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { byte[] data characteristic.getValue(); // 方案1按固定帧头识别如0xAA 0x55开头 if (data.length 2 data[0] (byte) 0xAA data[1] (byte) 0x55) { // 新数据包开始清空缓冲区 mReceiveBuffer.clear(); } // 方案2追加到缓冲区等待完整帧 mReceiveBuffer.addAll(Arrays.asList(ArrayUtils.toObject(data))); // 检查是否收到完整帧假设帧尾为0xCC 0x33 if (mReceiveBuffer.size() 4) { ListByte tail mReceiveBuffer.subList(mReceiveBuffer.size() - 2, mReceiveBuffer.size()); if (tail.get(0) (byte) 0xCC tail.get(1) (byte) 0x33) { // 解析完整帧 parseFrame(new byte[mReceiveBuffer.size()]); mReceiveBuffer.clear(); } } }5.3 权限适配与后台限制Android 12扫描必须动态申请BLUETOOTH_SCAN从Android 12API 31起蓝牙扫描权限升级为危险权限且需区分前台/后台场景!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / !-- Android 12以下兼容 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /运行时申请逻辑private void requestBlePermissions() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT}, REQUEST_CODE_PERMISSIONS); } } else { // 降级到ACCESS_FINE_LOCATION if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_PERMISSIONS); } } }注意BLUETOOTH_SCAN权限在Android 12必须显式声明usesPermissionFlagsneverForLocation否则Google Play审核不通过。6. 从demo到生产三个必须落地的加固技巧与我的后悔药清单6.1 技巧一用BluetoothGattCallback状态机替代裸回调杜绝状态错乱官方demo的mBluetoothGattCallback是全局单例但多个设备连接时易发生回调错绑。我现在的做法是为每个连接创建独立回调实例并内置状态机public class SafeGattCallback extends BluetoothGattCallback { private final AtomicReferenceState mState new AtomicReference(State.DISCONNECTED); private final WeakReferenceBluetoothLeService mServiceRef; public SafeGattCallback(BluetoothLeService service) { this.mServiceRef new WeakReference(service); } Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (status ! BluetoothGatt.GATT_SUCCESS) { mState.set(State.DISCONNECTED); return; } if (newState BluetoothProfile.STATE_CONNECTED) { mState.set(State.CONNECTED); gatt.discoverServices(); } else if (newState BluetoothProfile.STATE_DISCONNECTED) { mState.set(State.DISCONNECTED); } } Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status BluetoothGatt.GATT_SUCCESS mState.get() State.CONNECTED) { mState.set(State.SERVICES_DISCOVERED); } } public boolean isReady() { return mState.get() State.SERVICES_DISCOVERED; } private enum State { DISCONNECTED, CONNECTED, SERVICES_DISCOVERED } }为什么有效isReady()方法让业务层无需关心回调时序只需轮询状态即可安全调用readCharacteristic()。这比Handler.postDelayed()更可靠因为状态变更由系统回调驱动而非时间驱动。6.2 技巧二用adb shell dumpsys bluetooth_manager实时诊断连接瓶颈当遇到“连接慢”“断连频繁”时logcat信息太泛。直接读取蓝牙服务内部状态# 查看当前GATT连接数、缓存服务数、扫描状态 adb shell dumpsys bluetooth_manager | grep -A 10 -B 5 gatt\|scan # 输出示例 # GattServer: # Connected devices: 1 # Services cached: 3 # Characteristic cache size: 12 # ScanClient: # Active scan clients: 1 # Scan mode: LOW_LATENCY关键指标解读Connected devices 5Android系统对单设备GATT连接数有限制通常7个超限会导致新连接失败Services cached: 0服务发现未完成或被系统清空需检查onServicesDiscovered()是否触发Active scan clients: 0扫描已停止但UI仍显示“正在扫描”说明stopScan()未被调用6.3 技巧三为每个Characteristic绑定唯一OnDataReceivedListener解耦UI与协议官方demo用broadcastUpdate()通知UI但广播无法传递复杂对象。我改用接口回调并为每个Characteristic单独注册// 在BluetoothLeService中 private final MapUUID, OnDataReceivedListener mListeners new ConcurrentHashMap(); public void setOnDataReceivedListener(UUID charUuid, OnDataReceivedListener listener) { mListeners.put(charUuid, listener); } Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { UUID uuid characteristic.getUuid(); OnDataReceivedListener listener mListeners.get(uuid); if (listener ! null) { listener.onDataReceived(characteristic.getValue()); } } public interface OnDataReceivedListener { void onDataReceived(byte[] data); }UI层注册示例// DeviceControlActivity.java mBluetoothLeService.setOnDataReceivedListener( UUID.fromString(0000ffe1-0000-1000-8000-00805f9b34fb), data - runOnUiThread(() - { // 更新TextView不涉及GATT操作 mStatusText.setText(Received: Arrays.toString(data)); }) );我的后悔药清单从那以后我每次调试BLE都强制走一遍先用nRF Connect连一次确认外设本身GATT结构正确排除硬件问题抓HCI Snoop Log不依赖Logcat直接看协议层是否发出了Discover Services命令检查onServicesDiscovered()的status值永远不假设它是0打印出来连接后立即requestMtu(50)哪怕外设声称支持默认MTU也强制协商一次close()前调用refresh()Android 10以下防止下次连接复用脏缓存这些不是玄学是我在某医疗设备联调中连续三次因refresh()缺失导致整周返工后刻进肌肉记忆的操作。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑