Android 应用程序必须访问位于 Internet 上的数据,而 Internet 数据可以有几种不同的格式。本文将介绍在 Android 应用程序中如何使用三种数据格式:
XML
JSON 首先开发一个 Web 服务,将 CSV 数据转换成 XML、JSON 和 protocol-buffers 格式。然后构建一个样例 Android 应用程序,可以从 Web 服务中以任何一种格式提取数据并将其解析并显示给用户。
Google 的 protocol buffers
要进行本文中的练习,您需要最新的 Android SDK(参见 参考资料)和 Android 2.2 平台。SDK 还要求您安装一个 Java™ 开发包(JDK);本文中使用了 JDK 1.6.0_17。您不需要有 Android 物理设备;所有代码都将在 SDK 的 Android 仿真器中运行。本文并没有教您如何进行 Android 开发,因此建议您熟悉 Android 编程。当然,只凭借 Java 编程语言的知识也可以完成本文的学习。
您还需要一个 Java web 应用程序服务器来运行 Android 应用程序使用的 Web 服务。此外,也可以将服务器端代码部署到 Google App Engine。参见 下载 部分获得完整的源代码。
Day Trader 应用程序
您将开发一个简单的 Android 应用程序,叫做 Day Trader。Day Trader 允许用户输入一个或更多的股票代码并获取其所代表股票的最新价格信息。用户可以指定数据使用的格式:XML、JSON 或 protocol buffers。实际的 Android 应用程序通常不会提供此选择,但是通过实现此功能,您可以了解如何让您的应用程序处理每一种格式。图 1 展示了 Day Trader 用户界面:
图 1. 运行中的 Day Trader 应用程序
文本框及其旁边的 Add Stock 按钮允许用户输入感兴趣的每支股票的代码。用户按下 Download Stock Data 按钮后,会从服务器请求所有这些股票的数据,在应用程序中解析并显示在屏幕上。默认情况下,获取的是 XML 数据。通过菜单,您可以在 XML、JSON 或 protocol buffers 数据格式间切换。
清单1显示用于创建 图 1 中所示 UI 的布局 XML:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- >
- <LinearLayout android:orientation="horizontal"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content">
- <EditText android:id="@+id/symbol" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:width="120dip"/>
- <Button android:id="@+id/addBtn" android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/addBtnLbl"/>
- </LinearLayout>
- <LinearLayout android:orientation="horizontal"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content">
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:id="@+id/symList" />
- <Button android:id="@+id/dlBtn" android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/dlBtnLbl"
- />
- </LinearLayout>
- <ListView android:id="@android:id/list"
- android:layout_height="fill_parent" android:layout_width="fill_parent"
- android:layout_weight="1"
- />
- </LinearLayout>
上述中的大部分代码都简单明了。可以看到几个小部件创建了 图 1 所示的输入和按钮。还会看到一个 ListView,Android 小部件中真正的瑞士军刀。此 ListView 将用从服务器下载的股票数据填充。清单 2 显示了控制该视图的 Activity:
清单 2. Day Trader 主活动
- public class Main extends ListActivity {
- private int mode = XML; // default
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- final EditText input = (EditText) findViewById(R.id.symbol);
- final TextView symbolsList = (TextView) findViewById(R.id.symList);
- final Button addButton = (Button) findViewById(R.id.addBtn);
- final Button dlButton = (Button) findViewById(R.id.dlBtn);
- addButton.setOnClickListener(new OnClickListener(){
- public void onClick(View v) {
- String newSymbol = input.getText().toString();
- if (symbolsList.getText() == null ||
- symbolsList.getText().length() == 0){
- symbolsList.setText(newSymbol);
- } else {
- StringBuilder sb =
- new StringBuilder(symbolsList.getText());
- sb.append(",");
- sb.append(newSymbol);
- symbolsList.setText(sb.toString());
- }
- input.setText("");
- }
- });
- dlButton.setOnClickListener(new OnClickListener(){
- public void onClick(View v) {
- String symList = symbolsList.getText().toString();
- String[] symbols = symList.split(",");
- symbolsList.setText("");
- switch (mode){
- case JSON :
- new StockJsonParser().execute(symbols);
- break;
- case PROTOBUF :
- new StockProtoBufParser().execute(symbols);
- break;
- default :
- new StockXmlParser().execute(symbols);
- break;
- }
- }
- }
- }
- }
此 Activity 设置了 清单 1 中 XML 文件的布局,它将几个事件处理程序连接起来。首先,对于 Add Stock 按钮而言,代码读取文本框中的代码并将其添加到 symList TextView 中,用逗号分隔每个代码。接下来,对于 Download 按钮而言,处理程序从 symList TextView 中读取数据,然后 —基于 mode 变量— 使用三个不同的类之一从服务器下载数据。菜单设置 mode 变量的值;这个代码不是很重要,因此我在 清单 2 中省略了它。在了解各种数据下载/解析类之前,我先为您展示一下服务器如何提供此数据。提供股票数据
应用程序服务器需要能够做两件事。第一,它必须获取股票代码列表并检索它们的数据。然后,它需要接受一个格式参数并基于该格式编码数据。对于 XML 和 JSON 格式而言,该服务器将返回作为文本的串行化的股票数据。对于 protocol buffers 而言,它必须发送二进制数据。 清单 3 显示了处理这些步骤的 servlet:
#p#
清单 3. Stock Broker servlet
- public class StockBrokerServlet extends HttpServlet {
- public void doGet(HttpServletRequest request,
- HttpServletResponse response) throws IOException {
- String[] symbols = request.getParameterValues("stock");
- List<Stock> stocks = getStocks(symbols);
- String format = request.getParameter("format");
- String data = "";
- if (format == null || format.equalsIgnoreCase("xml")){
- data = Stock.toXml(stocks);
- response.setContentType("text/xml");
- } else if (format.equalsIgnoreCase("json")){
- data = Stock.toJson(stocks);
- response.setContentType("application/json");
- } else if (format.equalsIgnoreCase("protobuf")){
- Portfolio p = Stock.toProtoBuf(stocks);
- response.setContentType("application/octet-stream");
- response.setContentLength(p.getSerializedSize());
- p.writeTo(response.getOutputStream());
- response.flushBuffer();
- return;
- }
- response.setContentLength(data.length());
- response.getWriter().print(data);
- response.flushBuffer();
- response.getWriter().close();
- }
- public List<Stock> getStocks(String... symbols) throws IOException{
- StringBuilder sb = new StringBuilder();
- for (String symbol : symbols){
- sb.append(symbol);
- sb.append('+');
- }
- sb.deleteCharAt(sb.length() - 1);
- String urlStr =
- "http://finance.yahoo.com/d/quotes.csv?f=sb2n&s=" +
- sb.toString();
- URL url = new URL(urlStr);
- HttpURLConnection conn =
- (HttpURLConnection) url.openConnection();
- BufferedReader reader = new BufferedReader(
- new InputStreamReader(conn.getInputStream()));
- String quote = reader.readLine();
- List<Stock> stocks = new ArrayList<Stock>(symbols.length);
- while (quote != null){
- String[] values = quote.split(",");
- Stock s =
- new Stock(values[0], values[2],
- Double.parseDouble(values[1]));
- stocks.add(s);
- quote = reader.readLine();
- }
- return stocks;
- }
- }
这是一个简单的 Java servlet,只支持 HTTP GET 请求。它读入股票的值和格式请求参数。然后调用 getStocks() 方法。该方法调用 Yahoo! Finance 获取股票数据。Yahoo! 只支持 CSV 格式的数据,因此 getStocks() 方法将其解析到一个 Stock 对象列表。清单 4 展示了这个简单的数据结构:
清单 4. 股票数据结构
- public class Stock {
- private final String symbol;
- private final String name;
- private final double price;
- //getters and setters omitted
- public String toXml(){
- return "<stock><symbol>" + symbol +
- "</symbol><name><![CDATA[" +
- name + "]]></name><price>" + price +
- "</price></stock>";
- }
- public String toJson(){
- return "{ 'stock' : { 'symbol' : " +symbol +", 'name':" + name +
- ", 'price': '" + price + "'}}";
- }
- public static String toXml(List<Stock> stocks){
- StringBuilder xml = new StringBuilder("<stocks>");
- for (Stock s : stocks){
- xml.append(s.toXml());
- }
- xml.append("</stocks>");
- return xml.toString();
- }
- public static String toJson(List<Stock> stocks){
- StringBuilder json = new StringBuilder("{'stocks' : [");
- for (Stock s : stocks){
- json.append(s.toJson());
- json.append(',');
- }
- json.deleteCharAt(json.length() - 1);
- json.append("]}");
- return json.toString();
- }
- }
每个 Stock 都有三个属性— symbol、name 和 price — 和几个便捷的方法,以便将其自己转换成 XML 字符串或 JSON 字符串。它提供了一个工具方法,用于将 Stock 对象列表转换成 XML 或 JSON。回到 清单 3,根据格式请求参数,Stock 对象列表被转换成 XML 或 JSON 字符串并被发送回客户端。
XML 和 JSON 用例非常类似和直接。对于 protocol buffers,您必须生成 protocol buffers 格式的代码读写对象。为此,您需要使用 protocol buffers 规范格式定义数据结构。清单 5 展示了一个示例:
#p#
清单 5. 股票的 Protocol buffers 消息
- package stocks;
- option java_package = "org.developerworks.stocks";
- message Quote{
- required string symbol = 1;
- required string name = 2;
- required double price = 3;
- }
- message Portfolio{
- repeated Quote quote = 1;
- }
protocol buffers 消息格式类似于接口描述语言 (IDL),它与语言无关,因此可以将其与各种语言一起使用。在本例中,运行 protocol buffers 编译器(protoc)将 清单 5 中的代码编译成要用于客户端和服务器的 Java 类。有关将 protocol buffers 消息编译成 Java 类的详细信息,请参阅 Protocol Buffers Developer Guide(参见 参考资料)。
在 清单 3 中,一个名为 toProtoBuf() 的方法将 Stock 对象列表转换成一个 Portfolio 消息。清单 6 展示了该方法的实现:
清单 6. 创建组合消息
- public static Stocks.Portfolio toProtoBuf(List<Stock> stocks){
- List<Stocks.Quote> quotes = new ArrayList<Stocks.Quote>(stocks.size());
- for (Stock s : stocks){
- Quote q =
- Quote.newBuilder()
- .setName(s.name)
- .setSymbol(s.symbol)
- .setPrice(s.price)
- .build();
- quotes.add(q);
- }
- return Portfolio.newBuilder().addAllQuote(quotes).build();
- }
清单 6 中的代码使用了从 清单 5 中的消息生成的代码 — Quote 和 Portfolio 类。只需构建来自每个 Stock 对象的 Quote,然后将其添加到 清单 3 中返回到 servlet 的 Portfolio 对象即可。在 清单 3 中,servlet 直接打开到客户端的流并使用生成的代码编写到流的二进制协议 buffers 数据。
现在,您了解了服务器如何创建要发送到 Android 应用程序的数据。接下来将学习应用程序如何解析此数据。
清单 2 中的主 Activity 需要使用服务器可以发送的各种格式的数据。它还需要请求适当格式的数据并且数据一旦解析,就用它来填充其 ListView。因此,无论数据格式是什么,大部分功能都是通用的。
首先,创建一个抽象的基类,封装此通用功能,如 清单 7 所示:
清单 7. 数据解析器基类
- abstract class BaseStockParser extends AsyncTask<String, Integer, Stock[]>{
- String urlStr = "http://protostocks.appspot.com/stockbroker?format=";
- protected BaseStockParser(String format){
- urlStr += format;
- }
- private String makeUrlString(String... symbols) {
- StringBuilder sb = new StringBuilder(urlStr);
- for (int i=0;i<symbols.length;i++){
- sb.append("&stock=");
- sb.append(symbols[i]);
- }
- return sb.toString();
- }
- protected InputStream getData(String[] symbols) throws Exception{
- HttpClient client = new DefaultHttpClient();
- HttpGet request = new HttpGet(new URI(makeUrlString(symbols)));
- HttpResponse response = client.execute(request);
- return response.getEntity().getContent();
- }
- @Override
- protected void onPostExecute(Stock[] stocks){
- ArrayAdapter<Stock> adapter =
- new ArrayAdapter<Stock>(Main.this, R.layout.stock,
- stocks );
- setListAdapter(adapter);
- }
- }
清单 7 中的基类扩展了 android.os.AsyncTask。这是一个常用的用于异步操作的类。它抽象出线程和处理程序的创建,用于请求主 UI 线程。它是基于其输入和输出数据类型参数化的。对于所有解析器而言,输入总是一样的:股票代码字符串。 输出也是一样的:Stock 对象数组。基类获取 format,这是一个指定了要使用的数据格式的字符串。然后提供一个方法,发出适当的 HTTP 请求并返回一个流响应。最后,它覆盖 AsyncTask 的 onPostExecute() 方法并使用从解析器返回的数据为 Activity 的 ListView 创建一个 Adapter。
现在看到三个解析器的功能是通用的。我将为您展示更具体的解析代码,从 XML 解析器开始。
#p#
用 SAX 解析 XML
Android SDK 提供了几种使用 XML 的方式,包括标准 DOM 和 SAX。 对于一些对内存密集型情况,可以使用 SDK 的 pull-parser。大部分时候,SAX 是最快的方式。Android 包括一些便捷的 API 使得使用 SAX 更轻松。清单 8 显示了 Day Trader 应用程序的 XML 解析器
清单 8. XML 解析器实现
- private class StockXmlParser extends BaseStockParser{
- public StockXmlParser(){
- super("xml");
- }
- @Override
- protected Stock[] doInBackground(String... symbols) {
- ArrayList<Stock> stocks = new ArrayList<Stock>(symbols.length);
- try{
- ContentHandler handler = newHandler(stocks);
- Xml.parse(getData(symbols), Xml.Encoding.UTF_8, handler);
- } catch (Exception e){
- Log.e("DayTrader", "Exception getting XML data", e);
- }
- Stock[] array = new Stock[symbols.length];
- return stocks.toArray(array);
- }
- private ContentHandler newHandler(final ArrayList<Stock> stocks){
- RootElement root = new RootElement("stocks");
- Element stock = root.getChild("stock");
- final Stock currentStock = new Stock();
- stock.setEndElementListener(
- new EndElementListener(){
- public void end() {
- stocks.add((Stock) currentStock.clone());
- }
- }
- );
- stock.getChild("name").setEndTextElementListener(
- new EndTextElementListener(){
- public void end(String body) {
- currentStock.setName(body);
- }
- }
- );
- stock.getChild("symbol").setEndTextElementListener(
- new EndTextElementListener(){
- public void end(String body) {
- currentStock.setSymbol(body);
- }
- }
- );
- stock.getChild("price").setEndTextElementListener(
- new EndTextElementListener(){
- public void end(String body) {
- currentStock.setPrice(Double.parseDouble(body));
- }
- }
- );
- return root.getContentHandler();
- }
- }
清单 8 中的大部分代码都在 newHandler() 方法中,该方法创建一个 ContentHandler。如果熟悉 SAX 解析, 会知道 ContentHandler 通过响应 SAX 解析器触发的各种事件创建解析数据。newHandler() 方法使用 Android 便捷 API 指定使用事件处理程序的 ContentHandler。代码只是侦听在解析器遇到各种标记时触发的事件,然后选取数据,放到 Stock 对象列表中。 创建 ContentHandler 后,调用 Xml.parse() 方法来解析基类提供的 InputStream 并返回 Stock 对象数组。这是快速解析 XML 的方法,但是 —即使使用 Android 提供的便捷 API— 它也是非常冗长的。
使用 JSON
XML 是 Android 上的一等公民,鉴于依赖于 XML 的 Web 服务的数量,这是个好事。很多服务还支持另一个流行格式 JSON。它通常比 XML 简洁一些,但也是人们可读的,使得它更易于使用,并且可以更轻松地将其用于调试使用它的应用程序。Android 包括一个 JSON 解析器。(您可以从 JSON.org 网站获得该解析器,只是要去除几个手机不需要的类)。 清单 9 显示了使用中的解析器:
清单 9. JSON 解析器实现
- private class StockJsonParser extends BaseStockParser{
- public StockJsonParser(){
- super("json");
- }
- @Override
- protected Stock[] doInBackground(String... symbols) {
- Stock[] stocks = new Stock[symbols.length];
- try{
- StringBuilder json = new StringBuilder();
- BufferedReader reader =
- new BufferedReader(
- new InputStreamReader(getData(symbols)));
- String line = reader.readLine();
- while (line != null){
- json.append(line);
- line = reader.readLine();
- }
- JSONObject jsonObj = new JSONObject(json.toString());
- JSONArray stockArray = jsonObj.getJSONArray("stocks");
- for (int i=0;i<stocks.length;i++){
- JSONObject object =
- stockArray.getJSONObject(i).getJSONObject("stock");
- stocks[i] = new Stock(object.getString("symbol"),
- object.getString("name"),
- object.getDouble("price"));
- }
- } catch (Exception e){
- Log.e("DayTrader", "Exception getting JSON data", e);
- }
- return stocks;
- }
- }
可以看到在 Android 中使用 JSON 解析器是多么简单。您将来自服务器的流转换成传递给 JSON 解析器的字符串。您遍历对象图并创建 Stock 对象数组。如果使用过 XML DOM 解析,这看起来很类似,因为编程模型几乎一样。
像 DOM 一样,JSON 解析器可以用于内存密集型应用。在 清单 9 中,所有来自服务器的数据都表示为字符串,然后作为 JSONObject,最后作为 Stock 对象数组。换句话说,同一数据通过三种不同的方式表示。可以看到,对于大量数据而言,这可能是个问题。当然,一旦到达方法末尾,这三种数据表示方式中的两种都会落在范围之外,被垃圾回收器回收。但是,只是触发更频繁的垃圾回收可能会对用户体验带来负面影响,造成处理速度下降。如果内存效率和性能很重要,使用 protocol buffers 的解析器可能是个较好的选择。使用 protocol buffers 处理二进制
Protocol buffers 是一个由 Google 开发的与语言无关的数据串行化格式,旨在比 XML 更快地通过网络传送数据。它是 Google 用于服务器对服务器调用的事实 标准。Google 将该格式及其用于 C++、Java 和 Python 编程语言的绑定工具以开源方式提供。
在 清单 3 和 清单 6 中看到 protocol buffers 是二进制格式。如您所料,这使得数据很简洁。如果在客户端和服务器端启用 gzip 压缩,在使用 XML 和 JSON 时通常也可以得到类似的消息大小,但是 protocol buffers 仍然有一些大小上的优势。它还是一种可以迅速解析的格式。最后,它提供了一个相当简单的 API。 清单 10 显示了一个示例解析器实现:
清单 10. Protocol buffers 解析器实现
- private class StockProtoBufParser extends BaseStockParser{
- public StockProtoBufParser(){
- super("protobuf");
- }
- @Override
- protected Stock[] doInBackground(String... symbols) {
- Stock[] stocks = new Stock[symbols.length];
- try{
- Stocks.Portfolio portfolio =
- Stocks.Portfolio.parseFrom(getData(symbols));
- for (int i=0;i<symbols.length;i++){
- stocks[i] = Stock.fromQuote(portfolio.getQuote(i));
- }
- } catch (Exception e){
- Log.e("DayTrader", "Exception getting ProtocolBuffer data", e);
- }
- return stocks;
- }
- }
如 清单 3 所示,您可以使用 protocol buffers 编译器生成的 helper 类。这与服务器使用的 helper 类相同。可以编译它一次,然后在服务器和客户端共享它。 这样,您可以更轻松地直接从服务器的流读取数据并将其转换成 Stock 对象数组。这种简单编程也具有非常出色的性能。现在看一下此性能与 XML 和 JSON 的比较。
比较性能通常涉及某种微基准测试,此类基准测试很容易产生偏见或无意间得到不正确的结果。即使以公平方式设计微基准测试,很多随机因素也会对结果产生影响。尽管有这些问题,我还是要使用这样的微基准测试来比较 XML(大约 1300 ms)、JSON(大约 1150 ms)和 protocol buffers(大约 750 ms)。基准测试向服务器发送了一个关于 200 个股票的请求并测量了从发出请求到用于创建 ListView 的 Adapter 的数据准备就绪所需的时间量。对每个数据格式在两个设备上进行 50 次这样的操作:一个 Motorola Droid 和一个 HTC Evo,两个都通过 3G 网络。 图 2 显示了结果:
图 2. 比较数据格式速度
图 2 显示出,在此基准测试中 protocol buffers(大约 750 ms)比 XML (大约 1300 ms)几乎快两倍。很多因素影响着数据通过网络和被手持设备处理的性能。一个明显的因素是通过网络的数据量。二进制格式的 protocol buffers 比文本格式的 XML 和 JSON 在通过网络时小得多。然而,文本格式可以使用 gzip 进行有效地压缩,这是 Web 服务器和 Android 设备都支持的标准技术。图 3 显示了在打开和关闭 gzip 时通过网络的数据大小:
图 3. 不同格式的数据大小
图 3 应该增加了您对 XML 和 JSON 之类的文本内容的压缩效果的喜爱(更不用说 Web 格式、HTML、JavaScript 和 CSS 了)。protocol buffers 数据(大约 6KB)比原始 XML(大约 17.5KB)或 JSON(大约 13.5KB)数据小得多。但是一旦进行了压缩, JSON 和 XML(都是大约 3KB)实际上比 protocol buffers 小很多了。在本例中,它们都接近于 protocol-buffers 编码消息大小的一半了。
回到 图 2,速度的不同显然不能由通过网络的消息大小解释。protocol-buffers 消息比 XML 或 JSON 编码的消息大,但是通过使用 protocol buffers,您仍然能够削减半秒钟的用户等待时间。这是否意味着应该在 Android 应用程序中使用 protocol buffers 呢?这样的决定很少是固定的。如果要发送的数据量很小,则三种格式间的差异也不大。对于大量数据而言,protocol buffers 可能会有所不同。但是,像这样精心设计的基准测试无法替代对您自己的应用程序的测试。
结束语
本文介绍了如何使用 Internet 上流行的两种数据格式 XML 和 JSON 的方方面面。还讲到了第三种可能性,protocol buffers。像软件工程中的其他内容一样,选择技术主要就是权衡利弊。当您为一个局限的环境(比如 Android)开发时,这些决定的结果往往被放大了。我希望您现在拥有的关于这些后果的额外知识能够帮助您创建出色的 Android 应用程序。
【编辑推荐】