本文目錄
1.WebView的用法
- 使用HTTP協(xié)議訪問(wèn)網(wǎng)絡(luò)
- 2.1 使用HttpURLConnection
- 2.2 使用OkHttp
3.解析XML格式數(shù)據(jù)
- 3.1 Pull解析方式
- 3.2 SAX解析方式
4.解析JSON數(shù)據(jù)
- 4.1使用JSONObject
- 4.2 使用GSON
5.網(wǎng)絡(luò)編程的最佳實(shí)踐——HttpUtil封裝(巧用回調(diào)機(jī)制、框架)
1.WebView的用法
- 使用WebView控件,
借其在自己的應(yīng)用程序中嵌入一個(gè)瀏覽器,
以輕松展示各種網(wǎng)頁(yè);
新建一個(gè)WebViewTest項(xiàng)目,
修改activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
接下來(lái)修改MainActivity.java:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webView = (WebView) findViewById(R.id.web_view);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.loadUrl("http://www.baidu.com");
}
}
- 首先是findViewById實(shí)例化對(duì)象;
- getSettings()用來(lái)設(shè)置瀏覽器屬性;
setJavaScriptEnabled(true)讓W(xué)ebView支持JavaScript腳本; - 調(diào)用WebView 的setWebViewClient()方法,
傳入一個(gè)WebViewClient實(shí)例,
作用是:使當(dāng)需要從一個(gè)網(wǎng)頁(yè)跳轉(zhuǎn)到另外一個(gè)網(wǎng)頁(yè)時(shí),
目標(biāo)網(wǎng)頁(yè)仍然在當(dāng)前WebView中顯示,而不是打開(kāi)系統(tǒng)瀏覽器; - loadUrl()傳入網(wǎng)址,顯示網(wǎng)頁(yè)內(nèi)容;
接下來(lái),還需在AndroidManifest.xml中添加訪問(wèn)網(wǎng)絡(luò)的權(quán)限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.webviewtest">
<uses-permission android:name="android.permission.INTERNET"/>
...
</manifest>
至此,即可運(yùn)行程序了,其效果如下:

當(dāng)然還要注意一點(diǎn),如果你的模擬器和SDK是Android 9.0(API級(jí)別28),那運(yùn)行如上代碼會(huì)出現(xiàn)下面這個(gè)問(wèn)題:

原因是從Android 9.0(API級(jí)別28)開(kāi)始,默認(rèn)情況下禁用明文支持。
因此http的url均無(wú)法在webview中加載。 解決方法是在AndroidManifest.xml對(duì)應(yīng)的地方加入一句代碼即可:

android:usesCleartextTraffic="true"
解決之后便可以運(yùn)行成功了:
當(dāng)然,小伙伴們,生活往往沒(méi)那么簡(jiǎn)單,
百度搜索引擎框下面有很多吸引我們眼球的文章對(duì)吧,

亦或者:



- 這是因?yàn)槠渥远x了
scheme,
類(lèi)似的還有alipays://,weixin://等等。
而webView只能識(shí)別http://或https://開(kāi)頭的url,因此才會(huì)報(bào)此錯(cuò)。
處理方法,對(duì)于這種自定義scheme的url單獨(dú)處理即可。
修改代碼如下:
我們剛剛寫(xiě)了這段代碼對(duì)吧:
webView.setWebViewClient(new WebViewClient())
現(xiàn)在,
把傳入的WebViewClient實(shí)例變成一個(gè)以WebViewClient為父類(lèi)的匿名內(nèi)部類(lèi),
并重寫(xiě)shouldOverrideUrlLoading()方法,
在里面對(duì)方才報(bào)錯(cuò)中的自定義scheme進(jìn)行單獨(dú)處理即可:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
...
final WebView webView = (WebView) findViewById(R.id.web_view);
...
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try{
if(url.startsWith("baiduboxapp://") || url.startsWith("baiduboxlite://" )){
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
return true;
}
}catch (Exception e){
return false;
}
webView.loadUrl(url);
return true;
}
});
webView.loadUrl("http://www.baidu.com");
}
}
ok,這時(shí)候再運(yùn)行程序,便可以成功打開(kāi)各種推文了:

