最近看《第一行代码》看到了广播那一章,跟着书写到动态注册监听网络变化的那一节。其中一段代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class MainActivity extends AppCompatActivity {
...
class NetworkChangeReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
ConnectivityManager connectionManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()) {
Toast.makeText(context, "network is available", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show();
}
}
}
}
在参考官方文档)时,偶然发现上述代码中使用的Network.onAvailable()
方法在API 28也就是Android P被摒弃了,官方转而推荐使用ConnectivityManager.NetworkCallback
API来监听网络变化。既然如此,我就想了解一下官方推荐的方法怎么使用。
- 查阅了种种资料之后,我写了第一个版本试试看(最小SDK为Android M):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class MainActivity extends AppCompatActivity {
private NetworkRequest networkRequest;
private ConnectivityManager connMgr;
private ConnectivityManager.NetworkCallback networkCallback;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
connMgr = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE);
networkRequest = new NetworkRequest.Builder().build();
networkCallback = new NetworkCallbackImped();
connMgr.registerNetworkCallback(networkRequest, networkCallback);
}
protected void onDestroy() {
super.onDestroy();
connMgr.unregisterNetworkCallback(networkCallback);//此处这句可以不写,可以理解为退出应用后会自动取消注册
}
class NetworkCallbackImped extends ConnectivityManager.NetworkCallback{
public void onAvailable(Network network) {
super.onAvailable(network);
Toast.makeText(getBaseContext(), "网络正常", Toast.LENGTH_SHORT).show();
}
public void onLost(Network network) {
super.onLost(network);
Toast.makeText(getBaseContext(), "网络已断开", Toast.LENGTH_SHORT).show();
}
}
Java基础比较深或者对UI编写中给View实现监听器还有印象的朋友应该可以看出来在registerNetworkCallback()
方法中直接使用匿名内部类也完全可以。
- 这一版在初步测试里没发现什么问题,于是突发奇想想做个拓展,没有网络时在界面上显示一个红色背景的文本框
title
,有网络时文本框自动消失。于是抱着试试看的心态写了第二版:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class MainActivity extends AppCompatActivity {
private NetworkRequest networkRequest;
private ConnectivityManager connMgr;
private ConnectivityManager.NetworkCallback networkCallback;
private TextView title;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
title = findViewById(R.id.title);
connMgr = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE);
networkRequest = new NetworkRequest.Builder().build();
networkCallback = new NetworkCallbackImped();
connMgr.registerNetworkCallback(networkRequest, networkCallback);
}
class NetworkCallbackImped extends ConnectivityManager.NetworkCallback{
public void onAvailable(Network network) {
super.onAvailable(network);
Toast.makeText(getBaseContext(), "网络正常", Toast.LENGTH_SHORT).show();
title.setVisibility(View.GONE);
}
public void onLost(Network network) {
super.onLost(network);
Toast.makeText(getBaseContext(), "网络已断开", Toast.LENGTH_SHORT).show();
title.setVisibility(View.VISIBLE);
}
}
}
结果收到了android.view.ViewRootImpl$CalledFromWrongThreadException
,提示”Only the original thread that created a view hierarchy can touch its views.”,只有原始的创建了View结构的线程才能访问它的View。并且还提示错误来源于ConnectivityThread
。经过又一番查询得知,Android应用的主线程控制UI的绘制,又称为UI线程,如果从其他线程访问UI将会遇到这个异常。那为什么更改title
的可见性不是从主线程发出的呢?查询资料后发现,ConnectivityManager
中封装了ConnectivityService
,而ConnectivityService
中会用到ConnectivityThread
,所以是自动转到主线程之外了。
- 看来只能新建一个线程进行UI更新的相关操作了。这里使用了
Handler
来处理其他线程发给主线程更新UI的相关信息。这是第三版,其中把向主线程发送信息做成了方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75public class MainActivity extends AppCompatActivity {
private NetworkRequest networkRequest;
private ConnectivityManager connMgr;
private ConnectivityManager.NetworkCallback networkCallback;
private TextView title;
private MyHandler handler;
private final int CONN_AVAILABLE = 1;
private final int CONN_UNAVAILABLE = 0;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
title = findViewById(R.id.title);
handler = new MyHandler();
connMgr = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE);
networkRequest = new NetworkRequest.Builder().build();
networkCallback = new NetworkCallbackImped();
MyThread thread = new MyThread();
new Thread(thread).start();
}
private void sendConnInfo(int info){
Message message = new Message();
Bundle bundle = new Bundle();
bundle.putInt("conn", info);
message.setData(bundle);
handler.sendMessage(message);//给handler发送连接可用的信息
}
class NetworkCallbackImped extends ConnectivityManager.NetworkCallback{
public void onAvailable(Network network) {
super.onAvailable(network);
sendConnInfo(CONN_AVAILABLE);
}
public void onLost(Network network) {
super.onLost(network);
sendConnInfo(CONN_UNAVAILABLE);
}
}
class MyThread implements Runnable{
public void run() {
connMgr.registerNetworkCallback(networkRequest, networkCallback);
if (connMgr.getActiveNetworkInfo()==null){
sendConnInfo(CONN_UNAVAILABLE);
}
}
}
class MyHandler extends Handler{
public void handleMessage(Message msg) {
super.handleMessage(msg);
Bundle data = msg.getData();
int netConnectivity = data.getInt("conn");
switch (netConnectivity){
case CONN_UNAVAILABLE:
Toast.makeText(getBaseContext(), "好像没有网络了……", Toast.LENGTH_SHORT).show();
title.setVisibility(View.VISIBLE);
break;
case CONN_AVAILABLE:
Toast.makeText(getBaseContext(), "网络已恢复", Toast.LENGTH_SHORT).show();
title.setVisibility(View.GONE);
break;
default:
break;
}
}
}
}
这一版解决了android.view.ViewRootImpl$CalledFromWrongThreadException
,但是在更进一步的测试中发现了之前没发现的一个问题:开启了开发者模式中“始终保持数据网络”选项(下称“该选项”)时,在数据开启的状态下,开启WiFi时会出现最后结果为没有网络的情况。
- 仔细查看了官方文档后发现,
onAvailable()
方法是在框架连接了新的网络时调用:Called when the framework connects and has declared a new network ready for use.
而onLost()
方法则是在框架失去某个网络连接时调用:
Called when the framework has a hard loss of the network or when the graceful failure ends.
于是又在onLost()
方法中加上了NetworkCapabilities.hasTransport()
方法用于检测当前网络的类型。我们都知道,一般情况下,当开启数据的同时开启WiFi,系统会自动切换至WiFi,而经过多次实际检测,发现在开启该选项时,这个过程是先连接至WiFi(调用onAvailable()
方法),大概过30秒左右再断开数据(调用onLost()
方法),所以最后返回的结果是网络断开。
NetworkCapabilities.hasTransport()
方法也是官方文档)中建议用于替代旧的获取当前网络类型方法NetworkInfo.getType()
的方法,在实际使用中,可以发现新方法获取的信息确实比旧方法多,包括是否计费、是否使用VPN等均可以检测到。
所以在onLost()
方法中还得判断一下是不是完全没有网络了,这一点可以用能否获取到NetCapabilities
或NetInfo
实例来判断。第4版代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85public class MainActivity extends AppCompatActivity {
private ConnectivityManager connMgr;
private TextView title;
private MyHandler handler;
private final int CONN_AVAILABLE = 1;
private final int CONN_UNAVAILABLE = 0;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
title = findViewById(R.id.title);
handler = new MyHandler();
connMgr = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE);
MyThread thread = new MyThread();//新建Runnable实例
new Thread(thread).start();//启动新线程
}
//给handler发送连接可用的信息
private void sendConnInfo(int info){
Message message = new Message();
Bundle bundle = new Bundle();
bundle.putInt("conn", info);
message.setData(bundle);
handler.sendMessage(message);
}
class NetworkCallbackImped extends ConnectivityManager.NetworkCallback{
public void onAvailable(Network network) {
super.onAvailable(network);
sendConnInfo(CONN_AVAILABLE);
}
public void onLost(Network network) {
super.onLost(network);
//此处遇到问题,开启开发者选项“始终开启移动数据网络”时,在开启数据时打开wifi,最终呈现是网络断开,但是能用NetworkCapabilities检测到wifi
//查看文档发现onLost()方法是在有`network`断开而并非**所有`network`都断开**时,即有任意网络连接断开时即触发。
//onAvailable()方法是在有**新的**`network`可用时触发。
//经检测当wifi和移动网络同时连接时,先连接wifi,触发onAvailable()方法,但移动网络要过30s左右才断开,所以会触发onLost()方法。
//另有开启开发者选项“始终开启移动数据网络”时,数据WiFi双开时关闭WiFi后先调用`onAvailable()`方法后调用`onLost()`方法,且`NetworkCapabilities`为null的情况。
//结果发现出现此Bug的原因是关闭WiFi时刚好是之前讲的连接了WiFi后和断开移动网络前,所以只会调用`onLost()`方法。
//此处整理一下开启“始终开启移动数据网络”与否的双开网络变化情况。
//开启时:开着数据开WiFi时:先连接WiFi(但测试是数据),过约30s后断开数据。 双开关闭WiFi时:依据关WiFi的时机而定,在前述的30s内会无网络,30s之后马上切换至数据。
//关闭时:开着数据开WiFi时:先连接WiFi,再马上关闭数据。 双开关闭WiFi时:先关闭WiFi,再连接数据。
if (connMgr.getNetworkCapabilities(connMgr.getActiveNetwork()) == null) sendConnInfo(CONN_UNAVAILABLE);
}
}
class MyThread implements Runnable{
public void run() {
connMgr.registerNetworkCallback(new NetworkRequest.Builder().build(), new NetworkCallbackImped());
//初始化时判断有无网络
if (connMgr.getActiveNetworkInfo()==null){
sendConnInfo(CONN_UNAVAILABLE);
}
}
}
class MyHandler extends Handler{
public void handleMessage(Message msg) {
super.handleMessage(msg);
Bundle data = msg.getData();
int netConnectivity = data.getInt("conn");
switch (netConnectivity){
case CONN_UNAVAILABLE:
Toast.makeText(getBaseContext(), "好像没有网络了……", Toast.LENGTH_SHORT).show();
title.setText("好像没有网络了……");
title.setVisibility(View.VISIBLE);
break;
case CONN_AVAILABLE:
Toast.makeText(getBaseContext(), "网络已恢复", Toast.LENGTH_SHORT).show();
title.setVisibility(View.GONE);
break;
default:
break;
}
}
}
}
其实最后还是有一个Bug没有解决,那就是开启“始终保持移动网络连接”时,在上述的30s内(即开启数据后开启WiFi时系统连接WiFi但未关闭数据时)关闭WiFi时,最后会检测不到网络(表现为最后调用的是onLost()
且NetworkCapabilities
和NetInfo
均为null
)。个人猜测可能是NetworkCallback
的方法回调与上述两实例的获取之间存在时间差,也有可能是个体差异(测试机型为一加6,系统版本为ONEPLUS A6000_22_180810
及其上一版)。
在进一步用Nubia Z11(Android 6.0.1)测试时,情况又有所变化。在未开启“始终保持移动网络连接”选项时,会出现类似一加6开启该选项的情况(即30s延迟)。但开启该选项后,双开后断开WiFi始终检测不到网络。
如果有朋友知晓原因,恳请不吝解惑!
QQ: 504869112