尧图精选

Android蓝牙SPP通讯Demo全解析:权限、扫描、配对、连接与收发避坑指南

🕒 发布时间:2026/9/3 20:42:58 📁 来源:尧图网络
简介这是一个面向Android开发者的蓝牙通信示例工程完整演示了蓝牙控制的核心流程获取默认蓝牙适配器、检查并开启或关闭蓝牙、扫描周边设备、建立RFCOMM连接以及消息收发适合刚接触蓝牙接口或需要快速搭建通信原型的开发者学习。压缩包共包含1544个文件大小24.82MB主要文件类型有XML布局与配置、PNG图片、Class编译产物、JSON配置、Java源码等资源已有702人学习下载。示例中重点讲解了蓝牙适配器的典型用法包括调用开始扫描方法搜索附近设备使用广播接收器监听设备发现与扫描结束事件通过串口服务UUID创建蓝牙套接字连接并利用输入输出流完成数据发送和接收。同时展示了项目清单文件中的蓝牙权限配置、连接异常处理、缓冲区与超时机制以及设备列表和连接状态的界面交互设计。这份代码可作为后续扩展文件传输、多设备连接等功能的基础模板帮助开发者快速理解并改造自己的蓝牙应用。 先说我看到的现象网上搜“android 蓝牙通讯demo”翻出来十有八九是老项目targetSdkVersion 还是23以前的用法。新手照着抄编译能过一运行要么闪退要么扫描列表一片空白好不容易连上又秒断。我自己最早带人做这类需求时也栽过跟头后来才明白Android蓝牙通讯这一块的坑90%不是逻辑问题而是系统权限模型和API版本差异问题。这篇文章就把我实测下来能跑通的经典蓝牙SPP通讯Demo完整拆一遍把权限、扫描、配对、连接、收发数据这几个环节里的关键细节和翻车点一次说清。适合刚接触Android蓝牙开发的初学者也适合正在接单片机、串口模块但被系统版本折腾得头疼的开发者。1. 查清你做的到底是哪种“蓝牙通讯”很多新手拿到的第一个蓝牙Demo跑不通不是因为代码写错而是根本没用对蓝牙协议栈。Android里“蓝牙通讯”至少分成三个完全不同的方向搞混了后面全白做。1.1 经典蓝牙SPP和BLE GATT到底该做哪种如果你要做的场景是“手机和一块蓝牙串口模块比如HC-05、HC-06互相发字符串”那走的是经典蓝牙的SPP协议底层数据通道是RFCOMM Socket用起来最像TCP先配对再连接连通之后就是一个稳定的双向流。如果场景是“手机读取一个低功耗传感器的数据”比如手环、温湿度计、体重秤那走的是BLE低功耗蓝牙的GATT协议。BLE的数据通道完全不同需要先扫描再连接然后去发现Service和Characteristic读写都在Characteristic上操作不建立Socket连接。这两者的代码模型、权限要求、调试方式差别非常大。我的建议是拿到需求先问清楚对方设备是什么。如果你连的是HC-05、HC-06、杰理这类串口透传模块或者对方明确说用SPP、RFCOMM那就是经典蓝牙如果对方说低功耗、BLE、GATT那别用Socket那套老老实实去写GATT回调。1.2 别把A2DP、HFP这种音频通道当成数据通道热搜词里出现了“蓝牙a2dp切sco模式”“蓝牙音频接收器模块”说明有人把音频类的蓝牙Profile和数据通讯混在一起了。A2DP是传音乐的HFP是通话音的它们跟SPP是不同层的Profile就算连上了蓝牙耳机你也拿不到通用数据流Socket。App想通过A2DP通道收自定义数据系统层就不开放不用在这上面浪费时间。所以在Demo里要认清SPP只为双向数据流服务手机和蓝牙模块都能作为服务端或客户端。日常我们用手机做客户端蓝牙模块做服务端这样最简单。如果手头只有两台手机也可以让一台开服务端等另一台来连效果一样。1.3 一个能跑起来的开发环境要求环境方面没有太多玄学。建议直接用Android Studio当前稳定版AGP版本高一点没关系SDK编译版本至少设到34。蓝牙相关的API变化从Android 6到Android 14都有核心代码逻辑没变但权限弹窗和校验方式一直在改所以尽量用新环境去适配别用五年前的工程硬跑。2. 工程配置里最容易被卡住的三个Permission细节蓝牙Demo跑不通排第一的原因就是权限。这不是一句“清单里加上蓝牙权限”就完事了的Android 6、Android 12、Android 13/14分别改过三轮权限模型少任何一个都可能搜不到设备或直接闪退。2.1 清单文件里的权限声明随着系统版本一直在变下面这份AndroidManifest.xml是我实测可用的权限集合里面既有老系统需要的权限也有新系统需要的新权限uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /前两个是老权限从Android 12开始失效但如果你不写低版本手机真机上可能出问题。BLUETOOTH_SCAN和BLUETOOTH_CONNECT是Android 12引入的前者管扫描后者管连接和已配对列表。ACCESS_FINE_LOCATION在经典蓝牙扫描时不是必须但很多国产ROM扫描结果依赖定位开关为了兼容性和少被问“为什么搜不到设备”建议保留。2.2 动态权限申请从定位权限到蓝牙专属权限权限光写在清单里没用运行时要动态请求。不同版本系统要请求的权限不一样需要在代码里做分支判断private void checkAndRequestPermissions() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { requestPermissions(new String[]{ Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT }, REQUEST_CODE); } else if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { requestPermissions(new String[]{ Manifest.permission.ACCESS_FINE_LOCATION }, REQUEST_CODE); } }这里有个细节Android 12以下的手机BLUETOOTH权限是普通权限但扫描设备时依然涉及位置信息所以请求ACCESS_FINE_LOCATION。千万不要只请求蓝牙权限忘了定位权限否则回调里没有任何报错就是搜不到设备最坑的一种情况。2.3 Android 14上更容易被忽视的“运行时检查”到了Android 14即使你写了BLUETOOTH_CONNECT并弹窗请求调用连接类API时仍可能抛SecurityException。我在实际开发中踩过这个坑排查了很久才发现是部分国产ROM对蓝牙权限的“精确开关”做了额外校验。稳妥做法是每次要调用蓝牙API前先判断一下权限状态没有权限就提示用户去授权而不是直接try-catch。另外访问已配对设备列表getBondedDevices()在Android 12以上也必须持有BLUETOOTH_CONNECT权限否则会直接抛异常。这个点非常隐蔽很多教程里没提但几乎人人会踩。3. 把搜索、配对、连接串成一条完整链路权限处理完接下来就是跑通一条完整的“发现-配对-连接”链路。这一段是蓝牙Demo的核心骨架我按实际调用顺序来讲每一步都说明为什么这么写。3.1 拿到BluetoothAdapter检查蓝牙状态入口就是BluetoothAdapter。必须先判断设备支不支持蓝牙再判断蓝牙开没开。打开蓝牙有两种方式一是跳系统页面让用户手动开二是通过startActivityForResult弹系统授权框。BluetoothAdapter adapter BluetoothAdapter.getDefaultAdapter(); if (adapter null) { // 当前设备没有蓝牙模块 return; } if (!adapter.isEnabled()) { Intent enableBtIntent new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }3.2 扫描周边设备和读取已配对设备扫描用startDiscovery()扫描结果通过BroadcastReceiver接收。在扫描前一定要先cancelDiscovery()否则上一次扫描未结束新的扫描可能不会触发这个很容易被忽略。public void startScan() { if (adapter.isDiscovering()) { adapter.cancelDiscovery(); } adapter.startDiscovery(); }接收ACTION_FOUND广播IntentFilter filter new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_FOUND); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); registerReceiver(receiver, filter); private 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 ! null device.getName() ! null) { // 展示到列表记录name和address } } } };建议同时把adapter.getBondedDevices()里的已配对设备也加载进列表因为很多蓝牙模块之前已经配对过重新扫描反而看不到直接从已配对列表拿更稳。3.3 配对逻辑与状态监听设备如果还没配对调用device.createBond()触发系统配对弹窗。配对是异步的过程状态变化会通过ACTION_BOND_STATE_CHANGED广播发回来。这个广播里要重点看BOND_BONDED这个状态表示配对成功可以继续走连接流程。“扫码配对蓝牙”这个需求在系统原生API层面是不开放的App不能直接弹自定义扫码配对页能做的只有调用系统配对弹窗或者引导用户去系统蓝牙设置里手动配对。所以做Demo时别在配对交互上花太多精力直接让系统弹窗就好。3.4 建立Rfcomm Socket连接配对完成不等于连接完成。真正的数据连接要创建BluetoothSocket。经典蓝牙用createRfcommSocketToServiceRecord传入一个UUID作为服务标识。这里有个关键点服务端和客户端必须用同一个UUID才能连上。private static final UUID SPP_UUID UUID.fromString(00001101-0000-1000-8000-00805F9B34FB); private void connectDevice(BluetoothDevice device) { new Thread(() - { try { if (socket ! null) { socket.close(); } socket device.createRfcommSocketToServiceRecord(SPP_UUID); socket.connect(); if (socket.isConnected()) { runOnUiThread(() - statusText.setText(连接成功)); startReadLoop(); } } catch (IOException e) { runOnUiThread(() - statusText.setText(连接失败 e.getMessage())); } }).start(); }注意socket.connect()是阻塞操作Android不允许在UI线程直接调如果强行在UI线程执行轻则ANR重则直接崩。另外在连接之前最好先adapter.cancelDiscovery()因为扫描和连接同时进行会严重降低连接成功率这是官方文档明确建议过的实际测试确实如此。4. 这条数据通道真正干活的部分收发与粘包处理Socket一旦连上剩下的就是Java里普通的InputStream和OutputStream操作。但越简单的环节越容易写出“没毛病但就是不好用”的代码我重点讲收发两个方向的实际处理思路。4.1 发送端OutputStream注意事项发送就是拿socket.getOutputStream()write后必须flush。建议把发送封装成一个独立方法统一走UTF-8编码。private void sendMessage(String message) { if (socket null || !socket.isConnected()) { // 提示未连接 return; } try { OutputStream os socket.getOutputStream(); os.write(message.getBytes(UTF-8)); os.flush(); } catch (IOException e) { e.printStackTrace(); } }这里有个容易忽略的点不要每次发送都调用一次socket.getOutputStream()拿一次存成成员变量就行。频繁获取流对象在某些机型上会引发流状态错乱。另外如果对端是单片机串口模块它可能只认固定协议比如以\r\n结尾或者固定帧头发送前要看对端说明文档别自己随便定格式。4.2 接收端InputStream阻塞读与粘包处理接收端必须在一个独立线程里循环读InputStream因为read()是阻塞的放UI线程必死。下面这个是我常用的接收循环private void startReadLoop() { new Thread(() - { try { InputStream is socket.getInputStream(); byte[] buffer new byte[1024]; int bytes; while (socket.isConnected() (bytes is.read(buffer)) ! -1) { String data new String(buffer, 0, bytes, UTF-8); runOnUiThread(() - Log.d(BluetoothDemo, 收到: data)); } } catch (IOException e) { e.printStackTrace(); } }).start(); }说到粘包这是很多做过TCP通讯的人会比较敏感的问题。蓝牙Socket和TCP一样是流式传输数据边界不保证和一帧对一帧。也就是说对端可能一次发了“HELLO”“WORLD”两条消息你read()出来可能是“HELLOWORLD”也可能分两次读。处理思路和串口通讯差不多要么约定固定长度要么用结束符分帧。最简单的方案是让对端每条消息以\n结尾接收方按\n切分再逐条处理。这块别看简单实际项目里很多“数据看起来是乱码”的问题都是粘包引起的。4.3 可选的心跳与断开重连如果你要把Demo改成生产可用建议加一个心跳机制。硬件模块比较常见的情况是手机休眠、蓝牙栈被系统回收、对端断电这些都不会立刻触发socket异常导致App显示“已连接”实际已经断了。加心跳就是每几秒钟发一个约定好的短指令超过几次没收到对端的响应就判定超时关闭socket并更新UI。断线重连要小心一个坑连接失败或断开后旧的socket必须调用close()否则下次connect()大概率报“Socket closed”。很多二次连接失败的问题都出在这。5. 实测Demo时的典型翻车现场这部分都是我自己在真机上正经踩过、并且看着别人反复踩的坑。每一条都给到排查思路而不是直接丢结论。5.1 “Socket closed”是怎么发生的如果你在connect()里看到Socket closed异常优先去查资源释放。最常见的是你重复点击了连接按钮第二次点击时上一个socket还没释放系统直接拒绝。其次是蓝牙模块掉电或超出范围底层Socket被系统关闭。排查时可以打印socket.isConnected()但注意isConnected()只表示本地socket状态不代表远端还在不要把它当在线状态用。我一开始就吃过这个亏界面显示已连接实际上对面早就断电了。5.2 设备明明开了蓝牙却搜不到遇到扫描列表为空按这三步排查先在系统设置里进蓝牙页面看系统本身能不能搜到那个设备。如果系统都搜不到是设备没进入可发现模式很多蓝牙模块需要先断电再上电才能进入可被发现状态。如果系统能搜到但App搜不到检查权限重点是定位权限和定位服务是否开启。部分国产手机即使在Android 12以下也会在定位关闭时过滤掉扫描结果。5.3 配对成功但connect超时这个坑比较隐蔽。表现是系统已配对成功但App里调用socket.connect()一直超时。原因是配对完成和SPP服务可用之间有时间差尤其是HC-05这类模块配对完成后还需要几百毫秒才真正就绪。解决办法是配对成功的广播回调里不要立刻connect稍微延迟一点比如300到500毫秒实测会明显提高成功率。另外一个常见原因是对端模块没有开启服务端监听。如果你是用两台手机做测试必须在一台手机上先启动BluetoothServerSocket服务端另一台才能连进去。服务端代码很简单BluetoothServerSocket serverSocket adapter.listenUsingRfcommWithServiceRecord(DemoServer, SPP_UUID); BluetoothSocket clientSocket serverSocket.accept(); serverSocket.close();accept()是阻塞的也要放子线程。5.4 断开之后再连接就失败这是蓝牙Demo最常见到的“二进宫”问题。我先说结论连接前必须做三步清理第一取消扫描第二关闭旧的socket第三给对端留出重新进入待连接状态的时间。尤其是断开蓝牙模块后模块可能需要重新上电才能恢复服务端监听此时App反复重连只会一直超时。我再额外提醒一个真实场景当你用的是CSR8510这类USB蓝牙适配器配合Windows测试或者用杰理模块接单片机时手机端逻辑不变但硬件侧的供电和复位一定要稳。蓝牙模块在上电瞬间以及状态切换瞬间很容易丢配置我在调试时就遇到过好几次模块死锁怎么连都失败最后拔电重插才恢复。这也是为什么有人会在热搜里搜“蓝牙删除不了”“蓝牙接收器代码10”有时候手机与硬件反复配对失败操作系统会缓存错误配对信息需要到系统蓝牙设置里“忽略此设备”再重新配对这个操作在写Demo时也值得写进提示里。写在最后回到开头那个问题Android蓝牙通讯Demo为什么大多跑不通本质原因是很多教程用固定代码去适配不断变化的系统而Android的蓝牙权限从6.0到12.0再到14.0一直在调整。我这里分享一个我个人习惯的做法写Demo前先确认三件事第一对端设备是什么协议第二当前手机的系统版本和权限要求第三测试时尽量用一台自己熟悉的真机而不是模拟器。模拟器蓝牙就是个摆设省不了这一步。最后分享一个小技巧平时调试蓝牙通讯时我会在ListView里把设备的MAC地址也显示出来很多模块的默认名称都是HC-05这种多个设备同名只靠名称根本分不清看到MAC地址能少很多折腾。另外连接成功后先在文本框里发送“AT”测试一下很多串口模块回AT就表示通道通了这个习惯帮我区分过好多次“是蓝牙问题还是硬件协议问题”。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →