1. 从一次线上事故说起:你检测的“网络”,真的是用户手上的网络吗?
做安卓开发这么多年,我见过太多“明明代码检测到网络正常,用户那边却死活连不上”的诡异bug。去年接手一个项目,线上反馈突然暴增,用户集中在某个省份,一进App就提示“网络异常”,但同一个功能在测试机上怎么测都正常。查了一圈,问题出在团队引入的一个统一网络检测模块上,里面用了一个看起来很省事的属性:Application.internetReachability。
这个属性在Qt/QML开发里非常常见,很多跨平台项目会拿它判断网络连通性。但放在安卓上,它有一个非常坑的脾气:它返回的“网络状态”并不等价于“你的App能访问到目标服务器”。举个最简单的例子,手机连着Wi-Fi但Wi-Fi本身连不上外网时,系统层面的网络状态依然可能是“已连接”,这时候internetReachability给你返回一个看起来正常的枚举值,你的App就会傻乎乎地继续发请求,然后超时、失败、被用户骂。
这篇文章我不打算只聊Qt或原生安卓的单点知识,而是想以一个实际踩坑者的身份,把你从“用Application.internetReachability做网络判断”到“彻底搞懂安卓网络检测原理与替代方案”这条路上需要知道的东西讲清楚。适合正在做跨平台App开发、尤其用Qt/QML打包安卓的开发者,也适合那些被ConnectivityManager各种API折腾得头疼的原生安卓开发者。看完你至少能回答三个问题:为什么检测结果不可信?安卓真实网络状态应该怎么拿?拿到的状态怎么判断“能不能上网”?
2. 先搞懂Application.internetReachability到底检测了个啥
2.1 它是Qt封装的一层“系统状态翻译器”
Application.internetReachability不是安卓原生API,而是Qt框架里QmlApplication提供的网络状态属性。它的底层会调用不同平台的网络状态接口,然后翻译成统一枚举返回给QML层调用。在安卓平台上,它内部走的是ConnectivityManager,基本等价于getActiveNetworkInfo()这种老接口,但它拿到的信息级别是“系统当前是否存在可用网络接口”,而不是“你的App是否真的能通过这个接口访问外网”。
来看Qt里这个属性的枚举定义:
Unknown:还没拿到状态,初始化阶段常出现。NoAccess:当前没有网络连接。LocalAccess:本机有网络连接,但无法访问外网(比如Wi-Fi连上了但没网,或者只有局域网地址)。SharedAccess:已连接且看起来能访问公共网络。
很多开发者犯的错误是把SharedAccess当成“万能钥匙”,一旦等于SharedAccess就认为网络绝对可用,然后放心大胆发请求。但真相是:internetReachability的SharedAccess只说明系统认为网卡已连接到某个网络,且这个网络可能提供外网访问能力。它会忽略掉很多真实场景:运营商强制门户(连Wi-Fi后先弹验证页)、代理配置错误、DNS解析失败、防火墙拦截、服务器宕机后超时。
2.2 为什么在安卓上它更容易“失真”
我把同样一段QML代码跑在Windows、macOS、Linux和安卓上对比过,安卓端的“假阳性”概率明显更高。原因有几个。
第一,安卓系统本身对“网络可用”的定义就比较宽松。ConnectivityManager报告的是网络连接状态,比如Wi-Fi信号已关联、移动数据已开启,但它不主动验证这台设备是不是真能跟外部网络通信。系统里有个validate的概念,就是系统自己也会去ping一下谷歌的服务器来验证网络是否通,但这个验证结果在很多国产ROM上被改过、延迟过,甚至关掉了。
第二,安卓的网络状态变化是异步的。Wi-Fi从一个热点切换到另一个热点、从Wi-Fi切到蜂窝数据、飞行模式开关,这些都会触发网络状态广播,但状态更新到UI层有延迟。Application.internetReachability属性在这种切换窗口期内可能会返回旧值,你拿旧值做判断,自然会出现“明明刚断网,检测却还是已连接”。
第三,权限问题。internetReachability要正常工作,QT本身需要拿到ACCESS_NETWORK_STATE权限。如果你的AndroidManifest.xml没声明,这个属性返回的可能是Unknown或者一个误导性值,而不是你期望的“无网络”。
提示:我用Qt 5.15和Qt 6.2分别在安卓9、安卓10、安卓12上测过,
internetReachability的表现在老版本安卓上甚至更不稳定,因为它依赖的底层API在新系统里已经被替换过一轮了。
3. 安卓原生视角:你真正该关心的几个API
3.1 老接口getActiveNetworkInfo已经废了
如果你去翻以前的博客,会看到大量代码用ConnectivityManager.getActiveNetworkInfo()判断网络,然后检查isConnected()。这段代码在targetSdkVersion29及以下的设备上还能跑,但从安卓10开始,Google标记了getActiveNetworkInfo()为废弃API,虽然目前还能用,但它拿到的信息越来越粗糙,在很多新机上返回的结果并不可靠。
替代品是ConnectivityManager.registerDefaultNetworkCallback(),它会在系统默认网络(就是当前App实际流量走的那个网络)注册一个回调。这个回调的好处是,当你的App被切换到另一个网络时,能第一时间收到onAvailable和onLost事件,而且它是专门针对“默认网络”的,不会受到多网卡、多网络并存的影响。
我建议新项目直接把registerDefaultNetworkCallback作为首选网络状态监听方式,不要再碰那些老回调了。代码写起来也不复杂,但要注意回调线程默认在后台线程,更新UI需要自己切回主线程。
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { // 默认网络可用,但不能保证能访问外网 } override fun onLost(network: Network) { // 默认网络断开 } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { // 网络能力变化,这里可以检查是否validated val validated = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) val notRestricted = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) // 组合判断 } })3.2 用NetworkCapabilities做“有效验证”判断
真正解决“Wi-Fi连着但上不了网”问题的,是NetworkCapabilities里的NET_CAPABILITY_VALIDATED。这个flag表示系统已经做了外部连通性验证(通常通过连接一个固定的验证服务器),验证通过才会把这个flag加上。如果验证失败,这个网络会被标记为NET_CAPABILITY_NOT_VALIDATED——但注意,系统不一定会主动断开这个网络,很多情况下它还是会继续持有。
所以最严谨的“当前网络是否可用”判断逻辑是:
fun isNetworkUsable(connectivityManager: ConnectivityManager): Boolean { val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) }NET_CAPABILITY_INTERNET表示这个网络有访问Internet的能力,NET_CAPABILITY_VALIDATED表示系统验证过可以访问外网。这俩同时满足,才等于你访问外网大概率没问题。但还是那句话,系统验证能通不代表你的App一定顺、不代表你连的服务器一定通,所以这只是起点。
另外,我踩坑的时候发现一个细节:NET_CAPABILITY_VALIDATED不是一成不变的,如果你拿到的NetworkCapabilities对象是在网络验证完成前获取的,它的VALIDATED可能是false。所以在onCapabilitiesChanged回调里判断最可靠,因为这个回调只在网络能力变更时触发。
3.3 别忽略ACCESS_NETWORK_STATE权限
这个权限极其基础,但很多人做跨平台框架集成的项目时会被忽略。Application.internetReachability在安卓上要工作,ACCESS_NETWORK_STATE是必备的,没有它,你连获取网络状态的资格都没有。安卓的权限机制就是如此:不是你想查就能查,系统必须先知道你有这个意图。
还有个坑是android.permission.INTERNET。我见过有人只声明了ACCESS_NETWORK_STATE没声明INTERNET,结果网络状态检测正常但实际网络请求失败,还以为是检测逻辑错了。其实INTERNET权限管的是能不能建立网络套接字,ACCESS_NETWORK_STATE管的是能不能读取网络状态,俩缺一不可。做网络检测模块时,开清单就一起加上。
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />4. 从“检测状态”到“判断可用”:完整方案怎么搭
4.1 在QML侧,别再把internetReachability当最终结论
如果你是Qt/QML项目,第一步是调整心态:Application.internetReachability只配做“快速兜底提示”,比如图标显示、非关键UI状态切换,不能拿来做“是否允许用户操作”的唯一依据。
我现在的做法是把网络检测分成三层:
- 第一层:监听
Application.internetReachability,用于UI层的简易展示。NoAccess时直接显示“当前无网络”,LocalAccess或SharedAccess时显示“已连接网络”,但不会因为没有外网就阻止用户操作。 - 第二层:用一个自定义的QML组件,封装安卓原生网络能力查询。通过在QML里调用Java方法或者使用
QtAndroid库,直接拿NetworkCapabilities的VALIDATED状态,把结果同步回来。 - 第三层:真正的业务层判断,放在网络请求入口。比如HTTP客户端发起请求前,不去检查“是不是有网”,而是直接发请求,通过超时和错误码来判断网络是否真的可用。因为再精准的状态检测,都不如一次真实请求来得实在。
第三层这招很老土,但非常有效。网络检测本来就应该是“辅助手段”,真正的可靠逻辑还是要建立在“请求-响应”的闭环上。用户没网时请求会快速失败,连了假网时请求会超时,这两种情况你都能在业务层捕获,然后给出对应提示。用这个思路,比纠结API返回什么值省心得多。
4.2 一个可直接抄作业的安卓原生网络工具类
下面这个工具类我改过好几个版本,目前的版本已经上线跑了大半年,对付主流机型问题不大。它做了三件事:返回当前是否有默认网络、默认网络是否通过系统验证、以及监听回调。
object NetworkMonitor { private var valid = false private var available = false fun init(context: Context) { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager cm.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { super.onAvailable(network) available = true } override fun onLost(network: Network) { super.onLost(network) available = false valid = false } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { super.onCapabilitiesChanged(network, networkCapabilities) valid = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } }) } fun isAvailable(): Boolean = available fun isValidated(): Boolean = valid fun isTrulyUsable(): Boolean = available && valid }onAvailable只会告诉你“网络接口有了”,onCapabilitiesChanged会不断更新系统对网络连通性的验证结果。有些热点网络刚连上时VALIDATED一直是false,过几秒才变成true,这时候你的UI层如果只在onAvailable里做判断,就会误报。用这个类的时候要注意调用时机,init越早越好,最好在Application的onCreate阶段就初始化,因为网络状态变化是全局性的,不是页面级的。
4.3 在QML里如何安全地暴露给UI层
如果你一定想在QML层直接拿到这个状态,我建议不要直接在QML里依赖Application.internetReachability,而是把这个Java工具类封装成一个单例,通过网络状态变化发送信号到QML侧。
比如你在继承QQuickItem或者用QAndroidJniObject调用Java端接口,把结果存到一个全局QObject的属性里,用Navigation或者事件总线通知QML。核心代码大致如下:
// C++ 侧暴露给QML的属性 Q_PROPERTY(bool networkValid READ networkValid NOTIFY networkValidChanged)然后在Java侧拿到onCapabilitiesChanged后,通过JNI回调到C++,更新networkValid,触发信号,QML里自然就能绑定到可变状态。这样既保留了QML的声明式写法,又绕开了internetReachability不可靠的先天不足。
5. 实战排查:当检测结果和现实不符,按这个顺序查
5.1 从日志定位问题分层
遇到“检测显示有网,但实际发请求失败”的问题,先不要急着改代码。我一般按下面四步来定位:
第一步,查权限。项目里AndroidManifest.xml有没有ACCESS_NETWORK_STATE和INTERNET。很多从其他平台转过来的同事容易漏掉,因为没有权限时不会崩溃,只是默默返回错误状态,很难察觉。
第二步,区分link层和transport层。link层指的是Wi-Fi、蜂窝数据连接是否建立,transport层指的是数据链路能不能把包送到外网。状态检测API大多数时候只能告诉你link层的消息,而VALIDATED是接近transport层的信号。如果VALIDATED是true但请求还是失败,问题可能出在DNS、代理、服务器防火墙或你的后端本身。
第三步,打印NetworkCapabilities全部特性值看看。开发和测试阶段,可以在onCapabilitiesChanged里临时把networkCapabilities的所有hasCapability打出来。你会发现有的网络同时具备INTERNET和NOT_METERED,但VALIDATED是false——这就是典型的连上Wi-Fi但上不了外网场景。
第四步,看时序。是不是刚启动App时拿到的状态,和几秒后台字符串里出现的状态不一样?如果是,说明你的逻辑是在状态更新完成前就读取了旧值。这种情况下需要在onCapabilitiesChanged回调里做最终判断,而不是在页面初始化时一次性检查。
5.2 常见假象和对应的翻车场景
我整理了一个速查表,这些场景我全都在真实设备上踩过:
| 现象 | 实际原因 | 排查建议 |
|---|---|---|
internetReachability返回SharedAccess但页面请求超时 | 网络接口已连接,但系统验证未通过或验证延迟 | 改用NET_CAPABILITY_VALIDATED判断 |
| 从Wi-Fi切到蜂窝数据,状态更新滞后 | 网络回调未及时触发或回调线程阻塞 | 确认使用了registerDefaultNetworkCallback,避免在主线程阻塞 |
| 连上公司Wi-Fi后应用提示无网络 | Wi-Fi需要网页认证(Captive Portal) | 检查VALIDATED是否为false,提示用户完成认证 |
| 飞行模式开启后仍检测到网络 | 旧缓存状态未及时清除 | 在onLost中同时重置所有状态变量 |
| 某些国产ROM上网络状态不准 | ROM篡改或裁剪了系统网络验证服务 | 不要依赖系统验证,增加真实请求超时兜底 |
internetReachability始终为Unknown | 缺少ACCESS_NETWORK_STATE权限或系统服务异常 | 确认权限,并在页面加载后延迟一帧再读取状态 |
第一个场景最坑,因为SharedAccess这个单词太具有迷惑性。我见过不只一个团队把它当成“有公网访问能力”来判断。翻译一下internetReachability的语义:它表示“这台设备到达互联网的可达性”,但这个“可达性”是“网络栈层面”的,不是“应用层”的。就像你开车到了高速公路收费站,但前面排队过不去,系统只知道你“上了高速”,不知道你堵了多久。
5.3 用真实请求兜底的经典姿势
最终极的兜底方案,是发起一次真实的、轻量的网络请求。这招最土,但永远不会骗你。比如你可以设计一个PingServerHelper,在关键业务之前,请求一个你自己控制的、始终稳定的接口,设置2秒超时,返回200就算网络可用。
public boolean checkHttpReachable() { try { URL url = new URL("https://your-lightweight-probe.example.com/ping"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(1500); connection.setReadTimeout(1500); connection.setRequestMethod("GET"); int code = connection.getResponseCode(); connection.disconnect(); return code == 200; } catch (Exception e) { return false; } }真实请求的判断要轻、要快、要稳。别拿首页接口做检测,否则你的“检测接口”本身可能因为业务流量过大而响应变慢,造成误判。建议单独部署一个极简服务,只返回一个固定的小JSON,用来做网络连通性探测。而且这个探测请求也不要在UI线程执行,要放到子线程,避免超时卡顿。
6. 从“能用”到“好用”:再用经验给你三条独家建议
第一,不要在一个页面里同时监听Application.internetReachability和原生NetworkCallback,然后让两套逻辑互相覆盖。这种“双保险”最后一定会变成“双份bug”。如果你决定用原生方案,就彻底弃用QML属性,避免状态来源不统一。如果你非要保留QML属性做展示,那就让它只展示,不参与任何业务判断。
第二,网络状态变化时,给用户提示要“慢半拍”。因为网络切换窗口内出现短暂的断开和重连是非常正常的,用户可能正在电梯里,信号闪断一下立刻恢复。如果你看到onLost立刻弹“网络异常”,用户会很烦。更好的做法是:检测到onLost后,启动一个3-5秒的定时器,如果在定时器结束前网络恢复,就当作没发生过;如果定时器到了还是不可用,再提示。
第三,网络检测的结果必须可以被业务层覆盖。也就是说,即便你的网络检测模块说“有网”,HTTP请求仍然要按失败处理;即便检测模块说“无网”,也要允许某些本地缓存的场景继续运转。网络检测永远是“参考值”,业务永远以“实际结果”为准。我在多个项目里都是刻意不让检测逻辑阻断用户操作,而是让请求失败后的错误提示承担主要引导作用。原因很简单:你永远猜不到用户的手机处于什么奇怪的网络环境中,与其斩钉截铁说“没网”,不如把真实请求的错误信息展示给用户,同时给出重试按钮。
最后分享一个我个人的小习惯:在开发和测试阶段,把网络状态变化以及VALIDATED值的变更全部打到日志里,并打上一个独立的tag,比如NetStateChange。跑一段时间回归测试,再回看这些日志,你能非常直观地看到各种网络环境切换时的状态序列,很多问题一眼就能看出来。等逻辑稳定以后,再把详细日志关掉,只保留关键节点。这个习惯帮我省了大量排查时间,也让我对自己写的网络模块多了一分底气。