2026/10/21 1:26:25

Android蓝牙防丢器源码解析:RFCOMM连接与RSSI测距全链路

Android蓝牙防丢器源码解析:RFCOMM连接与RSSI测距全链路 简介蓝牙防丢器应用开发详解配套完整工程包面向安卓开发者与物联网入门学习者聚焦低功耗蓝牙技术从设备扫描、连接、指令控制到防丢事件响应的完整实现流程。压缩包共63个文件约4.19MB包含Java源码、XML布局与工程配置、可直接安装的APK、JAR依赖库以及PNG图标和OGG/MP3提示音等多类型资源目录结构清晰便于按模块阅读。工程采用Eclipse ADT环境源码涉及蓝牙适配器与蓝牙GATT核心接口可对照理解蓝牙低功耗协议下的数据交换、连接状态回调及应用层服务组织。压缩包内保留了调试用APK及编译中间产物方便安装验证与渐进式阅读。已有734人浏览学习适合希望从实际工程入手掌握蓝牙通信、安卓组件协作及界面联调的开发者参考也可作为智能防丢产品相关课程设计的起步模板。1. 蓝牙防丢器我拆完这份源码发现它把 Android 蓝牙链路全串起来了一提“蓝牙防丢器”第一反应是现在市面上的 BLE 小标签但这份源码给的是另一个思路用 Android 原生蓝牙 API从扫描、配对、RFCOMM 连接、RSSI 信号强度监测一路做到断连报警和双向查找整套链路一个环节都没少。项目基于 Eclipse ADT 构建零第三方依赖逻辑全部跑在系统 API 上连防误报的滑动窗口、掉线兜底轮询都给你写好了。对刚接触蓝牙开发的人它是能把 BroadcastReceiver、Handler 轮询、Service 生命周期全部串起来的完整样本对已经在做智能硬件的老开发它真正值得看的是连接失败降级策略和防误报机制——这两块直接决定一套防丢逻辑能不能从 Demo 进化成产品。2. 项目结构与技术选型Eclipse ADT 下的原生蓝牙方案零依赖怎么搭2.1 目录结构五个 Java 文件五条清晰的职责边界解压源码后先看 src 目录。整个工程是标准的 Eclipse Android 工程布局主代码集中在五个类里BluetoothAntiLost/ ├── src/com/antilost/ │ ├── MainActivity.java // 主界面连接状态、报警开关、寻找按钮 │ ├── DeviceListActivity.java // 扫描结果列表展示设备名与信号强度 │ ├── BluetoothService.java // 核心服务持有连接、状态流转、对外广播 │ ├── RssiMonitor.java // 信号监测滑动窗口均值与阈值判定 │ └── AlarmPlayer.java // 报警播放MediaPlayer 状态机管理 ├── res/ │ ├── layout/ // 主界面与列表布局 │ ├── values/strings.xml // 文案与提示 │ └── drawable/ // 图标与按钮背景 ├── AndroidManifest.xml // 权限声明与 Activity/Service 注册 └── project.properties // ADT 构建配置target 版本声明这五个类各管一段MainActivity 只做 UI 事件把“点击扫描”“点击报警”翻译成对 BluetoothService 的方法调用DeviceListActivity 只负责展示扫描结果BluetoothService 持有全局连接状态所有对外的状态变化都从这里发广播通知 UI 刷新RssiMonitor 是被注入到服务里的独立线程专门做信号采样与判定AlarmPlayer 则是一个没有任何业务逻辑的播放器封装。这样拆的好处是后面改动某一环不会波及全局逻辑这种布局思路挺适合还没有上手 MCU 通信的新手参照。2.2 技术选型为什么是传统蓝牙 RFCOMM而不是 BLE很多人一看到“防丢器”三个字第一反应是 BLE 低功耗方案但这套源码用的是传统蓝牙 RFCOMM 串口协议也就是蓝牙 2.0/3.0 时代的 SPP。原因很现实BLE 意味着设备端必须实现 GATT 服务要定义 Service、Characteristic、Notify 回调硬件门槛和概念门槛同时抬高而传统蓝牙只要模块上电处于可配对状态手机端配对成功后就能用类似网络 socket 的方式收发字节流理解成本低、调试直观。从硬件兼容性角度看这个选择更接地气。市面上大量廉价的蓝牙串口透传模块只支持 SPP 协议不支持 BLE GATT。如果一上来就选 BLE意味着设备端必须换成支持 BLE 的芯片或模块成本上升一截。传统蓝牙的代价是功耗高、连接建立慢但对一个以学习链路为主要目标的工程样本来说先跑通 RFCOMM 再去迁移 BLE路径其实更顺——因为距离判定、报警去重这些核心业务逻辑与协议底层是解耦的后面替换通信层时不需要重写业务。2.3 依赖与构建没有 Gradle只有一个 android.jar整个工程没有任何第三方库依赖build path 里只有 android.jar连 support-library 都没引入。这在 Eclipse ADT 时代很常见系统 API 完全够用。好处是编译环境干净坏处是如果要把工程迁移到 Android Studio 或适配高版本系统权限模型得手动补齐——尤其是 Android 12 之后蓝牙权限进一步细分这一块需要额外适配。Manifest 里的权限声明是三个正好对应蓝牙开发的三个层次uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /BLUETOOTH 是基础通信权限没有它连 socket 都创建不了BLUETOOTH_ADMIN 负责扫描、开关蓝牙、修改系统蓝牙状态ACCESS_FINE_LOCATION 是 Android 6.0 之后扫描蓝牙设备的强制前置条件。系统把蓝牙扫描视为一种位置信息来源不授予定位权限扫描结果永远是空的——这个坑几乎每个初次接触蓝牙开发的人都会踩一次后面避坑章节会专门展开。3. 核心模块拆解扫描、RFCOMM 连接、RSSI 测距到报警的完整链路3.1 设备扫描BroadcastReceiver 处理系统广播缓存 RSSI 是关键扫描部分用的是系统广播机制。注册一个 BroadcastReceiver然后调用 BluetoothAdapter 的 startDiscovery()系统每发现一个设备就发一条 ACTION_FOUND 广播设备对象和信号强度都附在 intent 里。private final BroadcastReceiver mReceiver 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); short rssi intent.getShortExtra( BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE); if (device ! null) { mDeviceList.add(device); mRssiMap.put(device.getAddress(), rssi); mListAdapter.notifyDataSetChanged(); } } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { mProgressBar.setVisibility(View.GONE); } } };这段代码最值得注意的地方是 mRssiMap 的缓存逻辑。ACTION_FOUND 广播里的 RSSI 是瞬时值如果不存下来等用户从列表里点击某个设备时信号强度早就失效了。很多入门项目只存了设备对象没存 RSSI导致后面的距离判定没有数据源。注册和反注册要严格跟 Activity 生命周期对齐onCreate 里 registerReceiveronDestroy 里 unregisterReceiver漏了反注册会在页面关闭后收到空指针异常。3.2 RFCOMM 连接线程UUID、socket 与隐藏方法降级连接部分采用独立的 ConnectThread。创建 RFCOMM socket 时传入一个关键参数——SPP 服务的标准 UUID所有蓝牙串口模块都在这个 UUID 上监听。private static final UUID SPP_UUID UUID.fromString(00001101-0000-1000-8000-00805F9B34FB); private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { mmDevice device; BluetoothSocket tmp null; try { tmp device.createRfcommSocketToServiceRecord(SPP_UUID); } catch (IOException e) { Log.e(ConnectThread, socket create failed, e); } mmSocket tmp; } Override public void run() { mAdapter.cancelDiscovery(); try { mmSocket.connect(); } catch (IOException e) { // 降级策略某些设备系统服务未注册 SPP尝试反射建连 try { Method m mmDevice.getClass().getMethod( createRfcommSocket, int.class); Log.w(ConnectThread, fallback attempt); } catch (Exception e2) { Log.e(ConnectThread, connect fatal, e2); } } } }参数说明SPP_UUID 是蓝牙串口服务的全球通用标识符自定义 APP 和硬件模块之间要建立连接两端必须使用同一个 UUID。run() 里第一行 cancelDiscovery() 非常重要——Android 的蓝牙发现过程会占用射频资源如果不取消直接 connect 很可能超时失败。代码后面那段反射调用 createRfcommSocket(int) 是老项目常见的降级手段针对部分设备系统服务没有正确注册 SPP 的情况。这个方案在 Android 8.0 之后因为非 SDK 接口限制已经失效看到报错不用慌只要主通道 UUID 能稳定连接这段可以直接删掉。3.3 RSSI 距离判定滑动窗口过滤抖动避免误报防丢器的核心体验是“离开一定距离就报警”。怎么判断距离这份源码没有用复杂的高斯模型而是采用工程上最实用的方式对 RSSI 信号做滑动窗口均值判断。public class RssiMonitor { private final ArrayDequeInteger mWindow new ArrayDeque(); private static final int WINDOW_SIZE 5; private static final int RSSI_THRESHOLD -70; public boolean isOutOfRange(short rssi) { mWindow.addLast((int) rssi); if (mWindow.size() WINDOW_SIZE) { mWindow.removeFirst(); } if (mWindow.size() WINDOW_SIZE) { return false; } int avg 0; for (int v : mWindow) { avg v; } avg / mWindow.size(); return avg RSSI_THRESHOLD; } }逻辑说明RSSI 是负值单位 dBm数值越小信号越弱。这里的意思是最近五次扫描的平均值低于 -70dBm系统判定设备已离开安全范围。为什么用平均值而不是单次采样蓝牙信号在室内受多径效应和人体遮挡影响抖动非常剧烈某次采样到 -85dBm 可能只是手机天线被手掌挡了一瞬间直接报警会制造大量误报。窗口均值是消除抖动的最朴素手段。参数调整的常见做法是看环境室内多墙环境阈值放宽到 -75 甚至 -80开阔环境收紧到 -65。窗口大小同理从 5 改成 3 会让响应更快但更容易抖动。这个环节是整个防丢器体验的成败所在阈值和窗口必须配合实际场景反复调试我在后面避坑部分还会详细说。3.4 掉线报警ACTION_ACL_DISCONNECTED 广播加 Handler 轮询双保险RSSI 判定处理的是“连接还在但信号弱”的情况但设备如果被直接拿走或突然断电可能没有机会再收到 RSSI 采样需要另一条更硬的机制——监听系统蓝牙协议栈发出的连接断开广播。private final BroadcastReceiver mACLReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(intent.getAction())) { BluetoothDevice device intent.getParcelableExtra( BluetoothDevice.EXTRA_DEVICE); if (device ! null device.equals(mConnectedDevice)) { startAlarm(); startReconnectLoop(); } } } };ACTION_ACL_DISCONNECTED 是链路层连接断开广播由蓝牙协议栈主动发出比任何 Rssi 轮询都可靠。同时BluetoothService 内部还有一个 Handler 做每分钟一次的状态检查防止系统因进程被压低而延迟广播。双保险的用意是广播响应快轮询兜底稳。如果两种机制在同一时间窗口内同时触发报警AlarmPlayer 内部有一层状态去重确保铃声不会叠加播放——这个去重细节在设计报警模块时非常关键。整套流程下来扫描、连接、测距、报警四个环节刚好覆盖一个防丢器的全部闭环。4. 运行与调试Eclipse 导入、真机连接蓝牙串口模块、打包 APK4.1 环境准备JDK 与 SDK 版本匹配Eclipse ADT 工程要跑起来版本匹配是第一道门槛。JDK 建议用 1.8ADT 插件用最后稳定版 23.0.xSDK Platform 建议安装 Android 4.4API 19——这是 project.properties 里的默认 target。如果本机只有更高的 API Level打开工程会提示 target 未设置或编译报错。project.properties 内容如下targetandroid-19可以手动改成 android-23编译也能过但要注意这会把工程拖入 Android 6.0 的运行时权限模型。源码里没有动态申请权限的逻辑只在 Manifest 做了声明所以运行时权限需要自己补——这个后面打包部分会讲到。一个比较稳妥的顺序是先保持 android-19 把工程跑通确认蓝牙链路正常再考虑升级 target 和适配新权限模型两步分开做排查问题范围会小很多。4.2 导入 Eclipse两个容易踩的坑工程解压后如果根目录包含 .project 和 .classpath 文件直接用 Eclipse 的 File - Import - Existing Projects into Workspace 选择工程目录。这里最容易踩的坑是“Copy projects into workspace”选项——勾选后 Eclipse 会把工程复制到工作区副本之后你改的是副本原目录不动经常出现“改了代码没效果”的错觉。建议不勾选直接引用原目录。导入后如果报 “missing required source folder: gen”这是 ADT 的注解处理没有自动执行先试 Project - Clean 清理一次。还不行就右键工程 Android Tools - Fix Project Properties这个操作会自动补齐缺失的 gen 目录和 R.java 编译产物。这两步处理掉编译问题后工程应该能直接跑在模拟器上——不过模拟器里看不到蓝牙设备后面真机部分才是重点。4.3 真机连接蓝牙串口模块这个项目必须在真机上跑Android 模拟器默认没有蓝牙硬件。硬件端需要准备一个蓝牙串口透传模块HC-05 或 HC-06 这类即可。两者都支持 SPP 协议HC-05 主从一体HC-06 只能做从机防丢场景手机做主设备、模块做从设备选哪一个都能用。模块上电后处于可配对状态先打开手机系统蓝牙设置手动搜索并配对默认配对密码一般是 1234 或 0000。系统级配对成功后再打开 APP 扫描。这里有一个关键注意事项如果系统蓝牙已经占用了该模块的连接通道APP 里的 RFCOMM socket 连接会失败因为 Android 同一时刻只允许一个射频通道对应一个设备。遇到这种情况先把系统蓝牙里对该模块的连接断开再回 APP 里重连。4.4 打包 APK 与动态权限适配老工程打包路径是右键工程 Android Tools - Export Signed APK签名密钥用普通 keystore 即可。真正需要提前处理的是 Android 6.0 以上的动态权限——Manifest 里的 ACCESS_FINE_LOCATION 在 6.0 以上运行时必须动态申请否则点击扫描后列表永远为空。第一次二次开发时第一件事就是在扫描入口补权限请求if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { requestPermissions( new String[]{ Manifest.permission.ACCESS_FINE_LOCATION }, 1); }这段代码的意义在于不是进入 APP 就弹窗而是用户真正点击“开始扫描”时才发起请求。这符合 Android 运行时权限的最佳实践。补充说明一点target 保持 android-19 时这段代码会走不通因为 checkSelfPermission 是 23 以上的 API所以做动态权限适配时project.properties 里的 target 也要一起升到 android-23。5. 避坑指南蓝牙防丢器开发里最容易翻车的五个细节5.1 扫描不到任何蓝牙设备现象是点了扫描按钮列表一直空转手机系统蓝牙里明明能看到设备APP 却什么都收不到。原因大概率是 Android 6.0 以上缺少定位权限系统把蓝牙扫描归类为位置信息采集不给定位权限扫描结果会被系统直接拦截。排查方式进入系统设置查看该 APP 的权限页确认“位置信息”已开启也可以执行下面的命令验证授权状态adb shell dumpsys package 你的包名 | grep -A 5 ACCESS_FINE_LOCATION如果授权正常但扫描还是空再看手机是否开启了“省电模式”——部分厂商的省电策略会延长蓝牙扫描间隔导致扫描结果迟迟不出现。解决办法是引导用户关闭该 APP 的电池优化白名单限制。5.2 能配对但连接秒断现象是系统设置里配对成功APP 里点击连接后一两秒内就回调失败。最常见的原因是系统蓝牙已经占用 RFCOMM 通道APP 去连接同一个设备时发生冲突另一种情况是部分模块出厂固件角色设定有兼容问题。解决这类问题第一步先断开系统蓝牙对该模块的连接再回 APP 重连更彻底的做法是 APP 发现设备后不依赖系统配对记录而是用 BluetoothDevice.createBond() 自己建立配对关系断开时也自己 removeBond完整管理配对的整个生命周期。5.3 APP 退到后台后报警失效现象是手机锁屏后不响铃回到前台才触发报警仿佛报警被延迟了。这是典型的国产 ROM 省电策略导致后台进程休眠BroadcastReceiver 和 Handler 的定时任务全部被挂起。代码层面的应对措施是把蓝牙连接和状态监测挪进一个前台 Service并加一条常驻通知系统在内存紧张时回收前台进程的优先级会低很多。同时要引导用户在系统设置里允许 APP 后台运行、关闭省电限制。后台存活与省电之间的矛盾是通信类 APP 绕不开的生态问题单靠代码无法根治。5.4 RSSI 忽高忽低导致距离判断不准现象是设备没有任何移动室内环境每隔几分钟就误报一次。原因是室内多径效应加人体遮挡RSSI 短时间抖动幅度可达 10 到 15dBm。单点采样显然不够窗口均值能缓解但还不够稳。建议在窗口均值之外再加一条变化方向约束连续两次采样都朝变弱方向移动才更新判定结果。比如第一次 -72dBm、第二次 -75dBm、第三次 -78dBm方向一致才触发弱信号预警如果采样值在 -70 上下反复横跳说明设备处于边界地带应该保持当前状态不误报。这一个方向判断加进去误报率会明显下降。5.5 点停止报警但铃声停不下来现象是用户点击了停止报警按钮铃声仍然在响重启 APP 才恢复正常。原因是报警播放器使用了带循环模式的 MediaPlayer停止时没有完整释放底层音频资源状态机错乱后 start() 无法正确重置。停止报警的完整流程必须是三步连走mPlayer.stop(); mPlayer.reset(); mPlayer.release(); mPlayer null;stop() 停止当前播放reset() 让播放器回到空闲状态release() 释放底层资源置空引用后下次报警重新创建 MediaPlayer。这套状态机写法是音频开发的基本功漏掉任何一步在低版本 Android 上都会出现类似玄学般的表现。6. 二次开发把防丢器改成低功耗寻物标签的三条路径6.1 路径一BLE 低功耗改造现在的防丢器产品形态几乎全是 BLE。升级方向集中在设备端和连接层设备端换成支持 BLE 的模块手机端把 RFCOMM socket 换成 BluetoothGatt 连接扫描逻辑从 startDiscovery 换成 startLeScanRSSI 读取从广播参数变成 onCharacteristicChanged 回调。这个项目里真正核心的“滑动窗口距离判定”和“报警去重”模块可以直接复用因为它们只关心 rssi 数值和连接状态与底层协议完全解耦。6.2 路径二增加断连位置记录在触发报警的广播回调里用 LocationManager 获取一次当前坐标写入 SQLite 或者 SharedPreferences。设备重新连接后把这些断连坐标在地图上打点就能形成一条关于“在哪儿丢的、最后出现在哪儿”的时空线索。这个功能对找钥匙、找包非常实用。实现时注意报警与定位必须合并成一个原子操作不要拆成两个入口否则回调时序错乱会拿到过期位置。6.3 路径三报警策略改造为时间段感知在 RssiMonitor 外层加一个策略接口把阈值判断从硬编码中抽离出来白天和夜间采用不同阈值public interface AlarmStrategy { boolean shouldAlarm(int avgRssi); }白天在开放空间把阈值收紧到 -65dBm夜间在家里放松到 -80dBm极大的减少夜间误报。测试时可以在界面上加一个 SeekBar 模拟不同距离的 RSSI 输入避免每次验证都要拿着手机跑到户外来回走。我从这个项目里学到的最大一个习惯是凡是涉及报警、状态切换这类有时序逻辑的代码动手改之前一定先把“扫描状态、连接状态、报警状态”的流转图画出来把所有入口合并到一个状态表里再动代码。防丢器这类通信项目逻辑散落写在各个回调里是最容易翻车的写法状态表已经是我的固定动作。希望帮到你。本文还有配套的精品资源点击获取