Android
上的 XML
Android
平臺是一個開源移動開發平臺。它允許您訪問各種移動設備的所有方面,這些移動設備從低級圖形設備到手機攝像頭上的硬件不一而足。由於 Android
可以實現這麼豐富的功能,因此您可能想知道爲何還要爲 XML
傷腦筋呢。並不是因爲使用 XML
是多麼地有趣;而是因爲它能提供一些特殊的支持。XML
經常用作 Internet
上的一種數據格式。如果您希望通過 Internet
訪問數據,則數據很有可能是 XML
格式。如果您希望發送數據給 Web
服務,那麼您可能也需要發送 XML
。簡而言之,如果您的 Android
應用程序將利用 Internet
,那麼您可能需要使用 XML
。幸運的是,您可以採用多種方法在 Android
上使用 XML
。
XML
解析器
Android
平臺最大的一個優勢在於它利用了 Java
編程語言。Android SDK
並未向您的標準 Java Runtime
Environment (JRE)
提供一切可用功能,但它支持其中很大一部分功能。Java
平臺支持通過許多不同的方式來使用 XML
,並且大多數與 XML
相關的 Java API
在 Android
上得到了完全支持。舉例來說,Java
的 Simple API for XML (SAX)
和 Document Object Model (DOM)
在 Android
上都是可用的。這些 API
多年以來一直都是 Java
技術的一部分。較新的 Streaming API for XML (StAX)
在 Android
中並不可用。但是, Android
提供了一個功能相當的庫。最後,Java XML Binding
API
在 Android
中也不可用。這個 API
已確定可以在 Android
中實現。但是,它更傾向於是一個重量級的 API
,需要使用許多不同類的實例來表示 XML
文檔。因此,這對於受限的環境,比如說 Android
針對的手持設備,不太理想。在後續小節中,我們將以 Internet
上的一個簡單的 XML
源爲例,來看看如何在 Android
應用程序中使用上述各種 API
來解析它。首先,我們來看看這個簡單應用程序的主要部分,它將通過 Internet
來使用 XML
。
Android
新聞閱讀器
應用程序將從熱門 Android
開發人員站點 Androidster
獲取一個 RSS
提要,並將它解析爲一組簡單的 Java
對象,您可以使用這些對象構建一個 Android ListView
(參見 下載
部分獲取源代碼)。這是一種典型的多態行爲 —
提供相同行爲的不同實現(不同的 XML
解析算法)。清單 1
展示瞭如何在 Java
代碼中使用一個接口建立這一模型。
清單 1. XML
提要解析器接口
package org.developerworks.android;
import java.util.List;
public interface FeedParser {
List<Message> parse();
}
|
在 清單 2
中,
Message
類是一個典型的 Plain
Old Java Object (POJO)
,它表示一種數據結構。
清單 2.
Message
POJO
public class Message implements
Comparable<Message>{
static
SimpleDateFormat FORMATTER =
new
SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
private
String title;
private
URL link;
private
String description;
private
Date date;
//
getters and setters omitted for brevity
public
void setLink(String link) {
try
{
this.link = new URL(link);
}
catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
public
String getDate() {
return FORMATTER.format(this.date);
}
public
void setDate(String date) {
//
pad the date if necessary
while
(!date.endsWith("00")){
date += "0";
}
try
{
this.date = FORMATTER.parse(date.trim());
}
catch (ParseException e) {
throw new RuntimeException(e);
}
}
@Override
public
String toString() {
// omitted for brevity
}
@Override
public
int hashCode() {
// omitted for brevity
}
@Override
public
boolean equals(Object obj) {
// omitted for brevity
}
//
sort by date
public
int compareTo(Message another) {
if
(another == null) return 1;
//
sort descending, most recent first
return another.date.compareTo(date);
}
}
|
清單 2
中的消息基本上是相當直觀的。通過允許日期和鏈接作爲簡單的對象被訪問,同時將它們表示爲較強類型的對象(
java.util.Date
和
java.net.URL
),它隱藏了一些內部狀態。它是一個典型的 Value Object
,因此它基於其內部狀態實現了
equals()
和
hashCode()
。它還實現了
Comparable
接口,因此您可以使用它進行排序(按日期)。在實踐中,提要中的數據始終是有序的,因爲沒有必要再進行排序。
每個解析器實現都需要提供一個 URL
給 Androidster
提要,並使用它打開一個到 Androidster
站點的 HTTP
連接。這一常見行爲自然是在 Java
代碼中建模,我們使用了一個抽象基類,如 清單 3
所示。
清單 3.
基本提要解析器類
public abstract class BaseFeedParser implements
FeedParser {
// names
of the XML tags
static
final String PUB_DATE = "pubDate";
static
final
String DESCRIPTION =
"description";
static
final
String LINK =
"link";
static
final
String TITLE =
"title";
static
final
String ITEM =
"item";
final
URL feedUrl;
protected BaseFeedParser(String feedUrl){
try
{
this.feedUrl = new URL(feedUrl);
}
catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
protected InputStream getInputStream() {
try
{
return feedUrl.openConnection().getInputStream();
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
}
|
基類存儲
feedUrl
並使用它打開了一個
java.io.InputStream
。 如果出現任何差錯,它會拋出一個
RuntimeException
,造成應用程序出現故障。基類還爲標記的名稱定義了一些簡 單的常量。清
單 4
顯示了提要中的一些示例內容,以便於您理解這些標記的重要性。
清單 4.
示例 XML
提要
<?xml version="1.0"
encoding="UTF-8"?>
<!-- generator="FeedCreator 1.7.2"
-->
<rss version="2.0">
<channel>
<title>android_news</title>
<description>android_news</description>
<link>http://www.androidster.com/android_news.php</link>
<lastBuildDate>Sun, 19 Apr 2009 19:43:45
+0100</lastBuildDate>
<generator>FeedCreator 1.7.2</generator>
<item>
<title>Samsung S8000 to Run Android, Play DivX, Take Over the
World</title>
<link>http://www.androidster.com/android_news/samsung-s8000-to-run-android-
play-divx-take-over-the-world</link>
<description>More details have emerged on the first Samsung
handset
to run Android. A yet-to-be announced phone called
the S8000 is being
reported ...</description>
<pubDate>Thu, 16 Apr 2009 07:18:51 +0100</pubDate>
</item>
<item>
<title>Android Cupcake Update on the Horizon</title>
<link>http://www.androidster.com/android_news/android-cupcake-update-
on-the-horizon</link>
<description>After months of discovery and hearsay, the
Android
build that we have all been waiting for is about
to finally make it
out ...</description>
<pubDate>Tue, 14 Apr 2009 04:13:21 +0100</pubDate>
</item>
</channel>
</rss>
|
如 清單 4
中的示例所示,一個
ITEM
對應於一個
Message
實例。項目的子節點(
TITLE
、
LINK
等)對應於
Message
實例的屬性。現在,您已經對提要有了一定的認識,並且已經創建了所有常用部分,接下來看看如何使用 Android
上可用的各種技術來解析這個提要。您將從 SAX
開始。
使用 SAX
在 Java
環境中,當您需要一個速度快的解析器並且希望最大限度減少應用程序的內存佔用時,通常可以使用 SAX API
。這非常適用於運行 Android
的移動設備。您可以在 Java
環境中照原樣使用 SAX API
,在 Android
上運行它不需要做任何修改。清單 5
顯示了
FeedParser
接口的一個 SAX
實現。
清單 5. SAX
實現
public class SaxFeedParser extends BaseFeedParser
{
protected SaxFeedParser(String feedUrl){
super(feedUrl);
}
public
List<Message> parse() {
SAXParserFactory factory = SAXParserFactory.newInstance();
try
{
SAXParser parser = factory.newSAXParser();
RssHandler handler = new RssHandler();
parser.parse(this.getInputStream(), handler);
return handler.getMessages();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
|
如果您以前使用過 SAX
,那麼這對您肯定非常熟悉。與任何 SAX
實現相同,大多數細節都在 SAX
處理程序中。在分解 XML
文檔時,處理程序從 SAX
解析器接收事件。在本例中,您創建了一個新的名稱爲
RssHandler
的類,並將它註冊爲解析器的處理程序,如 清單 6
所示。
清單 6. SAX
處理程序
import static
org.developerworks.android.BaseFeedParser.*;
public class RssHandler extends DefaultHandler{
private
List<Message> messages;
private
Message currentMessage;
private
StringBuilder builder;
public
List<Message> getMessages(){
return this.messages;
}
@Override
public
void characters(char[] ch, int start, int length)
throws SAXException {
super.characters(ch, start, length);
builder.append(ch, start, length);
}
@Override
public
void endElement(String uri, String localName, String name)
throws SAXException {
super.endElement(uri, localName, name);
if
(this.currentMessage != null){
if (localName.equalsIgnoreCase(TITLE)){
currentMessage.setTitle(builder.toString());
} else if
(localName.equalsIgnoreCase(LINK)){
currentMessage.setLink(builder.toString());
} else if (localName.equalsIgnoreCase(DESCRIPTION)){
currentMessage.setDescription(builder.toString());
} else if (localName.equalsIgnoreCase(PUB_DATE)){
currentMessage.setDate(builder.toString());
} else if (localName.equalsIgnoreCase(ITEM)){
messages.add(currentMessage);
}
builder.setLength(0);
}
}
@Override
public
void startDocument() throws SAXException {
super.startDocument();
messages = new ArrayList<Message>();
builder = new StringBuilder();
}
@Override
public
void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
super.startElement(uri, localName, name, attributes);
if
(localName.equalsIgnoreCase(ITEM)){
this.currentMessage = new Message();
}
}
}
|
RssHandler
類擴展了
org.xml.sax.helpers.DefaultHandler
類。該類爲 SAX
解析器生成的事件所對應的所有方法都提供了一個默認的非操作實現。這允許子類根據需要僅覆蓋一些方法。
RssHandler
提供了一個額外的 API
,即
getMessages
。它返回處理程序在從 SAX
解析器接收事件時所收集的
Message
對象列表。它有另外兩個內部變量,
currentMessage
針對被解析的
Message
實例,以及名稱爲
builder
的
StringBuilder
變量,用於存儲文本節點中的字符數據。解析器將相應事件發送給處理程序時會調用
startDocument
方法,這兩個變量的初始化操作就是在此時完成。
查看 清單 6
中的
startElement
方法。在 XML
文檔中每次遇到開始標記時都會調用它。您只關心該標記何時爲
ITEM
標記。對於這種情況,您將創建一個新的
Message
。現在來看
characters
方法。遇到文本節點中的字符數據時便會調用此方法。數據只是被添加到
builder
變量中。最後,我們來看
endElement
方法。遇到結束標記時會調用此方法。對於與某
Message
屬性相對應的標記,如
TITLE
和
LINK
,則使用
builder
變量中的數據在
currentMessage
上設置適當的屬性。如果結束標記是一個
ITEM
,則
currentMessage
將被添加到 Messages
列表中。所有這些都是非常典型的 SAX
解析;此處的一切都不是 Android
所特有的。因此,如果您知道如何編寫 Java SAX
解析器,則應該知道如何編寫 Android SAX
解析器。但是,Android SDK
確實在 SAX
上添加了一些便捷的特性。
更加簡單的 SAX
解析
Android SDK
提供了一個名稱爲
android.util.Xml
的實用類。清單 7
展示瞭如何使用這個相同的實用類來設置一個 SAX
解析器。
清單 7. Android
SAX
解析器
public class AndroidSaxFeedParser extends
BaseFeedParser {
public
AndroidSaxFeedParser(String feedUrl) {
super(feedUrl);
}
public
List<Message> parse() {
RssHandler handler = new RssHandler();
try {
Xml.parse(this.getInputStream(), Xml.Encoding.UTF_8, handler);
}
catch (Exception e) {
throw new RuntimeException(e);
}
return handler.getMessages();
}
}
|
注意,這個類仍然使用了一個標準的 SAX
處理程序,因此您僅僅重用了 清單 7
中所示的
RssHandler
。能夠重用 SAX
處理程序是非常不錯的,但其代碼稍微有些複雜。您可以想像,如果需要解析一個更加複雜的 XML
文檔,則處理程序可能會帶來各種各樣的 bug
。舉例來說,回頭看看
清單 6
中的
endElement
方法。注意,在嘗試設置屬性之前,它檢查了
currentMessage
是否爲 null
。現在,再回頭看看
清單 4
中的示例 XML
。 注意,
ITEM
標記外部有一些
TITLE
和
LINK
標記。這就是使用 null
檢查的原因。否則,每一個
TITLE
標記 會導致一個
NullPointerException
。Android
提供了自己獨有的 SAX API
(參見
清單 8
),
它排除了您編寫自己的 SAX
處理程序的需要。
清單 8.
經過簡化的 Android SAX
解析器
public class AndroidSaxFeedParser extends
BaseFeedParser {
public
AndroidSaxFeedParser(String feedUrl) {
super(feedUrl);
}
public
List<Message> parse() {
final Message currentMessage = new Message();
RootElement
root = new RootElement("rss");
final List<Message> messages = new ArrayList<Message>();
Element channel = root.getChild("channel");
Element item = channel.getChild(ITEM);
item.setEndElementListener(new EndElementListener(){
public void end() {
messages.add(currentMessage.copy());
}
});
item.getChild(TITLE).setEndTextElementListener(new
EndTextElementListener(){
public void end(String body) {
currentMessage.setTitle(body);
}
});
item.getChild(LINK).setEndTextElementListener(new
EndTextElementListener(){
public void end(String body) {
currentMessage.setLink(body);
}
});
item.getChild(DESCRIPTION).setEndTextElementListener(new
EndTextElementListener(){
public void end(String body) {
currentMessage.setDescription(body);
}
});
item.getChild(PUB_DATE).setEndTextElementListener(new
EndTextElementListener(){
public void end(String body) {
currentMessage.setDate(body);
}
});
try
{
Xml.parse(this.getInputStream(), Xml.Encoding.UTF_8,
root.getContentHandler());
}
catch (Exception e) {
throw new RuntimeException(e);
}
return messages;
}
}
|
新的 SAX
解析代碼並未使用 SAX
處理程序,而是使用了 SDK
中的 android.sax
包中的類。這些類允許您構建 XML
文檔的結構,並根據需要添加事件監聽程序。在以上代碼中,您聲明文檔將有一個
rss
根元素,並且它有一個
channel
子元素。然後,您聲明
channel
將有一個
ITEM
子元素,並且開始添加監聽程序。對於每個監聽程序,您都使用了一個實現了特定接口(
EndElementListner
或
EndTextElementListener
) 的匿名內部類。注意,您不需要跟蹤字符數據。不僅僅因爲這樣會更加簡單,更重要的是更加高效。最後,在調用 Xml.parse
實用方法時,您將傳遞一個通過根元素生成的處理程序。
清單 8
中的所有代碼都是可選的。如果您習慣 Java
環境中的標準
SAX
解析代碼,那麼您可以堅持使用它。如果您希望嘗試 Android SDK
所提供的便捷的包裝器,那麼也可以使用它。如果您完全不希望使用 SAX
會怎樣呢?可以使用一些備選方案。其中的首選方法就是 DOM
。
使用 DOM
Android
完全支持 DOM
解析,就像在桌面機器或服務器上使用 Java
代碼運行它一樣。清單 9
顯示了一個基於 DOM
的解析器接口實現。
清單 9.
基於 DOM
的提要解析器實現
public class DomFeedParser extends BaseFeedParser
{
protected DomFeedParser(String feedUrl) {
super(feedUrl);
}
public
List<Message> parse() {
DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
List<Message> messages = new ArrayList<Message>();
try
{
DocumentBuilder builder = factory.newDocumentBuilder();
Document dom = builder.parse(this.getInputStream());
Element root = dom.getDocumentElement();
NodeList items = root.getElementsByTagName(ITEM);
for (int i=0;i<items.getLength();i++){
Message message = new
Message();
Node item = items.item(i);
NodeList properties = item.getChildNodes();
for (int j=0;j<properties.getLength();j++){
Node property = properties.item(j);
String name =
property.getNodeName();
if (name.equalsIgnoreCase(TITLE)){
message.setTitle(property.getFirstChild().getNodeValue());
} else if (name.equalsIgnoreCase(LINK)){
message.setLink(property.getFirstChild().getNodeValue());
} else if (name.equalsIgnoreCase(DESCRIPTION)){
StringBuilder text
= new StringBuilder();
NodeList chars = property.getChildNodes();
for (int
k=0;k<chars.getLength();k++){
text.append(chars.item(k).getNodeValue());
}
message.setDescription(text.toString());
} else if
(name.equalsIgnoreCase(PUB_DATE)){
message.setDate(property.getFirstChild().getNodeValue());
}
}
messages.add(message);
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
return messages;
}
}
|
與第一個 SAX
示例類似,以上代碼完全沒有特定於 Android
的地方。DOM
解析器將所有 XML
文檔讀取到內存中,然後允許您使用 DOM API
遍歷 XML
樹、檢索所需的數據。這是非常直觀的代碼,並且,在某些方面比基於 SAX
的實現更加簡單。但是,DOM
通常更加佔用內存,因爲一切內容都會先讀取到內存中。這對於運行 Android
的移動設備來說是一個問題,但是當 XML
文檔始終保持很小的大小時是可行的。這可能意味着,Android
的開發人員會認爲 SAX
解析在 Android
應用程序上更加常見,因此爲它提供了額外的實用工具。Android
還提供了另一種類型的 XML
解析器,它就是 pull
解析器。
XML pull
解析器
如前所述,Android
並未提供對 Java StAX API
的支持。但是,Android
確實附帶了一個 pull
解析器,其工作方式類似於 StAX
。它允許您的應用程序代碼從解析器中獲取事件,這與 SAX
解析器自動將事件推入處理程序相反。清單 10
顯示了提要解析接口的一個 pull
解析器實現。
清單 10.
基於 Pull
解析器的實現
public class XmlPullFeedParser extends
BaseFeedParser {
public
XmlPullFeedParser(String feedUrl) {
super(feedUrl);
}
public
List<Message> parse() {
List<Message> messages = null;
XmlPullParser
parser = Xml.newPullParser();
try
{
// auto-detect the encoding from the stream
parser.setInput(this.getInputStream(), null);
int eventType = parser.getEventType();
Message currentMessage = null;
boolean done = false;
while (eventType != XmlPullParser.END_DOCUMENT && !done){
String name = null;
switch (eventType){
case XmlPullParser.START_DOCUMENT:
messages = new
ArrayList<Message>();
break;
case XmlPullParser.START_TAG:
name =
parser.getName();
if
(name.equalsIgnoreCase(ITEM)){
currentMessage = new Message();
} else if
(currentMessage != null){
if
(name.equalsIgnoreCase(LINK)){
currentMessage.setLink(parser.nextText());
} else if
(name.equalsIgnoreCase(DESCRIPTION)){
currentMessage.setDescription(parser.nextText());
} else if
(name.equalsIgnoreCase(PUB_DATE)){
currentMessage.setDate(parser.nextText());
} else if
(name.equalsIgnoreCase(TITLE)){
currentMessage.setTitle(parser.nextText());
}
}
break;
case XmlPullParser.END_TAG:
name =
parser.getName();
if
(name.equalsIgnoreCase(ITEM) &&
currentMessage != null){
messages.add(currentMessage);
} else if
(name.equalsIgnoreCase(CHANNEL)){
done = true;
}
break;
}
eventType = parser.next();
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
return messages;
}
}
|
pull
解析器的運行方式與 SAX
解析器相似。它提供了類似的事件(開始元素和結束元素),但您需要使用
(parser.next()
提取它們。事件將作爲數值代碼被髮送,因此您可以使用一個簡單 case-switch
。注意,解析並未像 SAX
解析那樣監聽元素的結束,而是在開始處完成了大部分處理。在
清單 10
的代碼中,當某個元素開始時,您可以調用
parser.nextText()
從 XML
文檔中提取所有字符數據。還需注意,您設置了一個標記(布爾變量
done
)來確定何時到達感興趣內容的結束部分。這允許您提 早停止讀取 XML
文檔,因爲您知道代碼將不會關心文檔的其餘部分。這有時非常實用,特別是當您只需要訪問一小部分 XML
文檔時。通過儘快停止解析,您可以極大地減少解析時間。這種優化對於連接速度較慢的移動設備尤爲重要。pull
解析器可以提供一些性能優勢以及易用性。它還可以用於編寫 XML
。
創建 XML
目前爲止,我一直專注於通過 Internet
解析 XML
。但是,有時您的應用程序可能需要將 XML
發送到遠程服務器。顯然,您可以只使用一個
StringBuilder
來創建 XML
字符串。另一種備選方法來自 清單 11
中的 Pull
解析器。
清單 11.
使用 pull
解析器編寫 XML
private String writeXml(List<Message>
messages){
XmlSerializer serializer = Xml.newSerializer();
StringWriter writer = new StringWriter();
try {
serializer.setOutput(writer);
serializer.startDocument("UTF-8", true);
serializer.startTag("", "messages");
serializer.attribute("", "number", String.valueOf(messages.size()));
for
(Message msg: messages){
serializer.startTag("", "message");
serializer.attribute("", "date", msg.getDate());
serializer.startTag("", "title");
serializer.text(msg.getTitle());
serializer.endTag("", "title");
serializer.startTag("", "url");
serializer.text(msg.getLink().toExternalForm());
serializer.endTag("", "url");
serializer.startTag("", "body");
serializer.text(msg.getDescription());
serializer.endTag("", "body");
serializer.endTag("", "message");
}
serializer.endTag("", "messages");
serializer.endDocument();
return writer.toString();
} catch
(Exception e) {
throw new RuntimeException(e);
}
}
|
XmlSerializer
類是 前一部分
所使用的
XmlPullParser
包的一部分。它沒有提取事件,而是將它們推出到數據流或編寫程序中。在本例中,它僅僅將事件推送到了一個
java.io.StringWriter
實例中。它提供了一個直觀的
API
,通過各種方法開始和結束文檔、處理元素以及添加文本或屬性。這是
StringBuilder
的一種出色的替換方案,因爲它可以更加輕鬆地確保您的 XML
具有良好結構。
結束語
您希望爲 Android
設備構建何種類型的應用程序?無論如何,如果它需要通過
Internet
使用數據,那麼都可能需要使用 XML
。在本文中,您看到 Android
提供了大量用於處理 XML
的工具。您可以選擇其中之一作爲自己的工具,或者您可以根據用例來進行選擇。大多數時間,使用 SAX
是比較安全的,並且 Android
提供了一種傳統的 SAX
使用方法,以及一個便捷的 SAX
包裝器。如果您的文檔比較小,那麼 DOM
可能是一種比較簡單的方法。如果您的文檔比較大,但您只需要文檔的一部分,則
XML Pull
解析器可能是更爲有效的方法。最後,對於編寫 XML
,Pull
解析器包也提供了一種便捷的方法。因此,無論您的 XML
需求如何,Android
都能在一定程度上滿足它們。
|