未分类

Android实战记(一)——用ConnectivityManager管理网络连接

最近看《第一行代码》看到了广播那一章,跟着书写到动态注册监听网络变化的那一节。其中一段代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity { 
...
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
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.NetworkCallbackAPI来监听网络变化。既然如此,我就想了解一下官方推荐的方法怎么使用。

  1. 查阅了种种资料之后,我写了第一个版本试试看(最小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
    33
    public class MainActivity extends AppCompatActivity {
    private NetworkRequest networkRequest;
    private ConnectivityManager connMgr;
    private ConnectivityManager.NetworkCallback networkCallback;
    @Override
    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);
    }

    @Override
    protected void onDestroy() {
    super.onDestroy();
    connMgr.unregisterNetworkCallback(networkCallback);//此处这句可以不写,可以理解为退出应用后会自动取消注册
    }
    class NetworkCallbackImped extends ConnectivityManager.NetworkCallback{
    @Override
    public void onAvailable(Network network) {
    super.onAvailable(network);
    Toast.makeText(getBaseContext(), "网络正常", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLost(Network network) {
    super.onLost(network);
    Toast.makeText(getBaseContext(), "网络已断开", Toast.LENGTH_SHORT).show();
    }

    }

Java基础比较深或者对UI编写中给View实现监听器还有印象的朋友应该可以看出来在registerNetworkCallback()方法中直接使用匿名内部类也完全可以。

  1. 这一版在初步测试里没发现什么问题,于是突发奇想想做个拓展,没有网络时在界面上显示一个红色背景的文本框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
    33
    public class MainActivity extends AppCompatActivity {
    private NetworkRequest networkRequest;
    private ConnectivityManager connMgr;
    private ConnectivityManager.NetworkCallback networkCallback;
    private TextView title;
    @Override
    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{
    @Override
    public void onAvailable(Network network) {
    super.onAvailable(network);
    Toast.makeText(getBaseContext(), "网络正常", Toast.LENGTH_SHORT).show();
    title.setVisibility(View.GONE);
    }

    @Override
    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,所以是自动转到主线程之外了。

  1. 看来只能新建一个线程进行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
    75
    public 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;
    @Override
    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{
    @Override
    public void onAvailable(Network network) {
    super.onAvailable(network);
    sendConnInfo(CONN_AVAILABLE);
    }

    @Override
    public void onLost(Network network) {
    super.onLost(network);
    sendConnInfo(CONN_UNAVAILABLE);
    }

    }

    class MyThread implements Runnable{
    @Override
    public void run() {
    connMgr.registerNetworkCallback(networkRequest, networkCallback);
    if (connMgr.getActiveNetworkInfo()==null){
    sendConnInfo(CONN_UNAVAILABLE);
    }
    }
    }

    class MyHandler extends Handler{
    @Override
    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时会出现最后结果为没有网络的情况。

  1. 仔细查看了官方文档后发现,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()方法中还得判断一下是不是完全没有网络了,这一点可以用能否获取到NetCapabilitiesNetInfo实例来判断。第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
85
public 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;
@Override
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{
@Override
public void onAvailable(Network network) {
super.onAvailable(network);
sendConnInfo(CONN_AVAILABLE);
}

@Override
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{
@Override
public void run() {
connMgr.registerNetworkCallback(new NetworkRequest.Builder().build(), new NetworkCallbackImped());
//初始化时判断有无网络
if (connMgr.getActiveNetworkInfo()==null){
sendConnInfo(CONN_UNAVAILABLE);
}
}
}

class MyHandler extends Handler{
@Override
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()NetworkCapabilitiesNetInfo均为null)。个人猜测可能是NetworkCallback的方法回调与上述两实例的获取之间存在时间差,也有可能是个体差异(测试机型为一加6,系统版本为ONEPLUS A6000_22_180810及其上一版)。

在进一步用Nubia Z11(Android 6.0.1)测试时,情况又有所变化。在未开启“始终保持移动网络连接”选项时,会出现类似一加6开启该选项的情况(即30s延迟)。但开启该选项后,双开后断开WiFi始终检测不到网络。

如果有朋友知晓原因,恳请不吝解惑!
QQ: 504869112

分享到