导读
本文讲述了七猫免费小说App网络状态监听框架升级的实现以及遇到的问题和解决办法。
七猫一直坚持“对用户好一点”的经营理念。作为七猫的主打App,在产品开发设计的过程中也一直在遵循这一理念。为了能够给用户带来更好的交互体验,我们需要准确的捕获App使用过程中设备的网络状态,及时的根据当前的网络环境做相应的操作。
在网络状态监听框架升级之前项目存在各种性能、体验问题,例如:
* 项目中监听网络状态相关api使用不统一;
* 未考虑获取网络状态是跨进程操作而直接在主线程中调用,直接导致了线上低端机发生卡顿、ANR等问题;
* 因为没有及时适配 Android 高版本 api、去除过时 api,导致在网络状态发生变化后,不能准确、及时地通知各业务模块,用户体验不佳。如用户在非Wi-Fi状态点击广告下载apk、无网络状态下页面一直显示加载中状态、用户切换到Wi-Fi未及时处理未完成的下载任务等。
所以这迫使我们升级改造网络监听框架来解决上述这些问题。
前期准备
关于网络状态监听相关的 API,在 Android 不同版本有较频繁的变化。传统的使用注册监听网络变化广播的方式在 Android 高版本中存在无法监听到广播的问题,而在获取具体的网络类型时,根据版本的不同许多 API 已过时,因此特别整理了以下网络状态监听功能实现的相关内容。
/*
网络相关的 API 根据版本的不同有很多的变化,但是在实际使用的时候,虽然有些 API 表明已过时了却任然可以使用,
比如:NetworkInfo.getType()与 NetworkInfo.getSubtype()在 28 的时候已经过时,但是 29 任然可以使用,可以获取具体的网络类型(2G、3G、4G、5G...)
下面是使用到的部分 API 的变化情况:
NetworkInfo: Deprecated in API level 29
NetworkInfo.getState(): Deprecated in API level 28
NetworkInfo.getType(): Deprecated in API level 28
NetworkInfo.getSubtype(): Deprecated in API level 28
ConnectivityManager.getActiveNetworkInfo(): Deprecated in API level 29
ConnectivityManager.getAllNetworkInfo(): Deprecated in API level 23
ConnectivityManager.getNetworkInfo(network): Added in API level 21, Deprecated in API level 29
ConnectivityManager.getAllNetworks(): Added in API level 21
*/
在Android 7.0及以上版本,Google 基于性能和安全原因对广播做了很多限制,监听网络变更的广播 CONNECTIVITY_CHANGE 不能再使用静态方式注册。下图中可以看见,CONNECTIVITY_CHANGE已被标记为Deprecated,并且谷歌推荐使用ConnectivityManager.NetworkCallback来监听网络变化。
但从谷歌官网可以看到,ConnectivityManager.NetworkCallback 是在 Android 5.0 才增加的系统 API。
技术实现
有了前期的准备工作,在技术实现时就可以针对不同情况设计相应的技术方案。下面会从整体框架的实现、具体监听的实现以及使用等方面做具体阐述。
1、网络状态监听流程图
2、初始化
在执行 Application.onCreate()时,会通过执行 NetworkManager.getDefault().init(application)来初始化网络监听框架。在init方法中会根据版本来做不同的处理:5.0 以下的低版本使用动态注册网络状态变化广播来监听,5.0 及以上的版本使用 ConnectivityManager.NetworkCallback 来注册回调监听。
public void init(Application application) {
...
if (Build.VERSION.SDK_INT < 21) {
// 动态注册网络状态监听
IntentFilter filter = new IntentFilter();
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
application.registerReceiver(receiver, filter);
} else {
NetworkRequest request = new NetworkRequest.Builder().build();
ConnectivityManager manager = (ConnectivityManager) getApplication().getSystemService(Context.CONNECTIVITY_SERVICE);
if (manager != null && networkCallback != null) {
manager.registerNetworkCallback(request, networkCallback);
}
}
// 初始化时,首次获取网络状态
NetworkUtils.updateNetworkState();
}
3、网络监听方式
根据版本不同分为两种方式:
(1)5.0以下的版本通过动态注册监听网络变化的广播来实现网络状态监听;
// Android 5.0 以下
public class NetworkStateReceiver extends BroadcastReceiver {
...
@Override
public void onReceive(Context context, Intent intent) {
...
if (ConnectivityManager.CONNECTIVITY_ACTION.equalsIgnoreCase(intent.getAction())) {
networkChanged();
}
}
private void networkChanged() {
// 在执行相关操作之前记录上次的网络状态
NetworkUtils.updateLastType();
/*将收到网络变化广播时的获取当前网络状态的操作放在异步执行,因为获取网络的过程需要跨进程,比较耗时,而收到广播变化时的onReceive()在主线程*/
WorkExecutor.getInstance().execute(new Runnable() {
@Override
public void run() {
NetworkUtils.updateNetworkState();
NetworkType oldType;
// 第一次对比的时候需要去获取初始化时的类型
if (isFirst) {
oldType = NetworkUtils.getLastNetworkType();
isFirst = false;
} else {
oldType = networkType;
}
networkType = NetworkUtils.getCurrentNetworkType();
if (oldType != networkType) {
NetworkUtils.networkChanged(networkList, networkType);
}
// 在执行相关操作之后更新上次的网络状态
NetworkUtils.updateLastType();
}
});
}
...
}
(2)5.0 及以上的版本使用ConnectivityManager.NetworkCallback 来实现网络状态的监听。
// Android 5.0及以上
public class NetworkCallbackImpl extends ConnectivityManager.NetworkCallback {
...
@Override
public void onAvailable(@NonNull Network network) {
...
// 测试发现:5.0 5.1 系统的手机网络变化时不会回调onCapabilitiesChanged,但会回调onAvailable
if (Build.VERSION.SDK_INT < 23) {
ConnectivityManager manager = NetworkManager.getDefault().getConnectivityManager();
NetworkCapabilities capabilities = null;
if (manager != null) {
capabilities = manager.getNetworkCapabilities(network);
}
networkChanged(capabilities);
}
}
@Override
public void onLost(@NonNull Network network) {
// 处理断网,注意:Wi-Fi切换到蜂窝网络时也会执行这里
}
@Override
public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
...
currentNetWork = String.valueOf(network);
networkChanged(networkCapabilities);
}
private void networkChanged(NetworkCapabilities networkCapabilities) {
try {
if (!isNeedUpdate) {
NetworkUtils.lostNetwork();
} else {
// 在执行相关操作之前记录上次的网络状态
NetworkUtils.updateLastType();
NetworkUtils.updateNetworkStateV21(networkCapabilities);
}
postNetworkChanged();
// 在执行相关操作之后更新上次的网络状态
NetworkUtils.updateLastType();
} catch (Throwable t) {
}
}
...
}
实现思路:通过使用广播和 NetworkCallback 来监听网络状态的变化,然后本地会存储 isNetworkAvailable 和 currentNetworkType 两个静态变量来保存当前网络的状态和类型,当网络发生变化时,重新获取网络状态,再更新isNetworkAvailable 和 currentNetworkType的值。
4、网络状态的获取方式
网络状态的具体获取方式又分为主动获取和被动获取:主动获取指的是在需要使用网络状态的时候直接去系统中获取当前的网络状态;被动获取是指接收到广播或NetworkCallback的回调时再去系统获取当前的网络状态。前者主要是用在用户没有授权网络监听相关权限之前需要使用网络状态时的获取方式;而后者是在有了相关权限之后的获取方式,绝大部分情况下都是的被动获取。
// 主动获取,需要返回结果
NetworkUtils.getNetworkState()
// 被动获取,不需要返回结果
NetworkUtils.updateNetworkState();
在获取网络状态时,因为有很多 API 在不同的版本中已被弃用,在有些版本可用,有些版本使用会直接 crash,所以做了如下兼容:
private void networkChanged(NetworkCapabilities networkCapabilities) {
try {
NetworkUtils.updateNetworkState();
} catch (Throwable t) {
t.printStackTrace();
try {
NetworkUtils.updateNetworkStateV21(networkCapabilities);
} catch (Throwable t) {
}
}
NetworkType oldType = networkType;
networkType = NetworkUtils.getCurrentNetworkType();
if (oldType != networkType) {
NetworkUtils.networkChanged(networkList, networkType);
}
}
因为需求是要获取具体的网络类型,所以会先使用 NetworkUtils.updateNetworkState()来更新网络状态,如果部分手机因为API过时出现异常时,就使用 NetworkUtils.updateNetworkStateV21()来更新网络状态,但是如果是手机网络,后者就不能获取具体的网络类型了(2G、3G、4G、5G等),只会返回默认的4G。
5、网络状态的使用方式
网络状态的具体使用方式又分为两种:第一种是直接使用,就是在需要网络状态的地方直接从内存中接获取 isNetworkAvailable 和 currentNetworkType 的值即可,这也是项目中使用最多的;第二种是在需要监听网络变化的方法上添加网络监听的注解,当网络变化了就调用该方法。比如在金币获取的逻辑中就使用被动监听网络:
@OnNetworkChange(onlyFromNoneToValid = false)
public void onNetworkChange(NetworkType networkType, NetworkType oldType) {
if (NetworkUtils.isNetworkEnabled()) {
// 网络连接,开始计时
} else {
// 网络断开,暂停计时
}
}
这里重点说一下第二种使用方式的实现思路:通过注册需要监听的方法,当网络发生变化时,通知这些注册的方法网络发生了变化,再根据需要执行相应的业务操作。如果不再使用就取消注册。这里主要是参考了 EventBus 的设计原理,和 EventBus 的用法也类似。
通过使用反射收集注册了 OnNetworkChange注解的所有方法,并保存到 HashMap 中:
// 查找所有注册的方法的info
static List<NetworkMethodInfo> findMethodList(Object obj) {
List<NetworkMethodInfo> list = new ArrayList<>();
Class<?> clazz = obj.getClass();
Method[] methods = clazz.getMethods();
for (Method method : methods) {
OnNetworkChange network = method.getAnnotation(OnNetworkChange.class);
if (network == null) {
continue;
}
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1) {
throw new RuntimeException(method.getName() + " allows only one parameter of type NetworkType");
}
NetworkMethodInfo info = new NetworkMethodInfo(parameterTypes[0], method);
list.add(info);
}
return list;
}
当网络发生变化时,会通过遍历HashMap的方式执行所有注册了 OnNetworkChange注解的方法。
到这里技术实现就全部阐述完了,但在真正的实战中也遇到了很多的问题,下面记录了一些具体的问题,并且给出了具体的分析和解决方案。
存在的问题以及解决办法
问题 1:部分手机在熄屏一段时间后,网络会断开,重新打开手机无法收到广播或网络的监听回调,就会无法更新最新状态。
原因分析:在使用网络状态的时候都是使用被动的更新的 isNetworkAvailable 和 currentNetworkType,当无法收到更新的信号时,网络状态就有可能不正确了。如果没网络获取的状态是有网络基本不会有问题,但是有网络获取到的是无网络状态肯定是不允许的。
解决方式:只针对没网的时候,当获取的网络状态是 NONE 的时候,重新去主动获取当前的网络,确保不会因为没有收到网络变化的广播或回调而导致的网络状态错误。使用网络状态的流程如下:
问题 2:当用户关闭网络后,此时网络是无网状态,可能会导致频繁的去获取网络状态,由此引发卡顿、ANR 等问题。
原因分析:阅读页面的底部广告和页面广告等多个广告可能会同时请求网络,导致同一时间段频繁获取网络状态引发卡顿;由于无网络时单独获取网络状态的操作是在调用的地方所在的线程,大部分情况是主线程,因此有可能导致 ANR。
解决方式:使用线程池的拒绝策略(ThreadPoolExecutor.DiscardPolicy),该线程池最大允许一个任务线程,当前任务没有执行完成时,拒绝新的任务;同时将获取网络状态的操作放在异步,减少了 ANR 的情况。
问题 3:线程池虽然队列为 1。但是执行速度很快的话。还是会一直在执行。
解决方式:限定获取网络状态的时间间隔,3s 内只允许主动获取一次。因为用户可以自己修改设备时间,最后又对时间差的结果做了负值情况的检查。
问题 4:获取网络状态的过程需要跨进程才能完成,比较耗时。
原因分析:对于 5.0 以下的手机,收到广播变化时的回调 onReceive()在主线程;5.0 及以上的手机,NetworkCallback 本就是异步的。
解决方式:对于 5.0 以下的手机,将收到网络变化广播时的获取当前网络状态的操作放在异步执行;5.0 及以上的手机,不需要做异步处理。
问题 5:低版本注册网络监听广播会出现问题:一次网络变化收到两次广播,导致代码重复执行。
原因分析:从日志分析发现:网络发生变化时,每次都会先收到一个我们在 InitAppTask 中注册的 NetworkStateReceiver,这个是对的;但是接着又收到了一个新 new 的 NetworkStateReceiver,这个并非我们注册的,具体原因可能要看 framework 源码。
解决办法:使用单利创建 NetworkStateReceiver,不允许其他地方创建新的 NetworkStateReceiver,这样就解决该广播重复的问题。
问题 6:由问题 5 修改的方案导致的问题:在高版本中,会因为没有注册 NetworkStateReceiver 而导致程序闪退。
原因分析:因为要 5.0 以下的版本使用 NetworkStateReceiver,因此需要在 AndroidManifest 中申明这个 receiver,但是用了单俐后,在高版本中不会注册该 receiver,外部也无法创建 NetworkStateReceiver 对象,因此当网络发生变化时,会报 NetworkStateReceiver 无法创建对象的异常。虽然在高版本中我们没有注册这个 receiver,但是在 AndroidManifest 中仍然申明了这个 receiver,而且因为低版本要使用,所以这个必须要写在 AndroidManifest 中。
解决办法:取消 NetworkStateReceiver 的单俐模式,在初始化 NetworkStateReceiver 的时候,会记录在 NetworkManager 中,并在 NetworkManager 里面提供获取该 receiver 的方法,在收到网络变化广播的时候,判断是不是我们注册的 receiver,不是我们注册的 receiver 就过滤掉。
问题7:调试过程中发现一个现象:当网络从 Wi-Fi 切换到手机网络时,中间会先经历一个无网络状态。
原因分析:当网络从 Wi-Fi 切换到手机网络时网络状态的变化顺序: Wi-Fi → 无网络 → 手机网络(如4G)。
解决办法:目前对项目中使用没有太大影响,后续有需求再做相应的修改。这一现象需要注意,而当网络从手机网络切换到 Wi-Fi 时不会经过无网络状态。
问题8:NPE问题,getAllNetworkInfo() 和 getAllNetworks()在低版本会有NPE的可能。
原因分析:高版本中该方法返回值为非空的,低版本的为可空的。而编译器是按高版本来执行检查的,会提示【"xxx != null" is alway true】,如果按提示操作,就会去掉非空判断,从而导致在低版本出现NPE的情况(偶现)。
解决办法:不管版本,都添加非空判断即可。
// 高版本中的API,该方法返回值为非空的
/**
* Returns an array of all {@link Network} currently tracked by the
* framework.
*
* @return an array of {@link Network} objects.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
@NonNull
public Network[] getAllNetworks() {
try {
return mService.getAllNetworks();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
// 低版本中的API,该方法返回值为可空的
/**
* Returns an array of all {@link Network} currently tracked by the
* framework.
* <p>This method requires the caller to hold the permission
* {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
*
* @return an array of {@link Network} objects.
*/
public Network[] getAllNetworks() {
try {
return mService.getAllNetworks();
} catch (RemoteException e) {
return null;
}
}
结论
这次改造升级网络状态监听框架有效的解决了导读中阐述的问题,并且其稳定性、兼容性以及正确性都经过了线上千万级用户量的数据验证。同时也让各业务团队在使用网络状态时更加的便利、简洁。这些都充分证明了本次网络状态监听框架升级的必要性与及时性。
希望这边文章能给读者们一些帮助,如果文中存在什么问题、或者有不一样的方案,也欢迎私信笔者指正、交流。(邮箱:hexuegang@qimao.com)
参考文献
https://developer.android.google.cn/reference/android/net/NetworkInfo?hl=en
https://developer.android.google.cn/reference/android/net/ConnectivityManager.NetworkCallback?hl=en
https://developer.android.google.cn/reference/kotlin/android/telephony/TelephonyManager?hl=en