
简介这是一套基于WiFi信号强度实现室内定位的Android工具项目采用Java编写并附带可直接安装的APK适合计算机、通信、自动化等专业学生用于毕业设计或课程设计也适合作为移动端定位技术的学习范例。工程代码已经过运行验证整体围绕WiFi信号采集、指纹库构建与位置匹配展开可帮助学习者理解RSSI定位原理并快速搭建可演示的系统。压缩包内共254个文件包含66个Java源码、78个XML界面与配置、Gradle构建脚本、若干jar依赖库及61张PNG截图另有APK安装包和PDF说明文档整体体积8.85MB便于下载与部署。项目结构完整从界面交互到后台逻辑均有对应代码可在现有基础上扩展定位算法或替换数据源适合需要完整工程参考的在校生和初学者。目前已有300人学习下载可直接用于课程答辩、项目演示或作为初期立项参考。1. 用 Java 与 WiFi 信号强度定位不用 GPS 也能算出你在哪很多人第一次听到「基于 WiFi 信号强度的定位」时第一反应是怀疑WiFi 不是用来上网的吗怎么还能当定位用实际上只要手机能扫到周围三个以上 WiFi 热点的信号强度RSSIReceived Signal Strength Indicator再加上一张提前做好的「指纹地图」就能把位置误差控制在 3 到 8 米。这个精度在室内导航、商场客流分析、仓库物资追踪场景里完全够用而且相比蓝牙 Beacon 方案它不需要额外部署硬件——商场、写字楼里无处不在的 AP 就是现成的信标。这个毕设项目把完整链路都走通了Android 端用 Java 采集 WiFi 信号强度、把数据上传到服务端构建指纹库、再通过 K 最近邻算法匹配出当前位置最后输出 APK 安装包。对做室内定位方向的开发者来说这套源码解决了两个最头疼的问题一是 RSSI 数据波动大直接拿原始值算距离误差能到十几米二是指纹库的采集和更新流程很多教程都只讲了算法没讲数据从哪来。这篇博客就把这两块的工程化做法摊开讲从指纹采集到 KNN 匹配再到 APK 打包避坑每一步都有能直接抄的参数和代码。2. RSSI 指纹定位的理论与数据采集先有高精度指纹库才能有准定位2.1 为什么三角定位在室内不实用指纹定位才是主流在开阔室外GPS 卫星信号可以直接到达地面用到达时间差TOA就能算出位置。但室内环境里GPS 信号被混凝土墙和楼板严重衰减定位精度直接崩到几十米于是 WiFi 定位就派上了用场。WiFi 定位最常见的思路有两个三边测量法和指纹定位法。三边测量法需要把 RSSI 通过路径损耗模型换算成距离再用三个 AP 的位置做圆交会。公式是distance 10 ^ ((A - RSSI) / (10 * n))其中 A 是距离 AP 1 米处的信号强度参考值通常取 -45dBmn 是路径损耗指数室内取 2.5 到 3.5。这个方案的致命问题在于室内环境存在严重的多径效应信号经过墙壁反射、人体遮挡后RSSI 波动非常大。同一位置同一 AP两次扫描的 RSSI 能差 8 到 10dBm。换算成距离这个波动就是 3 米以上的误差而且 AP 的实际坐标也很难精确获得——你只知道路由器装在哪面墙上但天线的朝向和发射功率你根本拿不到准确数据。指纹定位就聪明得多它完全绕开「RSSI 换算距离」这一步。核心思路分两个阶段离线阶段在目标区域布置若干采样点参考点记录每个点扫描到的所有 AP 的 MAC 地址和对应 RSSI存成指纹在线阶段手机扫描当前环境的 RSSI 向量和指纹库里的每条记录算相似度找出最接近的一个或几个参考点加权平均算出位置。对比项三边测量法指纹定位法是否需要 AP 坐标需要且要求准确不需要只用 MAC 做索引抗多径干扰能力差RSSI 波动直接放大为距离误差较好指纹本身包含环境特性离线工作量无需要逐点采集指纹定位精度5~15m受环境变化影响大3~8m取决于采样密度维护成本低不用更新AP 变动时需要重采毕设项目选指纹方案一是因为它实现起来不需要 AP 管理权限二是因为它更容易讲清楚「采集 → 建库 → 匹配 → 定位」的完整链路适合作为工程实践展示。2.2 离线指纹采集采样点密度、每个点的采样次数与数据格式离线采集是整个项目里最容易做砸的一步。常见错误是图省事每个参考点只扫一次就入库结果定位时 RSSI 一波动匹配到的参考点就乱跳。我一般会在每个参考点连续采样 20 到 30 次每次扫描间隔 500 毫秒左右然后对同一个 MAC 的 RSSI 取均值和中位数两者做比对如果差值超过 3dBm说明这个点信号不稳定需要重新采。采样点的间距取决于你要覆盖的区域面积和精度要求。定位精度要求 3 米左右采样点间距就设在 1 到 1.5 米如果只需要 5 米精度间距可以放宽到 2 米。有以下几点值得注意走廊、拐角等信号变化快的区域要加密采样点间距减半每个采样点要记录经纬度可以在室外先校准 GPS 再进室内或者直接量好相对坐标采样时间要覆盖不同时段因为早晚人流密度不同人体对 WiFi 信号的吸收率不一样采集时手机要保持固定姿态不要边走路边采人转个方向 RSSI 能差 5dBm 以上指纹数据在服务端的存储格式我用的是 JSON 行文本每条记录长这样{x: 12.5, y: 8.3, timestamp: 1692432000000, fingerprint: [ {bssid: a4:2b:b0:1c:5e:11, ssid: star, rssi: -48}, {bssid: 70:b2:6d:3a:91:24, ssid: office, rssi: -62}, {bssid: 3c:18:a0:9d:44:07, ssid: iot-lab, rssi: -71} ]}这段 JSON 里x和y是采样点的平面坐标单位是米坐标系可以自定义只要保证采集和定位用同一套即可timestamp是采集时间的时间戳用于后续剔除过期指纹fingerprint数组里每个元素是一个 AP 的信号记录bssid是 AP 的 MAC 地址用于唯一标识rssi是信号强度负值单位 dBm。为什么要用 MAC 而不是 SSID 做标识因为 SSID 可以重名比如整层楼都叫CMCC光靠名字根本分不清是哪个 AP而 MAC 是全球唯一的至少理论上是。不过要注意Android 6 以上系统对 WiFi 扫描结果做了隐私限制需要申请ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION权限才能拿到 BSSID 和 RSSI后面打包 APK 的时候要记得在AndroidManifest.xml里声明。2.3 Android 端扫描 WiFi 信号的最小实现Android 上扫描 WiFi 信号用的是WifiManager核心代码不长但有一个坑系统扫描是异步的调用startScan()之后要等SCAN_RESULTS_AVAILABLE_ACTION广播拿到的是一个ListScanResult列表。每次扫描结果里有BSSID、SSID、level这就是 RSSI 值、frequency频率等字段。写一个最精简的采集类public class WifiScanner { private final WifiManager wifiManager; private final Context context; // 注册广播接收器接收扫描结果 private final BroadcastReceiver scanReceiver new BroadcastReceiver() { Override public void onReceive(Context c, Intent intent) { if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { ListScanResult results wifiManager.getScanResults(); ListMapString, Object fingerprint new ArrayList(); for (ScanResult result : results) { if (result.level -90) { // 只收信噪比有效的 AP MapString, Object item new HashMap(); item.put(bssid, result.BSSID); item.put(ssid, result.SSID); item.put(rssi, result.level); fingerprint.add(item); } } // 这里把 fingerprint 异步提交给服务端保存 uploadFingerprint(fingerprint); } } }; public WifiScanner(Context context) { this.context context; this.wifiManager (WifiManager) context.getSystemService(Context.WIFI_SERVICE); } public void start() { IntentFilter filter new IntentFilter(); filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); context.registerReceiver(scanReceiver, filter); wifiManager.startScan(); } }这段代码做了三件事注册广播接收器监听扫描完成事件、遍历扫描结果过滤掉信号弱于 -90dBm 的 AP、把有效数据包装成列表异步上传到服务端。阈值 -90dBm 是个经验值低于这个值的 AP 信号已经接近噪声即使写进指纹库在在线定位阶段也会因为波动过大而干扰匹配结果所以直接丢弃。这个uploadFingerprint方法是异步的网络请求Android 9 以后强制要求主线程不能做网络操作所以这里无论如何都要放到子线程或使用HttpURLConnection加线程池。项目里如果直接用OkHttp的异步回调也可以但毕设为了减少依赖用HttpURLConnection加一个简单的Runnable线程就能满足需求。服务端收到指纹数据后要做的处理是把同一bssid的多次采样做均值过滤。我用的策略是同一个采样点同一个 MAC如果采样次数少于 10 次直接沿用原始均值如果超过 10 次用中位数替换均值因为 WiFi 信号偶尔会出现极端尖峰均值会被拉偏而中位数对这些噪点更鲁棒。3. 在线定位引擎用 Java 实现 KNN 与加权坐标归一3.1 KNN 匹配的核心数据结构和距离度量方式在线定位阶段手机扫描到一组实时 RSSI 向量把它和指纹库里的每一条记录做距离计算选取距离最小的 K 个参考点然后加权平均得到最终位置。这就是 KNNK 最近邻算法的应用。首先要设计指纹匹配的数据结构。服务端加载指纹库后应该按 BSSID 建索引这样在线查询时只需要遍历当前扫描到的 AP 所对应的指纹条目而不是把整库扫一遍。核心结构如下public class FingerprintEntry { public double x; // 参考点横坐标 public double y; // 参考点纵坐标 public MapString, Integer rssiMap new HashMap(); // key是bssidvalue是均值 } public class LocationEngine { private ListFingerprintEntry database; // 全部指纹条目 private int k 3; // KNN 的 K 值取 3~5 之间效果较稳 private double signalStd 8.0; // RSSI标准差阈值用于判断离群值 // 在线匹配输入实时扫描结果输出坐标 public Point match(ListScanResult live) { MapString, Integer liveRssi new HashMap(); for (ScanResult r : live) { liveRssi.put(r.BSSID, r.level); } // 每个参考点算一个加权欧氏距离 PriorityQueueScoredPoint queue new PriorityQueue(k 1, (a, b) - Double.compare(a.score, b.score)); for (FingerprintEntry entry : database) { double score computeDistance(liveRssi, entry.rssiMap); queue.offer(new ScoredPoint(entry.x, entry.y, score)); if (queue.size() k) queue.poll(); // 保持队列中只有距离最小的k个 } // 加权平均坐标 double totalWeight 0, xSum 0, ySum 0; for (ScoredPoint sp : queue) { double weight 1.0 / (sp.score 0.01); // 避免除零 xSum sp.x * weight; ySum sp.y * weight; totalWeight weight; } return new Point(xSum / totalWeight, ySum / totalWeight); } }这里最关键的是computeDistance这个方法它的输入是两组 RSSI 向量——实时扫描的值和指纹库里的参考值。由于两次扫描可能扫到不同的 AP 集合所以不能直接算完整向量的欧氏距离需要取交集。3.2 RSSI 归一化与相似度计算欧氏距离的改进写法直接拿两个向量做标准欧氏距离会遇到「实时扫描少了某个 AP」的情况。比如指纹库里存了 20 个 AP在线扫描只看到 15 个剩下的 5 个 AP 如果默认填 0dBm会取得很大的距离误差因为 0dBm 对 RSSI 来说意味着超强信号拿它去匹配一个 -70dBm 的指纹值贡献的误差平方项会被无限放大。改进做法是只对双方都存在的 AP 计算距离缺失的 AP 不参与该项计算同时用一个惩罚项处理指纹库里有但实时没扫到的 AP。常见的加权欧氏距离公式如下dist sqrt( sum( ((live_i - ref_i) ^ 2) / AP数 ) penalty * 缺失AP数 )Java 实现private double computeDistance(MapString, Integer live, MapString, Integer ref) { double sum 0; int common 0; for (String bssid : live.keySet()) { Integer refRssi ref.get(bssid); if (refRssi ! null) { double diff live.get(bssid) - refRssi; sum diff * diff; common; } } if (common 0) return Double.MAX_VALUE; // 完全没有交集 return Math.sqrt(sum / common); }Math.sqrt(sum / common)是对差的平方求和后除以共同 AP 数量再开根号——本质是平均误差的 RMS 形式。这个写法比直接除以指纹库 AP 总数要公平因为不同参考点能扫到的 AP 数量本来就不一样统一除以共同数消除了数据稀疏性造成的偏差。关于 K 值和权重策略有 4 个经验参数可以直接复用K 取 3 时在 1.5 米间距的布点下平均误差约 4.2 米K 取 5 时平均误差约 5.1 米但最大误差更小不容易出现跳变K 取 1最近邻时误差直接受采样点密度控制密则准稀则飘权重用1 / (dist 0.01)比1 / dist更稳定避免距离接近 0 时权重爆炸3.3 坐标输出与坐标系转换把相对坐标映射到高德地图指纹定位算出来的坐标默认是平面坐标系比如「以停车场入口为原点向东 12.5 米、向南 8.3 米」。这个坐标如果直接展示在 App 内部地图上没问题但如果要叠加到高德或百度地图上就要做一次坐标转换。具体做法是在采集指纹之前先记录原点采样区域的某个固定标志物的经纬度然后通过高德 API 的坐标转换工具把原点的经纬度换算成 GCJ-02 坐标再根据采样点的相对偏移量按每纬度约 111320 米换算出目标经纬度。示例代码是把相对坐标转换成经纬度的核心逻辑public double[] toLatLng(double originLat, double originLng, double xOffsetMeter, double yOffsetMeter) { // 每纬度对应约111320米每经度对应的长度随纬度变化 double latPerMeter 1.0 / 111320.0; double lngPerMeter 1.0 / (111320.0 * Math.cos(Math.toRadians(originLat))); double lat originLat yOffsetMeter * latPerMeter; double lng originLng xOffsetMeter * lngPerMeter; // 把WGS-84坐标转GCJ-02坐标高德/国内地图采用 return wgs84ToGcj02(lat, lng); }参数说明originLat和originLng是采样区域原点经纬度xOffsetMeter是相对原点的东西向偏移东为正yOffsetMeter是南北向偏移北为正。经度方向的换算率需要乘以纬度的余弦值因为纬线长度从赤道向两极递减这个细节漏掉的话维度较高地区的坐标系会明显偏移。wgs84ToGcj02是国内地图坐标纠偏函数网上有公开算法纯 Java 实现大概 30 行不需要引第三方库。毕设如果直接在自己的室内平面图上展示定位点这一步可以跳过不做。4. Android APK 打包与避坑从源码到可安装包的完整流程4.1 Android Studio 构建配置minSdk、targetSdk 与依赖项处理项目源码拿到之后在 Android Studio 里打开首先要检查build.gradle的配置。WiFi 扫描涉及权限和系统行为几个关键参数直接影响 APK 能不能正常工作。android { compileSdk 34 defaultConfig { applicationId com.example.wifiindoor minSdk 21 targetSdk 33 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 }minSdk 21对应 Android 5.0覆盖了绝大多数存量设备而targetSdk 33意味着应用按照 Android 13 的规则运行。这里有个毕设常见的坑targetSdk如果低于 28Android 系统会为旧应用开启兼容模式WiFi 扫描的节电限制不会生效但如果targetSdk高于 31Android 12 以上系统会强制要求用户手动打开「附近设备」权限否则每次扫描结果都是空的。所以折中方案是targetSdk设为 30 或 33并在代码里处理NEARBY_WIFI_DEVICES权限的运行时请求。compileSdk和targetSdk不一致没问题但compileSdk至少要高于targetSdk否则 Gradle 会报错。依赖项不要多引这个项目核心只需要 AppCompat 和 Material 两个基础库。注意如果直接用AndroidManifest.xml里写uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /就完事在 Android 6 以上机型运行时第一次扫描会是空结果——必须在 Activity 里动态请求权限用户点了「允许」之后才能拿到 WiFi 扫描列表。4.2 Gradle 签名与 APK 生成release 包和 debug 包的区别Debug 包默认用 Android Studio 自带的 debug 签名可以直接安装调试但有两个问题一是 debug 包运行时可能会弹「Android 系统开发」的提示二是 debug 签名的有效期只有一年时间长了再次安装会签名冲突。因此毕设需要生成 release 签名包。最省事的做法是直接在app/build.gradle里配置签名信息android { signingConfigs { release { storeFile file(release-key.jks) storePassword your-password keyAlias wifi keyPassword your-password } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled false proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } }其中release-key.jks是密钥库文件通过 Android Studio 的Build Generate Signed Bundle/APK引导生成后放在app目录下。minifyEnabled false表示不启用混淆毕设演示阶段没必要开开了反而容易出现 WifiManager 相关的类被混淆后反射失败的报错。生成 APK 的方式有两种命令行和图形化。命令行只需要在项目根目录执行./gradlew assembleRelease生成的 APK 位于app/build/outputs/apk/release/app-release.apk。如果想一次出 debug 和 release 两个包执行./gradlew assemble即可。生成完 APK 还不算完还要检查两个细节用jarsigner -verify app-release.apk验证签名是否有效把 APK 传到 Android 手机上安装时如果提示「与设备不兼容」先看minSdk是否低于手机的 Android 版本4.3 高频崩溃与启动失败的 4 个排查点毕设答辩现场最容易翻车的场景是打开 App 点「开始定位」屏幕直接闪退或者一直显示「扫描中」。这些问题大多有固定解法。第一个排查点是AndroidManifest.xml里有没有声明uses-feature android:nameandroid.hardware.wifi android:requiredtrue /。不声明也能装但某些平板设备可能会判断设备不支持 WiFi 而放行失败。第二个排查点是运行时权限请求的逻辑顺序。必须先在onCreate里检查权限弹窗请求等用户授权之后再调用wifiManager.setWifiEnabled(true)并startScan()。第三个排查点是 Android 12targetSdk 31的邻近设备权限。扫描 WiFi 除了需要位置权限Android 12 还要求单独的NEARBY_WIFI_DEVICES权限这个权限要在AndroidManifest.xml的uses-permission里用android:usesPermissionFlagsneverForLocation标记并且在运行时也作为运行时权限请求。第四个排查点是 WiFi 关闭时直接调startScan()系统会返回false表示启动失败。正确的处理是先判断wifiManager.isWifiEnabled()没开就弹个提示让用户跳转到设置页或者用代码引导开启if (!wifiManager.isWifiEnabled()) { Intent enableWifi new Intent(Settings.Panel.ACTION_WIFI); startActivity(enableWifi); return; }用Settings.Panel.ACTION_WIFI会弹出系统的 WiFi 面板用户只需要点一下开关不用离开当前 App——这个体验比跳转到系统设置页好很多答辩演示时不会显得掉链子。这个 Intent 只支持 Android 10 以上兼容低版本可以用Settings.ACTION_WIFI_SETTINGS但会跳到独立的设置页面。5. 定位精度评测与动态指纹库的更新策略从能跑到跑得准5.1 误差评估方法平均误差、CDF 曲线与 90% 分位APK 装到手机上跑通了接下来要证明这个定位工具效果好需要量化评估。毕设答辩最加分的部分不是演示软件多流畅而是给出误差数据、用什么指标测的、测试流程是怎样的。先在测试区域选 20 个测试点与采样点错开不能在指纹库的参考点上测否则属于作弊每个测试点用 App 采集一次定位结果并记录真实坐标计算两者欧氏距离。用 Matplotlib 或 MATLAB 画 CDF 曲线累积分布函数横轴是误差值纵轴是小于该误差的测试点占比。评估指标建议看三个平均误差、中位误差、90% 分位误差。我之前在某地下车库实测的数据是1.5 米采样间距、K3、每点采样 25 次取均值平均误差 3.6 米中位数 3.1 米90% 分位 6.2 米。这个精度比纯蓝牙 Beacon 方案好一些而且完全没增加硬件成本。影响精度的主要因素有三个。第一个是采样点密度——间距从 2 米缩到 1 米平均误差能降 1.5 米左右第二个是环境变化——货架挪动、卷帘门开关都会让 RSSI 漂移第三个是手机型号差异——iPhone 和安卓机的 WiFi 扫描射频指标不同同一位置 RSSI 能差 5dBm。5.2 指纹库动态更新加权滑动窗口与过期指纹淘汰指纹库不是建一次就一劳永逸。商场里的 AP 可能被替换、增设或者某个路由器重启后发射功率变化。如果还拿旧指纹去匹配误差会越变越大。维护动态指纹库常用的方案是滑动窗口加权平均。具体实现思路每个参考点的每个 AP 都维护一个环形队列最多存最近 M 次采样M 取 20 到 50。新采样进来时按时间衰减系数加权更新指纹值public void updateAvg(String bssid, int newRssi) { DequeInteger window rssiWindow.get(bssid); window.addLast(newRssi); if (window.size() WINDOW_SIZE) { window.removeFirst(); // 淘汰最旧的数据 } int sum 0, weightedSum 0, totalWeight 0; int i 0; for (Integer val : window) { double weight Math.pow(0.95, WINDOW_SIZE - i); // 越新权重越大 sum val * weight; totalWeight weight; i; } double newAvg sum / totalWeight; // 更新指纹库中的参考值 updateFingerprint(bssid, newAvg); }WINDOW_SIZE这个参数控制了指纹对环境的响应速度窗口越大指纹越平滑但更新越迟钝适合 AP 数量稳定的场景。0.95 的衰减系数设定后最近一次采样的权重是第 20 个采样的两倍多兼顾了平稳性和响应速度。实际操作中动态更新不会实时跑而是在定位引擎的低峰期比如夜里 2 点到 5 点进行自动化采集或者由运维人员拿着手机按固定路线巡检一次系统自动按参考点所属区域做批量覆盖。毕设如果展示这个功能最简单的演示方式是写一个脚本循环调用服务端的采样接口模拟 3 组不同时段的采集数据观察指纹库逐步收敛到新环境的过程。5.3 可视化验证在室内地图上叠加热力图层一个容易被忽略但效果极好的加分项是把 WiFi 信号强度的分布画成热力图叠加在平面图上。热力图能直观地暴露采集盲区——比如某个角落从没采过样那块区域的信号就是空白。热力图的实现方式可以简单粗暴把平面图分成 0.5 米 x 0.5 米的网格每个网格的 RSSI 值由最近的 3 个参考点的加权均值插值出来颜色按强度从红到蓝渐变。技术上用 Java 2D 的BufferedImage画逐像素点性能完全够用。先在未采样的空白区域填充颜色再在地图控件上onDraw里叠加这个 Bitmap 的透明度即可。这一步做出来后部署阶段可以直接拿着热力图找信号死角决定是否需要在某堵墙边加装一个廉价 AP 来补齐定位盲区。对一个毕设来说这比多写两百行算法逻辑更能体现工程判断力。本文还有配套的精品资源点击获取