參考文章
2. 使用HTTP協(xié)議訪問(wèn)網(wǎng)絡(luò)
HTTP基于android的工作原理簡(jiǎn)述
客戶端向服務(wù)器發(fā)出一條HTTP請(qǐng)求,
服務(wù)器收到請(qǐng)求之后會(huì)返回一些數(shù)據(jù)給客戶端,
然后客戶端再對(duì)這些數(shù)據(jù)進(jìn)行解析和處理就可以。一個(gè)瀏覽器的基本工作原理也就是如上了。
上面使用的WebView控件,
其實(shí)也就是app向百度服務(wù)器發(fā)起一條HTTP請(qǐng)求,
接著服務(wù)器分析出我們想要訪問(wèn)的是百度的首頁(yè),
于是會(huì)把該網(wǎng)頁(yè)的HTML代碼進(jìn)行返回,
然后WebView再調(diào)用手機(jī)瀏覽器的內(nèi)核對(duì)返回的HTML代碼進(jìn)行解析,
最終將頁(yè)面展示出來(lái)。也即WebView封裝了
發(fā)送HTTP請(qǐng)求、接受服務(wù)響應(yīng)、解析返回?cái)?shù)據(jù),以及最終頁(yè)面的展示這幾步工作。
- 下面暫時(shí)擺脫WebView,
手動(dòng)發(fā)送HTTP請(qǐng)求,直觀地理解一下HTTP協(xié)議的工作過(guò)程。
2.1 使用HttpURLConnection
- 首先,
獲取HttpURLconnection的實(shí)例:
a. 以參數(shù)為目標(biāo)的網(wǎng)絡(luò)地址,new 出一個(gè)URL對(duì)象;
b. url對(duì)象調(diào)用openConnection();
返回的結(jié)果轉(zhuǎn)型付給HttpURLConnection對(duì)象;
URL url = new URL("http://www.baidu.com");
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
- 得到HttpURLConnection實(shí)例之后,設(shè)置HTTP請(qǐng)求所使用的方法;
常使用的方法主要有兩個(gè):GET和POST。
GET表示希望從服務(wù)器獲取數(shù)據(jù),
POST希望提交數(shù)據(jù)給服務(wù)器:
connection.setRequestMethod("GET");
- 接下來(lái)進(jìn)行一些自由的定制,
如設(shè)置連接超時(shí)、讀取超時(shí)的毫秒數(shù)、服務(wù)器希望得到的一些消息頭等。
這些都是自己根據(jù)實(shí)際情況進(jìn)行編寫(xiě):
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
- 之后再調(diào)用getInputStream()方法,
就可以獲取到服務(wù)器返回的輸入流了,
后續(xù)的對(duì)輸入流進(jìn)行讀取即可:
InputStream in = connection.getInputStream();
- 最后調(diào)用disconnect()關(guān)閉HTTP連接:
connection.disconnect();
下面新建一個(gè)名為NetworkTest的空活動(dòng),調(diào)試一下上面的知識(shí)點(diǎn),
修改對(duì)應(yīng)的xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NetworkTest">
<Button
android:id="@+id/send_request"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Request"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/response_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>
以上,
- ScrollView用于滾動(dòng)查看內(nèi)容;
- Button用來(lái)發(fā)送HTTP請(qǐng)求;
- TextView用來(lái)顯示服務(wù)器返回的數(shù)據(jù);
接著修改NetworkTest.java:
public class NetworkTest extends AppCompatActivity implements View.OnClickListener{
TextView responseText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_network_test);
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){
sendRequestWithHttpURLConnection();
}
}
private void sendRequestWithHttpURLConnection() {
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL url = new URL("https://hao.#/");
connection = (HttpURLConnection)url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
InputStream in = connection.getInputStream();
//下面對(duì)獲取的輸入流進(jìn)行讀取
//InputStreamReader賦值給BufferedReader讓其來(lái)讀
reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
//一行一行地讀取并加進(jìn)stringbuilder
while((line = reader.readLine()) != null){
response.append(line);
}
showResponse(response.toString());
Log.d("NetworkTest: ", response.toString());
} catch (IOException e) {
e.printStackTrace();
}finally{
if(reader !=null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(connection !=null){
connection.disconnect();
}
}
}
}).start();
}
private void showResponse(final String response) {
runOnUiThread(new Runnable() {
@Override
public void run() {
responseText.setText(response);
}
});
}
}
- sendRequestWithHttpURLConnection()中:
開(kāi)啟一個(gè)子線程,
在子線程里使用HttpURLConnection發(fā)出一條HTTP請(qǐng)求,
請(qǐng)求的目標(biāo)地址就是百度的首頁(yè);
接著用BufferedReader讀取返回的輸入流,
轉(zhuǎn)成string傳給showResponse() - showResponse()中通過(guò)runOnUiThread()將返回的數(shù)據(jù)顯示到界面上;
運(yùn)行效果如下:關(guān)于runOnUiThread()方法,
因?yàn)锳ndroid不允許在子線程中進(jìn)行UI操作,
我們需要通過(guò)這個(gè)方法在子線程中將線程切換到主線程,
然后再更新UI元素。

