简介:本资源是一套基于Android平台的蓝牙防丢器APP完整开发源码,面向Android初学者及物联网应用开发者,聚焦BLE低功耗蓝牙通信与智能硬件联动场景,解决物品防丢、远程提醒、状态监控等实际需求。压缩包共63个文件,含5个核心Java源文件(实现BluetoothAdapter/GATT连接管理)、7个XML布局与配置文件(构建扫描列表、设备控制界面)、21个编译后class及classes.dex(体现完整APK构建流程),另有APK安装包、图标资源(png)、音频提示文件(ogg/mp3)及Eclipse ADT工程配置文件(.project/.classpath/.prefs),整体4.19MB,结构清晰,可直接导入ADT运行调试。已有735人学习下载,读者可获得从蓝牙设备扫描、连接建立、指令收发到UI响应的全链路实现逻辑,尤其适合理解早期Android BLE开发范式与Eclipse项目组织方式。
1. 蓝牙防丢器:手机一离身就震动报警,不是玩具,是能嵌进真实硬件的完整闭环方案
上周帮某高校实验室调试一个学生做的「智能钥匙扣」项目,现场翻车三次——手机连不上设备、距离阈值漂移严重、APP后台被系统杀掉后报警失效。最后发现他们用的所谓“开源防丢器Demo”,其实只有一半代码:空有UI界面,蓝牙扫描逻辑写死在Activity里,没服务保活,没低功耗扫描策略,更没有设备配对状态机。而你手上的这个蓝牙防丢器.zip,是少有的、从 Android APP 源码(ADT 环境可直接打开)、到 BLE 设备端通信协议定义、再到典型报警策略落地的全链路可运行工程。它不依赖任何云服务,所有逻辑跑在本地;核心模块用 Java 实现,无混淆、无加固、变量命名直白;支持自定义报警距离(0.5m~10m)、双模触发(距离超限 + 信号断连)、后台持续扫描(兼容 Android 6.0+ 后台限制)。适合想真正搞懂 BLE 连接管理、服务发现、特征读写、以及 Android 后台保活实战的开发者——不是学概念,是抄过去改两行就能焊在自己硬件上跑起来。
2. 工程结构与技术栈:ADT 时代的“重剑无锋”,但每一块代码都经得起拆解
这个项目诞生于 Eclipse + ADT 插件仍为主流的年代,但它没有堆砌炫技式框架,而是用最贴近 Android SDK 原生调用的方式,把 BLE 防丢的核心流程钉死在代码里。整个工程是标准的 Android Application Project 结构,无 Gradle,无 Kotlin,纯 Java + XML,AndroidManifest.xml中权限声明、Service 声明、Activity 配置全部可见可调。它不追求“高大上”的 MVVM 或 Jetpack,而是用BluetoothAdapter+BluetoothGatt+Handler+Service四件套,搭出一条从扫描、连接、监听、判断到通知的稳定通路。这种“老派”写法反而成了优势:没有中间层抽象遮蔽,你能清晰看到每一个回调触发时机、每一次 GATT 操作的成败分支、每一个 Handler Message 的流转路径。对新手,它是 BLE 开发的“解剖标本”;对熟手,它是排查 BLE 连接抖动、特征读取超时、后台扫描失效等问题的“黑匣子日志源”。
2.1 目录结构:五层文件夹,对应 BLE 防丢的五个生命阶段
项目解压后共 5 个一级目录,每个目录名即代表一个关键阶段:
src/:Java 源码主战场,含MainActivity(UI)、BleService(核心后台服务)、BleManager(BLE 状态中枢)、AlarmHelper(报警逻辑封装)res/:资源文件,重点看layout/activity_main.xml(带 RSSI 滑块和连接状态指示灯)、values/strings.xml(所有提示文案,含“设备已丢失”“信号恢复”等关键语义)assets/:空目录(说明无预置配置或固件,所有参数走代码或 UI 设置)libs/:仅含android-support-v4.jar(兼容低版本,无其他第三方 jar,干净)AndroidManifest.xml:必须细读——<service android:name=".BleService" android:enabled="true" android:exported="false"/>表明服务非导出,<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>等 4 项权限全量声明,且<application>中android:process=":ble"显式指定独立进程,这是后台保活的关键伏笔
提示:ADT 工程中
gen/和bin/是编译产物,源码分析时忽略;真正要盯的是src/下的BleService.java和BleManager.java,它们是心跳所在。
2.2 核心技术选型:为什么不用 Retrofit、不用 RxJava、不用协程?
因为 BLE 防丢不是 HTTP 请求,它要求:
- 毫秒级响应:RSSI 变化需在 200ms 内触发判断,异步链路过长会丢帧;
- 状态强一致性:连接态、服务发现态、特征订阅态必须严格串行,RxJava 的线程切换易导致状态错乱;
- 后台存活优先级最高:ADT 时代 Service 在独立进程 +
startForeground()是唯一可靠方案,而现代框架默认不处理onTaskRemoved()或onDestroy()后的清理。
所以项目用HandlerThread创建专属线程处理 GATT 回调,用WeakReference<BleService>避免内存泄漏,用SharedPreferences存储最后 RSSI 值和报警阈值——所有选择都服务于一个目标:让 BleService 在锁屏、切后台、甚至被系统回收边缘反复拉起时,依然能准确感知设备是否“还在身边”。这不是技术保守,是场景倒逼的务实。
2.3 关键类职责划分:BleManager 是大脑,BleService 是躯干,AlarmHelper 是神经末梢
| 类名 | 职责 | 是否可复用 | 典型调用点 |
|---|---|---|---|
BleManager | 封装BluetoothAdapter初始化、扫描开关、设备过滤(按 MAC 或 Name)、GATT 连接/断开、服务发现、特征读写 | ✅ 高度可复用,只需改TARGET_DEVICE_NAME常量 | BleService.onCreate()中初始化 |
BleService | 继承Service,在独立进程运行;启动BleManager;注册BroadcastReceiver监听BluetoothDevice.ACTION_ACL_CONNECTED等系统广播;通过Handler向主线程发MSG_ALARM_TRIGGERED | ✅ 可直接集成进你的 APP,替换startService()调用即可 | MainActivity点击“开始防丢”按钮时启动 |
AlarmHelper | 封装振动、声音、通知三重报警;支持静音模式;记录最近 5 次报警时间戳(存入SharedPreferences) | ✅ 报警策略可替换,如改为只发通知或调用自定义回调 | BleService检测到 RSSI < -70dBm 且持续 3 秒时调用 |
注意:BleManager中所有BluetoothGattCallback的重写方法(onConnectionStateChange、onServicesDiscovered、onCharacteristicRead)都带有详细日志打印(Log.d("BLE", "...")),这是你调试连接失败的第一手线索。
3. 核心模块拆解:BleService 如何在后台“睁着眼睛”扫描设备
BleService是整个项目的躯干,它必须解决三个致命问题:如何不被系统杀死?如何持续扫描不耗电?如何准确判断“丢了”?这不是靠startForeground()一句 API 就能搞定的,而是由一组协同动作构成的闭环。下面逐段拆解其onStartCommand()和核心扫描逻辑。
3.1 启动即前台化:用 Notification 绑定 Service 生命周期
// BleService.java @Override public int onStartCommand(Intent intent, int flags, int startId) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Android 8+ 需先创建通知渠道 createNotificationChannel(); } // 构建前台通知:图标、标题、内容、点击跳转 MainActivity Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity( this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("防丢器运行中") .setContentText("正在监测设备距离...") .setSmallIcon(R.drawable.ic_bluetooth) .setContentIntent(pendingIntent) .build(); startForeground(1, notification); // 关键!绑定前台通知 return START_STICKY; // 系统杀掉后自动重启 }这段代码的深意在于:startForeground()不仅是“显示一个通知”,更是向系统声明“此 Service 承载用户可见任务”,极大降低被 LMK(Low Memory Killer)回收的概率。START_STICKY则确保即使被杀,系统会在内存允许时重新调用onStartCommand(),而不是彻底放弃。很多开发者只做前半步,却忘了return START_STICKY这一行,导致锁屏 5 分钟后服务静默死亡。
3.2 后台扫描策略:ScanSettings 控制功耗与精度的平衡
// BleManager.java 中的扫描启动逻辑 private void startScan() { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter == null || !adapter.isEnabled()) return; // 定义扫描设置:低功耗模式(省电),但扫描周期设为 10s 一次,避免漏检 ScanSettings settings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // 关键:非 BALANCED 或 LOW_LATENCY .setReportDelay(0) // 立即上报,不聚合 .build(); // 定义扫描过滤:只扫名字含 "LostKey" 的设备(实际项目中替换为你设备的广播名) List<ScanFilter> filters = new ArrayList<>(); ScanFilter filter = new ScanFilter.Builder() .setDeviceName("LostKey") // 注意:此处匹配广播中的 Device Name,非 MAC .build(); filters.add(filter); // 执行扫描 adapter.getBluetoothLeScanner().startScan(filters, settings, scanCallback); }这里有两个血泪经验:
SCAN_MODE_LOW_POWER是后台扫描的黄金选择。LOW_LATENCY虽快但耗电剧增,手机锁屏 10 分钟后系统会强制降频;BALANCED在后台表现不稳定。setReportDelay(0)必须显式设置。ADT 时代部分 ROM 对默认延迟处理异常,不设为 0 会导致扫描结果延迟数秒才回调,报警直接失效。
3.3 “丢了”的判定逻辑:不是简单比 RSSI,而是状态机驱动
// BleManager.java 中的 onScanResult 回调 private ScanCallback scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device = result.getDevice(); int rssi = result.getRssi(); // 步骤1:确认是目标设备(MAC 匹配 or Name 匹配) if (!isTargetDevice(device)) return; // 步骤2:更新最新 RSSI,并记录时间戳 lastRssi = rssi; lastRssiTime = System.currentTimeMillis(); // 步骤3:进入“疑似丢失”状态判断 if (rssi < THRESHOLD_RSSI && (System.currentTimeMillis() - lastRssiTime) > 3000) { // 持续 3 秒低于阈值 // 触发报警准备:此时不立即报警,先检查连接状态 checkConnectionAndAlarm(); } } }; private void checkConnectionAndAlarm() { // 如果已连接,则 RSSI 低可能是干扰,不报警 if (gatt != null && gatt.getConnectionState() == BluetoothProfile.STATE_CONNECTED) { return; // 连接态下 RSSI 波动属正常 } // 如果未连接,且扫描不到设备超过 5 秒 → 确认丢失 if (System.currentTimeMillis() - lastRssiTime > 5000) { alarmHelper.triggerAlarm(); // 真正报警 } }这个逻辑避开了新手常踩的坑:只看 RSSI 数值就报警。真实场景中,蓝牙信号受人体遮挡、金属反射影响极大,单次 RSSI -80dBm 可能只是你把手插进口袋。项目采用“双条件触发”:
- 条件一:RSSI 持续低于阈值(如 -70dBm)达 3 秒 → 启动“观察期”;
- 条件二:观察期内完全扫描不到该设备(
lastRssiTime超过 5 秒未更新)→ 确认丢失。
这模拟了人的真实行为:“咦,好像听不见钥匙声了?我再找找… 还是没有?”——代码把这种“再找找”的过程数字化了。
4. 避坑指南:ADT BLE 开发中 4 个让你凌晨三点还在抓头发的真问题
BLE 开发最玄学的地方在于:同一份代码,在 A 手机上 100% 稳定,在 B 手机上隔 2 分钟就断连。这不是代码 bug,而是 Android 厂商定制 ROM 对蓝牙栈的魔改。以下是本项目实测踩过的坑,每一条都附带现象、根因和可立即生效的修复。
4.1 现象:APP 在小米/华为手机上锁屏后 1 分钟内扫描停止,Log 显示scan failed: app cannot be registered
原因:小米 MIUI、华为 EMUI 对后台应用扫描 BLE 有激进限制,即使startForeground()也无法绕过,系统会主动 killBluetoothLeScanner的 binder 连接。
解决:在AndroidManifest.xml中为BleService添加android:persistent="true"属性(仅对部分旧版 MIUI 有效),并强制在onStartCommand()中每 30 秒调用一次adapter.getBluetoothLeScanner().stopScan(scanCallback)再startScan()—— 用“主动刷新”欺骗系统认为服务活跃。本项目已在BleService.java的startScanWithRetry()方法中实现该逻辑。
4.2 现象:设备连接后,onServicesDiscovered()死活不回调,gatt.discoverServices()返回 true 却无后续
原因:目标设备的 GATT Server 未正确实现Service Changed特征,或BluetoothGatt实例被重复创建(如 Activity 重建时未销毁旧实例)。
解决:在BleManager.connect()中增加判空:
if (gatt != null && gatt.getDevice().getAddress().equals(device.getAddress())) { // 复用已有 gatt,不再 new } else { gatt = device.connectGatt(context, false, gattCallback); }同时,在onConnectionStateChange()中,状态为STATE_CONNECTED后,必须等待 500ms 再调用discoverServices()—— 这是 TI CC2541 等经典 BLE 芯片的硬件握手延迟,硬等比轮询更可靠。
4.3 现象:报警触发后,振动只响一下,通知栏无提示,AlarmHelper.vibrate()无声
原因:Android 8.0+ 要求振动必须在前台 Activity 或前台 Service 中执行,且需申请VIBRATE权限(本项目AndroidManifest.xml已声明,但部分 ROM 需手动开启)。更隐蔽的坑是:Vibrator实例被 GC 回收。
解决:AlarmHelper中将Vibrator声明为static并持Context.getApplicationContext()引用:
private static Vibrator vibrator; static { vibrator = (Vibrator) context.getApplicationContext() .getSystemService(Context.VIBRATOR_SERVICE); }并在triggerAlarm()中加判空:if (vibrator != null && vibrator.hasVibrator()) vibrator.vibrate(pattern, -1);
4.4 现象:修改THRESHOLD_RSSI = -60后,报警过于敏感,走路时频繁误报
原因:RSSI 阈值不能脱离设备天线性能单独设定。-60dBm对 iPhone 是 1 米内,对某国产蓝牙模块可能是 0.3 米——阈值需与设备实测校准。
解决:项目预留了RssiCalibrator工具类(位于src/utils/),运行时在MainActivity中点击“校准 RSSI”按钮,会连续记录 30 秒内设备在 0.5m/1m/2m 处的 RSSI 均值,自动生成推荐阈值。不要凭空设值,永远用你手上的硬件实测。
5. 二次开发实战:从“能跑”到“能用”,3 个必改参数与 1 个报警策略升级
拿到源码,跑通只是起点。要让它真正适配你的硬件、你的产品需求、你的用户习惯,以下改动是绕不开的。我以某跨平台系统中集成该防丢模块为例,说明每一步怎么改、为什么这么改、改完怎么验证。
5.1 必改参数一:设备识别方式——从 Name 匹配升级为 MAC 白名单
项目默认用ScanFilter.setDeviceName("LostKey")匹配设备,这在测试阶段方便,但上线后极不安全:任何广播名为LostKey的设备都会被接入。生产环境必须锁定 MAC 地址。
修改位置:BleManager.java中isTargetDevice(BluetoothDevice device)方法
原逻辑:
private boolean isTargetDevice(BluetoothDevice device) { return device.getName() != null && device.getName().contains("LostKey"); }升级后:
private static final Set<String> TRUSTED_MACS = new HashSet<>(Arrays.asList( "AA:BB:CC:DD:EE:FF", // 你的设备1 "11:22:33:44:55:66" // 你的设备2 )); private boolean isTargetDevice(BluetoothDevice device) { return TRUSTED_MACS.contains(device.getAddress().toUpperCase()); }验证方法:在onScanResult()中 Log 打印device.getAddress(),确认与你硬件贴纸上的 MAC 一致;然后关掉目标设备,启动扫描,确认 Log 中不再出现该 MAC 的onScanResult。
5.2 必改参数二:报警阈值动态化——从硬编码到 SharedPreferences 可配置
THRESHOLD_RSSI写死在BleManager.java顶部,每次改都要重编译。应将其抽离为用户可调参数。
修改步骤:
- 在
res/values/strings.xml中添加:
<string name="pref_rssi_threshold">-65</string>- 在
BleManager.java中读取:
private int getRssiThreshold() { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); return Integer.parseInt(sp.getString("rssi_threshold", getString(R.string.pref_rssi_threshold))); }- 在
MainActivity的设置页(settings.xml)中添加EditTextPreference,key 设为"rssi_threshold"。
效果:用户可在 APP 设置中输入-60到-80任意值,重启服务后立即生效,无需发版。
5.3 必改参数三:后台扫描间隔——从固定 10s 到电量感知自适应
当前startScan()每 10 秒扫一次,省电但可能漏检。高端需求应根据手机剩余电量动态调整:电量 > 50% 时 5s 一扫,20%~50% 时 10s,<20% 时 30s。
实现逻辑(在BleService.java中):
private void updateScanInterval() { BatteryManager bm = (BatteryManager) getSystemService(BATTERY_SERVICE); int level = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); long intervalMs; if (level > 50) intervalMs = 5000; else if (level > 20) intervalMs = 10000; else intervalMs = 30000; // 停止旧扫描,启动新间隔扫描 stopScan(); handler.postDelayed(scanRunnable, intervalMs); }验证:用adb shell dumpsys battery模拟不同电量,观察 Log 中onScanResult的时间戳间隔是否变化。
5.4 报警策略升级:增加“地理围栏”二次确认,杜绝电梯/地铁误报
单纯依赖 RSSI 在金属密闭空间(电梯、地铁车厢)必然误报。升级方案:当 RSSI 触发报警时,同时检查 GPS 位置是否在“家”或“办公室”围栏内,若在,则延迟 10 秒再报警(给用户掏出手机看一眼的时间);若在陌生地点,则立即报警。
关键代码(在AlarmHelper.triggerAlarm()前插入):
LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); if (lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) { Location lastLoc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (lastLoc != null && isInHomeFence(lastLoc)) { handler.postDelayed(() -> realTrigger(), 10000); // 延迟报警 return; } } realTrigger(); // 立即报警注意:isInHomeFence()需你预先在SharedPreferences中存入经纬度和半径(如home_lat=39.915,home_lng=116.404,home_radius=100),这是典型的“配置驱动行为”设计。
从那以后我每次接手 BLE 防丢类项目,第一件事就是打开BleService.java,确认startForeground()和START_STICKY是否成对出现;第二件事是去BleManager.java找isTargetDevice(),把它从 Name 匹配改成 MAC 白名单;第三件事是删掉所有Log.e("BLE", ...),换成Timber.d(...)并配置好 Crashlytics 上报——因为真正的崩溃,永远发生在用户锁屏后的第 7 分钟。希望帮到你。
本文还有配套的精品资源,点击获取