- 作者:老汪软件技巧
- 发表时间:2024-11-29 17:02
- 浏览量:
前言
早期监听网络状态用的是广播,后面安卓为我们提供了.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
) {}
该方法在注册成功以及能力变化时回调,参数是:
该方法触发的前提是,这个网络要满足addCapability方法传入的条件。具体有哪些网络能力,可以看一下源码,这里就不一一列出来。
public void onLost(@NonNull Network network) {}
onLost比较简单,在网络由满足条件变为不满足条件时回调。
封装
有了前面的基础,就可以开始封装,基本思路如下:
网络状态类
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()
// 满足条件的网络Flow
private val _networksFlow = MutableStateFlow?>(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,因为满足条件的网络可能有多个,例如:运营商网络,WIFI网络。
_networks是一个Map(默认是HashMap),KEY是Network,要当作HashMap的KEY是有条件的,我们看看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秒后继续尝试注册,如果注册成功,停止循环,否则一直重复循环。
建议把这个逻辑放在非主线程执行。
如果一直注册失败的话,这种降级策略有如下缺点:
有的读者可能知道有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 = allNetworksFlow
.mapLatest(::filterCurrentNetwork)
.distinctUntilChanged()
.flowOn(Dispatchers.IO)
过滤的逻辑在filterCurrentNetwork方法中:
private suspend fun filterCurrentNetwork(list: List<NetworkState>): NetworkState {
// 1.列表为空,返回一个代表无网络的状态
if (list.isEmpty()) return NetworkStateNone
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需要在其他进程使用,则需要在其他进程手动调用初始化。
感谢你的阅读,如果有问题欢迎一起交流学习,