最近在调试一个基于Android 16(API 36)的WiFi扫描功能时,遇到了一个奇怪的现象:当调用WifiManager.getScanResults()获取周边WiFi列表时,部分热点返回的SSID显示为"
作为移动端开发者,WiFi扫描是很多应用的基础功能——比如室内定位、网络切换优化、设备配网等场景都会用到。当SSID无法正常获取时,会导致基于SSID的业务逻辑全部失效。我在实际项目中就遇到过因此导致的智能家居设备配网失败问题,用户手机明明扫描到了设备热点,却因为SSID显示未知而无法完成后续流程。
这个问题的根源要从Android 10(API 29)引入的隐私保护政策说起。在Android 10之前,应用只要获取了ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION权限,就可以读取完整的WiFi扫描结果,包括SSID、BSSID、信号强度等所有信息。
但从Android 10开始,Google进一步收紧了权限策略:
Android 11(API 30)又追加了更严格的限制:
首先在AndroidManifest.xml中声明必要权限:
xml复制<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
在Activity或Fragment中动态请求权限:
kotlin复制private fun checkPermissions() {
when {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED -> {
startWifiScan()
}
ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) -> {
showRationaleDialog()
}
else -> {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_CODE_LOCATION
)
}
}
}
private fun showRationaleDialog() {
AlertDialog.Builder(this)
.setTitle("需要位置权限")
.setMessage("获取WiFi列表需要位置权限以保护用户隐私")
.setPositiveButton("确定") { _, _ ->
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_CODE_LOCATION
)
}
.setNegativeButton("取消", null)
.show()
}
获取扫描结果的完整代码示例:
kotlin复制private val wifiManager by lazy {
applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
}
private val wifiScanReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)
if (success) {
processScanResults()
} else {
// 扫描失败,使用缓存结果
processScanResults(wifiManager.scanResults)
}
}
}
private fun startWifiScan() {
val intentFilter = IntentFilter().apply {
addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
}
registerReceiver(wifiScanReceiver, intentFilter)
val success = wifiManager.startScan()
if (!success) {
// 扫描启动失败
processScanResults(emptyList())
}
}
private fun processScanResults(results: List<ScanResult>? = null) {
val scanResults = results ?: wifiManager.scanResults
scanResults.forEach { result ->
val ssid = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// Android 11+ 需要特殊处理
if (result.isSsidHidden()) {
"<hidden>"
} else {
result.wifiSsid?.toString()?.removeSurrounding("\"") ?: "<unknown ssid>"
}
}
else -> {
result.SSID.takeIf { it.isNotBlank() } ?: "<unknown ssid>"
}
}
Log.d("WiFiScan", "SSID: $ssid, BSSID: ${result.BSSID}, RSSI: ${result.level}dBm")
}
}
在Android 11及以上版本,即使拥有所有权限,某些情况下SSID仍可能显示为未知。这时需要检查WifiSsid对象:
kotlin复制@RequiresApi(Build.VERSION_CODES.R)
private fun getSsidCompat(scanResult: ScanResult): String {
return when {
scanResult.wifiSsid == null -> "<unknown ssid>"
scanResult.isSsidHidden() -> "<hidden>"
else -> scanResult.wifiSsid.toString().removeSurrounding("\"")
}
}
从Android 10开始,必须确保设备位置服务已开启:
kotlin复制private fun isLocationEnabled(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationManager.isLocationEnabled
} else {
Settings.Secure.getInt(
contentResolver,
Settings.Secure.LOCATION_MODE,
Settings.Secure.LOCATION_MODE_OFF
) != Settings.Secure.LOCATION_MODE_OFF
}
}
如果应用需要在后台获取WiFi信息,需要:
kotlin复制// 在Service中启动前台服务
private fun startForegroundScan() {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("WiFi扫描中")
.setContentText("正在获取周边WiFi信息...")
.setSmallIcon(R.drawable.ic_wifi_scan)
.build()
startForeground(NOTIFICATION_ID, notification)
startWifiScan()
}
权限验证:
设备状态检查:
代码层面检查:
建议添加详细日志帮助排查:
kotlin复制fun printScanResultDetails(result: ScanResult) {
Log.d("WiFiDebug", """
SSID: ${result.SSID}
BSSID: ${result.BSSID}
Capabilities: ${result.capabilities}
Frequency: ${result.frequency}MHz
Level: ${result.level}dBm
Timestamp: ${result.timestamp}
ChannelWidth: ${result.channelWidth}
CenterFreq0: ${result.centerFreq0}
CenterFreq1: ${result.centerFreq1}
OperatorFriendlyName: ${result.operatorFriendlyName}
VenueName: ${result.venueName}
""".trimIndent())
}
某些厂商ROM可能有额外限制:
对于只需要连接WiFi信息(而非扫描)的场景,可以使用:
kotlin复制val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
capabilities?.transportInfo?.let { info ->
if (info is WifiInfo) {
val ssid = info.ssid?.removeSurrounding("\"") ?: "<unknown>"
val bssid = info.bssid
// 注意:此方法也需要位置权限
}
}
Android 10引入的新API,适合需要连接特定WiFi的场景:
kotlin复制val specifier = WifiNetworkSpecifier.Builder()
.setSsid("MyWiFi")
.setWpa2Passphrase("password123")
.build()
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setNetworkSpecifier(specifier)
.build()
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.requestNetwork(request, object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
// WiFi已连接
}
})
对于需要大规模部署的企业应用,建议:
xml复制<!-- 企业WiFi配置示例 -->
<wifi>
<ssid>corpnet</ssid>
<hidden>false</hidden>
<security>WPA2</security>
<identity>employee</identity>
<anonymous-identity>anonymous</anonymous-identity>
<password>*****</password>
<phase2>MSCHAPV2</phase2>
<eap>PEAP</eap>
<subject-match>CN=radius.corp.com</subject-match>
</wifi>
过度扫描会导致电量消耗增加,建议:
kotlin复制val wifiScanWorkRequest = PeriodicWorkRequestBuilder<WifiScanWorker>(
15, TimeUnit.MINUTES // 最小间隔15分钟
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"wifiScanWork",
ExistingPeriodicWorkPolicy.KEEP,
wifiScanWorkRequest
)
避免重复处理相同热点:
kotlin复制private val wifiCache = mutableMapOf<String, ScanResult>()
private fun processResultsWithCache(results: List<ScanResult>) {
val newResults = results.filterNot { result ->
wifiCache[result.BSSID]?.let { cached ->
cached.SSID == result.SSID &&
abs(cached.level - result.level) < 5
} ?: false
}
newResults.forEach { result ->
wifiCache[result.BSSID] = result
// 处理新结果
}
}
WiFi信号常有波动,建议添加滤波算法:
kotlin复制private val rssiMap = mutableMapOf<String, Float>()
private fun getFilteredRssi(bssid: String, newRssi: Int): Int {
val alpha = 0.2f // 滤波系数
val filtered = rssiMap[bssid]?.let {
it * (1 - alpha) + newRssi * alpha
} ?: newRssi.toFloat()
rssiMap[bssid] = filtered
return filtered.toInt()
}
在androidTest目录下创建测试用例:
kotlin复制@RunWith(AndroidJUnit4::class)
class WifiScanTest {
@get:Rule
val permissionRule = GrantPermissionRule.grant(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_WIFI_STATE
)
@Test
fun testWifiScan() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val latch = CountDownLatch(1)
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val results = wifiManager.scanResults
assertTrue(results.isNotEmpty())
latch.countDown()
}
}
context.registerReceiver(
receiver,
IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
)
wifiManager.startScan()
assertTrue(latch.await(30, TimeUnit.SECONDS))
}
}
使用Android Studio的模拟器创建多个API级别的虚拟设备:
建议在以下品牌设备上测试:
重点验证:
如果应用收集WiFi信息,必须在隐私政策中明确说明:
建议:
针对欧洲用户需额外注意:
kotlin复制fun anonymizeWifiData(scanResult: ScanResult): Map<String, Any> {
return mapOf(
"ssid_hash" to scanResult.SSID.hashCode(),
"bssid_prefix" to scanResult.BSSID?.take(8) ?: "",
"rssi" to scanResult.level,
"frequency" to scanResult.frequency
)
}
Android 13进一步限制:
xml复制<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="32" />
建议实现功能降级逻辑:
kotlin复制fun getWifiInfoCompat(): WifiInfo {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
// 使用NEARBY_WIFI_DEVICES权限
getWifiInfoApi33()
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// 使用精细位置权限
getWifiInfoApi30()
}
else -> {
// 旧版本实现
getWifiInfoLegacy()
}
}
}
如果WiFi扫描限制影响核心业务,可考虑:
在实际项目中,我们最终采用的解决方案是组合使用WiFi扫描和蓝牙信标,当WiFi SSID不可用时自动切换到蓝牙定位方案,同时引导用户开启必要权限。这种渐进式方案上线后,配网成功率从原来的68%提升到了92%。