蓝牙防丢器Android源码解析:经典蓝牙SPP打通搜索连接到断开报警
简介一份针对蓝牙防丢器APP开发的完整源码工程面向需要学习BLE蓝牙通信与Android APP开发的初学者或进阶者。资源核心为BLEDemo3项目涵盖设备扫描、连接管理、命令发送、事件响应及用户界面等模块并基于Eclipse ADT环境构建可帮助理解Android蓝牙低功耗开发的整体流程。包内共63个文件以Java源码、XML界面布局、class编译文件为主辅以APK安装包、JAR库、声音资源及项目配置文件压缩后仅4.19MB便于快速下载分析。当前已有734人浏览学习说明主题受到关注。通过阅读源码读者可以掌握BluetoothAdapter与BluetoothGatt的用法、GATT协议交互、连接生命周期管理以及UI实时状态显示等关键知识点并可将方案迁移到钥匙、钱包、宠物等防丢场景或进一步扩展远程控制与云服务功能。1. 蓝牙防丢器一套源码把“搜索-连接-报警”做成完整闭环钥匙串上的防丢贴片和手机断开连接的那一刻你正站在电梯口对着手机屏幕发呆——迟到的报警提醒让找钥匙变成了一场翻包现场。这套「蓝牙防丢器」源码解决的正是这个场景它基于经典蓝牙协议用 Java 在 Eclipse ADT 环境下实现了从设备搜索、RFCOMM socket 连接、链路监测到断开报警的完整防丢闭环。手机和防丢贴片之间维持一条长连接一旦链路断开或连续多次检测异常App 立刻响铃加震动提醒你东西落下或已远离。适合正在做蓝牙外设配套 APP 的开发者也适合想找一份完整 APP 源码对照学习蓝牙连接流程的初学者。工程体量不大但搜索、连接、监测、报警四个环节的逻辑全部打通跑通一遍基本能掌握蓝牙开发的主干。2. 工程结构与技术选型Eclipse ADT 时代的蓝牙开发骨架2.1 技术选型经典蓝牙 SPP 协议与 Java这项目的选型带着明显的时代特征。Eclipse ADT 是 Android 早期的官方开发环境对应 Android 4.x 时代那时候 BLE 低功耗蓝牙还没普及手机和防丢配件之间做实时双向通信最稳的方案就是经典蓝牙的 SPP 串口协议。SPP 协议的实现方式很直白手机端通过 BluetoothAdapter 拿到远端设备对象用固定的 UUID 服务号创建一条 RFCOMM socket 通道连接建立后双方在这条通道上收发数据。对比今天主流的 BLE 方案经典蓝牙的差异点很明显对比项经典蓝牙 SPPBLE 低功耗蓝牙连接方式RFCOMM socketGATT 服务与 characteristic功耗较高适合持续供电设备极低纽扣电池可用数月传输速率高适合大数据量低适合小数据量控制指令断开判断socket.isConnected() 直观依赖 RSSI 阈值或心跳包代码复杂度阻塞式连接逻辑简单异步回调状态机较多放到今天看经典蓝牙的代码依然有学习价值它把连接的建立、维护、断开讲得非常直白先理解这套“建立 socket → 守护链路 → 断开报警”的模型再去看 BLE 的 GATT 连接那一套异步回调会轻松很多。而且不少工业级蓝牙设备至今还在用 SPP 协议这份源码里的连接代码拆出来可以直接复用。2.2 工程目录拆解每个文件在防丢链路里的位置拿到源码解压后先别急着导入 Eclipse把目录结构过一遍。典型的 ADT 工程长这样蓝牙防丢器/ ├── AndroidManifest.xml // 权限与组件声明 ├── src/com/antilost/demo/ │ ├── MainActivity.java // 主界面搜索、连接、状态展示、报警按钮 │ ├── DeviceListActivity.java // 设备列表页搜索结果的展示与选中回传 │ ├── ConnectThread.java // 连接线程socket 建立与断开 │ ├── ConnectionMonitor.java // 连接监视线程轮询链路状态 │ └── AlarmPlayer.java // 报警播放器铃声 震动 ├── res/ │ ├── layout/ │ │ ├── activity_main.xml // 主界面布局状态区 设备信息 按钮区 │ │ └── device_list.xml // 设备列表布局 │ ├── values/ │ │ ├── strings.xml // 字符串资源 │ │ └── colors.xml // 颜色资源 │ └── drawable/ │ └── ic_launcher.png // 应用图标 └── libs/ // 第三方库目录本项目为空几个关键文件在防丢链路里的位置先记一下。MainActivity 是入口负责蓝牙适配器的初始化和主界面刷新搜索按钮、连接状态显示、报警停止按钮都在这DeviceListActivity 是独立页面搜索结果全部在这展示用户点击某个设备后把设备对象通过 Intent 回传ConnectThread 和 ConnectionMonitor 是核心一个负责建立连接一个负责守护连接防丢判断全在 ConnectionMonitor 里AlarmPlayer 只干一件事——响铃加震动它被设计成独立类和连接逻辑完全解耦。这个分层有一个值得借鉴的设计报警逻辑不直接写在 Activity 里而是抽成 AlarmPlayer连接线程也不直接操作 UI而是通过 Handler 发消息。这样拆的好处是后续如果要加“远程报警”“静音时段”“音量渐强”这些功能只需要改 AlarmPlayer不用动连接代码。2.3 AndroidManifest 权限配置蓝牙开发第一步蓝牙开发的第一步是权限声明漏了权限后面所有功能都会翻车。这份源码的 AndroidManifest.xml 里声明了三个权限?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.antilost.demo !-- 蓝牙基础权限发起连接、查询状态 -- uses-permission android:nameandroid.permission.BLUETOOTH / !-- 蓝牙管理权限开启蓝牙、发起搜索 -- uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / !-- 震动权限报警时手机震动提醒 -- uses-permission android:nameandroid.permission.VIBRATE / application android:icondrawable/ic_launcher android:labelstring/app_name android:themestyle/AppTheme activity android:name.MainActivity android:labelstring/app_name intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity activity android:name.DeviceListActivity android:labelstring/device_list_title / /application /manifestBLUETOOTH 权限是基础权限用于发起连接、查询蓝牙状态这类常规操作BLUETOOTH_ADMIN 权限级别更高用于开启蓝牙、发起搜索这类管理性质的操作。两个权限缺一不可少了 BLUETOOTH_ADMINstartDiscovery 会直接抛 SecurityException。VIBRATE 权限是给 AlarmPlayer 的震动提醒用的声明了它才能调用 Vibrator 服务。注意如果这份老代码要迁移到 Android 6.0 以上系统还需要动态申请 ACCESS_FINE_LOCATION 定位权限。系统把蓝牙扫描归类到定位权限之下不申请的话搜索功能会静默失败这是老工程移植到新系统时最容易踩的坑。3. 核心模块拆解从设备搜索到报警响铃的完整链路3.1 设备搜索适配器发现与广播接收机制搜索设备的核心是 BluetoothAdapter 的 startDiscovery() 方法。它发起的是异步搜索结果通过系统广播返回所以 MainActivity 里注册了一个 BroadcastReceiver 来接收两种关键广播ACTION_FOUND发现新设备和 ACTION_DISCOVERY_FINISHED本次搜索结束。// MainActivity.java 中的广播接收器 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.getBondState() ! BluetoothDevice.BOND_BONDED) { mDeviceList.add(device); mAdapter.notifyDataSetChanged(); Log.d(TAG, 发现设备: device.getName() RSSI: rssi); } } // 搜索结束更新界面按钮状态 else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { mSearching false; updateUI(); } } };启动搜索的代码在搜索按钮的点击事件里// 点击搜索按钮 public void onSearchClick(View view) { // 先取消已有搜索避免重复搜索导致结果混乱 if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } mBluetoothAdapter.startDiscovery(); mSearching true; updateUI(); }这段代码里有几个细节值得注意。EXTRA_RSSI 是设备信号强度单位是 dBm数值一般在 -40 到 -100 之间越大表示信号越强、距离越近。源码把 RSSI 取出来显示在设备列表里用户可以选择信号最强的设备优先连接。BOND_BONDED 是系统常量表示设备已经和手机配对过过滤掉这些设备可以让列表更干净避免把以前连过的旧设备混进来。广播接收器的生命周期管理是关键必须在 onResume 里注册、在 onPause 里注销。漏掉注销的话Activity 销毁后仍然能收到系统广播轻则内存泄漏重则在一个不存在的界面上操作导致空指针崩溃。把这行注销代码放在 onPause 里能一次性解决整个项目里最容易犯的低级错误。3.2 建立连接ConnectThread 与 socket 生命周期用户从设备列表选中一个设备后DeviceListActivity 通过 Intent 把 BluetoothDevice 传回 MainActivity然后 MainActivity 启动 ConnectThread 建立连接。这段代码是整个项目的核心// ConnectThread.java public class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { mmDevice device; BluetoothSocket tmp null; try { // SPP 串口服务 UUID经典蓝牙通信的全球默认服务号 tmp device.createRfcommSocketToServiceRecord( UUID.fromString(00001101-0000-1000-8000-00805F9B34FB)); } catch (IOException e) { Log.e(TAG, 创建socket失败, e); } mmSocket tmp; } Override public void run() { // 连接前必须取消设备发现否则会显著降低连接速度 mBluetoothAdapter.cancelDiscovery(); try { mmSocket.connect(); // 连接成功通知主线程刷新UI mHandler.obtainMessage(MSG_CONNECT_SUCCESS).sendToTarget(); // 启动连接监视线程开始防丢判断 new ConnectionMonitor(mmSocket).start(); } catch (IOException e) { Log.e(TAG, 连接失败, e); mHandler.obtainMessage(MSG_CONNECT_FAILED).sendToTarget(); try { mmSocket.close(); } catch (IOException closeException) { Log.e(TAG, 关闭socket失败, closeException); } } } }UUID 00001101-0000-1000-8000-00805F9B34FB 是 SPP 串口服务在全球统一注册的服务号绝大多数经典蓝牙串口设备都认这个号。如果你的防丢器是定制固件可能使用私有 UUID这时要查设备厂商协议文档来替换。connect() 是一个阻塞调用可能耗时几秒甚至更久所以必须放在子线程里执行放在主线程会直接触发 ANR这是蓝牙开发入门最常见的一个翻车点。连接成功后的动作也值得琢磨源码没有停留在“连上就行”而是立刻启动 ConnectionMonitor 守护线程。这个线程每隔一秒检查一次 socket 的连接状态一旦发现异常就触发防丢报警。连接和守护分开两个线程各司其职后期想调整监测频率只需要改 ConnectionMonitor不用动连接逻辑。3.3 防丢判断与报警触发连续失败确认机制ConnectionMonitor 是防丢判断的核心它解决一个关键问题怎么区分“设备真丢了”和“信号瞬时波动”。如果检测到一次异常就报警用户坐在工位上手机都会响个不停。源码用了一个连续失败计数机制// ConnectionMonitor.java public class ConnectionMonitor extends Thread { private final BluetoothSocket mSocket; private int mMissCount 0; // 连续检测失败次数 private static final int MAX_MISS 3; // 连续3次判定为丢失 public ConnectionMonitor(BluetoothSocket socket) { mSocket socket; } Override public void run() { while (!isInterrupted()) { // socket 状态异常视为一次失败 if (!mSocket.isConnected()) { mMissCount; Log.w(TAG, 连接异常第 mMissCount 次); if (mMissCount MAX_MISS) { // 连续多次失败判定设备丢失通知主线程报警 mHandler.obtainMessage(MSG_DEVICE_LOST).sendToTarget(); break; } } else { mMissCount 0; // 状态恢复计数清零 } try { Thread.sleep(1000); // 每秒检查一次 } catch (InterruptedException e) { break; } } } }MAX_MISS 设为 3 是有实际考量的。蓝牙信号在室内受墙壁、金属物体、人体遮挡影响很大偶尔一次 isConnected() 返回 false 可能只是瞬时波动立刻报警只会让用户每次都冲过去检查。连续 3 次检测失败也就是 3 秒内持续异常才判定为真丢失这个阈值可以通过修改 MAX_MISS 调整室内环境信号稳定可以改成 2让报警更灵敏户外复杂环境改成 5减少误报。主线程收到 MSG_DEVICE_LOST 消息后调用 AlarmPlayer 的 startAlarm 方法// AlarmPlayer.java public void startAlarm(Context context) { // 使用系统闹钟铃声音量高、辨识度强 Uri alertSound RingtoneManager.getDefaultUri( RingtoneManager.TYPE_ALARM); mMediaPlayer new MediaPlayer(); try { mMediaPlayer.setDataSource(context, alertSound); mMediaPlayer.setLooping(true); // 循环播放直到用户手动停止 mMediaPlayer.prepare(); mMediaPlayer.start(); } catch (IOException e) { Log.e(TAG, 播放报警音失败, e); } // 同步触发震动pattern: 停0ms、震500ms、停1000ms循环 Vibrator vibrator (Vibrator) context.getSystemService( Context.VIBRATOR_SERVICE); long[] pattern {0, 500, 1000}; vibrator.vibrate(pattern, 0); }选择 TYPE_ALARM 而不是默认通知音是有意为之。通知音音量偏小放在口袋里很容易听不见闹钟铃声的音量和辨识度都更高是防丢报警场景下更合理的选择。震动模式 {0, 500, 1000} 的意思是立即开始、震动 500 毫秒、停止 1000 毫秒然后循环。这样的节奏比持续震动更容易让人注意到也不容易和普通消息提醒混淆。停止报警时调用 stopAlarm()同步释放 MediaPlayer 和 Vibrator避免资源占用。到这里整个防丢链路完成了闭环搜索设备 → 建立 socket 连接 → 每秒守护链路 → 连续 3 次异常触发报警 → 响铃加震动直到用户确认。这套逻辑看着简单但已经是防丢产品的最小完整实现。4. 运行与调试从 Eclipse 导入到真机跑通的全过程4.1 环境搭建Eclipse ADT 与 JDK 版本匹配这项目是老工程跑起来最关键的是环境版本匹配。Eclipse ADT 本身已经停止更新但跑老工程绰绰有余。我常用的组合是 JDK 1.7 ADT 23.0.2 Android SDK 4.4API 19这套组合跑绝大多数 ADT 时代的工程都顺滑。如果本机装的是新版 JDK注意在 Eclipse 的 Installed JREs 里把编译级别切到 1.7否则工程可能因为字节码版本不兼容报错。导入步骤就四条Eclipse 菜单 File → Import → Existing Android Code Into Workspace选中解压后的工程目录勾选 Copy projects into workspace然后点 Finish。如果导入后出现红色报错先看是不是 R.java 没有生成——右键项目选择 Project → Clean让它强制重建资源文件大部分情况这一步就好使了。如果还报错看 Console 面板里的具体信息最常见的两类一是 SDK 版本不匹配右键项目 Properties → Android把 Target 换成已安装的 SDK 版本二是 JDK 编译级别不匹配按上段的方式调整。注意Eclipse 对中文工程路径支持不好工程所在目录最好全英文否则可能出现资源文件无法编译的怪异问题属于环境类的玄学坑。4.2 真机调试为什么必须用真机以及老代码的新系统适配蓝牙功能在模拟器上是跑不起来的必须真机调试而且需要有实际的蓝牙防丢设备配合。调试前先打开手机蓝牙把防丢器调到配对模式。如果手头没有现成的防丢器用一块支持 SPP 的蓝牙串口模块也能替代连接逻辑完全一致。这里有个实际经验老工程最好用 Android 8.0 以下的手机跑。Android 8.0 起系统对隐式广播做了严格限制老代码里注册的部分系统广播可能收不到而且运行时权限机制变化较大蓝牙扫描需要动态申请定位权限。如果手头只有新手机需要在 MainActivity 里补一段权限申请代码把 ACCESS_COARSE_LOCATION 和 ACCESS_FINE_LOCATION 在 startDiscovery 之前申请到优先级很高——不申请的话搜索不到任何设备而且不报错排查起来特别费劲。4.3 调试技巧日志驱动的问题定位调试时建议打开 LogCat过滤 Tag 为 Antilost 的日志。源码里关键节点都有 Log 输出搜索开始、发现设备、连接成功、连接失败、链路断开。照着日志走一遍流程整个状态机一目了然。我在调试时会做一次“走远测试”连接成功后把防丢器放在桌上人往后退 10 到 15 米观察日志里 MSG_DEVICE_LOST 出现的时间点。这个时间点加上当时日志里的 RSSI 值就是验证报警灵敏度设置是否合理的最直接数据。连接失败时不要瞎猜按顺序排查会快很多。先看 ConnectThread 输出的异常堆栈如果是 read failed, socket might closed说明远端设备主动断了连接可能是防丢器没进入配对模式或电量不足如果是 Service discovery failed说明 UUID 对不上确认防丢器使用的服务号如果是 Connection timed out说明设备在范围内但连接握手超时重启一下防丢器再试。按这套顺序能解决八成问题剩下两成就靠多试几台设备验证——蓝牙兼容性这东西偶尔会给你来点玄学惊喜。5. 避坑指南蓝牙防丢器开发中五个高频问题蓝牙防丢器的开发流程看着不长实际跑起来几乎没有一次是顺风顺水的。下面这五个问题是我在调试过程中反复遇到的每条都按“现象 → 原因 → 解决”的顺序记录照着排查能省下不少时间来。5.1 搜索不到任何设备现象点击搜索按钮后列表一直为空LogCat 里连 ACTION_FOUND 广播都没出现。原因最常见的是权限没配齐或者防丢器没有进入可发现模式。还有一个隐蔽原因Android 6.0 及以上系统把蓝牙扫描归到定位权限管理之下老工程如果没做动态权限申请搜索功能会在新系统上静默失败连日志都没有。解决先确认 AndroidManifest 里 BLUETOOTH 和 BLUETOOTH_ADMIN 权限都在再把防丢器重新进入配对模式一般是长按按键 5 秒直到指示灯闪烁如果跑在新系统上在 MainActivity 里补上运行时定位权限申请拿到权限后再调用 startDiscovery。5.2 配对成功但连接一直失败现象设备能搜到也能配对但点击连接后几秒钟报连接失败日志显示 Service discovery failed。原因UUID 不匹配。防丢器的服务端用的是私有 UUID而代码里写死的是 SPP 默认值两端服务号对不上socket 握手自然失败。解决查设备厂商协议文档拿到私有 UUID 替换 createRfcommSocketToServiceRecord 的参数。没有文档的用通用蓝牙调试工具扫描设备服务把扫描到的 UUID 填进去。实在不行还有一个民间偏方用 createRfcommSocket(1) 直接指定通道号建立连接跳过服务发现但这个方法在不同设备上的兼容性差异大不到万不得已不推荐。5.3 报警误报频繁坐在工位上都响现象设备明明放在桌上距离不到一米App 却每隔几分钟响一次不胜其烦。原因ConnectionMonitor 的检测逻辑太敏感。socket 的 isConnected() 在信号受遮挡时会返回假阴性加上源码只检测一次异常就累加计数阈值设得太低瞬时波动直接触发了报警。解决把 MAX_MISS 从 3 调大到 5相当于连续 5 秒异常才报警。同时把 isConnected() 的判断换成实际读数据尝试从 socket 输入流读一个字节读到才算连接正常读不到才计一次失败。两种手段一起上误报率基本可以降到零。5.4 切到后台后 App 被杀防丢失效现象手机锁屏放口袋里过几分钟切回来看App 已经被系统回收防丢报警自然不响等于白装。原因老工程没有做后台保活。ConnectionMonitor 是普通线程Activity 被系统回收后线程也跟着没了。国产 ROM 的省电策略还会主动杀后台进程这个问题在新手机上尤其严重。解决把 ConnectionMonitor 迁移到一个前台服务里用 startForeground 发一个常驻通知让系统知道这个服务不能杀。同时监听灭屏广播在灭屏时保持服务运行。这是防丢类 App 必须做的一步改造不做的话“防丢”在后台场景下就是摆设。5.5 RSSI 显示不稳定没法判断距离现象设备列表里 RSSI 数值跳来跳去同一位置一会儿 -50 一会儿 -80根本没法用来看距离。原因RSSI 本身受环境干扰大电磁波在室内被墙体、金属反射后会形成多径效应数值波动是常态。老代码直接显示原始采样值没有做平滑处理所以看起来特别跳。解决对 RSSI 做滑动平均滤波取最近 5 次采样的平均值再显示。或者用低通滤波newValue oldValue * 0.7 currentValue * 0.3让数值变化更平滑。滤波之后 RSSI 稳定很多用来做距离判断才靠谱。6. 二次开发把防丢器改造成自己的蓝牙工具6.1 首选改造向 BLE 低功耗蓝牙迁移如果想把这份老工程用到现代硬件上第一件要做的事是迁移到 BLE。经典蓝牙和 BLE 的 API 差异很大搜索从 startDiscovery 换成 BluetoothLeScanner 的 startScan连接从建立 RFCOMM socket 换成连接 GATT 服务通过回调接收连接状态数据收发从 InputStream 换成 characteristic 的读写。迁移过程里原来 ConnectThread 那套阻塞式连接可以整体废弃改成异步回调模式。BLE 的优点是功耗低防丢贴片用纽扣电池能撑一年缺点是断开判断不像 socket 那么直观要依赖 RSSI 阈值或者定期读 characteristic 里的心跳字段。6.2 验证方法与实用功能补强迁移完先别急着加功能验证方法很重要把防丢器放在固定位置人拿着手机后退每隔 1 米记录一次 RSSI 和连接状态画出距离与信号强度的关系曲线用这条曲线校准报警阈值而不是凭感觉填一个数。之后可以再加一个 SQLite 断开记录表把每次断开的时间、设备地址、当时的 RSSI 存下来用户能回看“昨天下午 3 点设备在 -70dBm 时断开”结合时间线推断丢失位置。这个功能实现不难大幅提升 App 的实用价值。从那以后我每次拿到蓝牙类项目都强制先走一遍“老代码跑通 → 画清状态机 → 再改功能”的流程不急着动代码先把连接、监测、报警这三条线在纸上理一遍。这套蓝牙防丢器源码正是练这个流程的好素材——工程小、链路完整、坑点又足够典型跑通一遍收获会很大希望帮到你。本文还有配套的精品资源点击获取