Android应用程序通常必须访问驻留在Internet上的数据,并且Internet数据可以以几种不同的格式进行结构化。 在本文中,您将看到如何在Android应用程序中使用三种数据格式:
- XML格式
- JSON格式
- Google的协议缓冲区
首先,您将开发一个Web服务,该服务将CSV数据转换为XML,JSON和协议缓冲区格式。 然后,您将构建一个示例Android应用程序,该应用程序可以采用任何一种格式从Web服务中提取数据,并将其解析以显示给用户。
要执行本文中的练习,您需要最新的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让用户输入一个或多个股票代号,并检索其所代表股票的最新定价信息。 用户可以指定用于数据的格式:XML,JSON或协议缓冲区。 实际的Android应用程序通常不会提供这种选择,但是通过实现它,您将了解如何使您的应用程序使用每种格式。 图1显示了Day Trader用户界面:
图1.运行中的Day Trader应用程序
文本框和旁边的添加库存按钮使用户可以输入每种感兴趣库存的代码。 当用户按下“ 下载股票数据”按钮时,将从服务器请求所有这些股票的数据,在应用程序中对其进行解析并显示在屏幕上。 默认情况下,数据以XML格式检索。 通过菜单,您可以在XML,JSON或协议缓冲区之间切换数据格式。
清单1. Day Trader布局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中的大多数代码都很简单。 您会看到几个小部件来创建输入和按钮, 如图1所示。 您还将看到ListView
,这是名副其实的Android小部件的瑞士军刀。 将使用从服务器下载的库存数据填充此ListView
。 清单2显示了控制此视图的Activity
:
清单2.日交易者的主要活动
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文件,并连接了两个事件处理程序。 首先,对于“ 添加股票”按钮,代码从文本框中读取符号并将其添加到symList TextView
,并用逗号分隔每个符号。 接下来,对于“ 下载”按钮,处理程序从该symList TextView
读取数据,然后基于mode
变量,使用三种不同类之一从服务器下载数据。 菜单设置mode
变量的值。 这是相当琐碎的代码,因此在清单2中已将其省略。 在查看各种数据下载/解析类之前,我将向您展示服务器如何提供这些数据。
服务库存数据
您的应用程序的服务器需要能够做两件事。 首先,它必须获取一个股票代码列表,并为每个代码检索数据。 接下来,它需要接受格式参数,并根据该格式对数据进行编码。 对于XML和JSON格式,服务器将返回序列化为文本的股票数据。 对于协议缓冲区,它必须发送二进制数据。 清单3显示了处理这些步骤的servlet:
清单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;
}
}
这是一个仅支持HTTP GET
请求的简单Java servlet。 它读入stock和format-request参数的值。 然后,它调用getStocks()
方法。 此方法将调用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 ,根据format-request参数,将Stock
对象列表转换为XML或JSON字符串,然后发送回客户端。
XML和JSON用例非常相似和直接。 对于协议缓冲区,您必须以协议缓冲区格式生成代码读取和写入对象。 为此,您需要使用协议缓冲区规范格式定义数据结构。 清单5显示了一个示例:
清单5.协议为股票缓冲消息
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;
}
协议缓冲消息格式类似于接口描述语言(IDL),旨在与语言无关,因此您可以将其与各种语言一起使用。 在这种情况下,您将运行协议缓冲区编译器( protoc
)将清单5中的代码编译为将用于客户端和服务器的Java类。 有关将协议缓冲区消息编译为Java类的详细信息,请参阅《协议缓冲区开发人员指南》(请参阅参考资料 )。
在清单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中的代码使用了从Quote
5中的消息生成的代码( Quote
和Portfolio
类)。 您只需从每个Stock
对象构建一个Quote
,然后将其添加到Portfolio
对象,该对象返回清单3中的servlet。 在清单3中 ,该servlet直接向客户端打开一个流,并使用生成的代码将二进制协议缓冲区数据写入该流。
现在,您已经了解了服务器如何创建将发送到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解析器开始向您展示更具体的解析代码。
使用SAX解析XML
Android SDK提供了几种使用XML的方式,包括标准DOM和SAX。 对于某些占用更多内存的情况,可以使用SDK的请求解析器。 大多数情况下,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解析器触发的各种事件做出React来创建解析的数据。 newHandler()
方法使用Android便利性API通过事件处理程序指定ContentHandler
。 该代码仅侦听解析器遇到各种标签时触发的事件,然后挑选数据放入Stock
对象列表中。 创建ContentHandler
,将调用Xml.parse()
方法来解析基类提供的InputStream
并返回Stock
对象数组。 这是解析XML的快速方法,但即使使用Android提供的便捷API,它仍然相当冗长。
使用JSON
XML是Android上的一等公民,考虑到有多少Web服务依赖XML,这是一件好事。 许多服务还支持另一种流行的格式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
对象的数组。 换句话说,完全相同的数据以三种不同的方式表示。 您可以看到这可能对大量数据造成问题。 当然,一旦到达方法的最后,这三个数据表示形式中的两个将超出范围,可以由垃圾收集器回收。 但是,仅触发更频繁的垃圾回收会导致不稳定的运行速度,从而对用户体验产生负面影响。 如果内存效率和性能非常重要,则使用协议缓冲区的解析器可能是更好的选择。
使用协议缓冲区进行二进制处理
协议缓冲区是Google开发的与语言无关的数据序列化格式,旨在比XML更快地通过网络发送数据。 对于任何服务器到服务器的调用,这都是Google的事实上的标准。 Google公开了C ++,Java和Python编程语言的格式及其绑定工具。
在清单3和清单6中 ,您可以看到协议缓冲区是一种二进制格式。 如您所料,这使数据非常紧凑。 如果可以同时在客户端和服务器上启用gzip压缩,则通常可以通过XML和JSON获得类似的消息大小,但是协议缓冲区仍具有一些大小上的优势。 它也是一种可以很快解析的格式。 最后,它提供了一个相当简单的API。 清单10显示了一个示例解析器实现:
清单10.协议缓冲区解析器的实现
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中一样 ,您使用一个助手类,在这种情况下,它由协议缓冲区编译器生成。 这是服务器使用的相同的帮助程序类。 您可以编译一次并在服务器和客户端上共享它。 因此,您可以更轻松地直接从服务器的流中读取数据并将其转换为Stock
对象数组。 这是简单的编程,同时碰巧也具有出色的性能特征。 现在看一下这种性能如何与XML和JSON相提并论。
性能比较
性能比较通常涉及某种微基准,而众所周知,此类基准很容易以某种无意的方式产生偏差或变得不正确。 即使以合理的方式设计微基准测试,许多随机因素也会对任何结果产生怀疑。 尽管有这些警告,但我只是使用了一个微基准来比较XML(大约1300毫秒),JSON(大约1150毫秒)和协议缓冲区(大约750毫秒)的性能。 基准测试向服务器发送了200只股票的请求,并测量了从发出请求到准备好用于创建ListView
的Adapter
的数据所需的时间。 在两种不同的设备上,每种数据格式都通过3G网络完成了50次:摩托罗拉Droid和HTC Evo。 图2显示了结果:
图2.比较数据格式速度
图2中的图形显示,此微基准测试的协议缓冲区(大约750毫秒)几乎快于XML(大约1300毫秒)的两倍。 许多因素都会影响通过网络传输并由手持设备处理的数据的性能。 一个明显的因素是通过网络传输的数据量。 实际上,协议缓冲区是二进制格式,在网络上比XML和JSON等文本格式小得多。 但是,可以使用gzip有效地压缩文本格式,后者是Web服务器和Android设备都支持的标准技术。 图3显示了打开和关闭gzip时在线传输的数据大小:
图3.按格式的数据大小
图3应该使您更了解压缩对诸如XML和JSON(更不用说Web格式,HTML,JavaScript和CSS)之类的文本内容的影响。 协议缓冲区的数据(大约6KB)比原始XML(大约17.5KB)或JSON(大约13.5KB)小得多。 但是一旦开始压缩,JSON和XML(都大约3KB)实际上小于协议缓冲区。 在此示例中,它们接近协议缓冲区编码消息的大小的一半。
回到图2 ,速度的差异当然不能通过网络上的消息大小来解释。 协议缓冲区消息比XML或JSON编码消息大,但是通过使用协议缓冲区,您仍然可以节省用户等待查看数据的半秒时间。 这是否意味着您应该为Android应用程序使用协议缓冲区? 当然,这样的决定很少被割断和干掉。 如果发送的数据量很小,则三种格式之间的差异很小。 对于大量数据,也许协议缓冲区会有所作为。 但是,像这样的人为基准无法替代测试您自己的应用程序。
结论
在本文中,您探讨了使用两种流行的Internet上使用的数据格式XML和JSON的来龙去脉。 您还看到了第三种可能性,协议缓冲区。 像软件工程中的所有其他内容一样,选择一种技术就是权衡取舍。 当您针对诸如Android之类的受限环境进行开发时,这些决策的后果通常会被放大。 我希望您现在对这些后果有更多的了解,将有助于您创建出色的Android应用程序。
翻译自: https://www.ibm.com/developerworks/opensource/library/x-dataAndroid/index.html