- SAX解析方式
- 解析JSON格式数据
- 使用JSONObject
- 使用GSON
- 网络编程的最佳实践
9.4 SAX解析方式
<apps>
<app>
<id>1</id>
<name>Google Maps</name>
<version>1.0</version>
</app>
<app>
<id>2</id>
<name>Chrome</name>
<version>2.1</version>
</app>
<app>
<id>3</id>
<name>Google Play</name>
<version>2.3</version>
</app>
</apps>
通常情况下我们都会新建一个类继承自DefaultHandler
,并重写父类的5个方法。
public class ContentHandler extends DefaultHandler {
private String nodeName;
private StringBuilder id;
private StringBuilder name;
private StringBuilder version;
private static final String TAG = "ContentHandler";
// 文档开始解析时调用,该方法只会调用一次
@Override
public void startDocument() throws SAXException {
id = new StringBuilder();
name = new StringBuilder();
version = new StringBuilder();
}
// 当读取到第一个元素时开始做什么
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
//记录当前节点名
nodeName = localName;
}
// 表示读取字符串时做什么
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
//根据当前的节点名判断将内容添加到哪一个StrignBuilder对象中
if ("id".equals(nodeName))
{
id.append(ch,start,length);
}
else if ("name".equals(nodeName))
{
name.append(ch,start,length);
}
else if ("version".equals(nodeName))
{
version.append(ch, start, length);
}
}
// 表示读取到元素结尾
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if ("app".equals(localName)) {
Log.d(TAG, "id is : "+id.toString().trim());
Log.d(TAG, "name is : "+name.toString().trim());
Log.d(TAG, "version is : "+version.toString().trim());
//最后要将StringBuilder清空掉
id.setLength(0);
name.setLength(0);
version.setLength(0);
}
}
// 表示读取到文档结尾
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}
startDocument()
方法会在开始XML
解析的时候调用。
startElement()
方法会在开始解析某个节点的时候调用。
character()
方法会在获取节点中内容的时候调用。
endElement()
方法会在完成解析某个节点的时候调用。
endDocument()
方法会在完成整个XML解析的时候调用。
其中startElement()
,characters()
和endElement()
这三个方法是有参数的,从XML
中解析出来的数据就会以参数的形式传入到这些方法中。
需要注意的是,在获取节点中的内容时,characters()
方法可能会被调用多次,一些换行符也被当作内容解析出来,我们需要针对这种情况在代码中做好控制。
我们首先给id
,name
和version
节点分别定义了一个StringBuilder
对象,并在startDocument()
方法中对它们进行初始化。每当开始解析某个节点的时候,startElement()
方法就会得到调用,其中localName
参数记录着当前节点的名字,这里我们把它记录下来。
接着在解析节点中具体内容的时候就会调用characters()
方法,我们会根据当前的节点名进行判断,将解析出的内容添加到哪一个StringBuilder
对象中。
最后在endElement()
方法中进行判断,如果app
节点已经解析完成,就打印出id
,name
和version
的内容。需要注意的是,目前id
,name
和version
中都可能是包括回车或换行符的,因此在打印之前我们还需要调用一下trim()
方法,并且打印完成后还要将StringBuilder
的内容清空掉,不然的话会影响下一次内容的读取。
private void parseXMlWithSAX(String xmlData) {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
XMLReader xmlReader = factory.newSAXParser().getXMLReader();
ContentHandler handler = new ContentHandler();
//将ContentHandler的实例设置到XMLReader中
xmlReader.setContentHandler(handler);
//开始执行解析
xmlReader.parse(new InputSource(new StringReader(xmlData)));
}catch (Exception e) {
e.printStackTrace();
}
}
parseXMLWithSAX()
方法中先是创建了一个SAXParserFactory
的对象,然后在获取到XMLReader
对象,接着将我们编写的ContentHandler
实例设置到XMLReader
中,最后调用parse()
方法开始执行解析就好了。
9.4 解析JSON格式数据
比起XML,JSON的主要优势在于它的体积更小,在网络上传输的时候可以更省流量。但缺点在于它的语意性较差,看起来不如XML直观。
[{"id":"5","version":"5.5","name":"Clash of Clans"},
{"id":"6","version":"7.0","name":"Boom Beach"},
{"id":"7","version":"3.5","name":"Clash Royale"}]
9.4.1 使用JSONObject
解析JSON数据也有很多种方法,可以使用官方提供的JSONObject,也可以使用谷歌的开源库GSON。另外,一些第三方的开源库如Jackson,FastJSON等也非常不错。
由于我们在服务器中定义的是一个JSON数组,因此这里首先是将服务器返回的数据传入到了一个JSONArray对象中。然后循环遍历这个JSONArray,从中取出的每一个元素都是一个JSONObject对象,每个JSONObject对象中又会包含id,name和version这些数据。接下来只需要调用getString()方法将这些数据取出。
9.4.2 使用GSON
不过GSON并没有被添加到Android官方的API中,因此如果想要使用这个功能的话,就必须要在项目中添加GSON库的依赖。
compile 'com.google.code.gson:gson:2.8.0'
它主要就是可以将一段JSON格式的字符串自动映射成一个对象,从而不需要我们再手动去编写代码进行解析了。
比如说一段JSON格式的数据如下所示:
{"name":"Tom","age":"20"}
那我们就可以定义一个Person类,并加入name和age这两个字段,然后只需简单地调用如下代码就可以将JSON数据自动解析成一个Person对象了:
Gson gson = new Gson();
Person person = gson.fronJson(jsonData,Person.class);
如果需要解析的是一段JSON数组会稍微麻烦一点,我们需要借助TypeToken将期望解析成的数据类型传入到fromJson()方法中。
List<Person> people = gson.fromJson(jsonData,new TypeToken<List<Person>>().getType());
//使用GSON解析数据
private void parseJSONWithGSON(String jsonData)
{
Gson gson = new Gson();
List<App> appList = gson.fromJson(jsonData,new TypeToken<List<App>>(){}.getType());
for(App app : appList)
{
Log.d(TAG, "id is : "+app.getId());
Log.d(TAG, "name is : "+app.getName());
Log.d(TAG, "version is : "+app.getVersion());
}
}
9.5 网络编程的最佳实践
一个应用程序很可能会在许多地方都使用到网络功能,而发送HTTP请求的代码基本都是相同的。如果我们每次都去编写一遍发送HTTP请求的代码,这显然是非常差劲的做法。
通常情况下我们都应该将这些通用的网络操作提取到一个公共的类里,并提供一个静态方法,当想要发清网络请求的时候,只需要简单地调用一下这个方法即可。
但是需要注意,网络请求通常都是属于耗时操作,而sendHttpRequest()方法的内部并没有开启线程,这样就有可能导致在调用sendHttpRequest()方法的时候使得主线程被阻塞住。
如果我们在sendHttpRequest()方法中开启了一个线程来发起HTTP请求,那么服务器响应的数据是无法进行返回的,所有的耗时逻辑都是在子线程里进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,当然也就无法返回响应的逻辑了。
其实解决逻辑并不难,只需要使用Java的回调机制就可以了。
首先需要定义一个接口
public interface HttpCallbackListener
{
void onFinish(String response);
void onError(Exception e);
}
我们在接口中定义了两个方法,onFinish()方法表示当服务器成功响应我们请求的时候调用,onError()表示当进行网络操作出现错误的时候调用。这两个方法都带有参数,onFinish()方法中的参数代表着服务器返回的数据,而onError()方法中的参数记录着错误的详细信息。
public class HttpUtil
{
public static void sendHttpRequest(final String address,final
HttpCallbackListener listener)
{
new Thread(new Runnable()
{
@Override
public void run()
{
HttpURLConnection connection = null;
try
{
URL url = new URL(address);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null)
{
response.append(line);
}
if (listener != null)
{
//回调onFinish()方法
listener.onFinish(response.toString());
}
} catch (Exception e)
{
if (listener != null)
{
//回调onError()方法
listener.onError(e);
}
} finally
{
if (connection != null)
{
connection.disconnect();
}
}
}
}).start();
}
}
我们首先给sendHttpRequest()方法添加了一个HttpCallbackListener参数,并在方法的内部开启了一个子线程,然后在子线程里去执行具体的网络操作。注意,子线程中是无法通过return语句来返回数据的,因此这里我们将服务器响应的数据传入了HttpCallbackListener的onFinish()方法中,如果出现了异常就将异常原因传入到onError()方法中。
HttpUtil.sendHttpRequest("www.baidu.com", new HttpCallbackListener()
{
@Override
public void onFinish(String response)
{
//在这里根据返回内容执行具体的逻辑
}
@Override
public void onError(Exception e)
{
//在这里对异常情况进行处理
}
});
在HttpUtil中加入一个sendOkHttpRequest()方法
public static void sendOkHttpRequest(String address, Callback callback)
{
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(address)
.build();
client.newCall(request).enqueue(callback);
}
sendOkHttpRequest()方法中有一个okhttp3.Callback参数,这个是OkHttp库中自带的一个回调接口,类似与我们刚才自己编写的HttpCallbackListener。然后在client.newCall()之后没有像之前那样一直调用execute()方法,而是调用了一个enqueue()方法,并把okhttp3.Callback参数传入。相信聪明的你已经猜到了,OkHttp在enqueue()方法的内部已经帮我们开好子线程了,然后会在子线程中去执行HTTP请求,并将最终的请求结果回调到okhttp3.Callback当中。
那么我们在调用sendOkHttpRequest()方法的时候就可以这样写:
HttpUtil.sendOkHttpRequest("www.baidu.com", new Callback()
{
@Override
public void onResponse(Call call, Response response) throws IOException
{
//得到服务器返回的具体内容
String responseData = response.body().string();
}
@Override
public void onFailure(Call call, IOException e)
{
//在这里对异常情况进行处理
}
});
另外需要注意的是,不管是使用HttpURLConnection还是OkHttp,最终的回调接口都还是在子线程中运行的,因此我们不可以在这里执行任何的UI操作,除非借助runOnUiThread()方法来进行线程转换。