简介本资源是一套基于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.5m10m、双模触发距离超限 信号断连、后台持续扫描兼容 Android 6.0 后台限制。适合想真正搞懂 BLE 连接管理、服务发现、特征读写、以及 Android 后台保活实战的开发者——不是学概念是抄过去改两行就能焊在自己硬件上跑起来。2. 工程结构与技术栈ADT 时代的“重剑无锋”但每一块代码都经得起拆解这个项目诞生于 Eclipse ADT 插件仍为主流的年代但它没有堆砌炫技式框架而是用最贴近 Android SDK 原生调用的方式把 BLE 防丢的核心流程钉死在代码里。整个工程是标准的 Android Application Project 结构无 Gradle无 Kotlin纯 Java XMLAndroidManifest.xml中权限声明、Service 声明、Activity 配置全部可见可调。它不追求“高大上”的 MVVM 或 Jetpack而是用BluetoothAdapterBluetoothGattHandlerService四件套搭出一条从扫描、连接、监听、判断到通知的稳定通路。这种“老派”写法反而成了优势没有中间层抽象遮蔽你能清晰看到每一个回调触发时机、每一次 GATT 操作的成败分支、每一个 Handler Message 的流转路径。对新手它是 BLE 开发的“解剖标本”对熟手它是排查 BLE 连接抖动、特征读取超时、后台扫描失效等问题的“黑匣子日志源”。2.1 目录结构五层文件夹对应 BLE 防丢的五个生命阶段项目解压后共 5 个一级目录每个目录名即代表一个关键阶段src/Java 源码主战场含MainActivityUI、BleService核心后台服务、BleManagerBLE 状态中枢、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:enabledtrue android:exportedfalse/表明服务非导出uses-permission android:nameandroid.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 回调用WeakReferenceBleService避免内存泄漏用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 承载用户可见任务”极大降低被 LMKLow 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 的设备实际项目中替换为你设备的广播名 ListScanFilter 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:persistenttrue属性仅对部分旧版 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 SetString 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 namepref_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中添加EditTextPreferencekey 设为rssi_threshold。效果用户可在 APP 设置中输入-60到-80任意值重启服务后立即生效无需发版。5.3 必改参数三后台扫描间隔——从固定 10s 到电量感知自适应当前startScan()每 10 秒扫一次省电但可能漏检。高端需求应根据手机剩余电量动态调整电量 50% 时 5s 一扫20%~50% 时 10s20% 时 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_lat39.915,home_lng116.404,home_radius100这是典型的“配置驱动行为”设计。从那以后我每次接手 BLE 防丢类项目第一件事就是打开BleService.java确认startForeground()和START_STICKY是否成对出现第二件事是去BleManager.java找isTargetDevice()把它从 Name 匹配改成 MAC 白名单第三件事是删掉所有Log.e(BLE, ...)换成Timber.d(...)并配置好 Crashlytics 上报——因为真正的崩溃永远发生在用户锁屏后的第 7 分钟。希望帮到你。本文还有配套的精品资源点击获取