5、扫描机制(Scan):ScanSettings 与 ScanResult、扫描触发流程、扫描结果回调
Wi-Fi 扫描,说白了就是手机去「听」一下周围有哪些热点在喊话。这个机制看起来简单,但坑不少。我最早接触 Android Wi-Fi 时,以为扫描就是调个 API 完事,结果发现扫描时机、参数配置、结果回调,每一步都可能翻车。
今天我们就把它彻底拆开来看。
5.1 扫描的核心数据结构
先看两个最基础的类:ScanSettings 和 ScanResult。一个管「怎么扫」,一个管「扫到了啥」。
5.1.1 ScanSettings:扫描参数配置
ScanSettings 用来告诉系统扫描的「姿势」。比如只扫 5GHz、只扫某个频段、要不要被动扫描等。
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 低延迟模式
.setChannelSet(channelList) // 指定信道列表
.build();
常用的扫描模式有三种:
| 模式 | 说明 | 典型场景 |
|---|---|---|
| SCAN_MODE_LOW_LATENCY | 低延迟,快速返回结果 | 用户主动点击扫描 |
| SCAN_MODE_LOW_POWER | 省电,扫描间隔拉长 | 后台周期性扫描 |
| SCAN_MODE_HIGH_ACCURACY | 高精度,全信道扫描 | 定位场景 |
我的经验: 低延迟模式虽然快,但耗电也猛。我曾在某个项目中,用户频繁下拉刷新 Wi-Fi 列表,结果手机发烫。后来改成低功耗模式 + 缓存策略,体验好很多。
5.1.2 ScanResult:扫描结果实体
每个 ScanResult 代表一个被扫描到的 AP(接入点)。关键字段如下:
- SSID:热点名称,比如 "CMCC-5G"
- BSSID:MAC 地址,用来区分同名热点
- level:信号强度,单位 dBm,负值越小信号越弱
- frequency:频率,用来判断是 2.4G 还是 5G
- capabilities:加密方式,比如 WPA2-PSK
- timestamp:扫描时间戳,判断结果是否过期
for (ScanResult result : scanResults) {
String ssid = result.SSID;
int rssi = result.level;
String capabilities = result.capabilities;
// 处理每个热点
}
注意: 同一个 SSID 可能有多个 BSSID(比如商场里的 Mesh 网络)。如果你只按 SSID 去重,可能会漏掉信号更好的那个 AP。
5.2 扫描触发流程
扫描不是你想扫就能扫的。Android 对扫描频率有限制,防止 App 恶意耗电。我见过不少开发者一上来就疯狂调 startScan(),结果被系统限频,啥也扫不到。
5.2.1 应用层触发
标准流程是:
- 获取
WifiManager实例 - 注册
ScanResultCallback(Android 12 以上)或 BroadcastReceiver - 调用
startScan() - 等待回调
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
// Android 12+ 推荐方式
wifiManager.registerScanResultCallback(executor, new WifiManager.ScanResultCallback() {
@Override
public void onScanResultAvailable() {
List<ScanResult> results = wifiManager.getScanResults();
// 处理结果
}
});
wifiManager.startScan();
5.2.2 系统层的扫描调度
应用层调 startScan() 后,会走到 WifiScanningServiceImpl。这个服务会做几件事:
- 检查扫描限频(每个 App 每 30 秒最多一次)
- 合并多个 App 的扫描请求
- 下发扫描命令到 HAL 层
- 等待驱动返回结果
关键点: 系统会合并扫描请求。比如 App A 和 App B 同时请求扫描,实际只扫一次,结果分发给两个 App。这样省电又高效。
5.2.3 扫描限频机制
Android 对扫描频率有严格限制:
| 限制类型 | 限制值 | 说明 |
|---|---|---|
| 单 App 扫描间隔 | 30 秒 | 同一个 App 两次扫描至少间隔 30 秒 |
| 全局扫描间隔 | 15 秒 | 所有 App 合并后,扫描间隔不低于 15 秒 |
| 后台扫描限制 | 更严格 | App 在后台时,扫描频率进一步降低 |
我曾经踩过的坑: 有个 App 在后台每 10 秒扫一次,结果被系统判定为恶意行为,直接拉黑。后来我改成监听系统广播 + 按需扫描,才解决问题。
5.3 扫描结果回调
扫描结果怎么回到应用层?这里有两个路径:
5.3.1 传统 BroadcastReceiver 方式
老项目里常见这种写法:
IntentFilter filter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
context.registerReceiver(receiver, filter);
// 在 receiver 中
public void onReceive(Context context, Intent intent) {
boolean success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false);
if (success) {
List<ScanResult> results = wifiManager.getScanResults();
}
}
这种方式有个问题:广播是全局的,所有 App 都能收到。而且你没法知道扫描是否真的成功。
5.3.2 Android 12+ 的 ScanResultCallback
新 API 更优雅:
wifiManager.registerScanResultCallback(executor, new WifiManager.ScanResultCallback() {
@Override
public void onScanResultAvailable() {
// 扫描完成,可以取结果了
List<ScanResult> results = wifiManager.getScanResults();
}
@Override
public void onScanFailure(int reason) {
// 扫描失败,reason 表示失败原因
}
});
我个人更喜欢这种方式。回调更精确,而且能区分成功和失败。你想想看,如果扫描失败了你还在那等结果,是不是很尴尬?
5.4 扫描流程全景图
下面这张图把整个扫描链路串起来了:
5.5 避坑指南
最后分享几个我实际项目中遇到的坑:
- 扫描结果为空? 先检查 Wi-Fi 是否开启。很多新手忘了这步。
- 结果太旧? 检查
timestamp字段。我建议超过 10 秒的结果就不要用了。 - 扫描太慢? 看看是不是被限频了。可以用
adb shell dumpsys wifi查看扫描记录。 - 回调不触发? 确认是否在 UI 线程注册了 Callback。有些回调要求在主线程。
我的习惯: 每次扫描前先检查
WifiManager.isScanAlwaysAvailable()。如果返回 false,说明系统禁止扫描,这时候调 startScan() 也是白费力气。
嗯,扫描机制就聊到这儿。说白了就是「配置 → 触发 → 回调」三步走,但每一步都有细节。你把这些细节吃透了,Wi-Fi 扫描这块基本就稳了。