7. 连接WiFi(推荐方式):NetworkSpecifier与WifiNetworkSpecifier.Builder(Android 10+)、ConnectivityManager.requestNetwork()、自动连接流程

说实话,Android 10 之前的 WiFi 连接方式,我一直觉得有点「拧巴」。你要去操作 WifiManager,还要处理各种隐式广播,稍不留神就踩坑。我自己早期做项目时,就因为 addNetworkenableNetwork 的调用时序问题,在测试机上反复断连,差点被测试小姐姐追着打。

好在 Android 10 之后,Google 终于给出了一个更优雅的方案——NetworkSpecifier。说白了,它让你像「点菜」一样描述你要连的 WiFi,然后系统帮你搞定剩下的活。我个人习惯把这个流程叫做「声明式连接」。

7.1 核心思路:从「操作硬件」到「声明需求」

老方式里,你得自己管 WifiConfiguration,自己调 connect。新方式呢?你只需要告诉系统:

  • 我要连哪个 SSID?
  • 用什么加密方式?
  • 密码是多少?

剩下的扫描、认证、分配 IP——全交给 ConnectivityManager 去协调。你想想看,这就像你去餐厅吃饭,以前你得自己进厨房炒菜,现在你只管点单,厨师(系统)帮你做。

7.2 主角登场:WifiNetworkSpecifier.Builder

这个 Builder 是 Android 10 新增的。它的作用就是构建一个「网络描述符」。我一般这样用:

// 构建一个针对特定 SSID 的请求
val specifier = WifiNetworkSpecifier.Builder()
    .setSsid("MyHomeWiFi")
    .setWpa2Passphrase("my_password_123")
    .build()

嗯,这里要注意:setWpa2Passphrase 只适用于 WPA2 加密。如果你遇到的是企业级加密(比如 802.1X),那就得用 setWpa2EnterpriseConfig 或者 setWpa3EnterpriseConfig。我在项目中遇到过一家商场的热点,用的就是 WPA3-Enterprise,当时没注意,折腾了半天才发现是加密类型不匹配。

7.3 发起请求:ConnectivityManager.requestNetwork()

有了描述符,下一步就是把它扔给 ConnectivityManager。这里有个关键点:你需要创建一个 NetworkRequest,然后把 specifier 塞进去。

val request = NetworkRequest.Builder()
    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
    .setNetworkSpecifier(specifier)
    .build()

val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

val callback = object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) {
        // 网络可用!可以开始传输数据了
        Log.d("WiFiDemo", "网络已连接: $network")
        // 记得绑定网络,否则可能走蜂窝数据
        connectivityManager.bindProcessToNetwork(network)
    }

    override fun onUnavailable() {
        // 超时或无法找到匹配网络
        Log.w("WiFiDemo", "网络不可用,请检查 SSID 或密码")
    }

    override fun onLost(network: Network) {
        // 连接断开
        Log.d("WiFiDemo", "网络丢失: $network")
    }
}

connectivityManager.requestNetwork(request, callback)

你可能会问:为什么要 bindProcessToNetwork?我记得第一次写这个代码时,发现 WiFi 连上了,但 App 还是走蜂窝流量。后来查文档才明白——onAvailable 只是告诉你网络可用,但系统不会自动把你的进程流量切过去。你得手动绑定,或者用 Network.openConnection() 来指定网络。

7.4 自动连接流程:系统帮你做了什么?

当你调用 requestNetwork 后,系统内部会走一套完整的流程。我画了一张图,方便你理解:

1. 构建 NetworkSpecifier 2. 创建 NetworkRequest 3. requestNetwork() 发起 4. 系统扫描可用 WiFi 网络 匹配 SSID 和加密类型 匹配? 5. 发起连接(认证) WPA2/WPA3 握手 5. 回调 onUnavailable() 超时或未找到 6. onAvailable() 回调

从图上你能看到,整个流程是线性的。但有一个细节容易被忽略:系统会弹一个用户确认对话框。没错,即使你代码里写好了 SSID 和密码,Android 10+ 仍然会要求用户点击「连接」来确认。这是隐私和安全方面的考虑——防止 App 在后台偷偷连 WiFi。

⚠️ 注意: 用户确认对话框只会在第一次连接时弹出。如果用户拒绝,onUnavailable 会被触发。如果用户同意,后续系统会自动重连,不再弹框。

7.5 避坑指南:我曾经踩过的三个坑

讲几个我实际项目中遇到的坑,希望能帮你省点时间:

  • 坑一:忘记释放 Callback —— requestNetwork 返回的是一个 NetworkRequest 对象,如果你在 Activity 销毁时没有调用 unregisterNetworkCallback,会导致内存泄漏。我建议在 onStoponDestroy 里做清理。
  • 坑二:bindProcessToNetwork 的副作用 —— 绑定后,所有 socket 都会走这个网络。如果你同时需要蜂窝和 WiFi,建议用 Network.openConnection() 按需绑定,而不是全局绑定。
  • 坑三:SSID 带引号 —— 有些设备返回的 SSID 带双引号,比如 "MyWiFi"。但 setSsid 方法要求传入不带引号的原始字符串。我曾经因为这个 bug 排查了一下午。

7.6 什么时候用这个方式?

我个人建议:如果你的 App 只支持 Android 10+(API 29+),那就无脑用 NetworkSpecifier。它比老方式更安全、更稳定。但如果你的 App 还要兼容老设备,那就得同时保留 WifiManager 的 fallback 方案。

💡 小技巧: 可以用 Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q 来做版本判断,分别走新旧两套逻辑。

7.7 总结一下

说白了,NetworkSpecifier + ConnectivityManager.requestNetwork() 就是 Android 官方推荐的 WiFi 连接方式。它把复杂的底层操作封装起来,让你专注于「连哪个网」而不是「怎么连」。你只需要构建好描述符,发起请求,然后在回调里处理结果就行。

嗯,这一节的内容就到这里。代码不多,但背后的流程值得你多跑几遍试试。尤其是那个用户确认对话框,不同厂商的 ROM 表现可能不一样——我在小米和华为的设备上就遇到过不同的 UI 风格。


公众号:蓝海资料掘金营,微信deep3321