这节课讲述如何在网络资源使用的基础上写出拥有细粒度控制的程序,如果你的程序要执行大量的网络操作,你应该提供用户设置来允许用户控制你应用程序数据的习惯。 比如你的程序多久会进行数据同步,是否只有在拥有Wi-Fi的情况下才执行上传和下载操作,是否使用数据漫游等等。将这些控制变量提供给他们,当用户接近自己的(网络流量)限制时,就不太可能禁用你应用程序对后台数据的访问通道。因为他们可以精确的控制你的应用程序使用多少数据。
对于怎样写一个应用能够最大限度的减少下载和网络连接对电池寿命的影响的一般准则。 详见优化电池寿命和在不耗尽电池的情况下传输数据
检查设备网络连接 Check a Device's Network Connection
一个设备可以有不同类型的网络连接,这节课主要关注于使用Wi-Fi或者移动网络连接。对于可能存在的网络连接类型列表, 详见ConnectivityManager类 Wi-Fi通常速度更快,此外,移动数据通常是按量计费,因此比较贵。所以应用程序的一般策略是只有在Wi-Fi可用的情况下才会获取大量数据。 在你执行网络操作之前,最好先检查一下网络连接状态。这样可以避免你的应用在不经意间操作不应该操作的事务。如果网络连接不可用,程序应该提供良好的用户回应。
检查网络连接一般使用以下类:
- ConnectivityManager: 响应网络连接状态查询,同时能在网络连接状态发生改变时通知程序。
- NetworkInfo:描述给定类型的网络接口状态(无论是移动网络还是Wi-Fi)
以下代码片段测试了Wi-Fi和移动网络连接,它决定了这些网络接口是否可用(即网络连接是否存在) 和/或 是否已经连接网络(即网络连接存在以及是否有可能建立socket通道并传输数据):
1 2 3 4 5 6 7 8 9 10 | private static final String DEBUG_TAG = "NetworkStatusExample"; ... ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); boolean isWifiConn = networkInfo.isConnected(); networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); boolean isMobileConn = networkInfo.isConnected(); Log.d(DEBUG_TAG, "Wifi connected: " + isWifiConn); Log.d(DEBUG_TAG, "Mobile connected: " + isMobileConn); |
注意:你不应该立足于当前网络是否可用(做什么操作),而需要在执行网络操作之前检查isConnectioned() (看该方法返回是否已经网络连接成功了),在连接成功了之后再执行操作(因为有可能你的移动网络不够稳定,手机处于飞行模式,限制后台数据等状态;译者注:在这种情况下,你可能会得到当前网络可用,但是无法连接的情况;)
更简洁的检查网络接口是否可用的方式如下: getActiveNetworkInfo()方法返回一个NetworkInfo实例,这个实例表示的是系统能够找到并连接的第一个网络接口。如果网络接 口是可以连接的,但是getNetworkInfo返回的是null(这表示当前网络连接无效)
1 2 3 4 5 6 | public boolean isOnline() { ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return (networkInfo != null && networkInfo.isConnected()); } |
你也可以使用NetworkInfo.DetailedState查询更多细粒度的状态,但是这个很少是必须的。
管理网络运用 Manage Network Usage
你可以实现一个preferences activity,在你应用的网络资源使用上提供给用户一个明确的控制;
比如:
你也许仅仅允许你的用户在设备连接到Wi-Fi的情况下才能上传视频。
你也许会基于一个特定的时机(比如网络的可用性,时间间隔等等)才去做同步(或者不做同步)。
写一个支持网络通道并且能够管理网络运用的应用,你的manifest文件必须拥有正确的permission(权限)和intent filter(意图过滤)。 以下manifest文件节选包含了以下权限:android.permission.INTERNET 允许用户打开网络通道。android.permission.ACCESS_NETWORK_STATE允许程序访问网络相关信息。
你可以为ACTION_MANAGE_NETWORK_USAGE action声明一个intent filter(在Android4.0中有介绍)表明你的应用程序定义了一个提供控制(网络)数据运用选项的activity。 ACTION_MANAGE_NETWORK_USAGE显示了管理特定程序的网络数据运行的设置。当你的应用程序有了一个“设置选项”的activity,用以允许用户控制网络使用,你应该为这个activity定义一个intent filter。在这个简单的例子中,这个action是由SettingActivity类控制的,这个activity显示了一个“设置选项”的UI给用户,让用户决定什么时候下载数据源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?xml version"utf-8"?> <manifest xmlns:android="http://schemas./apk/res/android" package="com.example.android.networkusage" ...> <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="14" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <application ...> ... <activity android:label".SettingsActivity"> <intent-filter> <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> </application> </manifest> |
实现Preferences Activity Implement a Preferences Activity
正如你看到的上面的manifest文件片段,这个应用程序例子的SettingActivity给ACTION_MANAGE_NETWORK_USAGE action指定了一个intent filter。 SettingActivity是PreferenceActivity的子类,它显示了一个配置界面(见下图)让用户可以指定以下选项:
是否显示每个xml格式数据源的项目摘要,或者只是显示每个项目的连接.
是否在任何网络链接类型下都可以下载XML格式数据源,还是只有在Wi-Fi的情况下.
这里的SettingActivity,注意它实现了OnSharedPreferenceChangeListener,当用户改变了设置,就会触发[java.lang.String) onSharedPreferenceChanged()](http://docs.eoe/reference/android/content/SharedPreferences.OnSharedPreferenceChangeListener.html#onSharedPreferenceChanged(android.content.SharedPreferences,)方法,并在这方法中设置refresDisplay为true。这样导致当用户返回主activity时显示会刷新。
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 | public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 加载XML设定文件(XML preferences file) addPreferencesFromResource(R.xml.preferences); } @Override protected void onResume() { super.onResume(); // 每当键发生变化注册一个监听事件 getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); } @Override protected void onPause() { super.onPause(); // 注销onResume()方法中设置的监听. // 注销监听最好是当你的程序不使用它们以减少系统不必要的开销, 你可以在onPause()方法中做这件事 getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); } // 当用户改变设置的选项,onSharedPreferenceChanged()重新启动主activity作为一个新任务, // 设置refreshDisplay为true以表明主activity应该更新它的显示, 主activity查询PreferenceManager得到最新的设置. @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { // 设置refreshDisplay为true,这样当用户返回主activity,显示会根据新设置进行刷新. NetworkActivity.refreshDisplay = true; } } |
响应(选项)Preference改变 Response to Preference Changes
当用户在设置界面修改了选项时,通常都会对应用程序的行为产生影响。在这节代码片段中,应用在onStart()方法中检查设置选项,如果设置和设备的网络连接能够匹配(比如设置中是“Wi-Fi”并且设备也拥有Wi-Fi连接),应用就下载数据源并刷新显示。
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 | public class NetworkActivity extends Activity { public static final String WIFI = "Wi-Fi"; public static final String ANY = "Any"; private static final String URL android&sort=newest"; // 是否有一个Wi-Fi连接. private static boolean wifiConnected = false; // 是否有一个手机移动网络连接. private static boolean mobileConnected = false; // 显示是否需要刷新. public static boolean refreshDisplay = true; // 用户当前网络优先设置. public static String sPref = null; // BroadcastReceiver追踪网络连接变化. private NetworkReceiver receiver = new NetworkReceiver(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 注册BroadcastReceiver追踪网络连接变化. IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); receiver = new NetworkReceiver(); this.registerReceiver(receiver, filter); } @Override public void onDestroy() { super.onDestroy(); // 当应用销毁,注销BroadcastReceiver. if (receiver != null) { this.unregisterReceiver(receiver); } } // 如果网络连接和优先设置允许,刷新显示。 @Override public void onStart () { super.onStart(); // 得到用户网络设置 SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); // 获取设置的字符串,第二个参数为当获取不到设置的值时默认使用的值. sPref = sharedPrefs.getString("listPref", "Wi-Fi"); updateConnectedFlags(); if(refreshDisplay){ loadPage(); } } // 检查网络连接并设置wifiConnected和mobileConnected变量. public void updateConnectedFlags() { ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeInfo = connMgr.getActiveNetworkInfo(); if (activeInfo != null && activeInfo.isConnected()) { wifiConnected = ConnectivityManager.TYPE_WIFI; mobileConnected = ConnectivityManager.TYPE_MOBILE; } else { wifiConnected = false; mobileConnected = false; } } // 使用AsyncTask子类从stackoverflow.com下载XML数据源. public void loadPage() { if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) || ((sPref.equals(WIFI)) && (wifiConnected))) { // AsyncTask的子类 new DownloadXmlTask().execute(URL); } else { showErrorPage(); } } ... } |
检测连接改变 Detect Connection Changes
最后一点难点就是BroadcastReceiver的子类-NetworkReceiver。当设备的网络连接改变,NetworkReceiver会截取 CONNECTIVITY_ACTION并确定当前的网络状态,从而设置wifi连接和移动网络连接为true或false。其结果是,当用户下次再返回应用时,如果NetworkActivity.refreshDisplay被设置为ture的话,应用仅会下载最新的数据源并更新界面显示。
设立一个不必要的BroadcastReceiver是对系统资源的流失。这个简单的应用在onCreate方法中注册了BroadcastReceiver NetworkReceiver,并在onDestroy方法中将其注销。这比在manifest文件中声明<receiver>标签更轻量级,当你在manifest文件中声明<receiver>,它可以在任何时候唤醒你的程序,即使你已经几周都没有运行过它了。在主activity中注册和注销NetworkReceiver,可以确保在用户离开程序后不会被唤醒。如果你在manifest中声明了一个<receiver>,并且确切的知道在哪里你需要用到它,你可以用[int, int) setComponentEnabledSetting()](http://docs.eoe/reference/android/content/pm/PackageManager.html#setComponentEnabledSetting(android.content.ComponentName,)方法适当的启用和禁用它。 下面是一个NetworkReceiver:
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 | public class NetworkReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { ConnectivityManager conn = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = conn.getActiveNetworkInfo(); // 检查用户首选项和网络连接.基于这个结果,决定刷新界面或保持当前显示. // 如果用户偏好Wi-Fi, 检查设备是否拥有Wi-Fi连接. if (WIFI.equals(sPref) && networkInfo != ConnectivityManager.TYPE_WIFI) { // 如果设备有Wi-Fi连接,设置refreshDisplay为true // 这是因为当用户返回到应用时,显示应该被刷新. refreshDisplay = true; Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show(); // 如果设置为任何网络并且这里有一个网络连接,设置refreshDisplay为true. } else if (ANY.equals(sPref) && networkInfo != null) { refreshDisplay = true; // 否则, 应用无法下载内容--可能是因为没有网络连接(mobile或Wi-Fi), // 或者是应为用户首选项是Wi-Fi,但没有Wi-Fi连接.设置refreshDisplay为false。 } else { refreshDisplay = false; Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show(); } |