Android - 监听网络状态
前言
早期监听网络状态用的是广播,后面安卓为我们提供了android.net.ConnectivityManager.NetworkCallback
,ConnectivityManager
有多个方法可以注册NetworkCallback
,通过不同方法注册,在回调时逻辑会有些差异,本文探讨的是以下这个方法:
public void registerNetworkCallback(
@NonNull NetworkRequest request,
@NonNull NetworkCallback networkCallback
)
首先需要创建NetworkRequest
:
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
addCapability
方法的字面意思是添加能力,可以理解为添加条件,表示回调的网络要满足指定的条件。
这里添加了NetworkCapabilities.NET_CAPABILITY_INTERNET
,表示回调的网络应该要满足已连接互联网的条件,即拥有访问互联网的能力。
如果指定多个条件,则回调的网络必须同时满足指定的所有条件。
创建NetworkRequest
实例之后就可以调用注册方法了:
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
manager.registerNetworkCallback(request, _networkCallback)
_networkCallback
用来监听网络变化,下文会介绍。
重点来了,每个App只允许最多注册100个回调,如果超过会抛RuntimeException
异常,所以在注册时要捕获异常并做降级处理,下文会提到。
NetworkCallback
有多个回调方法,重点关注下面2个方法:
public void onCapabilitiesChanged(
@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities
) {}
该方法在注册成功以及能力变化时回调,参数是:
Network
,网络NetworkCapabilities
,网络能力
该方法触发的前提是,这个网络要满足addCapability
方法传入的条件。具体有哪些网络能力,可以看一下源码,这里就不一一列出来。
public void onLost(@NonNull Network network) {}
onLost
比较简单,在网络由满足条件变为不满足条件时回调。
封装
有了前面的基础,就可以开始封装,基本思路如下:
- 定义一个网络状态类
- 维护一个满足条件的网络状态流
Flow
,并在状态变化时,更新Flow
- 注册
NetworkCallback
回调,开始监听
网络状态类
interface NetworkState {
/** 网络Id */
val id: String
/** 是否Wifi网络 */
val isWifi: Boolean
/** 是否手机网络 */
val isCellular: Boolean
/** 网络是否已连接,已连接不代表网络一定可用 */
val isConnected: Boolean
/** 网络是否已验证可用 */
val isValidated: Boolean
}
NetworkState
是接口,定义了一些常用的属性,就不赘述。
internal data class NetworkStateModel(
/** 网络Id */
val netId: String,
/** [NetworkCapabilities.TRANSPORT_WIFI] */
val transportWifi: Boolean,
/** [NetworkCapabilities.TRANSPORT_CELLULAR] */
val transportCellular: Boolean,
/** [NetworkCapabilities.NET_CAPABILITY_INTERNET] */
val netCapabilityInternet: Boolean,
/** [NetworkCapabilities.NET_CAPABILITY_VALIDATED] */
val netCapabilityValidated: Boolean,
) : NetworkState {
override val id: String get() = netId
override val isWifi: Boolean get() = transportWifi
override val isCellular: Boolean get() = transportCellular
override val isConnected: Boolean get() = netCapabilityInternet
override val isValidated: Boolean get() = netCapabilityValidated
}
NetworkStateModel
是实现类,具体的实例在onCapabilitiesChanged
方法回调时,根据回调参数创建,创建方法如下:
private fun newNetworkState(
network: Network,
networkCapabilities: NetworkCapabilities,
): NetworkState {
return NetworkStateModel(
netId = network.netId(),
transportWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI),
transportCellular = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR),
netCapabilityInternet = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET),
netCapabilityValidated = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED),
)
}
private fun Network.netId(): String = this.toString()
通过NetworkCapabilities.hasXXX
方法,可以知道Network
网络的状态或者能力,更多方法可以查看源码。
网络状态流Flow
接下来在回调中,把网络状态更新到Flow
:
// 满足条件的网络
private val _networks = mutableMapOf<Network, NetworkState>()
// 满足条件的网络Flow
private val _networksFlow = MutableStateFlow<List<NetworkState>?>(null)
private val _networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network) {
super.onLost(network)
// 移除网络,并更新Flow
_networks.remove(network)
_networksFlow.value = _networks.values.toList()
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
super.onCapabilitiesChanged(network, networkCapabilities)
// 修改网络,并更新Flow
_networks[network] = newNetworkState(network, networkCapabilities)
_networksFlow.value = _networks.values.toList()
}
}
在onLost
和onCapabilitiesChanged
中更新_networks
和_networksFlow
。
_networksFlow
的泛型是一个List<NetworkState>
,因为满足条件的网络可能有多个,例如:运营商网络,WIFI网络。
_networks
是一个Map
,KEY
是Network
,我们看看Network
源码:
public class Network implements Parcelable {
@UnsupportedAppUsage
public final int netId;
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Network)) return false;
Network other = (Network)obj;
return this.netId == other.netId;
}
@Override
public int hashCode() {
return netId * 11;
}
@Override
public String toString() {
return Integer.toString(netId);
}
}
把其他非关键代码都移除了,可以看到它重写了equals
和hashCode
方法,所以把它当作HashMap
这种算法容器的KEY
是安全的。
细心的读者可能会有疑问,NetworkCallback
的回调方法是在什么线程执行的,回调中直接操作Map
是安全的吗?
默认情况下,回调方法是在子线程按顺序执行的,这里的重点是按顺序,所以在子线程也是安全的,因为没有并发。可以在注册时,调用另一个重载方法传入Handler
来修改回调线程,这里就不继续探讨,有兴趣的读者可以看看源码。
开始监听
接下来可以注册回调,开始监听了。上文提到,每个App最多只能注册100个回调,我们的降级策略是:
如果注册失败,直接获取当前网络状态,并更新到Flow
,延迟1秒后继续尝试注册,如果注册成功,停止循环,否则一直重复循环。
建议把这个逻辑放在非主线程执行。
如果一直注册失败的话,这种降级策略有如下缺点:
- 每隔1秒获取一次网络状态,所以有一定的延迟,当然你可以把间隔设置的更小,这个取决于你的业务。
- 最多只能获取到一个满足条件的网络,因为是通过
ConnectivityManager.getActiveNetwork()
来获取当前网络状态的。
有的读者可能知道有getAllNetworks()
方法获取所有网络,但是该方法已经被废弃了,不建议使用。
了解降级策略后,可以看代码了:
private suspend fun registerNetworkCallback() {
// 1.创建请求对象,指定要满足的条件
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
while (true) {
// 2.注册监听,要捕获RuntimeException异常
val register = try {
manager.registerNetworkCallback(request, _networkCallback)
true
} catch (e: RuntimeException) {
e.printStackTrace()
false
}
// 3.获取当前网络状态
val currentList = manager.currentNetworkState().let { networkState ->
if (networkState == null) {
emptyList()
} else {
listOf(networkState)
}
}
if (register) {
// A: 注册成功,更新Flow,并停止循环
_networksFlow.compareAndSet(null, currentList)
break
} else {
// B: 注册失败,间隔1秒后重新执行上面的循环
_networksFlow.value = currentList
delay(1_000)
continue
}
}
}
代码看起来比较长,实际逻辑比较简单,我们来分析一下。
第1步上文已经解释了,就不赘述了。
后面的逻辑是在while
循环中执行的,就是上面提到的降级策略逻辑。
最后根据注册的结果,会走2个分支,B分支是注册失败的降级策略分支。
A分支是注册成功的分支,把当前状态更新到Flow
,并停止循环。
注意:这里更新Flow
用的是compareAndSet
,这是因为注册之后有可能onCapabilitiesChanged
已经回调了最新的网络状态,此时不能用currentList
直接更新覆盖,而要进行比较,如果是null
才更新,因为null
是默认值,表示onCapabilitiesChanged
还未被回调。
这也解释了上文中定义Flow
时,默认值为什么是一个null
,而不是一个空列表,因为默认值设置为空列表有歧义,它到底是默认值,还是当前没有满足条件的网络,注册时就没办法compareAndSet
。
最后我们对外暴露Flow
就可以了:
/** 监听所有网络 */
val allNetworksFlow: Flow<List<NetworkState>> = _networksFlow.filterNotNull()
用filterNotNull()
把默认值null
过滤掉。
监听当前网络
实际开发中,大部分时候,仅仅需要知道当前的网络状态,而不是所有的网络状态。有了上面的封装,我们可以很方便的过滤出当前网络状态:
/** 监听当前网络 */
val currentNetworkFlow: Flow<NetworkState> = allNetworksFlow
.mapLatest(::filterCurrentNetwork)
.distinctUntilChanged()
.flowOn(Dispatchers.IO)
过滤的逻辑在filterCurrentNetwork
方法中:
private suspend fun filterCurrentNetwork(list: List<NetworkState>): NetworkState {
// 1.列表为空,返回一个代表无网络的状态
if (list.isEmpty()) return NetworkState
while (true) {
// 2.从列表中查找网络Id和当前网络Id一样的状态,即当前网络状态
val target = list.find { it.id == manager.activeNetwork?.netId() }
if (target != null) {
return target
} else {
// 3.如果本次未查询到,延迟后继续查询
delay(1_000)
continue
}
}
}
第2步中有个获取网络Id的扩展函数,上文已经有列出,但未做解释,实际上就是调用Network.toString()
。
为什么会有第3步呢?因为我们是在回调中直接更新Flow
,可能导致filterCurrentNetwork
立即触发,相当于在回调里面直接查询manager.activeNetwork
。
在NetworkCallback
的回调中,同步调用ConnectivityManager
的所有方法都可能有先后顺序问题,即本次调用查询到的状态,可能并非最新的状态,这个在源码中有解释,有兴趣的读者可以看看源码。
上面的currentNetworkFlow
,我们用了mapLatest
,如果在delay
时,列表又发生了变化,则会取消本次过滤,重新执行filterCurrentNetwork
。
当然了distinctUntilChanged
也是必须的,假如当前网络activeNetwork
是WIFI,另一个满足条件的运营商网络发生变化时也会执行过滤,过滤的结果还是WIFI,就会导致重复回调。
最后建议把这个过滤切换到非主线程执行,可以使用flowOn
。
实际上,如果你只想监听当前网络,不需要知道所有网络,那么在注册回调的时候可以使用registerDefaultNetworkCallback
来监听,此时回调的逻辑和本文介绍的稍有差异,这个方法要求API 24
,具体可以看一下源码注释,这里就不展开。
挂起等待网络
有了上面的封装,在协程中,我们可以轻松实现:
在某个操作之前,判断网络已连接才执行,如果未连接则挂起等待。
suspend fun fAwaitNetwork(
condition: (NetworkState) -> Boolean = { it.isConnected },
): Boolean {
if (condition(FNetwork.currentNetwork)) return true
FNetwork.currentNetworkFlow.first { condition(it) }
return false
}
FNetwork.currentNetwork
是一个获取当前网络状态的属性,最终获取的方法如下:
private fun ConnectivityManager.currentNetworkState(): NetworkState? {
val network = this.activeNetwork ?: return null
val capabilities = this.getNetworkCapabilities(network) ?: return null
return newNetworkState(network, capabilities)
}
fAwaitNetwork
调用时,先直接获取一次当前网络状态,如果满足条件,则立即返回,如果不满足条件则开始监听currentNetworkFlow
,遇到第一个满足条件的网络时,恢复执行。
上层可以通过返回值true
或者false
知道本次调用是立即满足的,还是挂起等待之后满足的。
模拟使用代码:
lifecycleScope.launch {
// 判断网络
fAwaitNetwork()
// 发起请求
requestData()
}
结束
库已经封装好了,在这里:network
该库会在主进程自动初始化,开箱即用,如果你的App需要在其他进程使用,则需要在其他进程手动调用初始化。
感谢你的阅读,如果有问题欢迎一起交流学习,
来源:juejin.cn/post/7442541343685214217