Android传统蓝牙聊天室开发实战:RFCOMM连接与权限适配
1. 为什么从传统蓝牙聊天室切入Android蓝牙开发很多人一上来就想搞BLE低功耗蓝牙觉得传统蓝牙已经是过时技术。但我在实际项目里带过不少新人发现一个规律直接啃BLE协议栈的人十有八九会在GATT回调的异步泥潭里迷失方向。传统蓝牙Classic Bluetooth的Socket模型反而更接近大家熟悉的TCP网络编程思维用它来建立蓝牙开发的整体认知框架效率高得多。这个聊天室Demo的核心价值不在于做一个能用的聊天工具而在于通过它把蓝牙开发中最基础也最容易出问题的几个环节全部跑通设备发现、配对绑定、RFCOMM通道建立、双向数据流读写、连接状态管理。这些环节在BLE里同样存在只是换了一套API和协议层。你把传统蓝牙这套流程吃透了后面切到BLE只是换个接口的事。适合谁来参考这篇内容如果你已经写过Android的基础界面对Activity生命周期、广播接收器、后台线程这些概念不陌生但还没碰过蓝牙相关的API那这个聊天室的实现路径就是为你准备的。我会把每个环节的选型理由、参数含义、容易踩的坑都拆开讲代码可以直接抄作业但更重要的是理解为什么这么写。传统蓝牙聊天室的本质是什么两台Android设备各自创建一个蓝牙服务端和一个蓝牙客户端通过RFCOMM协议建立一条类似TCP的可靠传输通道然后在这条通道上跑自定义的聊天消息协议。听起来简单但Android的蓝牙API在权限管理、线程调度、设备兼容性上埋了不少雷下面逐个拆解。2. 蓝牙权限与适配从Android 6到Android 12的完整避坑路线2.1 权限声明的版本差异Android的蓝牙权限模型在几个大版本里改过好几次这是新手最容易翻车的地方。你在网上搜到的代码可能只声明了BLUETOOTH和BLUETOOTH_ADMIN在Android 12以上的设备上直接崩溃。先看完整的权限清单按版本分层!-- Android 11及以下 -- uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / !-- Android 12及以上 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.BLUETOOTH_ADVERTISE /这里有几个关键点需要解释清楚。BLUETOOTH和BLUETOOTH_ADMIN是普通权限安装即授予不需要运行时申请。但ACCESS_FINE_LOCATION是危险权限从Android 6.0开始必须运行时申请。为什么扫描蓝牙设备需要定位权限因为蓝牙扫描结果可以推断出用户的地理位置这是系统层面的隐私保护策略。Android 12引入的BLUETOOTH_SCAN、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE是新的运行时权限把之前混在定位权限里的蓝牙操作独立出来了。neverForLocation这个标志告诉系统我的应用不会用蓝牙扫描结果来推断位置这样在Android 12上就不需要再申请定位权限了。2.2 运行时权限申请的实操写法我见过太多项目在onCreate里直接调requestPermissions然后回调里不管三七二十一就开始扫描。正确的做法是按版本分支处理private static final int REQUEST_BT_PERMISSIONS 1001; private void requestBluetoothPermissions() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { // Android 12 String[] perms { Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_ADVERTISE }; if (!hasPermissions(perms)) { requestPermissions(perms, REQUEST_BT_PERMISSIONS); } else { initBluetooth(); } } else if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { // Android 6.0 - 11 if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { requestPermissions( new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_BT_PERMISSIONS); } else { initBluetooth(); } } else { // Android 6.0以下安装即授予 initBluetooth(); } }注意在Android 12上如果你只申请了BLUETOOTH_SCAN但没有加neverForLocation标志系统仍然会要求定位权限。这个标志必须写在Manifest里运行时申请是改不了的。2.3 蓝牙适配器的获取与状态检查拿到权限之后第一步是获取BluetoothAdapter。这个对象是整个传统蓝牙开发的入口代表本机的蓝牙硬件。BluetoothManager bluetoothManager (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); BluetoothAdapter bluetoothAdapter bluetoothManager.getAdapter(); if (bluetoothAdapter null) { // 设备不支持蓝牙直接提示用户 Toast.makeText(this, 本设备不支持蓝牙, Toast.LENGTH_SHORT).show(); finish(); return; } if (!bluetoothAdapter.isEnabled()) { // 蓝牙未开启拉起系统设置页 Intent enableIntent new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableIntent, REQUEST_ENABLE_BT); }这里有个细节BluetoothAdapter.getDefaultAdapter()在API 18之后已经被废弃推荐用BluetoothManager来获取。虽然老方法还能用但在某些定制ROM上会返回null用BluetoothManager更稳妥。3. RFCOMM通道的建立UUID选择与Socket连接逻辑3.1 为什么是RFCOMM而不是L2CAP传统蓝牙支持两种传输协议L2CAP和RFCOMM。L2CAP是底层协议面向数据包需要自己处理分片和重组RFCOMM是在L2CAP之上模拟串口通信的协议提供面向连接的可靠字节流跟TCP的语义非常接近。对于聊天室这种场景RFCOMM是天然的选择。你不需要关心数据包怎么分片、怎么保证顺序Socket的输入输出流直接读写就行。Android的BluetoothSocket底层就是RFCOMM的实现API设计也跟java.net.Socket高度相似。3.2 UUID的选取原则RFCOMM通道用一个UUID来标识服务。两台设备必须使用相同的UUID才能建立连接。这个UUID可以是标准UUID也可以自己生成一个。// 自定义UUID随便生成一个就行只要两端一致 private static final UUID MY_UUID UUID.fromString(fa87c0d0-afac-11de-8a39-0800200c9a66);我一般建议自己生成一个UUID不要用那些公开的标准UUID。原因很简单如果你的应用和另一个使用相同标准UUID的应用同时运行系统可能会把连接路由到错误的服务上。用UUID.randomUUID()生成一个然后硬编码到两端代码里这是最省事的做法。3.3 服务端与客户端的Socket创建服务端用listenUsingRfcommWithServiceRecord创建监听Socketprivate class AcceptThread extends Thread { private final BluetoothServerSocket serverSocket; private volatile boolean running true; public AcceptThread(BluetoothAdapter adapter) { BluetoothServerSocket tmp null; try { // 第一个参数是服务名会显示在系统蓝牙设置里 tmp adapter.listenUsingRfcommWithServiceRecord( BluetoothChat, MY_UUID); } catch (IOException e) { Log.e(TAG, 创建服务端Socket失败, e); } serverSocket tmp; } public void run() { BluetoothSocket socket; while (running) { try { // accept()会阻塞直到有客户端连接或调用cancel() socket serverSocket.accept(); } catch (IOException e) { break; } if (socket ! null) { // 连接成功交给管理类处理 connected(socket, socket.getRemoteDevice()); try { serverSocket.close(); } catch (IOException e) { Log.e(TAG, 关闭服务端Socket失败, e); } break; } } } public void cancel() { running false; try { serverSocket.close(); } catch (IOException e) { Log.e(TAG, cancel时关闭失败, e); } } }客户端用createRfcommSocketToServiceRecord创建连接Socketprivate class ConnectThread extends Thread { private final BluetoothSocket socket; private final BluetoothDevice device; public ConnectThread(BluetoothDevice device) { this.device device; BluetoothSocket tmp null; try { tmp device.createRfcommSocketToServiceRecord(MY_UUID); } catch (IOException e) { Log.e(TAG, 创建客户端Socket失败, e); } socket tmp; } public void run() { // 连接前先取消扫描扫描会严重拖慢连接速度 bluetoothAdapter.cancelDiscovery(); try { socket.connect(); } catch (IOException e) { // 连接失败关闭Socket并通知UI try { socket.close(); } catch (IOException e2) { Log.e(TAG, 关闭失败Socket出错, e2); } connectionFailed(); return; } // 连接成功重置ConnectThread引用 synchronized (BluetoothChatService.this) { connectThread null; } connected(socket, device); } public void cancel() { try { socket.close(); } catch (IOException e) { Log.e(TAG, cancel时关闭失败, e); } } }提示cancelDiscovery()这一步绝对不能省。蓝牙扫描和连接共用射频资源扫描期间连接成功率极低而且速度慢得让人抓狂。我实测过不取消扫描的情况下连接成功率大概只有三成左右。3.4 连接失败后的重试策略createRfcommSocketToServiceRecord创建的Socket在某些设备上第一次连接会失败这是已知的兼容性问题。一个常见的workaround是用反射调用隐藏的createRfcommSocket方法指定通道号1// 备用方案仅在标准方式失败时使用 try { Method m device.getClass().getMethod( createRfcommSocket, new Class[]{int.class}); socket (BluetoothSocket) m.invoke(device, 1); } catch (Exception e) { Log.e(TAG, 反射创建Socket也失败了, e); }这个方案不是万能的但在我经手的设备里大概能解决七八成的首次连接失败问题。注意这是非公开API不同厂商的ROM可能行为不一致只能作为兜底方案。4. 连接管理线程模型读写流与状态同步4.1 ConnectedThread的设计思路连接建立之后需要启动一个专门的线程来管理数据读写。这个线程持有BluetoothSocket的输入输出流循环读取数据并回调给UI层。private class ConnectedThread extends Thread { private final BluetoothSocket socket; private final InputStream inStream; private final OutputStream outStream; private volatile boolean running true; public ConnectedThread(BluetoothSocket socket) { this.socket socket; InputStream tmpIn null; OutputStream tmpOut null; try { tmpIn socket.getInputStream(); tmpOut socket.getOutputStream(); } catch (IOException e) { Log.e(TAG, 获取流失败, e); } inStream tmpIn; outStream tmpOut; } public void run() { byte[] buffer new byte[1024]; int bytes; while (running) { try { // read()会阻塞直到有数据或连接断开 bytes inStream.read(buffer); if (bytes 0) { byte[] data new byte[bytes]; System.arraycopy(buffer, 0, data, 0, bytes); // 回调给UI层处理 handler.obtainMessage( MESSAGE_READ, bytes, -1, data).sendToTarget(); } } catch (IOException e) { Log.e(TAG, 连接断开, e); connectionLost(); break; } } } public void write(byte[] data) { try { outStream.write(data); // 通知UI层消息已发送 handler.obtainMessage( MESSAGE_WRITE, -1, -1, data).sendToTarget(); } catch (IOException e) { Log.e(TAG, 写入失败, e); } } public void cancel() { running false; try { socket.close(); } catch (IOException e) { Log.e(TAG, 关闭Socket失败, e); } } }这里有几个设计决策值得说明。第一read()是阻塞调用所以必须放在独立线程里不能跑在主线程上。第二缓冲区大小设为1024字节这是RFCOMM单次传输的常见上限设太大没意义设太小会增加系统调用次数。第三每次读取后都新建一个byte数组来传递数据避免后续读取覆盖掉之前的内容。4.2 线程安全的状态管理BluetoothChatService需要管理多个线程的引用并且这些引用会被不同线程访问所以状态同步必须做好。public class BluetoothChatService { // 连接状态常量 public static final int STATE_NONE 0; public static final int STATE_LISTEN 1; public static final int STATE_CONNECTING 2; public static final int STATE_CONNECTED 3; private int state STATE_NONE; private ConnectThread connectThread; private ConnectedThread connectedThread; private AcceptThread acceptThread; // 所有状态变更方法都加synchronized public synchronized void start() { // 取消所有已有线程 if (connectThread ! null) { connectThread.cancel(); connectThread null; } if (connectedThread ! null) { connectedThread.cancel(); connectedThread null; } if (acceptThread null) { acceptThread new AcceptThread(bluetoothAdapter); acceptThread.start(); } setState(STATE_LISTEN); } public synchronized void connect(BluetoothDevice device) { // 连接前先清理 if (state STATE_CONNECTING) { if (connectThread ! null) { connectThread.cancel(); connectThread null; } } if (connectedThread ! null) { connectedThread.cancel(); connectedThread null; } connectThread new ConnectThread(device); connectThread.start(); setState(STATE_CONNECTING); } public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) { // 连接成功后清理其他线程 if (connectThread ! null) { connectThread.cancel(); connectThread null; } if (connectedThread ! null) { connectedThread.cancel(); connectedThread null; } if (acceptThread ! null) { acceptThread.cancel(); acceptThread null; } connectedThread new ConnectedThread(socket); connectedThread.start(); setState(STATE_CONNECTED); } private synchronized void setState(int newState) { state newState; // 通知UI层状态变化 handler.obtainMessage(MESSAGE_STATE_CHANGE, newState, -1).sendToTarget(); } }注意synchronized加在方法上锁的是BluetoothChatService实例。所有会修改线程引用的方法都必须加锁否则可能出现一个线程正在取消Socket另一个线程同时在创建新Socket的情况导致状态错乱。4.3 消息协议的简单设计聊天室的消息格式不需要太复杂但至少要能区分消息类型。我一般用一个字节的头部来标识// 消息类型定义 private static final byte MSG_TYPE_TEXT 0x01; private static final byte MSG_TYPE_EXIT 0x02; // 发送文本消息 public void sendTextMessage(String text) { byte[] textBytes text.getBytes(StandardCharsets.UTF_8); byte[] packet new byte[textBytes.length 1]; packet[0] MSG_TYPE_TEXT; System.arraycopy(textBytes, 0, packet, 1, textBytes.length); connectedThread.write(packet); } // 接收端解析 private void handleReceivedData(byte[] data) { if (data.length 1) return; byte type data[0]; switch (type) { case MSG_TYPE_TEXT: String text new String(data, 1, data.length - 1, StandardCharsets.UTF_8); // 更新UI显示 break; case MSG_TYPE_EXIT: // 对方主动断开 break; } }用UTF-8编码是必须的中文消息用默认编码在某些设备上会乱码。头部加一个字节的类型标识后续要扩展图片、文件传输也方便。5. 设备发现与列表展示的实操细节5.1 已配对设备与未配对设备的区分BluetoothAdapter.getBondedDevices()返回的是已经配对过的设备列表。这些设备可以直接连接不需要再走配对流程。未配对的设备需要通过扫描发现然后发起配对请求。// 获取已配对设备 SetBluetoothDevice bondedDevices bluetoothAdapter.getBondedDevices(); for (BluetoothDevice device : bondedDevices) { // 需要BLUETOOTH_CONNECT权限才能获取名称 String name device.getName(); String address device.getAddress(); // 添加到列表 }注意在Android 12上device.getName()和device.getAddress()都需要BLUETOOTH_CONNECT权限没授权的话会抛SecurityException。5.2 广播接收器的注册与过滤扫描结果通过广播接收器回调需要注册ACTION_FOUND、ACTION_DISCOVERY_FINISHED等几个Actionprivate final BroadcastReceiver receiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (BluetoothDevice.ACTION_FOUND.equals(action)) { BluetoothDevice device intent.getParcelableExtra( BluetoothDevice.EXTRA_DEVICE); // 跳过已配对的避免重复添加 if (device.getBondState() ! BluetoothDevice.BOND_BONDED) { // 添加到未配对列表 } } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { // 扫描结束更新UI状态 } } }; // 注册 IntentFilter filter new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_FOUND); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); registerReceiver(receiver, filter);扫描本身是个耗电操作startDiscovery()之后大概持续12秒左右会自动停止。不要频繁调用startDiscovery()如果正在扫描中再调一次系统会忽略第二次请求。5.3 配对流程的触发与监听点击未配对设备时发起配对请求// 需要BLUETOOTH_CONNECT权限 device.createBond();配对结果通过ACTION_BOND_STATE_CHANGED广播回调if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { BluetoothDevice device intent.getParcelableExtra( BluetoothDevice.EXTRA_DEVICE); int bondState intent.getIntExtra( BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR); if (bondState BluetoothDevice.BOND_BONDED) { // 配对成功可以发起连接了 } else if (bondState BluetoothDevice.BOND_NONE) { // 配对失败或被取消 } }配对过程中系统会弹出配对码确认对话框这个界面是系统级的应用无法定制。用户确认后配对完成之后就可以直接连接了。6. 那些文档里不会写的踩坑记录6.1 连接建立后立即发送数据导致丢失这个问题我踩过好几次。socket.connect()返回成功之后底层RFCOMM通道可能还没有完全就绪如果立刻调用write()发送数据对方可能收不到。解决方案是在连接成功后加一个短暂的延迟或者等对方先发一个握手消息。我一般会在connected()回调里延迟200毫秒再允许发送handler.postDelayed(() - { // 允许发送消息 isReady true; }, 200);200毫秒是个经验值大部分设备够用了。如果你追求更稳妥可以设计一个应用层的握手协议连接建立后客户端先发一个握手包服务端收到后回复确认双方都确认后才开始正常通信。6.2 多设备同时连接时的UUID冲突如果你的应用需要同时连接多个设备每个连接必须用不同的UUID。我见过一个项目用同一个UUID创建了三个客户端Socket结果只有第一个能连上后面两个全部超时。正确的做法是为每个连接生成独立的UUID或者用同一个UUID但确保服务端能区分不同的连接请求。RFCOMM的通道号是有限的同时连接数一般不要超过7个。6.3 后台线程未正确终止导致的内存泄漏AcceptThread的accept()和ConnectedThread的read()都是阻塞调用如果Activity销毁时没有正确取消这些线程它们会一直挂着持有Socket和Context的引用造成内存泄漏。在onDestroy()里必须调用stop()Override protected void onDestroy() { super.onDestroy(); if (chatService ! null) { chatService.stop(); } if (receiver ! null) { unregisterReceiver(receiver); } }stop()方法里要依次取消所有线程并关闭Socket。注意cancel()方法里关闭Socket会触发阻塞调用的IOException线程会自然退出不需要额外干预。6.4 不同厂商ROM的兼容性差异国产ROM在蓝牙权限和后台限制上有很多定制。我遇到过几种典型情况问题现象可能原因应对方案扫描不到任何设备定位权限未授予或定位服务未开启检查权限提示用户开启定位配对后连接超时ROM限制了后台蓝牙操作引导用户将应用加入后台白名单连接成功但收不到数据ROM的省电策略冻结了后台线程使用前台服务保持进程活跃名称显示为null权限不足或设备未广播名称检查BLUETOOTH_CONNECT权限这些问题的共同点是代码逻辑没问题但系统层面做了限制。排查的时候先用系统自带的蓝牙功能验证硬件是否正常然后再看应用层的日志。6.5 消息边界问题RFCOMM是字节流协议不保证每次read()返回的正好是一条完整消息。如果发送方连续快速发送多条消息接收方可能一次读到多条消息的拼接也可能一条消息分两次读到。解决这个问题有两种思路。第一种是在消息头部加长度字段接收方根据长度来切分。第二种是用分隔符比如每条消息以换行符结尾接收方按行读取。我倾向于第一种更可靠// 消息格式[4字节长度][1字节类型][N字节内容] public void writeMessage(byte type, byte[] content) { ByteBuffer buffer ByteBuffer.allocate(5 content.length); buffer.putInt(content.length 1); // 长度包含类型字节 buffer.put(type); buffer.put(content); connectedThread.write(buffer.array()); }接收端维护一个缓冲区每次读取后尝试解析完整消息不完整就等下次数据。7. 从聊天室Demo到实际项目的扩展方向这个聊天室跑通之后你手里就有了一套可用的蓝牙通信框架。往实际项目上靠有几个方向可以扩展。第一个方向是文件传输。在消息协议里加一个文件类型把文件分片传输接收端重组。关键点是分片大小要合适太小了传输效率低太大了容易丢包。我一般用4KB左右的分片配合确认重传机制。第二个方向是多人聊天。传统蓝牙的RFCOMM是点对点连接要做多人聊天需要实现一个星型拓扑一个设备作为服务端同时监听多个客户端连接服务端负责消息转发。每个客户端连接对应一个ConnectedThread服务端维护一个连接列表收到消息后遍历列表转发给其他客户端。第三个方向是切换到BLE。传统蓝牙的配对流程对用户来说还是有点重BLE的广播和扫描机制更适合物联网场景。但BLE的GATT协议栈比RFCOMM复杂得多建议先用传统蓝牙把连接管理、线程模型、消息协议这些通用问题解决掉再迁移到BLE上。我在实际项目里发现蓝牙开发最耗时间的不是写代码而是排查各种设备兼容性问题。同一套代码在不同品牌、不同系统版本的设备上表现可能完全不一样。所以日志一定要打全每个关键节点都要有日志输出出问题的时候才能快速定位是权限问题、连接问题还是数据问题。

相关新闻

HTML面试题必备清单,再也不怕丢失!

HTML面试题必备清单,再也不怕丢失!

每次面试都得看一遍面试题,每次准备的都放在了 Word 上,但是都误删丢失了,所以记录在这个地方吧。html面试题问题:什么是语义化标签?为什么要使用语义化标签?答案:语义化标签是指使用具有明确含义的 HTML 标签来描述页面内容,如 <header>、<nav>、<main…

2026/10/15 5:03:07 阅读更多 →
Spark BulkLoad 写入 HBase 多列场景实战:HFile 生成与避坑指南

Spark BulkLoad 写入 HBase 多列场景实战:HFile 生成与避坑指南

1. 项目背景与核心需求拆解1.1 为什么会有这个需求手里有一批数据要进HBase&#xff0c;量还不小&#xff0c;每天增量在千万级别。最开始用的是HBase原生的Put API&#xff0c;单条或者批量提交&#xff0c;跑了一段时间发现两个问题&#xff1a;一是写入吞吐上不去&#xff0…

2026/10/15 5:02:07 阅读更多 →
数据库设计避坑指南:范式权衡、索引优化与扩展性实战

数据库设计避坑指南:范式权衡、索引优化与扩展性实战

搞数据库设计这么多年&#xff0c;我越来越觉得这活儿像“打地基”。房子盖歪了还能装修找补一下&#xff0c;地基要是没打好&#xff0c;后面住着天天漏风渗水&#xff0c;想返工就得推倒重来。数据库表结构一旦设计失误&#xff0c;等系统上线、数据堆积如山的时候&#xff0…

2026/10/15 5:02:07 阅读更多 →

最新新闻

T100嵌入式平台Java jar包加载与反射调用全链路指南

T100嵌入式平台Java jar包加载与反射调用全链路指南

简介&#xff1a;本资源是一份面向Java开发人员与鼎捷T100/TiTop ERP系统二次开发工程师的实操指南&#xff0c;聚焦于在T100 4GL环境中集成并调用外部Java功能——解决传统4GL无法直接处理复杂业务逻辑&#xff08;如加密、HTTP通信、第三方SDK对接&#xff09;的技术瓶颈。文…

2026/10/15 5:45:36 阅读更多 →
COBOL报表开发实战:从VSAM读取到JCL调度的金融级输出

COBOL报表开发实战:从VSAM读取到JCL调度的金融级输出

简介&#xff1a;本资源是一份面向COBOL初学者与金融/政务系统维护人员的报表开发实践指南&#xff0c;聚焦传统主机环境下结构化报表生成的核心技术实现。内容以完整可运行的rpt001程序为载体&#xff0c;系统讲解ID Division标识定义、Environment Division中双文件&#xff…

2026/10/15 5:45:36 阅读更多 →
单卡24GB显存跑通70B大模型:airllm分层加载推理实战

单卡24GB显存跑通70B大模型:airllm分层加载推理实战

1. 当大模型装不进显卡时&#xff0c;我在单卡上跑通了70B推理第一次看到 airllm 这个项目名的时候&#xff0c;我正被一个很现实的问题卡住&#xff1a;手头只有一张 24GB 显存的卡&#xff0c;却要跑一个 70B 参数量的模型做推理验证。按常规思路&#xff0c;这几乎是不可能完…

2026/10/15 5:45:36 阅读更多 →
小程序按需注入未通过?uni-app项目组件注入优化实战指南

小程序按需注入未通过?uni-app项目组件注入优化实战指南

微信小程序的开发者&#xff0c;尤其是用 HBuilderX 写 uni-app 的朋友&#xff0c;大概率都撞上过这么一条构建提示&#xff1a;本地设置里勾上了“启用组件按需注入”&#xff0c;编译一跑&#xff0c;直接给你来一句“启用组件按需注入未通过”。第一次见到这个提示&#xf…

2026/10/15 5:45:36 阅读更多 →
Ponytail:React 18+ 组件状态可视化调试工具

Ponytail:React 18+ 组件状态可视化调试工具

1. “Ponytail”不是发型&#xff0c;而是开发者圈里悄然走红的轻量级调试辅助工具最近在几个前端协作群和开源项目 Issue 区里&#xff0c;频繁看到“ponytail skill”“ponytail 插件”这类词——不是美妆博主聊扎马尾技巧&#xff0c;也不是时尚穿搭指南&#xff0c;而是一群…

2026/10/15 5:45:36 阅读更多 →
Agent长期记忆实战:从存储到检索,解决记岔与遗忘难题

Agent长期记忆实战:从存储到检索,解决记岔与遗忘难题

1. 为什么“存下来”不等于“记得住”很多人第一次给 Agent 加记忆&#xff0c;思路特别朴素&#xff1a;搞个向量库&#xff0c;把对话历史一股脑塞进去&#xff0c;用的时候按相似度捞几条出来拼进提示词。跑个 demo 看着挺像那么回事&#xff0c;一旦对话轮次拉长、话题来回…

2026/10/15 5:44:36 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时&#xff0c;网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上&#xff0c;依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里&#xff0c;扩缩容到底难在哪做过大模型推理服务部署的人&#xff0c;大概都经历过这样的场景&#xff1a;一个团队共用一套推理集群&#xff0c;A 业务跑的是 7B 的小模型&#xff0c;B 业务跑的是 72B 的大模型&#xff0c;C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子&#xff1a;一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减&#xff0c;为了把启动时间压进两秒以内&#xff0c;我手贱地把一串自认为“没用”的 systemd 服务给 disable 了&#xff0c;其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →