8.1 问题
应用程序需要解析从API或其他资源返回的XML格式的响应结果。
8.2 解决方案
(API Level 1)
可以通过实现org.xml.sax.helpers.DefaultHandler的一个子类来解析数据,它使用的是基于事件的SAX方式(Simple API for XML)。android有三种用于解析XML数据的主要方式:DOM(文档对象模型)、SAX和Pull。这其中最容易实现的就是SAX解析器,它也是内存效率最高的。SAX解析通过遍历XML数据来实现,并在每个元素的开头和结尾产生回调事件。
8.3 实现机制
为了进一步介绍如何解析XML,先来看一下请求RSS/ATOM新闻源时返回的XML格式数据(参见以下代码)。
RSS基本结构
-
-
...
在各组
自定义的RSS解析处理程序
public class RSSHandler extends DefaultHandler {
public class NewsItem {
public String title;
public String link;
public String description;
@Override
public String toString() {
return title;
}
}
private StringBuffer buf;
private ArrayList feedItems;
private NewsItem item;
private boolean inItem = false;
public ArrayList getParsedItems() {
return feedItems;
}
//在每个新元素开始时调用
@Override
public void startElement(String uri, String name, String qName, Attributes atts) {
if("channel".equals(name)) {
feedItems = new ArrayList();
} else if("item".equals(name)) {
item = new NewsItem();
inItem = true;
} else if("title".equals(name) && inItem) {
buf = new StringBuffer();
} else if("link".equals(name) && inItem) {
buf = new StringBuffer();
} else if("description".equals(name) && inItem) {
buf = new StringBuffer();
}
}
//在每个元素结束时调用
@Override
public void endElement(String uri, String name, String qName) {
if("item".equals(name)) {
feedItems.add(item);
inItem = false;
} else if("title".equals(name) && inItem) {
item.title = buf.toString();
} else if("link".equals(name) && inItem) {
item.link = buf.toString();
} else if("description".equals(name) && inItem) {
item.description = buf.toString();
}
buf = null;
}
//调用元素中的字符数据
@Override
public void characters(char ch[], int start, int length) {
//Don't bother if buffer isn't initialized
if(buf != null) {
for (int i=start; i
在每个元素开始和结束时都会通过startElement()方法通过RSSHandler。在这之间,组成元素值的字符会传递给character()回调方法。当解析器遍历文档,会产生如下步骤:
(1)当解析器碰到第一个元素时,会初始化项列表。
(2)对于遇到的每个项元素,会初始化一个新的NewsItem模型。
(3)在每个项元素的内部,数据元素被置入一个StringBuffer中,然后插入NewsItem的成员中。
(4)当到达每个项的结尾时,会把NewsItem添加到列表中。
(5)解析完成后,feedItems中包含了源数据中的所有项。
接下来,使用第6节的API示例中介绍的一些技巧来下载最新的RSS格式的Google新闻内容(参见以下代码)。
解析XML并显示各个项内容的Activity
public class FeedActivity extends Activity implements ResponseCallback {
private static final String TAG = "FeedReader";
private static final String FEED_URI = "http://news.google.com/?output=rss";
private ListView mList;
private ArrayAdapter mAdapter;
private ProgressDialog mProgress;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mList = new ListView(this);
mAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1);
mList.setAdapter(mAdapter);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View v, int position, long id) {
NewsItem item = mAdapter.getItem(position);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(item.link));
startActivity(intent);
}
});
setContentView(mList);
}
@Override
public void onResume() {
super.onResume();
//获取RSS源数据
try{
RestTask task = RestUtil.obtainGetTask(FEED_URI);
task.setResponseCallback(this);
task.execute();
mProgress = ProgressDialog.show(this, "Searching", "Waiting For Results...", true);
} catch (Exception e) {
Log.w(TAG, e);
}
}
@Override
public void onRequestSuccess(String response) {
if (mProgress != null) {
mProgress.dismiss();
mProgress = null;
}
//处理响应数据
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser p = factory.newSAXParser();
RSSHandler parser = new RSSHandler();
p.parse(new InputSource(new StringReader(response)), parser);
mAdapter.clear();
for(NewsItem item : parser.getParsedItems()) {
mAdapter.add(item);
}
mAdapter.notifyDataSetChanged();
} catch (Exception e) {
Log.w(TAG, e);
}
}
@Override
public void onRequestError(Exception error) {
if (mProgress != null) {
mProgress.dismiss();
mProgress = null;
}
//显示错误
mAdapter.clear();
mAdapter.notifyDataSetChanged();
Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show();
}
}
这个示例修改之后会显示一个ListView,其中的数据就是从RSS源解析出来的。在这个示例中,我们为列表添加一个OnItemClickListener,用户点击时会在浏览器中加载新闻项的链接。
当数据从API的响应回调方法返回时,Android内置的SAX解析器会遍历XML字符串。SAXParser.parse()会使用RSSHandler的实例来处理XML,从XML中解析的内容会用来填充RSSHandler的feedItems列表。接收器在逐个处理解析出来的项,将其添加到ArrayAdapter中,最终显示在ListView中。
XMLPullParser
由框架提供的XmlPullParser是另一种高效解析传入的XML数据的方式。和SAX一样,解析过程也是基于流的,由于解析开始之前并不需要加载整个XML数据结构,因此在解析大文档源时也就不需要太多的内存。下面让我们看一下使用XmlPullParser解析RSS源数据的实例。但与SAX不同,我们必须手动地干预每一步的数据流解析过程,即使是我们不感兴趣的标签元素。
以下代码包含一个工厂类,它会迭代源数据以构造元素模型。
用来将XML解析成模型对象的工厂类
public class NewsItemFactory {
/* 数据模型类 */
public static class NewsItem {
public String title;
public String link;
public String description;
@Override
public String toString() {
return title;
}
}
/*
* 将 RSS 源解析为一个NewsItem 元素的列表
*/
public static List parseFeed(XmlPullParser parser) throws XmlPullParserException, IOException {
List items = new ArrayList();
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (parser.getName().equals("rss") ||
parser.getName().equals("channel")) {
//跳过这些元素,但允许解析它们内部的元素
} else if (parser.getName().equals("item")) {
NewsItem newsItem = readItem(parser);
items.add(newsItem);
} else {
//跳过其他元素以及它们的子元素
skip(parser);
}
}
//返回解析后的列表
return items;
}
/*
*将每个 - 元素解析为一个NewsItem
*/
private static NewsItem readItem(XmlPullParser parser) throws XmlPullParserException, IOException {
NewsItem newsItem = new NewsItem();
//开头必须是有效的
- 元素
parser.require(XmlPullParser.START_TAG, null, "item");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (name.equals("title")) {
parser.require(XmlPullParser.START_TAG, null, "title");
newsItem.title = readText(parser);
parser.require(XmlPullParser.END_TAG, null, "title");
} else if (name.equals("link")) {
parser.require(XmlPullParser.START_TAG, null, "link");
newsItem.link = readText(parser);
parser.require(XmlPullParser.END_TAG, null, "link");
} else if (name.equals("description")) {
parser.require(XmlPullParser.START_TAG, null, "description");
newsItem.description = readText(parser);
parser.require(XmlPullParser.END_TAG, null, "description");
} else {
//跳过其他元素以及它们的子元素
skip(parser);
}
}
return newsItem;
}
/*
* 读取当前元素的文本内容,该内容start和end标签之间包含的数据
*/
private static String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
String result = "";
if (parser.next() == XmlPullParser.TEXT) {
result = parser.getText();
parser.nextTag();
}
return result;
}
/*
* 辅助方法,用来跳过当前元素以及该元素的子元素
*/
private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
/*
* 对于每个新标签,会把一个depth计数器加1。到达每个标签的结尾时会把
* 计时器减1并且在end标签与开始时的标签匹配时会返回
*/
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}
Pull解析过程的工作原理就是把数据流作为一系列的事件来处理。应用程序通过调用next()方法或该方法的一个或多个指定变体来告诉解析器处理下一个事件。以下是解析器会处理的事件类型:
- START_DOCUMENT :当解析器首次初始化时会返回这个事件。在首次调用next()、nextToken()或nextTag()之前,解析器都会是这个状态。
- START_TAG :解析器刚刚读取标签元素的开始部分。标签的名称可以通过getName()获得,里面的任何属性也可以通过getAttributeValue()和相关的方法获得。
- TEXT :读取标签元素内部的字符数据,可以通过getText()获取。
- END_TAG :解析器刚刚读取标签元素的结尾部分。和它相匹配的开始标签的名称可以通过getName()获得。
- END_DOCUMENT :表明到达了数据量的结尾。
由于必须自己操作解析器,因此我们创建了一个辅助方法skip(),它可以帮助解析器跳过我们不感兴趣的标签。这个方法从当前位置开始遍历所有的内嵌子元素,直到找到匹配的结束标签,并把它们全部跳过。这里使用了一个depth计数器,碰到每个开始标签时会递增,碰到每个结束标签时会递减。当depth计数器到达0时,我们就找到了与开始位置相匹配的结束标签了。
本例中,在调用parseFeed()方法时,解析器首先会迭代数据流来查找可以转换为NewsItem的
分析每个
可见XmlPullParser非常灵活,原因是可控制整个过程的每一步,但这也要求写更多的代码来完成相同的结果。以下代码清单展示了使用新的解析器来完成源数据显示的Activity。
显示解析的XML源的Activity
public class PullFeedActivity extends Activity implements ResponseCallback {
private static final String TAG = "FeedReader";
private static final String FEED_URI = "http://news.google.com/?output=rss";
private ListView mList;
private ArrayAdapter mAdapter;
private ProgressDialog mProgress;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mList = new ListView(this);
mAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1);
mList.setAdapter(mAdapter);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View v, int position, long id) {
NewsItem item = mAdapter.getItem(position);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(item.link));
startActivity(intent);
}
});
setContentView(mList);
}
@Override
public void onResume() {
super.onResume();
//获取RSS 源数据
try{
RestTask task = RestUtil.obtainGetTask(FEED_URI);
task.setResponseCallback(this);
task.execute();
mProgress = ProgressDialog.show(this, "Searching", "Waiting For Results...", true);
} catch (Exception e) {
Log.w(TAG, e);
}
}
@Override
public void onRequestSuccess(String response) {
if (mProgress != null) {
mProgress.dismiss();
mProgress = null;
}
//处理响应数据
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(new StringReader(response));
//跳过第一个标签
parser.nextTag();
mAdapter.clear();
for(NewsItem item : NewsItemFactory.parseFeed(parser)) {
mAdapter.add(item);
}
mAdapter.notifyDataSetChanged();
} catch (Exception e) {
Log.w(TAG, e);
}
}
@Override
public void onRequestError(Exception error) {
if (mProgress != null) {
mProgress.dismiss();
mProgress = null;
}
//显示错误
mAdapter.clear();
mAdapter.notifyDataSetChanged();
Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show();
}
}
使用Xml.newPullParser()可以实例化一个新的XmlPullParser,通过setInput()可以将数据源的输入流作为一个Reader。本例中,从Web服务器返回的数据已经是字符串了,所以我们把它封装成一个StringReader来让解析器解析。我们可以把解析器传给NewsItemFactory,之后会返回NewsItem元素的列表,我们把它添加到ListAdapter中,然后像之前那样显示出来。
提示:
还可以使用XmlPullParser解析应用程序中绑定的本地XML数据。把你的原始XML放到资源文件中(如res/xml),然后你就可以实例化一个XmlResourceParser,它会使用Resourse.getXml()预加载你的本地数据。