注:
如網(wǎng)址不正確,
會(huì)出現(xiàn)
NetworkSecurityConfig: No Network Security Config specified, using platform default的提示。
另外,用手機(jī)熱點(diǎn)也是如此?。。?br> 只有穩(wěn)定PC端接線網(wǎng)絡(luò)或其熱點(diǎn)才可順利使用??!
服務(wù)器返回的就是這些HTML代碼,
只是通常瀏覽器都會(huì)將這些代碼解析成漂亮的網(wǎng)頁(yè)再展示出來(lái);如果想
提交數(shù)據(jù)給服務(wù)器,
只需將HTTP請(qǐng)求方法改成POST,
并在獲取輸入流之前把要提交的數(shù)據(jù)寫(xiě)出即可。
每條數(shù)據(jù)都要以鍵值對(duì)的形式存在,
數(shù)據(jù)與數(shù)據(jù)之間用“&”符號(hào)隔開(kāi),如提交用戶名和密碼:
connection.setRequestMethod("POST");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456");
2. 使用OkHttp
- OkHttp由Square公司開(kāi)發(fā),其不僅在接口封裝上面做的簡(jiǎn)單易用,
就連在底層實(shí)現(xiàn)上也是自成一派,
比起原生的HttpURLConnection,可以說(shuō)是有過(guò)之而無(wú)不及,
現(xiàn)在已經(jīng)成了廣大Android開(kāi)發(fā)者的首選網(wǎng)絡(luò)通信庫(kù)。
OkHttp項(xiàng)目主頁(yè)地址:https://github.com/square/okhttp
使用之前,需添加OkHttp庫(kù)依賴(lài),
打開(kāi)app/buid.gradle,在dependencies閉包中添加如下內(nèi)容:
implementation("com.squareup.okhttp3:okhttp:3.14.0")
添加此依賴(lài),會(huì)自動(dòng)下載兩個(gè)庫(kù):OkHttp庫(kù)、Okio庫(kù)(是前者的通信基礎(chǔ))。
- 注意,添加前最好是訪問(wèn)一下OkHttp項(xiàng)目主頁(yè)查看當(dāng)前最新的版本是多少,再在gradle處添加依賴(lài);
下面是OkHttp具體用法
- 首先,需要?jiǎng)?chuàng)建OkHttpClient實(shí)例,如下:
OkHttpClient client = new OkHttpClient();
- 接下來(lái),如想發(fā)起一條HTTP請(qǐng)求,需創(chuàng)建
Request對(duì)象:
Request request = new Request.Builder().build();
- 當(dāng)然上述代碼只是創(chuàng)建一個(gè)空的Request對(duì)象,
需要在build()方法之前可連綴很多其他方法豐富此Request對(duì)象。
Request request = new Request.Builder()
.url("https://www.baidu.com")
.build();
- 之后調(diào)用OkHttpCilent的newCall()方法創(chuàng)建一個(gè)
Call對(duì)象,
并調(diào)用它的execute()方法發(fā)送請(qǐng)求,
并獲取服務(wù)器返回的數(shù)據(jù):
Response response = client.newCall(request).execute();
- request存請(qǐng)求;
- newCall接收request
- execute執(zhí)行request
- Response對(duì)象接收服務(wù)器返回的數(shù)據(jù);
- 下面得到返回的具體內(nèi)容
String responseData = response.body().string();
如果發(fā)起一條POST請(qǐng)求,會(huì)比GET復(fù)雜些;
- 需先構(gòu)建
RequestBody對(duì)象存放待提交的參數(shù):
RequestBody requestBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build();
- 然后在
Request.Builder中以RequestBody對(duì)象為傳入的參數(shù)調(diào)用post()方法,:
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(requestBody)
.build();
- 接下來(lái)的操作就和
GET請(qǐng)求一樣了,
調(diào)用execute()方法發(fā)送請(qǐng)求并獲取服務(wù)器返回的數(shù)據(jù)即可。
現(xiàn)在改用OkHttp的方式把剛剛NewworkTest的內(nèi)容再實(shí)現(xiàn)一遍:
@Override
public void onClick(View v) {
if(v.getId() == R.id.send_request){
// sendRequestWithHttpURLConnection();
sendRequestWithOkHttp();
}
}
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://hao.#/")
.build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
showResponse(responseData);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private void showResponse(final String response) {
runOnUiThread(new Runnable() {
@Override
public void run() {
responseText.setText(response);
}
});
}
運(yùn)行時(shí)有可能出現(xiàn)下面的問(wèn)題,解決方法是把gradle中minsdk改成26以上即可:
運(yùn)行效果圖:

3.解析XML格式數(shù)據(jù)
- 通常,每個(gè)需要訪問(wèn)網(wǎng)絡(luò)的應(yīng)用程序都會(huì)有一個(gè)自己的服務(wù)器,
我們可以向服務(wù)器提交數(shù)據(jù)或者從服務(wù)器上獲取數(shù)據(jù); - 為了雙方能夠快速知道文本的用途,一般在網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)都是格式化后的;
這種數(shù)據(jù)會(huì)有一定的結(jié)構(gòu)規(guī)格和語(yǔ)義;
當(dāng)另一方收到數(shù)據(jù)消息之后就可以按照相同的結(jié)構(gòu)規(guī)格進(jìn)行解析,從而去除他想要的那部分內(nèi)容;
搭建一個(gè)本地服務(wù)器
- 在網(wǎng)絡(luò)上傳輸數(shù)據(jù)最常用的格式有兩種:
XML和JSON
在開(kāi)始學(xué)習(xí)這兩種數(shù)據(jù)格式之前,
我們還需要搭建一個(gè)本地服務(wù)器,
-
進(jìn)度大概進(jìn)行到
可以在本地服務(wù)器文件夾下放置文件,
然后在本地瀏覽器可以訪問(wèn)即可;
這里提供兩種方法:
- 可以使用單模塊原生的本地服務(wù)器Apache,
具體的操作我之前已經(jīng)寫(xiě)過(guò)一篇詳細(xì)的博文:


