资讯详情

资讯详情

Android扫码连接WiFi实战:二维码解析与自动入网全流程

简介面向Android开发者的WiFi二维码自动连接实现资料包聚焦扫码解析、WiFi配置注入与系统连接三大环节适合需要为应用增加扫码连网功能的中级开发者。包内共141个文件压缩包约2.16MB以Java源码、class编译产物、XML配置与布局、PNG图标资源为主同时包含jar依赖库与可直接安装验证的APK便于对照代码与运行效果学习。内容详解了Zxing/ML Kit二维码扫描接入、WiFi信息JSON解析、WifiManager添加并保存网络、WifiConfiguration参数设置以及Android 6.0以上WRITE_SETTINGS动态权限申请等关键技术点并提供连接失败时的用户交互与容错思路。已有2014人学习下载对希望快速落地扫码连网功能的开发者具有直接参考价值。1. Android 扫描 WiFi 二维码自动连接从“扫出来一串字”到“真的连上网”说到 Android 扫描 WiFi 二维码自动连接这个需求网上的教程大多停在“能扫出字符串”这一步。真正做过一次访客入网工具的人都知道二维码扫出来只是一段WIFI:T:WPA;S:xxx;P:yyy;;文本离“手机自动连上这个热点”还差着权限申请、格式解析、系统网络请求三座大山。我拆过一个模拟项目X里的无线入网组件核心就干这一件事相机对准二维码识别后自动发起连接。做完之后回头看这套流程能复用的部分很多但坑也不少。这篇笔记适合正在做会议签到、酒店入住、门店访客上网这类功能的 Android 开发者也适合想弄明白扫码自动连网到底怎么实现的人。先把原理和格式看透再动手写代码能少走很多弯路。2. 到底扫的是什么WiFi 二维码格式、SDK 选型与 Android 权限演进2.1 WIFI: 格式拆解T、S、P、H 字段与转义规则大多数路由器管理后台生成的 WiFi 二维码其实遵循一套非正式的公开约定内容以WIFI:开头后面跟着一组以分号分隔的键值对常见的键有四个。T 表示认证类型取值通常是WPA、WEP或nopassS 是 SSID也就是WiFi名称P 是密码开放网络时可以没有H 表示是否为隐藏网络true或false。一个典型二维码内容长这样WIFI:T:WPA;S:OfficeNet;P:pssw0rd;H:false;;注意末尾有两个分号。这不算笔误而是很多生成器固定会在最后一个键值对后面再补一个分号解析器要兼容这种情况。另外字段顺序不固定有的生成器会把 H 放在 P 前面所以不能假设第一个字段一定是 T。这里有一个比字段顺序更容易翻车的点转义。WiFi 密码里如果出现分号、冒号、逗号、双引号或反斜杠生成器一般会用反斜杠把它们转义。比如密码是abc;123二维码内容里对应的 P 字段会写成P:abc\;123。如果解析时直接split(;)密码就会被截断成abc接下来怎么连都是密码错误。字段含义常见取值是否必填T认证类型WPA / WEP / nopass建议有SSSID 名称字符串支持中文必填P密码字符串支持转义字符开放网络可不填H是否隐藏网络true / false可选拿到这份资源以后第一件事不是接相机而是把这套字符串解析规则定清楚。解析器的容错性直接决定后面所有步骤能不能走通我见过太多项目在连接阶段排查半天最后发现是解析时把密码弄短了。2.2 扫码库怎么选ZXing core CameraX 的组合边界扫码解析库的选型网上争论很多我按这套流程的实际体验说结论。ZXing 的老版本把扫码 UI、相机管理、二维码解析全揉在一起直接依赖整套库会带进来很多用不到的活动和布局。现在更推荐只引它的 core 模块用它做纯解析相机预览交给 CameraX这样预览和二维码识别是两条清晰的线出问题也好定位。ML Kit 的扫码能力确实强识别速度快对模糊、反光的容忍度也更高但它在部分国产 ROM 上依赖移动服务框架离线场景下要多做一层适配。如果你这个 Android 扫码连接功能要打包成内部工具装到各种品牌手机上我一般不太愿意赌框架是否完整。ZBar 历史悠久解析速度也快但它的 Android 版本维护节奏偏慢新架构的手机上要留意 so 库兼容。所以我的选型结论是业务侧只依赖 ZXing core 做文本解析相机侧用 CameraX 的 ImageAnalysis 做逐帧分析。CameraX 对生命周期绑定做得比较省心configuration change 时不需要手动释放相机ImageAnalysis 的背压模式也够用。真要处理极端反光或者歪斜的二维码再在 analyzer 里补一个多帧确认的逻辑或者升级到 ML Kit 做备选不影响整体架构。2.3 Android 权限演进取舍从定位到 NEARBY_WIFI_DEVICESAndroid 的 WiFi 相关权限近几年一直在收紧这是做自动连接时最容易懵的地方。Android 8.0 之前扫描 WiFi 列表只需要CHANGE_WIFI_STATE这类基础权限从 Android 8.0 开始调用getScanResults()必须要有定位权限Android 10 开始应用主动调用enableNetwork()切换网络的行为被限制系统要求用户通过弹窗确认才能加入网络到了 Android 13也就是 targetSdk 33 以上又新增了NEARBY_WIFI_DEVICES权限专门管理 WiFi 连接这类近距离交互能力。Android 版本关键权限连接网络方式用户交互9 及以下ACCESS_FINE_LOCATION CHANGE_WIFI_STATEWifiManager.addNetwork enableNetwork无需额外确认10 ~ 12ACCESS_FINE_LOCATION CHANGE_WIFI_STATEWifiNetworkSpecifier ConnectivityManager系统弹窗需用户点连接13 及以上NEARBY_WIFI_DEVICES声明 neverForLocation 可免定位WifiNetworkSpecifier系统弹窗需用户点连接这里有一个很多人忽略的边界Android 10 之后所谓“自动连接”并不是应用私下把网络切过去而是应用发起一个网络请求系统弹出一个列表让用户选择要连接的热点用户确认后才真正加入。所以做产品功能时要管理好预期不要把“自动连接”理解成完全无感的静默切换。你要做的是把扫码、解析、请求、状态回调这套链路处理好让用户只需要点一下系统弹窗就可以了。3. 解析与入网从二维码文本到 Wifi Network 的核心实现3.1 相机帧解析ImageAnalysis 里把 YUV 帧交给 ZXing扫码这一层的核心是把 CameraX 的ImageAnalysis.Analyzer回调里拿到的帧转换成 ZXing 能识别的二进制图。CameraX 默认输出的ImageProxy是 YUV_420_888 格式ZXing 不能直接吃需要先把 YUV 旋转并转换成 RGB 位图。竖屏状态下预览帧是旋转了 90 度的直接解析会导致二维码识别率下降很多所以旋转这步不能省。class QrCodeAnalyzer( private val onQrResult: (String) - Unit ) : ImageAnalysis.Analyzer { private val reader QRCodeReader() override fun analyze(imageProxy: ImageProxy) { imageProxy.image?.let { image - // 把 ImageProxy 中的 YUV 帧旋转后转成 Bitmap val bitmap imageProxy.toBitmap() val width bitmap.width val height bitmap.height val pixels IntArray(width * height) bitmap.getPixels(pixels, 0, width, 0, 0, width, height) val source RGBLuminanceSource(width, height, pixels) val binaryBitmap BinaryBitmap(HybridBinarizer(source)) try { val result reader.decode(binaryBitmap) result.text?.takeIf { it.startsWith(WIFI:) }?.let(onQrResult) } catch (_: NotFoundException) { // 当前帧没有二维码继续下一帧 } catch (_: ChecksumException) { // 二维码被遮挡或有脏污忽略这一帧 } finally { imageProxy.close() } } ?: imageProxy.close() } }这段代码里toBitmap()没有直接写在 Analyzer 里因为 YUV 到 Bitmap 的转换代码比较长单独抽出去更好维护。转换的核心思路是先读 Y、U、V 三个 plane 的 buffer按照旋转角度做平面重排再合成 RGB。这里有个性能细节不要在analyze()里每次都 new 大数组pixels数组可以复用否则连续扫描时 GC 压力会很大。reader.decode()抛出的异常要分开捕获NotFoundException是正常情况每帧都可能没有二维码但ChecksumException出现说明帧数据有损坏多半是转码或裁剪时越界了。最后imageProxy.close()必须放在所有分支的出口。CameraX 的 ImageAnalysis 底层 buffer 数量有限不关闭的话几秒钟后相机就会黑屏这个坑后面专门有一章细说。3.2 手写 WIFI 字符串解析器转义反转义与容错拿到WIFI:开头的文本之后解析这一步决定了后面能不能连上。我见过不少项目直接用split(;)遇到密码里带分号就翻车。正确做法是逐字符扫描遇到反斜杠时把下一个字符当作普通字符拼进去遇到分号时作为一个键值对的结束标记。data class WifiConfig( val type: String, val ssid: String, val password: String, val hidden: Boolean ) fun parseWifiQrCode(content: String): WifiConfig? { if (!content.startsWith(WIFI:)) return null val raw content.removePrefix(WIFI:) val fields mutableMapOfString, String() var currentKey: Char? null val currentValue StringBuilder() var index 0 while (index raw.length) { val c raw[index] when { // 识别 T: S: P: H: 这类键名 currentKey null c.isLetter() index 1 raw.length raw[index 1] : - { currentKey c index 2 } // 反斜杠转义把下一个字符当作普通字符 currentKey ! null c \\ index 1 raw.length - { currentValue.append(raw[index 1]) index 2 } // 分号代表当前字段结束 currentKey ! null c ; - { fields[currentKey.toString()] currentValue.toString() currentKey null currentValue.clear() index } else - { if (currentKey ! null) { currentValue.append(c) } index } } } // 兼容末尾可能没有分号的情况 if (currentKey ! null) { fields[currentKey.toString()] currentValue.toString() } return WifiConfig( type fields[T] ?: nopass, ssid fields[S] ?: , password fields[P] ?: , hidden fields[H].equals(true, ignoreCase true) ) }这个解析器最核心的就是那一节转义处理逻辑。很多路由器生成二维码时会对;、:、,、、\做转义处理其中反斜杠本身也可能被转义成\\所以要在看到反斜杠时无条件吞掉下一个字符而不是只在特定字符前才处理。这样写能兼容大多数生成器的输出。字段缺失时type 默认成nopass比较稳妥因为开放网络二维码经常不写 T 字段或者写成T:nopass。S 是必填的如果解析出来是空字符串说明二维码格式不对可以直接给用户提示“无法读取网络名称”。3.3 Android 9 及以下WifiManager 添加网络的老路低版本系统上的实现相对直接通过WifiManager.addNetwork()把配置写进系统再enableNetwork()切换过去。这段逻辑在 Android 10 以下依然是有效方案但 targetSdk 29 以上时行为会变所以要把两套方案分开维护。fun connectWithWifiManager( wifiManager: WifiManager, config: WifiConfig ): Boolean { val wifiConfiguration WifiConfiguration().apply { SSID \${config.ssid}\ when (config.type.uppercase()) { NOPASS - { allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE) } WEP - { allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WEP_104) // 这里需要按 WEP 密钥格式做额外校验 wepKeys[0] \${config.password}\ wepTxKeyIndex 0 } else - { allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK) preSharedKey \${config.password}\ } } hiddenSSID config.hidden } val networkId wifiManager.addNetwork(wifiConfiguration) if (networkId -1) { return false } wifiManager.disconnect() return wifiManager.enableNetwork(networkId, true) }注意 SSID 和密码都手动包了一层双引号。系统层的 WifiConfiguration 在解析 SSID 和密码时双引号是字符串边界的标记不加引号虽然在某些机型上也能过但遇到中文 SSID 或特殊字符时很容易出现连接后马上断开的诡异问题。WEP 是个特例它的密码格式不是纯字符串长度和十六进制格式都有要求真正做 WEP 支持时要单独加校验逻辑现在家用路由器基本都用 WPA/WPA2 了。这段代码里addNetwork()返回 -1 是常见失败信号可能是配置格式不对也可能是权限不足。enableNetwork(networkId, true)的第二个参数表示是否“禁用其他网络”传 true 才会让当前热点变成默认连接。3.4 Android 10 及以上WifiNetworkSpecifier 的正确姿势Android 10 之后系统不再允许应用静默调用enableNetwork()切换网络强制走WifiNetworkSpecifierConnectivityManager.requestNetwork()的方式。这个方案的好处是不需要在系统里保存网络配置每次扫码后发起一个临时的网络请求退出场景后系统自动遗忘适合访客网络这种一次性连接场景。fun requestWifiNetwork( connectivityManager: ConnectivityManager, config: WifiConfig, callback: ConnectivityManager.NetworkCallback ) { val specifierBuilder WifiNetworkSpecifier.Builder().apply { setSsid(config.ssid) if (config.type.uppercase() ! NOPASS) { setWpa2Passphrase(config.password) } } val networkRequest NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .setNetworkSpecifier(specifierBuilder.build()) .build() connectivityManager.requestNetwork( networkRequest, callback, 10_000 ) }这里有个细节开放网络只setSsid()就可以了不要画蛇添足去设置空密码。设置了 WPA2 passphrase 之后系统在弹窗列表里会过滤掉不支持 WPA2 的热点。隐藏网络在 WifiNetworkSpecifier 里的处理也靠 SSID 匹配不需要额外显式标明 hidden 字段。第三个参数是超时时间我习惯给 10 秒。超过这个时间还没有匹配到目标网络系统会回调onUnavailable()这时候要提示用户检查密码是否错误、热点是否在附近或者是否已经被系统弹窗拦截了。注意requestNetwork()发起后用户会在系统 UI 里看到连接请求用户点了“连接”才会真正走网络建立流程Android 10 上这是绕不过去的交互。4. 把流程串起来工程配置、运行时权限与连接状态机4.1 Manifest 依赖与权限配置整套流程要跑起来Manifest 里的权限声明必须一次配齐少一个都可能导致扫码正常但连接失败。我通常按目标版本区分来写最低兼容到 Android 8.0targetSdk 指向 33 或更高。uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / uses-permission android:nameandroid.permission.CHANGE_WIFI_STATE / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION android:maxSdkVersion32 / uses-permission android:nameandroid.permission.NEARBY_WIFI_DEVICES android:usesPermissionFlagsneverForLocation /ACCESS_FINE_LOCATION加maxSdkVersion32的逻辑是Android 13 及以上用NEARBY_WIFI_DEVICES替代定位权限来做 WiFi 连接而且声明neverForLocation后系统知道你不会用 WiFi 信息推导位置就不会要求定位权限。如果 targetSdk 33 以上还保留定位权限反而会在隐私弹窗上多一道不必要的解释。依赖层面CameraX 和 ZXing core 是标准的 Gradle 依赖按需引入即可。注意不要引 ZXing 的完整扫码 UI 模块否则会带来不必要的 Activity 和资源文件跟自己的相机页面容易冲突。我在模拟项目X里遇到过一次资源合并冲突最后排查出来就是完整 ZXing 模块里带了一个同名布局。4.2 运行时权限Android 13 的 NEARBY_WIFI_DEVICES 请求权限声明只是第一步运行时请求必须做。Android 13 的NEARBY_WIFI_DEVICES属于运行时权限要像定位权限一样动态申请Android 12 及以下则要申请定位权限。这一块如果写死了到了新系统上会直接闪退或永远连不上。private val permissionLauncher registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result - val granted result.values.all { it } if (granted) { startCameraPreview() } else { showPermissionDeniedTip() } } fun checkAndRequestPermissions() { val required if (Build.VERSION.SDK_INT 33) { arrayOf( Manifest.permission.CAMERA, Manifest.permission.NEARBY_WIFI_DEVICES ) } else { arrayOf( Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION ) } permissionLauncher.launch(required) }这里要注意的是权限拆分逻辑Android 12 以下申请定位权限Android 13 以上申请附近 WiFi 设备权限Camera 两个版本都要。有些开发者图省事直接全申请结果在 Android 13 上弹了两个完全无关的权限说明用户很容易拒绝。更好的做法是在扫码页进入时先只请求 Camera 权限等二维码解析出了WIFI:文本、即将发起连接前再请求网络相关权限这样用户能理解“为什么要这个权限”。4.3 连接状态机与超时处理扫码之后到连接成功之间存在一个异步等待窗口。如果不对状态做收敛管理用户连续扫几个二维码前面的回调还没回来后面的请求又发出去状态就乱套了。我习惯用一个简单的状态对象把流程切成分散的阶段。sealed interface WifiConnectState { data object Idle : WifiConnectState data class Connecting(val ssid: String) : WifiConnectState data class Connected(val ssid: String) : WifiConnectState data class Failed(val reason: String) : WifiConnectState } private val state mutableStateOfWifiConnectState(WifiConnectState.Idle) fun handleQrText(content: String) { val config parseWifiQrCode(content) ?: run { state.value WifiConnectState.Failed(二维码格式无法识别) return } state.value WifiConnectState.Connecting(config.ssid) requestWifiNetwork( connectivityManager, config, object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { state.value WifiConnectState.Connected(config.ssid) } override fun onUnavailable() { state.value WifiConnectState.Failed(匹配不到目标网络或用户取消) } } ) }状态机的收益在遇到异常情况时尤其明显。比如用户扫了一个密码错误的二维码onUnavailable()返回失败紧接着又扫了一个正确的二维码这时候Connecting状态会把上一次的失败状态覆盖掉UI 上不会出现“失败”和“连接中”同时闪烁的问题。在 UI 侧Connecting状态要禁用扫码按钮防止连续请求把系统弹窗堆成连环弹窗。超时时间要放在网络请求层而不是 UI 层不要用定时器去猜系统什么时候回调。10 秒超时是经验值热点信号弱或路由器响应慢时可以放宽到 15 秒但不要超过 20 秒否则用户会以为功能坏了。5. 避坑与常见问题五个容易翻车的现场5.1 扫描正常但连不上多半是定位权限丢了有同事曾在某机型上反馈二维码能扫出来解析也成功了但一直连接失败错误提示是“未找到网络”。后来抓日志发现wifiManager.getScanResults()返回的是空列表目标 SSID 根本不在扫描结果里。原因很直接Android 8.0 以上系统要求定位权限才能访问 WiFi 扫描结果而那个页面的权限请求只给了 Camera没给定位权限。解决方式就是在发起网络请求前先检查权限要么跳转设置页要么在扫码页用统一入口把权限全部请求完。Android 13 上则要换成NEARBY_WIFI_DEVICES逻辑是一样的。5.2 addNetwork 返回 true 却没网targetSdk 29 的 enableNetwork 玄学模拟项目X早期版本里我在 Android 10 手机上调用addNetwork()返回了有效的 networkIdenableNetwork()也返回 true但手机就是没连上目标热点原来连接的网络反而断了。这是 targetSdk 29 之后enableNetwork()行为被系统限制的典型表现API 还在返回码也正常但系统不再允许应用直接切换活动网络真正的网络选择交给用户确认。解决方式是严格按 targetSdk 版本分流Android 10 以上必须走WifiNetworkSpecifier不要再留恋老 API。5.3 密码带分号解析器按 split(;) 必然翻车某次实测现场发现扫码后一直提示密码错误。查了一下二维码生成器密码是Abc;123但解析器用split(;)把密码截断成了Abc。Zxing 解析出的文本里分号前可能还带着反斜杠转义没做反转义就会把这个反斜杠也当成密码内容。解决方法是统一用逐字符解析器遇到\;时把分号当普通字符拼进值里而不是当字段分隔符。从那以后我把这个解析器单拎出来做了单元测试专门造了一批含特殊字符的用例。5.4 SSID 不加双引号中文热点掉线重连的罪魁祸首用WifiManager添加网络时手拼的WifiConfiguration里 SSID 必须带双引号。有些机型不加引号也能连上导致代码看起来没问题。但中文 SSID 和带空格的 SSID 在系统底层会被当成两种不同的匹配方式一种按字面匹配一种按转义字符串匹配表现就是连上后频繁掉线或者连上了但wifiManager.connectionInfo.ssid里带着引号。密码同样要加引号尤其是包含特殊字符的密码。这个问题在 Android 9 及以下特别常见因为老版本对配置的校验更宽松。5.5 摄像头越扫越卡忘了 close 的 ImageProxyImageAnalysis 的analyze()回调里每一帧的ImageProxy都占用一个底层 buffer如果不调用close()buffer 耗尽之后 CameraX 会停止送帧表现就是页面越来越卡最后取景器黑屏。这个问题隐蔽在连接逻辑里因为只要二维码快速识别成功Analyzer 就会提前返回某个分支里忘了 close直到下次连续扫描时才暴露。解决方式是把close()放在finally块里并顺手在 Analyzer 构造时加一个帧率计数做调试输出正常应该在 20 帧每秒以上。6. 验证与进阶自己生成 WiFi 二维码做端到端自测6.1 本地生成带转义的 WiFi 测试二维码做扫码功能不能指望每台路由器都能生成带特殊字符的二维码最稳的办法是自己生成测试码。用 Python 的 qrcode 库可以快速造出各种边界用例关键是转义顺序要对先转义反斜杠本身再转义分号、冒号、逗号和双引号。import qrcode def escape_wifi_field(value: str) - str: # 顺序不能反先处理反斜杠自身 value value.replace(\\, \\\\) value value.replace(;, \\;) value value.replace(:, \\:) value value.replace(,, \\,) value value.replace(\, \\\) return value def build_wifi_qr(ssid: str, password: str, auth: str WPA, hidden: bool False): payload ( WIFI:T: auth ; S: escape_wifi_field(ssid) ; P: escape_wifi_field(password) ; H: str(hidden).lower() ;; ) qr qrcode.QRCode(versionNone, box_size8, border2) qr.add_data(payload) qr.make(fitTrue) qr.make_image(fill_colorblack, back_colorwhite).save(wifi_test.png) print(payload) # 测试含分号和双引号的密码 build_wifi_qr(某会议室, Abc;123\x, authWPA, hiddenFalse)生成完二维码后拿一部测试手机扫码对比解析器输出的 SSID 和密码是否与原始值完全一致。这一步能同时验证转义和反转义逻辑是否正确不用等连网失败再倒退排查。6.2 “真的连上了”的四个验证维度连接成功的判断不能只看onAvailable()回调那只是系统层面为这个网络建立了连接不代表能上网。我一般会做四层验证第一层看解析字段SSID 和密码长度是否正确第二层看系统连接状态WifiManager.getConnectionInfo().ssid是否等于目标 SSID第三层看数据通路ping 网关或内网地址看是否通第四层看是否能主动断开避免之前连接的网络被静默替换掉。验证维度 检查方法 通过标准 解析层 打印 parseWifiQrCode 的输出 SSID/密码与原始值一致 系统层 getConnectionInfo().ssid 去除引号后与目标 SSID 相同 数据层 ping 网关或内网域名 丢包率 0延迟在正常范围 交互层 连接成功后断网重扫 可再次发起请求并连接这套端到端自测流程是踩了几次坑之后才固化下来的。最深刻的一次教训是当时只验证了系统层连接成功没验证数据通路结果发现手机连上了热点但 DHCP 没拿到 IP整个体验是“显示已连接但上不了网”。从那以后我每次改完解析或连接逻辑都会强制走一遍这四层验证QR 码也固定放在测试手机相册里随手就能扫。希望这篇拆解能帮你把 Android 扫描 WiFi 二维码自动连接的链路一次性捋顺少陪系统权限和转义规则熬夜。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →