声明:本文主要是参考 android官方API说明文档来的。
本课程主要介绍了如何连接网络,如何监控网络连接(包括网络连接的更改),还介绍了如何让用户控制网络使用率。同时也介绍了如何解析和使用XML数据。
通过本课程的学习,你将能够创建高效的网络使用率(在下载内容和解析数据的时候)的Android程序,能够最大限度的减少网络流量。
-------------------------------------------------------------------------
一、连接到网络
本节通过讲述一个简单的程序来阐述Android的网络连接。它讲述了网络连接的最佳实践,在你创建简单的网络连接时应该遵循。
请注意,执行本节课所描述的网络操作,应用程序清单必须包括下列权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />①选择HTTP Client
大多数Android程序的网络连接使用HTTP来接收和发送数据。Android包括两个HTTP 客户端:HttpURLConnection和Apache 的 HttpClient。这个两个客户端都支持HTTPS、流媒体上传和下载、配置超时时间、IPv6和连接池(connection pooling)。我们(官方)推荐在android 2.3和更高版本中使用HttpURLConnection。
②检查网络连接
在你的app尝试连接网络前,app应该使用getActiveNetworkInfo()
和isConnected()检测网络连接是否可用。需要注意,用户的设备可能离开了网络连接范围或者用户关闭了WI-FI和移动数据连接。更多关于网络连接的内容请查看: Managing Network Usage
public void myClickHandler(View view) { ... ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); if (networkInfo != null && networkInfo.isConnected()) { // 联网取得数据 } else { //显示网络连接错误 } ... }③在单独的线程中执行网络操作
为了提高用户体验,网络操作需要在单独的线程中进行(不能在UI线程中)。异步任务 AsyncTask类提供了一个简单的方法来在UI线程外生成一个新的线程(任务)。
在下面的例子中,myClickHandler()方法提供了new DownloadWebpageTask().execute(stringUrl)
. DownloadWebpageTask 是AsyncTask的子类 DownloadWebpageTask 实现了AsyncTask中的几个方法:
doInBackground()
方法执行 downloadUrl()
,这个方法来根据URL下载web页面需要的数据,当执行完后返回一个字符串。
onPostExecute() 获得字符串结果,并把它显示到UI上。
public class HttpExampleActivity extends Activity { private static final String DEBUG_TAG = "HttpExample"; private EditText urlText; private TextView textView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); urlText = (EditText) findViewById(R.id.myUrl); textView = (TextView) findViewById(R.id.myText); } // When user clicks button, calls AsyncTask. // Before attempting to fetch the URL, makes sure that there is a network connection. public void myClickHandler(View view) { // Gets the URL from the UI's text field. String stringUrl = urlText.getText().toString(); ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); if (networkInfo != null && networkInfo.isConnected()) { new DownloadWebpageTask().execute(stringUrl); } else { textView.setText("No network connection available."); } } // Uses AsyncTask to create a task away from the main UI thread. This task takes a // URL string and uses it to create an HttpUrlConnection. Once the connection // has been established, the AsyncTask downloads the contents of the webpage as // an InputStream. Finally, the InputStream is converted into a string, which is // displayed in the UI by the AsyncTask's onPostExecute method. private class DownloadWebpageTask extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... urls) { // params comes from the execute() call: params[0] is the url. try { return downloadUrl(urls[0]); } catch (IOException e) { return "Unable to retrieve web page. URL may be invalid."; } } // onPostExecute displays the results of the AsyncTask. @Override protected void onPostExecute(String result) { textView.setText(result); } } ... }
④连接和下载数据
在异步线程中进行网络操作,你可以用HttpURLConnection 来执行GET 操作来下载数据。等调用connect() 方法后,你就可以调用getInputStream() 方法来获得下载数据的InputStream 。
在下面的代码片段中,在doInBackground()
方法中调用downloadUrl() 方法。downloadUrl() 方法使用指定的URL通过HttpURLConnection 来连接网络。一旦连接建立,app调用getInputStream() 来接受数据的InputStream 。
// Given a URL, establishes an HttpUrlConnection and retrieves // the web page content as a InputStream, which it returns as // a string. private String downloadUrl(String myurl) throws IOException { InputStream is = null; // Only display the first 500 characters of the retrieved // web page content. int len = 500; try { URL url = new URL(myurl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(10000 /* milliseconds */); conn.setConnectTimeout(15000 /* milliseconds */); conn.setRequestMethod("GET"); conn.setDoInput(true); // Starts the query conn.connect(); int response = conn.getResponseCode(); Log.d(DEBUG_TAG, "The response is: " + response); is = conn.getInputStream(); // Convert the InputStream into a string String contentAsString = readIt(is, len); return contentAsString; // Makes sure that the InputStream is closed after the app is // finished using it. } finally { if (is != null) { is.close(); } } }需要注意的是
getResponseCode()
方法返回网络连接的
status code (状态码),这个是非常有用的,通过这个返回的状态码,能够获得连接的附加信息。比如 状态码为 200带表连接成功,服务器应答成功。
⑤将InputStream 转换成String
一个InputStream
是一个可读的bytes。一旦你得到一个InputStream
, 通常就是把它解码或者转换成目标数据类型。例如,你正在下载图片,你可以解码并把它显示出来,如下:
InputStream is = null; ... Bitmap bitmap = BitmapFactory.decodeStream(is); ImageView imageView = (ImageView) findViewById(R.id.image_view); imageView.setImageBitmap(bitmap);在上面的例子中, InputStream 代表web页面的text类型数据。下面的代码展示了怎么转换
InputStream
成字符串来让activity在UI线程中显示。
// Reads an InputStream and converts it to a String. public String readIt(InputStream stream, int len) throws IOException, UnsupportedEncodingException { Reader reader = null; reader = new InputStreamReader(stream, "UTF-8"); char[] buffer = new char[len]; reader.read(buffer); return new String(buffer); }二、管理网络连接
这个主要是介绍如何控制网络资源的使用。如果您的应用程序执行大量的网络操作,你应该提供一个网络设置来让使用者控制程序应该怎么使用网络。比如,你的程序同步数据的频率、是否只在wi-fi下来上传/下载数据、当处于漫游状态时怎么来使用网络数据,等等。当提供这些设置后,使用者就不太可能会禁用你的程序的后台数据连接当使用者
的数据流量不充足时。因为使用者能够通过设置来精确的控制你的程序对网络数据的使用。
①检测设备的网络连接
设备有很多种网络连接类型。本教程主要关注是否是 wi-fi和移动网络连接。要查看所有网络连接类型请查看ConnectivityManager
Wi-Fi连接通常更快速,移动网络通常是昂贵的,并且是限量的,通常情况下应该在Wi-Fi网络情况下进行大数据的网络传输。
当要执行网络操作时,最好进行网络状态的检测。在执行网络操作时进行网络状态检测,能够让你的程序避免得到错误的结果,并且能够省电。如果网络连接不可用,你的程序应该优雅的响应。检测网络状态通常用到下面几个类:
ConnectivityManager :回答了关于网络连接的状态查询。它还通知应用程序时的网络连接的变化。
NetworkInfo :描述网络连接类型,目前只有两种(Wi-Fi或者移动网络)。
这段代码的测试网络连通性的Wi-Fi和移动。它确定这些网络接口是否可用(即,网络连接是否是可能的)或连接(即,网络连接是否存在以及是否有可能建立套接字和数据传递):
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);也可以这么写:
private void updateConnectedFlags() {
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
if (activeInfo != null && activeInfo.isConnected()) {
wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI;
mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE;
} else {
wifiConnected = false;
mobileConnected = false;
}
}
需要注意的是,在执行网络操作之前,你不能只是检测网络连接是否存在,还应该检测连接是否可用(能够连接进行数据传输)
isConnected()
,因为
isConnected() 后能够排除飞行模式、受限的后台数据连接情况。
检测网络是否可用的一个简单方法如下。该方法getActiveNetworkInfo()
该方法返回 NetworkInfo
表示一个网络连接的接口,或者返回null(如果没有网络接口,即网络连接不可用)。
public boolean isOnline() { ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return (networkInfo != null && networkInfo.isConnected()); }若要查询更细粒度的状态,你可以使用 NetworkInfo.DetailedState,但是这应该很少使用的。
②管理网络使用
你可以实现一个偏好设置activity,让用户显式控制网络资源的应用程序的使用情况。例如:
- 你可能会允许用户上传视频,只有当设备连接到Wi-Fi网络。
- 你可能同步(或不),这取决于特定的标准,例如网络可用性,时间间隔,等等。
要编写一个支持网络访问和管理网络使用的应用程序,你的manifest文件中必须有正确的权限和意图过滤器。
要在mainifest中声明如下权限:
- android.permission.INTERNET 允许应用打开网络套接字。
- android.permission.ACCESS_NETWORK_STATE 允许应用查询网络连接状态
在mainifest中声明意图过滤器如下:
有网络设置的activity要声明action为ACTION_MANAGE_NETWORK_USAGE
,声明了这个后,能够在系统的后台数据设置那里 打开activity。 本示例中的activity是 SettingsActivity 。如下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/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" android:name=".SettingsActivity"> <intent-filter> <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> </application> </manifest>
SettingsActivity 是 PreferenceActivity
的子类, 能够让用户进行如下设置:
- 是否显示每一个XML feed条目的摘要或者只是显示条目的连接。
- 是否只是在Wi-Fi下载XML提要,或者在Wi-Fi和移动网络下都可以下载。
下面是SettingsActivity 的实现,需要注意的是,它实现了 OnSharedPreferenceChangeListener
接口,当用户改变设置的时候会回调这个接口来设置refreshDisplay 为true。这将导致当用户再次回到 main activity的时候来刷新页面。
public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Loads the XML preferences file addPreferencesFromResource(R.xml.preferences); } @Override protected void onResume() { super.onResume(); // Registers a listener whenever a key changes getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); } @Override protected void onPause() { super.onPause(); // Unregisters the listener set in onResume(). // It's best practice to unregister listeners when your app isn't using them to cut down on // unnecessary system overhead. You do this in onPause(). getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); } // When the user changes the preferences selection, // onSharedPreferenceChanged() restarts the main activity as a new // task. Sets the refreshDisplay flag to "true" to indicate that // the main activity should update its display. // The main activity queries the PreferenceManager to get the latest settings. @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { // Sets refreshDisplay to true so that when the user returns to the main // activity, the display refreshes to reflect the new settings. NetworkActivity.refreshDisplay = true; } }
接下来就讲述如何相应用户的设置,当用户在设置页改变了设置后,通常就会影响程序的行为。在下面代码片段中程序会在onStart() 方法中检测设置的改变 。如果用户设置项和当前设备网络连接状态相匹配,程序会根据用户的设置来下载XML和刷新显示。
public class NetworkActivity extends Activity { public static final String WIFI = "Wi-Fi"; public static final String ANY = "Any"; private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"; // Whether there is a Wi-Fi connection. private static boolean wifiConnected = false; // Whether there is a mobile connection. private static boolean mobileConnected = false; // Whether the display should be refreshed. public static boolean refreshDisplay = true; // The user's current network preference setting. public static String sPref = null; // The BroadcastReceiver that tracks network connectivity changes. private NetworkReceiver receiver = new NetworkReceiver(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Registers BroadcastReceiver to track network connection changes. IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); receiver = new NetworkReceiver(); this.registerReceiver(receiver, filter); } @Override public void onDestroy() { super.onDestroy(); // Unregisters BroadcastReceiver when app is destroyed. if (receiver != null) { this.unregisterReceiver(receiver); } } // Refreshes the display if the network connection and the // pref settings allow it. @Override public void onStart () { super.onStart(); // Gets the user's network preference settings SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); // Retrieves a string value for the preferences. The second parameter // is the default value to use if a preference value is not found. sPref = sharedPrefs.getString("listPref", "Wi-Fi"); updateConnectedFlags(); if(refreshDisplay){ loadPage(); } } // Checks the network connection and sets the wifiConnected and mobileConnected // variables accordingly. public void updateConnectedFlags() { ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeInfo = connMgr.getActiveNetworkInfo(); if (activeInfo != null && activeInfo.isConnected()) { wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI; mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE; } else { wifiConnected = false; mobileConnected = false; } } // Uses AsyncTask subclass to download the XML feed from stackoverflow.com. public void loadPage() { if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) || ((sPref.equals(WIFI)) && (wifiConnected))) { // AsyncTask subclass new DownloadXmlTask().execute(URL); } else { showErrorPage(); } } ... }
③监听设备网络连接的改变
最后一块要讲的是BroadcastReceiver
的子类NetworkReceiver
当设备网络连接改变后,系统会发送广播,NetworkReceiver
会拦截action为 CONNECTIVITY_ACTION 的广播(就是设备网络连接变化的广播),来获得当前的网络连接是什么状态,相应的改变wifiConnected 和mobileConnected 标志为true或者false,并根据用户的设置来标记NetworkActivity.refreshDisplay 为true或这false,当用户再次回到程序的时候,能够更新显示内容。
建立不必要的BrodcastReceiver会消耗系统资源该实例中,示例中的NetworkReceiver 在 onCreate()
方法中进行 注册,在onDestroy() 方法中接触注册。它比在manifest中声明的更轻巧,如果在manifest中声明BroadcastReceiver,它会在任何必要的时候唤醒你的应用程序,即使你的程序没有启动,通过在activity的onCreate() 中注册,在 onDestroy()
解除注册,可以确保程序不会在用户离开后被唤醒。如果确实需要在manifest中通过<receiver>标签来声明BroadcastReceiver,你可以调用setComponentEnabledSetting() 方法来启用或者禁用它。
NetworkReceiver 的代码实现:
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(); // Checks the user prefs and the network connection. Based on the result, decides whether // to refresh the display or keep the current display. // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection. if (WIFI.equals(sPref) && networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { // If device has its Wi-Fi connection, sets refreshDisplay // to true. This causes the display to be refreshed when the user // returns to the app. refreshDisplay = true; Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show(); // If the setting is ANY network and there is a network connection // (which by process of elimination would be mobile), sets refreshDisplay to true. } else if (ANY.equals(sPref) && networkInfo != null) { refreshDisplay = true; // Otherwise, the app can't download content--either because there is no network // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there // is no Wi-Fi connection. // Sets refreshDisplay to false. } else { refreshDisplay = false; Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show(); } }