- 或者學(xué)過(guò)PHP的朋友也可以使用PhpStudy集成環(huán)境(中的Apache模塊)來(lái)做服務(wù)器,具體的相關(guān)我也寫(xiě)過(guò)相關(guān)的博文哈:
- PhpStudy集成環(huán)境下載、安裝以及配置啟動(dòng)檢測(cè)
- phpStudy啟動(dòng)界面的功能簡(jiǎn)介
- Apache服務(wù)啟動(dòng)失敗解決方法
- PHPstudy | 使用站點(diǎn)管理器來(lái)創(chuàng)建虛擬主機(jī)

文件內(nèi)容:

<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>
接著瀏覽器鍵入localhost/get_data.xml即可訪問(wèn):

- 當(dāng)然,鍵入
127.0.0.1/get_data.xml也是可以的:
- 解析XML格式數(shù)據(jù)有很多方式,
Pull和SAX解析是常用的兩種。
3.1 Pull解析方式
這里我們依舊在NetworkTest 這個(gè)活動(dòng)上面做開(kāi)發(fā),重用方才網(wǎng)絡(luò)通信的代碼,把重心放在XML數(shù)據(jù)解析上;
以上,我們已經(jīng)準(zhǔn)備好XML格式的數(shù)據(jù),
現(xiàn)在編寫(xiě)代碼從中解析出我們想要得到的那部分內(nèi)容;
修改NetworkTest.java:
@Override
public void onClick(View v) {
if(v.getId() == R.id.send_request){
// sendRequestWithHttpURLConnection();
sendRequestWithOkHttp();
}
}
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
//http://10.0.2.2/對(duì)于模擬器來(lái)說(shuō)是電腦本機(jī)IP地址**
Request request = new Request.Builder()
.url("http://47.100.78.251/testPackage/get_data.xml")
.build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
parseXMLWithPull(responseData);//解析服務(wù)器返回的數(shù)據(jù)**
// showResponse(responseData);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private void parseXMLWithPull(String xmlData) {
try{
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();//獲取一個(gè)XmlPullParserFactory 實(shí)例
XmlPullParser xmlPullParser = factory.newPullParser();//借助XmlPullParserFactory 實(shí)例得到 XmlPullParser對(duì)象
xmlPullParser.setInput(new StringReader(xmlData));//XmlPullParser對(duì)象 setInput 把服務(wù)器返回的數(shù)據(jù)傳入即可開(kāi)始解析
int eventType = xmlPullParser.getEventType();//通過(guò)getEventType()得到當(dāng)前的解析事件
String id = "";
String name = "";
String version = "";
//如果不到文末
//解析事件不為XmlPullParser.END_DOCUMENT,說(shuō)明解析工作沒(méi)完成
responseText.setText("");
while(eventType != XmlPullParser.END_DOCUMENT){
//解析事件不為XmlPullParser.END_DOCUMENT,說(shuō)明解析工作沒(méi)完成
String nodeName = xmlPullParser.getName();//獲取當(dāng)前結(jié)點(diǎn)名字
switch (eventType) {
//開(kāi)始解析某個(gè)節(jié)點(diǎn)
case XmlPullParser.START_TAG:
if ("id".equals(nodeName)) {
id = xmlPullParser.nextText();//獲取節(jié)點(diǎn)具體內(nèi)容
} else if ("name".equals(nodeName)) {
name = xmlPullParser.nextText();
} else if ("version".equals(nodeName)) {
version = xmlPullParser.nextText();
}
break;
//完成解析某個(gè)節(jié)點(diǎn)
case XmlPullParser.END_TAG:
if ("app".equals(nodeName)) {
//解析完一個(gè)app節(jié)點(diǎn)后打印獲取到的內(nèi)容
String finalId = id;
String finalName = name;
String finalVersion = version;
runOnUiThread(new Runnable() {
@Override
public void run() {
responseText.append("MainActivity: id is " + finalId + "\n");
responseText.append("MainActivity: name is " + finalName + "\n");
responseText.append("MainActivity: version is " + finalVersion + "\n");
}
});
}
break;
default:
break;
}
eventType = xmlPullParser.next();//獲取下一個(gè)解析事件
}
} catch (Exception e) {
e.printStackTrace();
}
}

