
简介android蓝牙聊天应用源码面向Android开发初学者及蓝牙通信入门者提供一套可直接编译运行、便于二次开发的蓝牙聊天示例。项目从权限声明开始依次讲解BluetoothAdapter适配器管理、设备发现与配对、BluetoothSocket连接、输入输出流双向传输、断开连接以及BroadcastReceiver监听蓝牙状态等核心流程并演示了设备列表、消息输入与发送等典型UI交互。资源共52个文件以Java源码、XML布局、class文件为主附带可直接安装的APK、界面截图和源码说明文档压缩包仅4.14MB轻量易下载。配图与说明文档可帮助快速理解Java源码和界面布局降低学习门槛。目前已有191人学习适合希望通过完整实例快速掌握Android蓝牙通信原理、数据收发及异常处理的开发者。1. 从“能聊”到“聊得稳”一份蓝牙聊天源码究竟值钱在哪不少开发者第一次接触 Android 蓝牙都是从一份“蓝牙聊天”的源码包开始的。界面通常不漂亮代码也不一定符合最新的架构规范但它把 Android 蓝牙通信的完整链路摊开了本机蓝牙状态的监听、远端设备的发现与配对、BluetoothSocket的连接建立、输入输出流的阻塞读写、以及跨线程的消息分发。这些恰恰是车载系统、工业手持终端、医疗外设调试工具里最常用到的基础能力——拨开 UI 看通信层你会发现它本质上是一个基于 RFCOMM 的即时消息系统。有人会问现在都什么年代了直接 WebSocket 或者 MQTT 不香吗但蓝牙的优势在于离线、低功耗、无需基站。对做 Android 系统定制、物联网设备调试、或者对讲类硬件的从业者来说理解这份源码的价值不在聊天本身而在于把 SDP 服务发现、UUID 匹配、socket 生命周期和线程模型一次性打通。接下来我不去复述某个具体开源包的代码行而是按一线工程师拿到这类资料后会怎么拆解、怎么改、怎么避坑的顺序完整讲一遍。提示本文所有代码片段为通用实现示例用于说明原理实际工程中请以你持有的源码包为准。2. 蓝牙聊天的技术底座从 Radio 层到 RFCOMM 的完整链路2.1 经典蓝牙还是 BLE先搞清这份源码跑在哪条协议栈上拿到“android 蓝牙聊天的应用源码”这类包第一步不是急着导入 Android Studio而是确认它使用的是经典蓝牙Bluetooth Classic还是低功耗蓝牙BLE。绝大多数名为“蓝牙聊天”的源码包走的是经典蓝牙因为实现逻辑直观BluetoothAdapter负责本机蓝牙开关和扫描BluetoothDevice代表远端设备BluetoothSocket承载数据流。相比之下BLE 的聊天实现通常要自己定义 GATT 服务端和客户端的 UUID、处理 MTU 协商和分包复杂度高出一个量级。怎么看打开源码里的AndroidManifest.xml检查有没有BLUETOOTH_CONNECT、BLUETOOTH_SCANAndroid 12或旧的BLUETOOTH、BLUETOOTH_ADMINAndroid 11 及以下权限。再全局搜一下BluetoothGatt或GattServer关键字——如果完全没有基本可以断定是经典蓝牙。这决定了后面所有的调试工具选型和手机兼容性判断。经典蓝牙聊天使用 RFCOMM 协议它模拟串口通信把数据包装成流。应用层只需拿到 InputStream 和 OutputStream然后像读写文件一样处理即可。但“像文件一样”恰恰是最大的错觉蓝牙链路是无线传输随时可能断开系统缓冲区足够小时写入会阻塞读线程则可能无限期挂起。2.2 UUID 不是随便填的 128 位串SDP 服务发现的匹配逻辑一个典型的最小服务端建立连接代码如下// 服务端注册一个 SPP 服务并等待客户端连接 BluetoothAdapter adapter BluetoothAdapter.getDefaultAdapter(); BluetoothServerSocket serverSocket adapter.listenUsingRfcommWithServiceRecord( BluetoothChat, // 服务名称会出现在远端设备可见服务里 UUID.fromString(00001101-0000-1000-8000-00805F9B34FB)); // SPP 标准 UUID BluetoothSocket socket serverSocket.accept(); // 阻塞等待客户端建立连接的代码// 客户端通过远端设备的 UUID 发起连接 BluetoothDevice device adapter.getRemoteDevice(XX:XX:XX:XX:XX:XX); BluetoothSocket socket device.createRfcommSocketToServiceRecord( UUID.fromString(00001101-0000-1000-8000-00805F9B34FB)); socket.connect(); // 阻塞直到连接建立或超时第一段逻辑说明listenUsingRfcommWithServiceRecord中的 UUID 就是 SDP 服务发现的凭据。聊天双方必须使用同一个 UUID否则服务发现阶段会直接失败。第二段说明客户端createRfcommSocketToServiceRecord会先从远端设备的 SDP 记录里查找匹配 UUID 的服务通道然后建立连接。这里有一个从业者必须知道的细节UUID 的作用不只是“密码”。Android 系统内部会为它映射一个 RFCOMM channel 编号。如果你使用的是自定义 UUID则服务端注册时系统会分配一个动态 channel如果使用的是00001101-0000-1000-8000-00805F9B34FBSPP 标准 UUID多数设备会映射到 channel 1。这解释了为什么某些源码里直接写死 UUID 也能跑通——但强烈不建议因为不同厂商对标准 UUID 的处理并完全一致自定义 UUID 更可控。注意从 Android 6.0 开始蓝牙相关权限被归类为危险权限需要运行时动态申请从 Android 12API 31起还需要精确区分BLUETOOTH_SCAN和BLUETOOTH_CONNECT两个权限。2.3 源码里的经典三层架构Adapter、Service、Activity 各管什么稍加整理的蓝牙聊天源码一般会按三层职责拆分这是值得保留下来的骨架模块核心职责关键成员 / 方法易错点BluetoothAdapter系统蓝牙状态入口enable()、startDiscovery()、getDefaultAdapter()不支持蓝牙的环境返回 nullChatService常为 Service 或线程管理类管理连接状态、收发线程ConnectedThread、AcceptThread、ConnectThread线程退出时未关闭 socketChatActivity界面交互、消息展示obtainMessage()、Handler.handleMessage()直接操作子线程 UI我第一次把一个“能聊天”的源码跑通后做了个压力测试拔出其中一台手机的蓝牙模块模拟硬件关闭结果 App 直接闪退。原因就在 ChatService 没有对BluetoothAdapter.getDefaultAdapter()返回 null 的情况做防御。这类边界处理恰恰是生产级源码和教学级源码的分水岭。3. 把源码跑起来从导入到双机联调的完整步骤3.1 运行环境与编译参数Android Studio 版本不是越高越好这类源码绝大多数是几年前写的可能面向 API 23~28。直接拿最新的 Android Studio 打开通常会被 Gradle 版本兼容性卡住。我一般会先做三件事把compileSdkVersion和targetSdkVersion调到我本机已安装的 SDK 版本但minSdkVersion保持源码原值避免引入新权限模型导致逻辑改动过大。将 Gradle 版本升到与 IDE 匹配的版本但先不升级 Android Gradle PluginAGP到最新因为老源码里的compile关键字和apt用法在高版本 AGP 下会被直接报错。检查android:exported属性。如果项目里有带intent-filter的 ActivitytargetSdk 31 时必须显式声明该属性否则安装失败。改完后同步优先解决报错再跑。常见的报错是defaultConfig里缺少vectorDrawables.useSupportLibrary true以及res目录下的图片格式问题。3.2 双机联调的真实现场一台做服务端一台做客户端无论是 Android Studio 还是其他 IDE模拟器之间无法直接进行蓝牙发现与配对。正确姿势是准备两台真机并且最好分别为不同品牌以覆盖兼容性差异。操作步骤为两台设备都打开蓝牙和定位服务Android 6.0 要求定位权限用于蓝牙扫描。服务端设备先进入“聊天”界面触发listenUsingRfcommWithServiceRecord进入等待连接状态。客户端设备进入“扫描设备”列表等到发现服务端设备点击后发起连接。连接成功后通过OutputStream.write(byte[])发送消息对端InputStream.read(byte[])读出内容。如果连接失败顺着三个方向排查权限是否全部授予服务端是否真的进入了 accept 状态很多源码只在点击某个按钮后才开始监听UUID 是否一致。还有一个隐蔽坑部分国产 ROM 在蓝牙配对弹窗出现时如果 App 退到后台连接会被系统挂起。解决方法是提前在系统蓝牙设置里完成配对再回到 App 内建立连接。3.3 调试利器从 logcat 到蓝牙 HCI 日志的逐层开关代码能跑通之后下一步是掌握日志分析技巧。Android 的蓝牙日志分为两层应用层logcat和协议栈层HCI snoop log。应用层调试点主要是BluetoothAdapter的状态回调比如adb logcat -s BluetoothChat:I BluetoothAdapter:D这个命令会过滤出BluetoothChat和BluetoothAdapter两个 tag 的日志。连接失败时重点看是否有Service discovery failed、read failed, socket might closed or timeout等关键字。比如read failed, socket might closed or timeout, read ret: -1通常意味着远端主动断开而Service discovery failed则指向 UUID 不匹配或 SDP 记录查询失败。协议栈层则要抓 HCI 日志进行抓包分析# 开启蓝牙 HCI snoop log无需 root开发者选项里也有对应开关 adb shell settings put global bluetooth_hci_log 1 # 复现问题后抓取日志文件Android 12 之后路径有变化 adb pull /data/misc/bluetooth/logs/这份日志里能看到 LMP 层的连接建立、角色切换Master/Slave、SNIFF 状态切换等。如果设备频繁进入 SNIFF 省电模式导致收发延迟增大可以从这个日志里找到证据再决定是否用L2CAP通道替代 RFCOMM。大部分做车载或对讲机应用的从业者到这一层就已经超过了大多数阅读源码的初学者。4. 从“可以聊”到“聊得稳”协议设计、异常恢复和参数调优4.1 不只是 print为聊天源码换上可扩展的消息帧协议教学源码通常直接把字符串转 byte 数组后发出接收端按行读取。这种实现聊个天够用但一旦要传文件、心跳、状态通知就暴露问题了无法区分一条完整消息的边界遇到中文字符串用read(byte[])时还可能发生半个字符被截断的乱码。我一般会加一个极简的帧协议。下面是一个简单的帧协议设计给了实际可用的数据结构| 2 byte 长度字段 (小端序) | 1 byte 消息类型 | N byte 消息体 |对应的发送端代码示意// 发送带帧头的消息 public void sendMessage(byte[] payload, byte type) throws IOException { int len payload.length; byte[] frame new byte[len 3]; frame[0] (byte) (len 0xFF); frame[1] (byte) ((len 8) 0xFF); frame[2] type; System.arraycopy(payload, 0, frame, 3, len); outputStream.write(frame); outputStream.flush(); }接收端代码示意// 接收端按帧解析 DataInputStream dis new DataInputStream(inputStream); while (running) { int len dis.readUnsignedShort(); // 先读 2 字节长度 byte type dis.readByte(); byte[] body new byte[len]; dis.readFully(body); callback.onMessage(body, type); }逻辑说明第一段中outputStream.write必须是完整写入整个 frame然后立即 flush避免积压在缓冲区。outputStream可以使用BufferedOutputStream包装但要注意在写入后调用flush()否则数据可能停留在内部缓冲区而延迟发出。第二段接收端用readUnsignedShort()和readFully()组合保证恰好读取一个完整帧的数据。参数说明长度字段用 2 字节理论最大为 65535 字节对普通文本聊天足够。若后续要传文件应将长度字段扩展到 4 字节或将文件分包处理。消息类型字段预留 1 字节可定义0x01文本,0x02图片,0x03心跳这样扩展时不影响老设备解析。4.2 断了怎么接连接状态机与退避重连的策略无线链路的网络特征是易断蓝牙尤甚。源码级实现里断开后通常直接回到监听状态。但实际使用中比如车载蓝牙与手机连接场景是断开后三秒内自动重连。常见的做法是维护一个连接状态机enum BluetoothChatState { STATE_NONE, // 无连接 STATE_LISTEN, // 等待远端连接 STATE_CONNECTING, // 正在连接远端 STATE_CONNECTED // 已连接 }状态机转移逻辑通常这样组织// 状态切换统一入口 public synchronized void setState(BluetoothChatState state) { this.state state; handler.obtainMessage(Constants.MESSAGE_STATE_CHANGE, state.ordinal(), -1).sendToTarget(); }当STATE_CONNECTED状态下连接异常断开时不同源码的触发路径不同有的是读线程readFully()抛IOException有的是系统关闭 socket 导致读返回 -1。统一的兜底逻辑是关闭所有 socket 和流切换为STATE_LISTEN或按业务需要进入重连调度。重连策略我用一个简单的指数退避参数表已配置好重连次数等待时长注释11s首次失败立即重试22s仍然失败加倍34s继续加倍48s最多到 8s 封顶510s固定间隔避免频繁扫描耗电这里的等待时长是每次重连发起前阻塞的毫秒数需要放在子线程中处理不能在主线程 sleep。另外重连前必须重新执行cancelDiscovery()来保证设备发现机制不会占用 RFCOMM 资源很多代码漏了这一步导致重连永远失败。4.3 缓冲区与线程模型两个参数和三次 read 的边界收发线程的可信写法是各占一条线程。读线程负责阻塞读取并分发消息写线程由 UI 触发写入。这是通用做法但有一个隐藏细节如果将输出流包装为BufferedOutputStream那么每次write后不flush实际发送的时间点会与预期不符。InputStream.read(byte[])的返回值表示本次实际读取的字节数。在阻塞模式下它可能等到至少一个字节才返回。如果对端发送了 10 字节而你这个byte[]只有 4 字节read 会返回 4剩余 6 字节留在内核缓冲区等待下次读取。这就是为什么必须用readFully()或循环读取直到满足协议长度而不是“读一次当作一条消息”。Android 蓝牙底层 socket 的接收缓冲区大小一般由系统协议栈决定不同厂商差异较大。应用层能做的是在建立连接后设置 TCP-like 参数但 RFCOMM 并不完全等同 TCPtry { socket.setReceiveBufferSize(64 * 1024); // 建议 32KB~128KB过大无意义 socket.setSendBufferSize(64 * 1024); } catch (IOException e) { Log.w(TAG, set buffer size failed, e); }setReceiveBufferSize在 RFCOMM 上并不总是生效它实际影响的是内核 socket 内存水位。如果传输大消息建议应用层自行分包加序号。如果传输的是高频心跳一个小包一个 flush 反而会更可靠。4.4 源码里常见的线程同步问题三处必须先改的隐患很多教学版源码在工程化层面有三个毛病我拿到源码后会优先处理第一处ConnectedThread里的cancel()方法直接close()socket但读线程正阻塞在read()上。在 Java 层close 一个正被 read 的 socket 会导致IOException这没问题但个别源码在 cancel 后还引用流对象做二次关闭造成空指针。修正方式所有关闭操作放在 try-catch 中且每个流只关一次。第二处Handler的消息队列在高频消息下可能堆积。默认sendMessage是异步投递如果发送速率过快UI 线程 handleMessage 忙不过来延时逐渐增大。建议使用sendMessageDelayed时先removeMessages去重或者改为obtainMessagesetDatasendToTarget以减少对象创建。第三处也是最容易被忽略的BluetoothAdapter.startDiscovery()是一个重量级操作会占用蓝牙协议栈约 12 秒。如果在这个窗口期内调用connect()连接会异常隐式断开。正确顺序是先确认停止发现再发起连接。if (bluetoothAdapter.isDiscovering()) { bluetoothAdapter.cancelDiscovery(); // 必须先停止发现再建立连接 } socket.connect();cancelDiscovery()是异步的不要立刻执行下一行。严谨的做法是注册ACTION_DISCOVERY_FINISHED广播在回调里发起连接。若不想引入广播复杂度至少加一个 100~300ms 的延时来等待底层状态稳定这是一个实用的小技巧。5. 当聊天源码不再“聊天”把蓝牙通道改造成通用数据管道5.1 消息类型扩展与业务解耦聊天的底层能力是“双向透明传输”。在这个层面之上能承载的业务远超“聊”。比如硬件调试工具将手机和嵌入式板卡用蓝牙配对后板卡侧通过串口透传模块将日志数据发给手机手机 App 解析并显示。这块的源码改法很直接把 ChatService 改名为 DataChannelService把消息帧里的 type 字段利用起来。具体的做法是定义消息路由表// 消息路由分发 public interface MessageHandler { void handle(byte[] payload); } // 注册: type 0x01 - 文本显示0x02 - 日志实时图表更新 router.register(0x01, textHandler); router.register(0x02, logChartHandler);逻辑说明这里的MessageHandler接口负责把不同类型的数据分发到对应的 UI 处理器。如此改造之后蓝牙模块只做数据管道业务层与通信层完全解耦。做车载或对讲类项目时这比在 Activity 里堆 if-else 清晰得多后续即便从 RFCOMM 切到 L2CAP 或 BLE业务代码也不必重写。5.2 大文件传输场景下的 MTU、分包与校验普通聊天源码处理的是短消息但真实项目常需要传文件。RFCOMM 理论最大包长一般为 1017 字节L2CAP 的默认 MTU 减去协议头Android 上层屏蔽了 MTU 协商细节因此应用层更适合用固定 1024 字节分包并加上序号与校验。一个可落地的文件发送流程先发送文件头消息文件名UTF-8 字节数组 总大小4 字节。循环读取文件每次读取 1024 字节包上 2 字节序号发送。接收端收到文件头后打开输出流边收边落盘每收 n 个包回复一次 ACK发送端超时未收到 ACK 则重发未确认包。这里有几个参数值得记录参数建议值说明数据包大小1024 字节适应 RFCOMM 底层 MTU避免 IP 分片ACK 频率每 16 包平衡带宽与丢包重传粒度超时时间2000ms低于 1000ms 易误判高于 5000ms 影响速度队列深度未确认包最多 64 个滑动窗口控制内存占用这套做法是通用的传输控制思路与具体某份源码无关。真正生产环境中还可以升级为在 L2CAP 上实现可靠传输层但那需要更深入地处理内核缓冲区和流量控制普通应用场景用上面的方案已经足够稳定。5.3 连接参数的进阶玩法让蓝牙更快、更省电、更抗干扰如果项目对连接质量有要求可关注的细节点有扫描模式BluetoothAdapter.startDiscovery()会强制设备进入可发现模式同时执行查询扫描。这会显著增加功耗并可能干扰已建立的连接。如果只是要连接已知 MAC 地址的设备不必走 discovery直接getRemoteDevice(mac)然后连接即可。链路超时蓝牙的 link supervision timeout 默认值通常为 20 秒取决于厂商。这意味着如果对端突然移出范围、无任何报文本端需要 20 秒才能感知连接断开。对于要求快速切换的场景可以尝试使用BluetoothDevice.connectGatt的 autoconnect 参数连接中断后自动重连但经典蓝牙 RFCOMM 没有直接暴露这个参数。实测更有效的方式是应用层心跳每 5 秒发一个空包3 个未响应则判定断线。这比任何底层参数都直接。关于 SNIFF 模式当链路空闲时蓝牙协议栈会自动进入 SNIFF 省电状态此时响应延迟可达 10ms 到 100ms 级别。如果聊天应用要求低延迟比如对讲可以考虑在建立 RFCOMM 后通过反射调用BluetoothDevice的非公开接口setSniffMode来请求不进入省电。但这个方法依赖具体 Android 版本内部实现兼容性风险高不建议写入正式版本。5.4 源码工程的模块化重构思路最后把源码包引向工程化教学版源码往往是单模块工程Service、Handler、Adapter 全部搅在一起。我一般按如下边界做轻量重构bluetooth/包封装BluetoothChannel、BluetoothServer、BluetoothScanner对外暴露 start/stop/send/receive 接口。protocol/包定义帧格式、编解码器、消息类型常量。ui/包聊天界面、设备列表、配对管理。重构的价值并非为了好看而是为了可单元测试。蓝牙逻辑无法在 JVM 单元测试中跑通但协议层的编解码可以。把byte[]的打包与解包逻辑从通信层剥离后你才可以无硬件地测试协议兼容性——这部分会在实践中越来越多地被团队要求。6. 藏在源码之外的进阶验证技巧有一件事是大多数源码包不会告诉你它的代码能跑通不代表在真实网络条件下稳定。把应用放进弱网环境、锁屏场景、多任务切换场景里压一遍才能暴露它是否需要重构。第一个技巧使用蓝牙 HCI snoop log 来验证连接断开时应用层的错误路径是否被正确触发。具体操作流程是先把日志枚举出来再对复现后的错误路径逐一验证确保应用层每个分支都真实走到了 close 和清理逻辑。# 1. 开启 HCI 日志开发者选项或命令 adb shell settings put global bluetooth_hci_log 1 # 2. 杀掉蓝牙协议栈进程使设置生效部分机型需要 adb shell killall com.android.bluetooth # 3. 复现断开问题后获取日志 adb pull /data/misc/bluetooth/logs/btsnoop_hci.log ./抓到的日志可用 Wireshark 打开过滤hci_evt_disconnect_complete里面会给出断开原因码。比如0x08表示连接超时0x13表示远端用户主动断开0x3E表示连接失败频繁。原因码能极大缩小问题的排查范围是环境干扰、对端行为还是本端超时参数过短。第二个技巧把手机固定在 USB 连接电脑上跑adb shell top -H -p pid观察蓝牙收发线程的 CPU 占用。一个常见问题是某些源码在read()返回 -1 后没有退出循环而是继续创建新流反复读取导致 CPU 占用飙升、设备发热。用这个命令可以快速确认线程是否异常存活。第三个技巧测试“收发大消息 频繁断开重连”组合场景。经验表明很多源码单独跑聊天没问题但一旦连续传输 1MB 以上的数据内存占用就会线性上涨。排查方式是在 Android Studio 的 Profiler 里观察内存分配定位是否在接收循环里反复创建大数组。如果发现每次接收都new byte[1024]一个可选的优化方式是复用主缓冲区数组——但要注意并发写入问题这会在连接断开时引发数组越界风险。把这几个技巧和数据指标用来核对源码质量CPU 占用在空转状态下应接近 0%文本消息从发送到展示的端到端延迟一般在 20ms 到 80ms 范围超过 200ms 就要怀疑是 SNIFF 模式或 UI 主线程阻塞连续断开重连 100 次进程内存不应有明显增长。跑完这三项再回头读源码里的线程模型和缓冲区逻辑就会清楚每一行代码为什么会那样写。本文还有配套的精品资源点击获取