安卓開發之解析XML和JSON格式資料
參考書作者:郭霖
我會將所學的知識簡單記錄下來以便於大家快速查閱資料,另外郭霖大俠沒有提到的東西我也會作出補充
我們
通常情況下,每個需要訪問網路的應用程式都會有一個自己的伺服器,我們可以向伺服器提交資料,也可以從伺服器
上獲取資料。在網路上傳輸資料時最常用的格式用兩種:XML和JSON
解析XML格式資料
我們就搭建一個最簡單的Web伺服器,在這個伺服器上提供一段XML文字,然後我們程式訪問這個伺服器,再對得到的XML文字
進行解析
大家先下載好Apache伺服器(百度搜索或者直接在官網上下載),一直預設安裝就行了,
下面開啟瀏覽器驗證一下
接下來在你的安裝目錄Apache\htdocs目錄下新建一個名為get_data.xml的檔案,編輯這個檔案
<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>
在瀏覽器訪問http://127.0.0.1/get_data.xml可以看到內容 現在我們就解析這個返回的XML檔案
為了方便起見,我們在下面這個專案上進行解析XML
比較常用的解析XML方法有Pull解析和SAX解析當然還有DOM解析,這裡我們只介紹前兩種解析
Pull和SAX解析方式
Pull解析我們只需要新增一個私有方法,然後在sendRequestWithOkHttp方法中呼叫parserXMLWithPull方法
SAX解析我們需要建立一個新的類ContentHandler,這個類繼承自DefaultHandler類,並重寫父類的5個方法
我們修改MainActivity中的程式碼
package com.gougoucompany.clarence.networktest; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.net.HttpURLConnection; import java.net.URL; import javax.xml.parsers.SAXParserFactory; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class MainActivity extends AppCompatActivity implements View.OnClickListener{ TextView responseText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button sendRequest = (Button) findViewById(R.id.send_request); responseText = (TextView) findViewById(R.id.response_text); sendRequest.setOnClickListener(this); } @Override public void onClick(View v) { if(v.getId() == R.id.send_request) { //sendRequestWithURLConnection(); /*今天,我們有許多出色的網路通訊庫都可以替代原生的HttpURLConnection,而其中OkHttp是比較出色的一個 * 現在已經成為了廣大安卓開發者首選的網路通訊庫 OkHttp的專案主頁地址是: http://github.com/square/okhttp * 可以檢視最新的版本 * 我們要現在app/build.gradle檔案中加入OkHttp庫的依賴,會自動下載兩個庫OkHttp和Okio庫 * 我們來看OkHttp的使用步驟 * 1. 建立OkHttpClient例項 * 2. 建立一個Request物件 * 3. 使用OkHttpClient的newCall()方法建立一個Call物件,並呼叫它的execute()方法來發送請求和接受伺服器返回的資料 * 4. 使用Response物件接受伺服器返回的資料 然後使用response.body().string()方法獲得具體的內容 * 這種是使用"GET"方法提交請求 * * 下來看如何使用"POST"方法提交請求 * 先構建一個RequestBody物件來存放待提交的資料 * RequestBody requestBody = new FormBody.Builder() * .add("username", "admin") * .add("password", "123456") * .builder(); * 然後在Request.Builder構造器呼叫post()方法將RequestBody物件傳入 * Request request = new Request.Builder() * .url("http://www.baidu.com") * .post(requestBody) * .build();後面的都一樣了*/ sendRequestWithOkHttp(); } } private void sendRequestWithOkHttp() { //開啟執行緒來發起網路請求 new Thread(new Runnable () { @Override public void run() { try { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() //指定訪問的伺服器地址是電腦本機 .url("http://10.0.2.2/get_data.xml") //通過url()方法設定目標的網路地址 .build(); Response response = client.newCall(request).execute(); String responseData = response.body().string(); Log.d("MainActivity", responseData); //showResponse(responseData); parseXMLWithPull(responseData); //parseXMLWithSAX(responseData); } catch(Exception e) { e.printStackTrace(); } } }).start(); } private void parseXMLWithSAX(String xmlData) { /*parseXMLWithSAX()方法中先是建立了一個SAXParserFactory物件,然後 * 再獲取到XMLReader物件,接著將我們編寫的ContentHandler的例項設定到XMLReader中 * ,最後呼叫parse()方法執行解析就好了*/ 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(); } } private void parseXMLWithPull(String xmlData) { try { //獲得一個XmlPullParserFactory例項 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); //得到XmlPullParser物件 XmlPullParser xmlPullParser = factory.newPullParser(); //呼叫xmlPullParser的setInput方法將伺服器返回的XML資料傳入開始解析 xmlPullParser.setInput(new StringReader(xmlData)); //獲得當前的解析事件 int eventType = xmlPullParser.getEventType(); String id = ""; String name = ""; String version = ""; while (eventType != XmlPullParser.END_DOCUMENT) { //getName()方法獲得當前節點的名字 String nodeName = xmlPullParser.getName(); //如果發現節點名等於id,name或version,就呼叫nextText()方法來獲取節點內具體的內容 switch(eventType) { //開始解析節點 case XmlPullParser.START_TAG: { if ("id".equals(nodeName)) { id = xmlPullParser.nextText(); } else if ("name".equals(nodeName)) { name = xmlPullParser.nextText(); } else if ("version".equals(nodeName)) { version = xmlPullParser.nextText(); } break; } //完成解析某個節點就將id,name,version全都打印出來 case XmlPullParser.END_TAG: { if("app".equals(nodeName)) { Log.d("MainActivity", "id is " + id); Log.d("MainActivity", "name is " + name); Log.d("MainActivity", "version is " + version); } break; } default: break; } eventType = xmlPullParser.next(); } } catch(Exception e) { e.printStackTrace(); } } private void sendRequestWithURLConnection() { //開啟執行緒來發起網路請求 new Thread(new Runnable() { @Override public void run() { HttpURLConnection connection = null; BufferedReader reader = null; try{ URL url = new URL("https://www.baidu.com"); //獲取HttpURLConnection例項 connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); //設定連線超時 connection.setConnectTimeout(8000); //設定讀取超時的毫秒數 connection.setReadTimeout(8000); //獲取到伺服器返回的輸入流,位元組輸入流InputStream物件 InputStream in = connection.getInputStream(); //下面對獲取到的輸入流進行讀取 reader = new BufferedReader(new InputStreamReader(in)); StringBuilder response = new StringBuilder(); String line; while((line = reader.readLine()) != null) { response.append(line); } showResponse(response.toString()); } catch (Exception e) { e.printStackTrace(); } finally { if(reader != null) { try{ reader.close(); } catch(IOException e) { e.printStackTrace(); } } if(connection != null) { connection.disconnect(); //將Http連線關閉掉 } } } }).start(); } private void showResponse(final String response) { runOnUiThread(new Runnable() { @Override public void run() { //在這裡進行UI操作,將結果顯示到介面上 responseText.setText(response); } }); } }
新建類ContentHandler
package com.gougoucompany.clarence.networktest;
import android.util.Log;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Created by Clarence on 2018/4/13.
* Sax解析是一種特別常用的xml解析方式,雖然用法比Pull解析要複雜一些,但在
* 語義方面會更加清楚
* 通常情況下我們都會新建一個類繼承自DefaultHandler,並重寫父類的5個方法
*/
public class ContentHandler extends DefaultHandler {
private String nodeName;
private StringBuilder id;
private StringBuilder name;
private StringBuilder version;
//開始xml解析的時候呼叫
@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;
}
//characters()方法會在獲取節點中的內容的時候呼叫
//StringBuilder的append(char[], int offset, int len)方法將陣列從下標offset開始的len個字元依次新增到當前字串的末尾
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
//根據當前節點名判斷將內容新增到哪一個StringBuilder物件中
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("ContentHandler", "id is " + id.toString().trim());
Log.d("ContentHandler", "name is " + name.toString().trim());
Log.d("ContentHandler", "version is " + version.toString().trim());
//最後要將StringBuilder清空掉 java.lang.StringBuilder.setLength(int newLength)來改變字元序列的長度
id.setLength(0);
name.setLength(0);
version.setLength(0);
}
}
//會在完成整個xml解析的時候呼叫
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}
下面是解析伺服器傳送的xml檔案後得到的資訊,我們將它顯示到日誌Debug中
需要注意的是:模擬機訪問127.0.0.1都是訪問模擬器本身,你想在模擬器上訪問安裝模擬器的電腦,
那麼就使用Android內建的ip:10.0.2.2 另外記住要開啟模擬器網路開關
解析JSON格式資料
我們在Apache\htdocs目錄中新建一個get_data.json的檔案,然後編輯這個檔案,並加入如下JSON格式的內容
[{"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"}]
在瀏覽器中訪問http://127.0.0.1/get_data.json解析JSON資料也有很多方法,可以使用官方提供的JSONObject,也可以使用谷歌的開源庫GSON。另外,一些第三方的
開源庫如Jackson、FastJSON等也非常不錯。這裡我們介紹前兩種
JSONObject解析 首先將伺服器返回的資料傳入到了一個JSONArray物件中,然後迴圈遍歷這個JSONArray,從中取出的每一個元素都是一個JSONObject物件,每個JSONObject物件中又會包含id,name和version這些資料。接下來只需要呼叫getString()
方法將這些資料取出,並打印出來即可。
要使用GJSON,必須在專案中新增GSON庫的依賴。編輯app/build.gradle檔案,在dependencies閉包中新增如下內容:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:24.2.1'
testCompile 'junit:junit:4.12'
compile 'com.squareup.okhttp3:okhttp:3.10.0'
compile 'com.google.code.gson:gson:2.7'
}
GSON庫可以將一段JSON格式的字串自動對映成一個物件。
eg: 比如一段JSON格式的資料 {"name" : "Tom", "age" : 20}
我們可以定義一個Person類,並加入name和age這兩個欄位
Gson gson= new Gson();
Person person = gson.fromJson(jsonData, Person.class);
如果需要解析的是一段JSON陣列會稍微麻煩一點,我們需要藉助TypeToken將期望解析的資料型別
傳入到fromJson()方法中
List<Person> people = gson.fromJson(jsonData, new TypeToken<List<Persion>>(){}.getType());
fromJson中提供兩個引數,分別是json字串以及需要轉換成物件的型別
new TypeToken<List<People>>(){}.geType(),這個位置的引數是一個Type,表示是xx型別,但是Type是個
介面如下:
public interface Type { /** * Returns a string describing this type, including information * about any type parameters. * * @implSpec The default implementation calls {@code toString}. * * @return a string describing this type * @since 1.8 */ default String getTypeName() { return toString(); } }
new XXX();這樣是一個建構函式,但是介面是不能直接new的,所以這時用到了匿名內部類,實現介面稱為一種具體的型別
TypeToken,它是gson提供的資料型別轉換器,可以支援各種資料型別轉換,先呼叫TypeToken<T>的構造器得到匿名內部類,
再由該匿名內部類物件呼叫getType()方法得到想要轉換成的type,這裡type就是List<People>型別
我們先增加一個App類,並加入id、name和version這三個欄位,並自動生成getter和setter方法
package com.gougoucompany.clarence.networktest;
/**
* Created by Clarence on 2018/4/14.
*/
public class App {
private String id;
private String name;
private String version;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
}
然後修改MainActivity中的程式碼
我們只要新增一個私有的方法,然後在這個方法完成解析任務
//使用GSON開源庫解析json格式的資料
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("MainActivity", "id is " + app.getId());
Log.d("MainActivity", "name is " + app.getName());
Log.d("MainActivity", "version is " + app.getVersion());
}
}
這樣點選按鈕之後就會打印出資料
優化程式:
一個應用程式很可能會在許多地方都是用到網路功能,而傳送HTTP請求的程式碼基本都是相同的,我們應該將這些
通用的網路操作提取到一個公共的類裡,並提供一個靜態方法,當想要發起網路請求的時候,只需簡單呼叫一下這個
方法即可 新建一個HttpUtil工具類
先是使用HttpURLConnection來處理網路請求
package com.gougoucompany.clarence.networktest;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Created by Clarence on 2018/4/14.
* 將通用的網路操作提取到一個公共的類裡
*/
public class HttpUtil {
public static String sendHttpRequest(String address) {
HttpURLConnection connection = null;
try {
URL url = new URL(address);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
/*httpUrlConnection.setDoOutput(true);以後就可以使用conn.getOutputStream().write()
httpUrlConnection.setDoInput(true);以後就可以使用conn.getInputStream().read();
get請求用不到conn.getOutputStream(),因為引數直接追加在地址後面,因此預設是false。
post請求(比如:檔案上傳)需要往服務區傳輸大量的資料,這些資料是放在http的body裡面的,
因此需要在建立連線以後,往服務端寫資料. 因為總是使用conn.getInputStream()獲取服務端
的響應,因此預設值是true。 */
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
//InputStreamReader是位元組流通向字元流的橋樑:它使用指定的charset讀取位元組並將其解碼為字元
//為了達到效率,可以在BufferedReader內包裝InputStreamReader
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
} catch(Exception e) {
e.printStackTrace();
//返回異常的名稱
return e.getMessage();
} finally {
if(connection != null) {
connection.disconnect();
}
}
}
}
注意:網路請求通常都是屬於耗時操作,而sendHttpRequest()方法內部並沒有開啟執行緒,這樣就可能導致在
呼叫sendHttpRequest()方法的時候使得主執行緒被阻塞住
但是在sendHttpRequest()方法中開啟一個執行緒來發起HTTP請求,那麼伺服器相應的資料是無法進行返回的,所有的
耗時邏輯都是在子執行緒裡進行的,sendHttpRequest()方法會在伺服器還沒來得及響應的時候就執行結束了,當然也就
無法返回響應的資料了。那麼我們可以使用java的回撥機制來解決這個問題
首先需要定義一個介面
* Created by Clarence on 2018/4/14. * 我們在介面中定義了兩個方法,onFinish()方法表示當服務成功相應我們的請求的時候呼叫 * onError()表示當進行網路操作出現錯誤的時候呼叫.onFinish()方法中的引數代表著伺服器返回的 * 引數,而onError()方法中的引數記錄著錯誤的詳細資訊
package com.gougoucompany.clarence.networktest;
/**
* Created by Clarence on 2018/4/14.
*/
public interface HttpCallbackListener {
void onFinish(String response);
void onError(Exception e);
}
接著修改HttpUtil中的程式碼
package com.gougoucompany.clarence.networktest;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Created by Clarence on 2018/4/14.
* 將通用的網路操作提取到一個公共的類裡
*/
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);
/*httpUrlConnection.setDoOutput(true);以後就可以使用conn.getOutputStream().write()
httpUrlConnection.setDoInput(true);以後就可以使用conn.getInputStream().read();
get請求用不到conn.getOutputStream(),因為引數直接追加在地址後面,因此預設是false。
post請求(比如:檔案上傳)需要往服務區傳輸大量的資料,這些資料是放在http的body裡面的,
因此需要在建立連線以後,往服務端寫資料. 因為總是使用conn.getInputStream()獲取服務端
的響應,因此預設值是true。 */
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
//InputStreamReader是位元組流通向字元流的橋樑:它使用指定的charset讀取位元組並將其解碼為字元
//為了達到效率,可以在BufferedReader內包裝InputStreamReader
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()方法中。
現在sendHttpRequest()方法接受兩個引數,我們還需將HttpCallbackListener例項傳入
HttpUtil.sendHttpRequest(address,, new HttpCallbackListener() {
@Override
public void onFinish(String response) {
//在這裡根據返回內容執行具體的邏輯
}
@Override
public void onError(Exception e){
//在這裡對異常情況進行處理
}
}
使用OkHttp來處理網路請求就非常的簡單了
我們在HttpUtil類中新增一個靜態方法如下
public static void sendOkHttpRequest(String address, okhttp3.Callback callback) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(address)
.build();
client.newCall(request).enqueue(callback);
}
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3;
import java.io.IOException;
public interface Callback {
/**
* Called when the request could not be executed due to cancellation, a connectivity problem or
* timeout. Because networks can fail during an exchange, it is possible that the remote server
* accepted the request before the failure.
*/
void onFailure(Call call, IOException e);
/**
* Called when the HTTP response was successfully returned by the remote server. The callback may
* proceed to read the response body with {@link Response#body}. The response is still live until
* its response body is {@linkplain ResponseBody closed}. The recipient of the callback may
* consume the response body on another thread.
*
* <p>Note that transport-layer success (receiving a HTTP response code, headers and body) does
* not necessarily indicate application-layer success: {@code response} may still indicate an
* unhappy HTTP response code like 404 or 500.
*/
void onResponse(Call call, Response response) throws IOException;
}
可以看到okhttp3.Callback是OkHttp庫中自帶的一個回撥介面,類似於我們剛才自己編寫的HttpCallbackListener
OkHttp在equeue()方法的內部幫我們開啟好子執行緒,然後會在子執行緒中去執行HTTP請求,並將最終的結果回撥到okhttp3.Callback中
我們在呼叫sendOkHttpRequest()方法的時候可以這樣寫。
HttpUtil.sendOkHttpRequest(address, new okhttp3.Callback(){
@Override
public void onResponse(Call call, Response response) throws IOException{
//得到伺服器返回的具體內容
String responseData = resposne.body().toString(0;)
}
@Override
public void onFailure(Call call, IOException e) {
//在這裡對異常情況進行處理
}
})
要注意的是不論是HttpURLConnection還是OkHttp,最終的回撥介面都是在子執行緒中執行,因此我們不可以在這裡執行
任何的UI操作,除非藉助RunOnUiThread()方法切換到主執行緒中執行。