Pull解析的使用思路是:
通過(guò)XmlPullParserFactory等一系列API,
得到一個(gè)XmlPullParser實(shí)例,
再把數(shù)據(jù)
傳給XmlPullParser實(shí)例xmlPullParser.setInput(new StringReader(xmlData));
接著就可以開(kāi)始解析了;把XML的文末標(biāo)志、起始標(biāo)簽以及結(jié)束標(biāo)簽,
約定為一個(gè)事件int eventType = xmlPullParser.getEventType();
- 文末標(biāo)志事件用于判斷文件是否解析完,
- 起始標(biāo)簽事件用于 判斷 以及 獲取標(biāo)簽節(jié)點(diǎn)中的內(nèi)容,
- 結(jié)束標(biāo)簽事件則用于 判斷 以及
去實(shí)現(xiàn)一個(gè)解析階段結(jié)束后的業(yè)務(wù)邏輯;
3.2 SAX解析方式
- 除了Pull解析,SAX解析也是一種常用的解析方式,
其用法比Pull解析復(fù)雜一些,
但語(yǔ)義上會(huì)更清楚;
用法:
- 新建一個(gè)類(lèi)繼承自DefaultHandler,并重寫(xiě)父類(lèi)5個(gè)方法。
public class MyHandler extends DefaultHandler {
@Override
public void startDocument() throws SAXException {
super.startDocument();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
super.startElement(uri, localName, qName, attributes);
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
super.characters(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
super.endElement(uri, localName, qName);
}
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}
- startDocument()在開(kāi)始XML解析時(shí)候調(diào)用;
- startElement()在開(kāi)始解析某個(gè)節(jié)點(diǎn)時(shí)調(diào)用;
- characters()在獲取節(jié)點(diǎn)中的內(nèi)容時(shí)候調(diào)用;
- endElement()在完成解析某個(gè)節(jié)點(diǎn)的時(shí)候調(diào)用;
- endDocument()在完成整個(gè)XML解析時(shí)調(diào)用;
startElement()、characters()、endElement()三個(gè)方法是有參數(shù)的,
從XML中解析的數(shù)據(jù)會(huì)以參數(shù)的形式傳入到這些方法中;在獲取節(jié)點(diǎn)中的內(nèi)容時(shí),
characters()方法可能會(huì)被調(diào)用多次,
一些換行符也被當(dāng)做內(nèi)容解析出來(lái),
我們需要針對(duì)這種情況在代碼中做好控制;
實(shí)踐
- 新建一個(gè)類(lèi)繼承自DefaultHandler,并重寫(xiě)父類(lèi)5個(gè)方法:
(注意代碼中的注釋?zhuān)?/strong>
public class ContentHandler extends DefaultHandler {
public String nodeName;
//面向XML文件配置全局屬性
public StringBuilder id;
public StringBuilder name;
public StringBuilder version;
@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 {
//記錄當(dāng)前節(jié)點(diǎn)
nodeName = localName;
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
//根據(jù)當(dāng)前的節(jié)點(diǎn)名判斷將內(nèi)容添加到哪一個(gè)StringBuilder對(duì)象中
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)){
// !!!!!!!!!!!
//id\name\version中可能包含回車(chē)或換行符,需調(diào)用trim()方法除去
// !!!!!!!!!!!
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清空,避免影響下一次內(nèi)容讀取
id.setLength(0);
name.setLength(0);
version.setLength(0);
}
}
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}
修改MainActivity:
@Override
public void onClick(View v) {
if(v.getId() == R.id.send_request){
// sendRequestWithHttpURLConnection();
sendRequestWithOkHttp();
}
}
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
//http://10.0.2.2/對(duì)于模擬器來(lái)說(shuō)是電腦本機(jī)IP地址**
Request request = new Request.Builder()
.url("http://47.100.78.251/testPackage/get_data.xml")
.build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
// parseXMLWithPull(responseData);//解析服務(wù)器返回的數(shù)據(jù)**
parseXMLWithSAX(responseData);//解析服務(wù)器返回的數(shù)據(jù)**
// showResponse(responseData);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private void parseXMLWithSAX(String xmlData) {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
XMLReader xmlReader = factory.newSAXParser().getXMLReader();
ContentHandler handler = new ContentHandler();
//將ContentHandler的實(shí)例設(shè)置到XMLReader中
xmlReader.setContentHandler(handler);
//開(kāi)始執(zhí)行解析
xmlReader.parse(new InputSource(new StringReader(xmlData)));
} catch (Exception e) {
e.printStackTrace();
}
}
運(yùn)行程序,點(diǎn)擊按鈕,查看日志:
除了Pull和SAX,還有DOM解析方式可用;
4.解析JSON數(shù)據(jù)
JSON的體積比XML更小,網(wǎng)絡(luò)傳輸更省流量,
但語(yǔ)義性差,不如XML直觀。在對(duì)應(yīng)的服務(wù)器目錄下,新建一個(gè)get_data.json文件,內(nèi)容如下:
[{"id":"5","name":"Clash of Clans","version":"5.5"},
{"id":"6","name":"Boom Beach","version":"7.0"},
{"id":"7","name":"Clash Royale","version":"3.5"}]
4.1使用JSONObject
解析JSON數(shù)據(jù)也有很多方法,可使用官方的JSONObject,
谷歌的開(kāi)源庫(kù)GSON,
或第三方的開(kāi)源庫(kù)如Jackson、FastJSON等.我們?cè)诜?wù)器中定義的json文件get_data.json的內(nèi)容是一個(gè)JSON數(shù)組,
因此這里獲取到服務(wù)器的數(shù)據(jù)之后,
直接將數(shù)據(jù)傳入到一個(gè)JSONArray對(duì)象中;然后循環(huán)遍歷這個(gè)JSONArray,
從中取出的每一個(gè)元素都是一個(gè)JSONObject對(duì)象;這個(gè)JSONObject對(duì)象又會(huì)包含id、name和version這些數(shù)據(jù),
即我們定義的json文件中的鍵值;接著只要調(diào)用getString()將這些數(shù)據(jù)取出來(lái)即可;
使用JSONObject,修改MainActivity:
@Override
public void onClick(View v) {
if(v.getId() == R.id.send_request){
// sendRequestWithHttpURLConnection();
sendRequestWithOkHttp();
}
}
//子線程中進(jìn)行!?。。。。。。?!
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
//http://10.0.2.2/對(duì)于模擬器來(lái)說(shuō)是電腦本機(jī)IP地址**
Request request = new Request.Builder()
.url("http://47.100.78.251/testPackage/get_data.json")
.build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
parseJSONWithJSONObject(responseData);
// parseXMLWithPull(responseData);//解析服務(wù)器返回的數(shù)據(jù)**
// parseXMLWithSAX(responseData);//解析服務(wù)器返回的數(shù)據(jù)**
// showResponse(responseData);
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
}
}).start();
}
private void parseJSONWithJSONObject(String responseData) throws JSONException {
//我們?cè)诜?wù)器中定義的json文件get_data.json的內(nèi)容是一個(gè)JSON數(shù)組
JSONArray jsonArray = new JSONArray(responseData);
responseText.setText("");
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String id = jsonObject.getString("id");
String name = jsonObject.getString("name");
String version = jsonObject.getString("version");
runOnUiThread(new Runnable() {
@Override
public void run() {
responseText.append("MainActivity: id is " + id + "\n");
responseText.append("MainActivity: name is " + name + "\n");
responseText.append("MainActivity: version is " + version + "\n");
}
});
}
}
運(yùn)行程序:
4.2 使用GSON
- 添加依賴(lài):
implementation 'com.google.code.gson:gson:2.8.5'
它主要可以將一段JSON格式的字符串自動(dòng)映射成一個(gè)對(duì)象(定義一個(gè)類(lèi)對(duì)應(yīng)),
不需手動(dòng)編寫(xiě)代碼解析。比如說(shuō)一段json格式的數(shù)據(jù)如下所示:
{"name":"Tom","age":20}
則定義一個(gè)Person類(lèi),
加入name和age這兩個(gè)字段;
面向
需要解析的json數(shù)據(jù)定義JavaBean類(lèi),
如果一個(gè)json數(shù)據(jù)有十幾幾十個(gè)鍵值對(duì),
而我們的業(yè)務(wù)只需要取其中的5個(gè)鍵值,
那這個(gè)JavaBean類(lèi),就定義需要的5個(gè)字段即可,
Gson會(huì)將json數(shù)據(jù)字符串,
根據(jù)我們定義的JavaBean類(lèi),
提取出相應(yīng)的數(shù)據(jù)并映射成對(duì)應(yīng)的List;
json字符串中有多少套JavaBean類(lèi)字段對(duì)應(yīng)的鍵值,
映射得到的List的size就有多少;
- 接著簡(jiǎn)單調(diào)用如下代碼即可將JSON數(shù)據(jù)
自動(dòng)解析成一個(gè)Person對(duì)象了:
Gson gson = new Gson();
Person person = gson.fromJson(jsonData, Person.class);
- 如果需要解析的是
一段JSON數(shù)組會(huì)稍微麻煩一點(diǎn),
需要借助TypeToken將期望解析成的數(shù)據(jù)類(lèi)型傳入到fromJson()方法中,如:
List<Person> people = gson.fromJson(jsonData, new TypeToken<List<Person>>(){}.getType());
基本用法就如上所述了,
接下來(lái)用GSON解析一下一開(kāi)始的數(shù)據(jù);
- 首先新建一個(gè)App類(lèi):
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:
@Override
public void onClick(View v) {
if(v.getId() == R.id.send_request){
// sendRequestWithHttpURLConnection();
sendRequestWithOkHttp();
}
}
//!?。。。。。?!
//注意這里在子線程中進(jìn)行??!
// UI更新需要切到主線程
// ?。。。。。?!
private void sendRequestWithOkHttp() {
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
//http://10.0.2.2/對(duì)于模擬器來(lái)說(shuō)是電腦本機(jī)IP地址**
Request request = new Request.Builder()
.url("http://47.100.78.251/testPackage/get_data.json")
.build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
// parseJSONWithJSONObject(responseData);
parsJSONWithGSON(responseData);
// parseXMLWithPull(responseData);//解析服務(wù)器返回的數(shù)據(jù)**
// parseXMLWithSAX(responseData);//解析服務(wù)器返回的數(shù)據(jù)**
// showResponse(responseData);
} catch (IOException e) {
e.printStackTrace();
}
// catch (JSONException e) {
// e.printStackTrace();
// }
}
}).start();
}
private void parsJSONWithGSON(String jsonData) {
Gson gson = new Gson();
List<App> appList = gson.fromJson(jsonData, new TypeToken<List<App>>(){}.getType());
for (App app : appList) {
runOnUiThread(new Runnable() {
@Override
public void run() {
responseText.append("MainActivity: id is " + app.getId() + "\n");
responseText.append("MainActivity: name is " + app.getName() + "\n");
responseText.append("MainActivity: version is " + app.getVersion() + "\n");
}
});
}
}
運(yùn)行程序,效果同上。
5.網(wǎng)絡(luò)編程的最佳實(shí)踐
-
(
方法提取)
應(yīng)用程序很可能會(huì)在許多地方都使用網(wǎng)絡(luò)功能,
而發(fā)送HTTP請(qǐng)求的代碼基本相同,
所以我們不能每次都去編寫(xiě)一遍發(fā)送HTTP請(qǐng)求的代碼,
通常應(yīng)該把通用的網(wǎng)絡(luò)操作提取到一個(gè)公共類(lèi)里,
并提供一個(gè)靜態(tài)方法,
當(dāng)想要發(fā)起網(wǎng)絡(luò)請(qǐng)求的時(shí)候,
只需要簡(jiǎn)單地調(diào)用一下這個(gè)方法即可。
(耗時(shí)操作)
另外,
網(wǎng)絡(luò)請(qǐng)求通常都是屬于耗時(shí)操作,
我們提取的發(fā)送HTTP請(qǐng)求的方法內(nèi)部
如果沒(méi)有開(kāi)啟子線程,
則有可能導(dǎo)致在調(diào)用的時(shí)候使得主線程阻塞,
這里則需開(kāi)啟子線程來(lái)發(fā)起HTTP請(qǐng)求,
(數(shù)據(jù)返回)
另外還要考慮到,
如果我們?cè)谝粋€(gè)請(qǐng)求方法內(nèi)部的
開(kāi)啟了一個(gè)子線程來(lái)發(fā)送HTTP請(qǐng)求,
那服務(wù)器響應(yīng)的數(shù)據(jù)是無(wú)法進(jìn)行返回的,
所有的耗時(shí)邏輯都是在子線程里進(jìn)行的,
這個(gè)請(qǐng)求方法會(huì)在服務(wù)器還沒(méi)來(lái)得及響應(yīng)的時(shí)候就執(zhí)行結(jié)束了,
當(dāng)然也就無(wú)法返回響應(yīng)的數(shù)據(jù)了;
- 遇到這種既需要
子線程來(lái)處理耗時(shí)操作,
又要求能實(shí)時(shí)接收到服務(wù)器響應(yīng)到的數(shù)據(jù)的情況,
可以考慮使用Java的回調(diào)機(jī)制來(lái)實(shí)現(xiàn):
- 實(shí)現(xiàn)一個(gè)接口就是寫(xiě)一個(gè)插座,
把封裝的東西寫(xiě)進(jìn)實(shí)現(xiàn)接口的類(lèi)中,
把這個(gè)(匿名內(nèi)部)類(lèi)賦給回調(diào)方法(如setOnClickListener())
- 內(nèi)部
抽象調(diào)用,外部具體實(shí)現(xiàn)(的方法);
內(nèi)部只管調(diào)用,
外部只管實(shí)現(xiàn)(具體的處理方法、邏輯)!- 連接處:
外部實(shí)現(xiàn)的方法,
封裝在一個(gè)匿名內(nèi)部類(lèi)接口實(shí)現(xiàn)類(lèi)實(shí)例中,
將實(shí)例傳給抽象調(diào)用的工具類(lèi)的設(shè)置方法或者構(gòu)造方法,
實(shí)現(xiàn)內(nèi)外連接;
這樣內(nèi)部調(diào)用的接口方法,
就是源自外部傳進(jìn)來(lái)的
接口實(shí)例的
(在外部具體實(shí)現(xiàn)好的)接口方法(邏輯);
數(shù)據(jù)在內(nèi)部抽象調(diào)用時(shí)傳遞給內(nèi)部 未具體實(shí)現(xiàn)的 接口方法,
這里抽象調(diào)用的未具體實(shí)現(xiàn)的 接口方法實(shí)際上在程序運(yùn)行使用時(shí),
就是外部具體實(shí)現(xiàn)的接口及其方法(處理邏輯);
(來(lái)自set傳進(jìn)來(lái)的 外部實(shí)現(xiàn)好諸接口方法的接口實(shí)例)
如此一來(lái)數(shù)據(jù)便通過(guò)回調(diào)機(jī)制,
由內(nèi)而外間接傳遞給了外部(給外部處理)。
小結(jié):
內(nèi)部形式抽象調(diào)用,
外部具體邏輯實(shí)現(xiàn),
外部實(shí)現(xiàn)封裝在接口類(lèi)實(shí)例中傳進(jìn)來(lái),
使得內(nèi)部的形式調(diào)用能夠調(diào)用到了外部的具體邏輯實(shí)現(xiàn);
- 首先需要定義一個(gè)接口,這里取名
HttpCallbackListener:
onFinish(String response)
當(dāng)服務(wù)器成功響應(yīng)請(qǐng)求時(shí)調(diào)用,參數(shù)為服務(wù)器返回的數(shù)據(jù);
onError(Exception e)
當(dāng)進(jìn)行網(wǎng)絡(luò)操作出現(xiàn)錯(cuò)誤時(shí)調(diào)用,參數(shù)記錄錯(cuò)誤的詳細(xì)信息;
public interface HttpCallbackListener {
void onFinish(String response);
void onError(Exception e);
}
-
接著新建一個(gè)剛剛說(shuō)的放著提取了
通用網(wǎng)絡(luò)操作的公共類(lèi):
listener.onFinish(response.toString());
回調(diào)外部傳進(jìn)來(lái)的寫(xiě)好的
匿名內(nèi)部接口類(lèi)的具體實(shí)現(xiàn)好的方法,
這里公共類(lèi)中抽象調(diào)用,
調(diào)用公共類(lèi)方法的地方則具體實(shí)現(xiàn)接口類(lèi)(常用匿名內(nèi)部類(lèi)方式實(shí)現(xiàn)),
實(shí)現(xiàn)好了賦到這里來(lái);
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();
//對(duì)獲取到的輸入流進(jìn)行讀取
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null){
response.append(line);
}
if (listener != null){
//回調(diào)onFinish方法
listener.onFinish(response.toString());
}
} catch (Exception e) {
if (listener != null){
//回調(diào)onError方法
listener.onError(e);
}
}finally {
if (connection != null){
connection.disconnect();
}
}
}
}).start();
}
}
-
sendHttpRequest()注意點(diǎn):HttpCallbackListener參數(shù);內(nèi)部開(kāi)啟子線程,子線程中進(jìn)行具體的網(wǎng)絡(luò)操作;
子線程中是無(wú)法通過(guò)return語(yǔ)句來(lái)返回?cái)?shù)據(jù)的,
因此這里將服務(wù)器響應(yīng)的數(shù)據(jù)
傳入了HttpCallbackListener的onFinish()方法中,
在調(diào)用者(調(diào)用公共類(lèi)方法者)處的接口(匿名)實(shí)現(xiàn)類(lèi)中處理,
調(diào)用剛剛說(shuō)的在外部(調(diào)用者處)
實(shí)現(xiàn)好的接口(匿名)實(shí)現(xiàn)類(lèi)實(shí)例中的具體的onFinish()方法;
將異常原因
傳入了HttpCallbackListener的onError()方法中,
在調(diào)用者(調(diào)用公共類(lèi)方法者)處的接口(匿名)實(shí)現(xiàn)類(lèi)中處理,
調(diào)用剛剛說(shuō)的在外部(調(diào)用者處)
實(shí)現(xiàn)好的接口(匿名)實(shí)現(xiàn)類(lèi)實(shí)例中的具體的onError()方法;
公共類(lèi)調(diào)用案例:(如上所述)利用回調(diào)機(jī)制將響應(yīng)數(shù)據(jù)成功返回給調(diào)用方:
String address = "http://www/baidu.com";
HttpUtil.sendHttpRequest(address, new HttpCallbackListener(){
@Override
public void onFinish(String response){
//在這里根據(jù)返回內(nèi)容執(zhí)行具體的邏輯
}
@Override
public void onError(Exception e){
//在這里對(duì)異常情況進(jìn)行處理
}
});
使用HttpURLConnection的寫(xiě)法總體比較復(fù)雜;
使用OkHttp會(huì)簡(jiǎn)單一些;在HttpUtil中加入一個(gè)
sendOkHttpRequest()方法:
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);
}
- 注意:
方法中有一個(gè)okhttp3.Callback參數(shù),
這個(gè)是OkHttp庫(kù)中自帶的一個(gè)回調(diào)接口,
類(lèi)似于我們剛剛自己編寫(xiě)的HttpCallbackListener;
然后在client.newCall()之后,
沒(méi)有像之前那樣直接調(diào)用execute(),
而是調(diào)用了enqueue(),
并把okhttp3.Callback參數(shù)傳入,
OkHttp在enqueue()中已經(jīng)幫我們開(kāi)好了子線程,
在子線程中去執(zhí)行HTTP請(qǐng)求,
并將最后的請(qǐng)求結(jié)果回調(diào)到okhttp3.Callback,
(也就是說(shuō),
我們剛剛在sendHttpRequest()做的事情,
子線程、請(qǐng)求、數(shù)據(jù)返回,
OkHttp都幫我們做好了)
最后,
我們?cè)谕獠繉?shí)例化一個(gè)接口對(duì)象并具體實(shí)現(xiàn)方法,
再把接口實(shí)例傳進(jìn)來(lái)sendOkHttpRequest(),
賦值給對(duì)應(yīng)的enqueue()方法,
完成任務(wù)!
參考自《第一行代碼》
