这节课里面包含了连接网络的基本任务,监视网络连接,包括网络变化,并且让用户可以控制网络的使用。同样表述了如何解析和使用xml数据。
这节课中包含一个展示怎样典型的网络操作的示例应用。
经过这节课的学习,你将会拥有使得android app可以高效地网络下载以及解析数据的能力,与此同时使用最少的网络流量。
提示:要查看Volly请参照 课程Transmitting Network Data Using Volley一节,这是一个使得android app的网络连接更加简单和快速的http库。volley可以在open 的AOSP仓库中获取。Volley可以让你对于网络操作的组织更加合理并提高你的app网络操作的性能。
课程列表:
连接到网络
学习怎样连接网络,选择hTTP客户端,以及在非UI线程上面进行网络操作。
管理网络使用
学习怎样检查设备的网络连接,创建可以控制网络的UI,响应网络中的一些变化。
解析XML数据
学习怎样解析和使用xml数据。
连接到网络
这节课教你创建一个简单的使用网络的app。及时你在app中使用最简单的网络连接,也最好使用这个例子中展示的实践。
要进行网络连接,你需要在manifest中添加下面的权限。
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
选择一个HTTP客户端
大多数Adrnodi应用使用HTTP来在网络连接中接受和发送数据。android平台中包含HttpURLConnection客户端,它支持HTTPs,流上传和下载,设置超时时间,IPv6,以及连接池。
检查网络连接
当你的app使用网络连接之前,他应该检查网络连接是否可用。使用getActiveNetWorkInfo()方法,以及isConnceted()方法。注意,设备可能不在网络的范围之内,以及用户可能禁用看wifi连接以及移动数据。更多的讨论参见Managing Network Usage。
public void myClickHandler(View view) { ... ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); if (networkInfo != null && networkInfo.isConnected()) { // fetch data } else { // display error } ... }
在其他线程中进行网络操作
网络操作可能引起不可预期的延迟。为了防止这样的事情影响用户交互,所有网络的操作要在非UI线程当中。AsyncTask类提供了一种最简单个的分离出UI线程的方法。更多的这个话题的讨论请参见博客 Muitithreading For Perfoermance。
在下面的代码片当中,myClickahandler方法触发了一个new DownloasWebpageTask().execute(stringUrl)。其中的DownloadWebpageTask类是AsyncTask的子类。DownloadWebpageTask类重写了下面的方法。
- doInBackgound()中执行了方法downloadUri(). 它会把网页uri作为参数传递。方法 downloadUrl() 获取并运行网页的内容。当结束的时候,就返回一个结果字符串。
- 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); } } ... }
代码片中的事件的发生顺序如下:
1.当点击按钮的时候出发myClickHandler()方法,app会传递指定的URL给AsyncTask的子类 DownloadWebpageTask。
2.AsyncTask中的doInBackgound方法调用downloadUrl()方法。
3.downloadUri方法接受一个URL字符串作为参数并使用它创建了一个URL对象。
4.URL对象被用来建立一个HttpURLConnection。
5.一旦连接建立,HttpURLConncetion对象使用InputStream获取网页内容。
6.InputStream被传入到readIt()方法中,它把流转换成字符处。
7.最后,AsycTask的onPostExecute()方法吧字符串显示到UI。
连接和下载数据
在你进行网络交互的线程里面,你可以实用HttpURIConnection GET操作来获取你的数据。当你调用connect以后,你通过使用getInputSteam()方法来获取一个读取数据的InputStream。
在下面的代码片当中,doInbackgound方法电泳了downloadURL()方法。这个方法获取传入的URL并使用它,通过HttpURLConncetion来连接网络。一旦连接被建立,app就可以使用getInputStream获取InputStream来获取数据。
// Given a URL, establishes an HttpUrlConnection and retrieves // the web page content as a InputStream, which it returns as // a string.使用给的URL建立一个HttpUrlConnection并使用可以返回字符串的InputStream获取网页的内容。 private String downloadUrl(String myurl) throws IOException { InputStream is = null; // Only display the first 500 characters of the retrieved 只显示获取的网页中的前500个字符。 // 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 保证在app用完以后关闭InputStream。 // finished using it. } finally { if (is != null) { is.close(); } } }
注意到getResourse方法返回连接的状态码。这对于获取连接的附加的信息十分有用。状态码200表示连接成功。
把输入流转化成一个字符串
一个InputStream是以字节为单位的可读资源。一旦你获取到了InputStream,通常你需要转码或者转化到目标数据类型。例如,如果你正在下载图片数据,你可能会如这样来解码和显示。
InputStream is = null; ... Bitmap bitmap = BitmapFactory.decodeStream(is); ImageView imageView = (ImageView) findViewById(R.id.image_view); imageView.setImageBitmap(bitmap);
在上面的例子里面,InputStream承载了网页的文本。下面是怎样把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); }
管理网络使用
这节课中教你怎样写一个对于网络具有细粒度管理的app。如果你的app要进行很多的网络操作,你需要给用户提供让他们自己控制数据习惯的方法,,比如你的应用多久同步一次数据,是否仅在wifi环境下进行加载和,在数据漫游的时候是否使用数据等等。给用户提供这些细节的设置,可以让在进行后台数据限定的时候不太可能限制你的app,因为他们可以精细的定制你的应用使用多少流量。
关于怎样让应用在下载和网络连接的时候使用最少的电量,请参见Optimizing Battery Life 和 Transferring Data Without Draining the Battery。
检查设备的网络连接
设备可以具有各种各样的网络连接。这节课关注使用wifi或者移动流量。想看完全的网络列表,请查看ConnectivityManager。
wifi是一种典型的快速网络。同时,移动数据都是计量的,昂贵的。一个对于app获取大数据的策略就是只有在wiif可用的时候才会开始。
在你进行网络操作之前,检查当前的网络连接状态是一个很好的习惯。此外,这可以防止你的app错误的使用了网络的类型。如果你个网络连接不可用,app应该优雅地给予响应。检查网络连接,你通常使用如下的类:
- ConnectivityManager: 响应关于网络连接的查询。它也可以在网络连接发生变化的时候提醒app。
- NetworkInfo: 描述给出的网络(当前是wifi还是流量)状态。
下面的代码片检测当前的网络的类型是数据流量还是wifi。他决定了这些网络接口是否可用(也就是网络连接是不是可能的)同时(或者 或)可连接(也就是网络连接是否存在并可以建立socket来传递数据)。
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);
足以到你不能依据一个网络是否可用二做出决策。你应该在进行网络操作之前看isConnected方法,因为isConnected可以处理移动网络、飞行模式
以及限制后台数据的情况。
一个更加简明的检查网络是否可用的方法如下。方法getActiveNetworkInfo()方法返回一个NetworInfo实例,代表了接口第一个发现的连接着的网络,或者在没有任何网络连接的情况下放回null(意味着互联网不可用)。
public boolean isOnline() { ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return (networkInfo != null && networkInfo.isConnected()); }
更细粒度的获取网络状态可以使用NetworkInfo.DetailedState,但是一般都不是很必要。
管理网络使用
你可以构建一个偏好设置的activity让用户可以显式的设置app对于网络资源的使用。例如:
- 你可以让用户只可以在wifi连接下进行视频上传。
- 你可以设定同步或者不同步,在特定的网络连接以及时间区间,等等。
要写一个支持网络访问以及可以管理网络的app,你需要在manifest里面设置正确的权限以及intent过滤器。
- 下面引用的 manifest包含下面几个授权:
* android.permission.INTERNTE——允许应用打开网络socket。
* androdi.permssion.ACCESS_NETWORK_STATE_允许用户获取网络的信息。
- 你可以声明一个intent filter 包含action ACTION_MANAGE_NETWORK_USAGE(在android4.0引入)来告诉系统在你的应用中有一个可以控制数据使用的activity。ACTION_MANAGE_NETWORK_USAGE显示一个特定的app的网络数据使用管理设置。如果你的app中有一个让用户控制网络使用的activity,你需要在你的intent filter中声明。在例子app中,这样的一个intent对应的是类SettingActivity,它显示一个设置ui,让用户决定下载源。
构建一个设置app
如你在上面看到的 manifest的引用,示例app有一个intent fileter,对应 ACTION_MANAGE_NETWORK_USAGE action。SettingsActivity是PreferenceActivity的子类。它显示定制屏幕来让用户指定下面的几项:
* 是否显示每一个xml 填充实体的照耀,或者及你进显示每一个实体的连接。
* 是否在任何连接可用的情况下下载xml填充还是只在wifi下进行。
下面就是SettingsActivity。要注意其中构建了一个OnSharedPreferenceChangeListener。当用户改变了一个设置,就会触发onSharedPreferenceChanged(), 它会把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加载xml preference 文件 addPreferencesFromResource(R.xml.preferences); } @Override protected void onResume() { super.onResume(); // Registers a listener whenever a key changes 当按键变化时候,注册一个listener getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); } @Override protected void onPause() { super.onPause(); // Unregisters the listener set in onResume().注销在onResume()中注册的listener。 // It‘s best practice to unregister listeners when your app isn‘t using them to cut down on 在你的app不使用的时候注销listener是很好的习惯,可以减少不必要的系统开销。 // unnecessary system overhead. You do this in onPause(). //在onPause()中进行这样的操作。 getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); } // When the user changes the preferences selection, 当用户改变了偏好选择,onSharedPreferenceChanged会用一个新栈来重启mainActivity。把refreshDispaly标志位设置为true来指示main activity应该更新它的显示。 main activity会查询 PreferenceManager来获取最新的设置。 // 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//设置resfereshDisplay为true这样当用户返回main activity的时候,就会更新显示最新的内容。
// activity, the display refreshes to reflect the new settings. NetworkActivity.refreshDisplay = true; } }
响应Preference 变化
当用户在设置屏幕里面改变preference,通常都会对app的行为有影响。在下面的代码片里面,app在onStart()里面检查preference 设置。如果设置和网络连接之间存在比配,那么ap就会下载填充,并更新显示(比如设置到wifi并且现在也恰好是wifi连接)。
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.是否有wifi连接 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.跟踪网络连接变化的BroadcastReceiver。 private NetworkReceiver receiver = new NetworkReceiver(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Registers BroadcastReceiver to track network connection changes.//注册监听网络连接的BroadCastReceiver 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.当app销毁的时候注销BroadcastReceiver。 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获取preference的字符串值,后一个参数是如果这个值没有找到的话的默认值。 // 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//检查网络设置,并分别设置wifiConnected和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.//使用AsyncTask的子类来从stackoverflow上下载xml feed。 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。要点是下一次用户返回这个app的时候,如果NetwordActivity.refresh设置为true,app会只下载最新的fedd并更新显示。
设立一个接受不必要调用的BroadcastReceiver是对系统资源的浪费。例子的app在onCreade中注册了BroadcastReceiver NetwordReceiver,并且在onCreate里面进行注销。这比在manifest中声明<receiver>标签更加轻量级。当你在<receiver>标签中声明的时候,可以随时唤醒你的 app,尽管你的app已经几个星期都没有运行过了。通过在main Activity中注册和注销BroadcastReceiver,可以确保当用户离开app的时候app不会被唤醒。如果你在manifest中声明了<receiver>,并且你清楚自己什么时候需要使用,你可以使用setCompanentEnableSetting()来启用或者停用。
下面就是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//检查用户偏好和网络连接,根据结果决定是否刷新屏幕或者保留现在的显示。如果用户选择是仅wifi,那么检查设备时候有网络连接。 // 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//如果设备有wifi连接,设置refreshDisplay为true。这使得用户返回主界面的时候进行界面刷新。 // 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//如果设置是任意网络(按照排除结果应该是移动数据)把更新标志设置为true。 // (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//除此之外,app不能下载内容,因为没有网络连接(移动数据或者wifi),或者因为用户的设置是wifi,而现在没有网络连接。设置refreeshDisplay为false。 // 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(); } }
解析xml数据
拓展标记语言,xml是一种按照机器可读的文档编码 规则。xml是互联网上传递信息的流行的形式。网站上频繁更新的 内容比如新闻网站或者博客,总是提供xml填充,这样的话外部程序可以保持和内容并列一致。上传和解析xml是网络连接app常用的一种任务。这节课解释怎样解析xml文档并使用其中的数据。
选择一个解析器
我们推荐XmlPullParser,它是android上高效的可维护的解析xml的方法。历史上android对于这个接口有两种实现。
* KXmlParser 通过 XmlPullParserFactory.newPullParser().
* ExpatPullParser, 通过 Xml.newPullParser().
使用哪一种都是可以的,例子中使用的是ExpatPullParser,通过Xml.newPullParser()。
解析填充内容
解析一个填充的第一步就是决定哪一个阈是你感兴趣的。解析器就会提取这些领域的数据,并忽略其他的部分。
下面是一个我们例子中的xml填充的摘要。每一个对于StackOverlow.com的pos都出现在填充当中,作为一个包含了内部多个标签的实体标签。
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ..."> <title type="text">newest questions tagged android - Stack Overflow</title> ... <entry> ... </entry> <entry> <id>http://stackoverflow.com/q/9439999</id> <re:rank scheme="http://stackoverflow.com">0</re:rank> <title type="text">Where is my data file?</title> <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/> <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/> <author> <name>cliff2310</name> <uri>http://stackoverflow.com/users/1128925</uri> </author> <link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file" /> <published>2012-02-25T00:30:54Z</published> <updated>2012-02-25T00:30:54Z</updated> <summary type="html"> <p>I have an Application that requires a data file...</p> </summary> </entry> <entry> ... </entry> ... </feed>
示例app从entry标签中提取数据,以及他的标签标题,链接,和摘要。
实例化解析器
下一个步骤就是实例化一个解析器并开始解析进程。在这个代码片里面,实例化一个parser,不去执行namespace,并使用提供的InputStream作为他的输入。它将随着nextTag()的调用来开始解析进程,并触发readFeed()方法,它提取并处理app感兴趣的数据。
public class StackOverflowXmlParser { // We don‘t use namespaces private static final String ns = null; public List parse(InputStream in) throws XmlPullParserException, IOException { try { XmlPullParser parser = Xml.newPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(in, null); parser.nextTag(); return readFeed(parser); } finally { in.close(); } } ... }
读取填充
readFeed()方法没有真的做处理数据填充(源)的工作。它寻找 entry 标签作为递归处理填充的起始点。如果一个标签不是 entry标签,就会跳过它。一旦整个填充都被递归的处理了,readFeed()就会返回包含从填充中提取的数据的List(包含内部数据)。接着parser就会返回这个List。
private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException { List entries = new ArrayList(); parser.require(XmlPullParser.START_TAG, ns, "feed"); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); // Starts by looking for the entry tag if (name.equals("entry")) { entries.add(readEntry(parser)); } else { skip(parser); } } return entries; }
解析XML
解析xml的步骤如下:
1.如同Analyze the Feed中描述的那样,定义你想要包含在你app中 的tag。整个例子从entry标签,以及其子标签title,link, summary中提取数据。
2.创建下面的方法:
* 一个read方法来读取你感兴趣的标签,比如说readEntry(),readTitle()等等。解析器冲输入流当中读取标签。当他们遇到对应的标签名字,比如说entry, title,link或者summary的时候,就会调用对应的标签的方法。或者跳过这个标签。
*从不同的标签中读取数据,以及前进到下一个标签中的方法。例如:
- 对于title和summary标签,解析器调用readText()方法。这个方法通过代用parser.getText()方法来为这些标签提取数据。
- 对于link标签, 首先提取links的标签,来判断这个link是不是我们感兴趣的类型。接着使用parser.getAttributeValue()来提取link的值。
- 对于entry标签, parser调用readEntry()。这个方法解析entry的内部标签并返回拥有数据成员title, link, summary的Entry对象。
* 一个帮助者skip()方法用来递归。更过的关于这个话题的讨论,请参见Skip Tags You Don’t care about。
下面的代码片展示了解析器怎样解析entries , titles, links,以及 summaries。
public static class Entry { public final String title; public final String link; public final String summary; private Entry(String title, String summary, String link) { this.title = title; this.summary = summary; this.link = link; } } // Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off // to their respective "read" methods for processing. Otherwise, skips the tag.//解析entry的内容。如果遇到了titile summary或者link标签,就把他们交给对应的read方法来处理,或者跳过这个标签。 private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, ns, "entry"); String title = null; String summary = null; String link = null; while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (name.equals("title")) { title = readTitle(parser); } else if (name.equals("summary")) { summary = readSummary(parser); } else if (name.equals("link")) { link = readLink(parser); } else { skip(parser); } } return new Entry(title, summary, link); } // Processes title tags in the feed.//解析填充源中的title标签 private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, ns, "title"); String title = readText(parser); parser.require(XmlPullParser.END_TAG, ns, "title"); return title; } // Processes link tags in the feed.// 处理填充源当中的link标签。 private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException { String link = ""; parser.require(XmlPullParser.START_TAG, ns, "link"); String tag = parser.getName(); String relType = parser.getAttributeValue(null, "rel"); if (tag.equals("link")) { if (relType.equals("alternate")){ link = parser.getAttributeValue(null, "href"); parser.nextTag(); } } parser.require(XmlPullParser.END_TAG, ns, "link"); return link; } // Processes summary tags in the feed.//处理其中的summary标签 private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException { parser.require(XmlPullParser.START_TAG, ns, "summary"); String summary = readText(parser); parser.require(XmlPullParser.END_TAG, ns, "summary"); return summary; } // For the tags title and summary, extracts their text values.对于title和summary标签获取其中的文本。 private String readText(XmlPullParser parser) throws IOException, XmlPullParserException { String result = ""; if (parser.next() == XmlPullParser.TEXT) { result = parser.getText(); parser.nextTag(); } return result; } ... }
跳过你不关心的标签
按照上面的解析方法,还有一个步骤就是跳过你不感兴趣的标签。下面是parser的skip()方法。
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { if (parser.getEventType() != XmlPullParser.START_TAG) { throw new IllegalStateException(); } int depth = 1; while (depth != 0) { switch (parser.next()) { case XmlPullParser.END_TAG: depth--; break; case XmlPullParser.START_TAG: depth++; break; } } }
下面是它如何工作的:
* 如果当前的事件不是一个START_TAG就跑出异常。
* 如果消费了一个START_TAG,一直到END_TAG之间的事件都要包含进去。
* 为了确保他在正确的END_TAG标签上停住而不是原始的START_TAG之后的第一个标签,要保持跟踪内部的层级。
这样的话,如果当前的元素有内部的 元素,那么depth的值直到所有 的原始START_TAG和END_TAG之间的标签都别消费了以后才能为0。例如我们看看parser是怎样跳过<author>元素的,它内部有两个元素<name>和<uri>。
*第一次进入while循环以后,在<author>以后的下一个标签是<name>的START_TAG,depth的值增长到了2.
* 第二次进入while循环的时候,parser遇到的第二个标签是</name> END_TAG。depth的值减小到了1.
*第三次进入while循环的时候,遇到的是<uri>的START_TAG。depth的值增长到了2.
*第四次进入while循环的时候,遇到的是</uri>的END_TAG。depth的值降低到了1.
*第五次进入while循环的时候,遇到的时候</author>的END_TAG。depth的值降低到了0,表明了<author>标签被成功的跳过了。
消费XML数据
案例中的应用使用了AsyncTask来获取和解析XML填充。这使得处理可以脱离主线程。当处理完成的时候,就会在main Activity中更新UI(NetworkActivity)。
在下面 的引用中,loadPage()方法做了下面的事情:
*使用xml填充源的URL初始化一个string变量。
* 如果用户设置那个和网络连接允许,就会触发一个new DownloadXmlTask().execute(url)。这个语句实例化了一个DownloadXmlTask对象,(AsyncTask子类)并调用了他的execute方法,他会下载并解析填充的内容源。并在UI上显示。
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; public static String sPref = null; ... // Uses AsyncTask to download the XML feed from stackoverflow.com. public void loadPage() { if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) { new DownloadXmlTask().execute(URL); } else if ((sPref.equals(WIFI)) && (wifiConnected)) { new DownloadXmlTask().execute(URL); } else { // show error } }
AsyncTask的子类如下所示,DownloadXmlTask,实现了下面的方法:
* doInBackgound()执行了方法loadXmlFromNetwork()。它将URL作为一个参数传入。loadXmlFromNetwork()方法获取并处理填充源。当他完成的时候,就返回结果的字符串。
* onPostExecute()获取返回的字符串并在ui上显示。
// Implementation of AsyncTask used to download XML feed from stackoverflow.com. private class DownloadXmlTask extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... urls) { try { return loadXmlFromNetwork(urls[0]); } catch (IOException e) { return getResources().getString(R.string.connection_error); } catch (XmlPullParserException e) { return getResources().getString(R.string.xml_error); } } @Override protected void onPostExecute(String result) { setContentView(R.layout.main); // Displays the HTML string in the UI via a WebView WebView myWebView = (WebView) findViewById(R.id.webview); myWebView.loadData(result, "text/html", null); } }
下面是关于在DownloadXmlTask中被调用的loadXmlFromNetwork()。它完成了下面的工作:
1.实例化一个StatckOverflowXmlParser。同样创建 了一个List变量盛装Entry对象,(entries),title, url 以及summary,盛装从xml填充元的这些域中获取出来的值。
2.调用downloadUrl()方法,获取这个feed填充元并作为一个InputStream返回。
3.使用stackOverflowXmlParser来解析InputStream。StackOverflowXmlParser弹出包含从填充源中数据解析出来的实体的List。
4.处理这个List实体,把HtML标记和填充元数据项结合。
5.通过AsyncTask的onPostExcute()方法返回要在mainActivity UI显示的HTML字符串。
// Uploads XML from stackoverflow.com, parses it, and combines it with // HTML markup. Returns HTML string. private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException { InputStream stream = null; // Instantiate the parser StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser(); List<Entry> entries = null; String title = null; String url = null; String summary = null; Calendar rightNow = Calendar.getInstance(); DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa"); // Checks whether the user set the preference to include summary text SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); boolean pref = sharedPrefs.getBoolean("summaryPref", false); StringBuilder htmlString = new StringBuilder(); htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>"); htmlString.append("<em>" + getResources().getString(R.string.updated) + " " + formatter.format(rightNow.getTime()) + "</em>"); try { stream = downloadUrl(urlString); entries = stackOverflowXmlParser.parse(stream); // Makes sure that the InputStream is closed after the app is // finished using it. } finally { if (stream != null) { stream.close(); } } // StackOverflowXmlParser returns a List (called "entries") of Entry objects. // Each Entry object represents a single post in the XML feed. // This section processes the entries list to combine each entry with HTML markup. // Each entry is displayed in the UI as a link that optionally includes // a text summary. for (Entry entry : entries) { htmlString.append("<p><a href=‘"); htmlString.append(entry.link); htmlString.append("‘>" + entry.title + "</a></p>"); // If the user set the preference to include summary text, // adds it to the display. if (pref) { htmlString.append(entry.summary); } } return htmlString.toString(); } // Given a string representation of a URL, sets up a connection and gets // an input stream. private InputStream downloadUrl(String urlString) throws IOException { URL url = new URL(urlString); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(10000 /* milliseconds */); conn.setConnectTimeout(15000 /* milliseconds */); conn.setRequestMethod("GET"); conn.setDoInput(true); // Starts the query conn.connect(); return conn.getInputStream(); }