专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
郭霖  ·  一篇文章带你彻底掌握Optional ·  昨天  
郭霖  ·  音视频基础能力之 Android ... ·  2 天前  
鸿洋  ·  掌握这17张图,掌握RecyclerView ... ·  2 天前  
51好读  ›  专栏  ›  郭霖

Android - 监听网络状态

郭霖  · 公众号  · android  · 2024-12-06 08:00

正文



/   今日科技快讯   /

近日,韩国研究机构SNE Research发布全球动力电池统计数据,今年1-10月,全球动力电池总装车辆达686.7 GWh,同比增长25%。这一增速较去年同期放缓19个百分点。

共有六家中企进入该榜单。其中,宁德时代前10月实现了252.8 GWh的动力电池装机量,稳居全球榜首,是全球唯一一家装机量突破200 GWh的企业,占据了全球36.8%的市场份额。宁德时代的动力电池客户涵盖极氪、问界、理想在内的中国主要本土汽车主机厂,也包括了特斯拉、宝马、梅赛德斯奔驰和大众等海外汽车品牌。

/   作者简介   /

大家周五好,明天即将迎来短暂的周末休息时光,简单调整下,我们下周见!

本篇文章来自Sunday1990的投稿,文章主要分享了Android 开发中监听网络状态的相关知识,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

Sunday1990的博客地址:
https://juejin.cn/user/3843548382274455/posts

/   前言   /

早期监听网络状态用的是广播,后面安卓为我们提供了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()
// 满足条件的网络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,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> = _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。地址如下:
https://github.com/zj565061763/network

该库会在主进程自动初始化,开箱即用,如果你的App需要在其他进程使用,则需要在其他进程手动调用初始化。感谢你的阅读,如果有问题欢迎一起交流学习,

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
高效稳定 | Compose 的类型安全导航
原创:写给初学者的Jetpack Compose教程,edge-to-edge全面屏体验

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注