Android學習筆記——網路技術
參考書籍:Android第一行程式碼(第二版).郭霖著
在手機端使用HTTP協議和伺服器端進行網路互動,並對伺服器端返回的資料進行解析。這是Android最常使用的網路技術。
1、WebView
藉助WebView控制元件,可在應用程式中嵌入一個瀏覽器。
新建一個WebViewTest專案,修改佈局檔案:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
修改主程式:
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);//getSettiongs()用於設定一些瀏覽器屬性,這裡讓WebView支援JavaScript指令碼
webView.setWebViewClient(new WebViewClient());//當需要從一個網頁跳轉到另一個網頁是,希望目標網頁仍然在當前WebView顯示,而不是開啟瀏覽器
webView.loadUrl("http://www.baidu.com");
}
}
還需宣告訪問網路的許可權:
<uses-permission android:name="android.permission.INTERNET"/>
在開始執行之前,需保證模擬器(電腦正常上網)或手機聯網。
點選連結還可瀏覽更多網頁。
2、使用HTTP協議訪問網路
原理:客戶端向伺服器發出一條HTTP請求,伺服器接收到請求後會返回一些資料給客戶端,客戶端再對這些資料進行解析和處理就行了。
WebView已經在後臺處理好了傳送HTTP請求、接收服務響應、解析返回資料及頁面展示這幾步工作,封裝得很好。需手動傳送HTTP請求的方式進行深入理解。
(1)使用HttpURLConnection
Android6.0之前傳送HTTP請求一般有兩種方式:HttpURLConnection和HttpClient(API數量過多、擴充套件困難等缺點)。Android6.0中HttpClient功能被完全移除。
HttpURLConnection用法:
a、獲取HttpURLConnection例項。new一個URL物件,傳入目標網路地址,呼叫openConnection()方法:
URL url = new URL("http://www.baidu.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
b、設定HTTP請求所使用的方法。常用兩種:GET(希望從伺服器獲取資料)和POST(希望提交資料給伺服器)。寫法:
connection.setRequestMethod("GET");
c、進行自由定製,如設定連線超時、讀取超市的毫秒數,及伺服器希望得到的一些訊息頭等。如:
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
d、呼叫getInputStream()獲取伺服器返回的輸入流,讀取輸入流:
InputStream in = connection.getInputStream();
e、呼叫disconnect()方法關閉HTTP連線。如:
connection.disconnect();
例,新建一個NetworkTest專案,修改佈局檔案:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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>
修改主程式:
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){
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.360.cn/");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
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();
}
}
}
}).start();
}
private void showResponse(final String response){
//Android不允許在子執行緒中進行UI操作,需通過此方法將執行緒切換到主執行緒,再更新UI元素
runOnUiThread(new Runnable() {
@Override
public void run() {
//在這裡進行UI操作,將結果顯示到介面上
responseText.setText(response);
}
});
}
}
同樣需新增訪問網路許可權宣告。執行程式,點選按鈕:
注:如網址不正確,會出現NetworkSecurityConfig: No Network Security Config specified, using platform default的提示。
伺服器返回的就是這種HTML程式碼,只是通常瀏覽器會將程式碼解析成漂亮的網頁後再展示。
如果想提交資料給伺服器,只需將HTTP請求方法改成POST,並在獲取輸入流之前把要提交的資料寫出即可。每條資料都要以鍵值對的形式存在,資料與資料之間用“&”符號隔開,如提交使用者名稱和密碼:
connection.setRequestMethod("POST");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456");
(2)使用OkHttp
使用之前,需新增OkHttp庫依賴,開啟app/buid.gradle,在dependencies閉包中新增如下內容:
compile 'com.squareup.okhttp3:okhttp:3.8.1'
新增此依賴,會自動下載兩個庫:OkHttp庫、Okio庫(是前者的通訊基礎)。
用法:
a、建立OkHttpClient例項,如下:
OkHttpClient client = new OkHttpClient();
b、如想發起一條HTTP請求,需建立Request物件:
Request request = new Request.Builder().url("http://www.baidu.com").build();//build()方法之前可連綴很多其他方法豐富此Request物件
c、呼叫OkHttpCilent的newCall()方法建立一個Call物件,並呼叫它的execute()方法傳送請求並獲取伺服器返回的資料:
Response response = client.newCall(request).execute();//Response物件就是伺服器返回的資料
String responseData = response.body().string();//得到返回的具體內容
d、如果發起一條POST請求,需先構建RequestBody物件存放待提交的引數:
RequestBody requestBody = new FormBody.Builder().add("username", "admin").add("password", "123456").build();
然後在Request.Builder中呼叫post()方法,並傳入RequestBody物件:
Request request = new Request.Builder().url("http://www.baidu.com").post(requestBody).build();
接下來呼叫execute()方法傳送請求並獲取伺服器返回的資料即可。
修改MainActivity:
...
public void onClick(View v) {
if (v.getId() == R.id.send_request){
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.360.cn/").build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
showResponse(responseData);//將伺服器返回的資料顯示到介面上
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
...
重新執行程式,點選按鈕:
與HttpURLConnection實現的功能一樣。
3、解析XML格式資料
通常,每個需要訪問網路的應用程式都會有一個自己的伺服器。在網路上傳輸資料最常用的格式有兩種:XML和JSON。
進入…\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.0</version>
</app>
<app>
<id>3</id>
<name>Google Play</name>
<version>2.3</version>
</app>
</apps>
(1)Pull解析方式
解析XML格式資料有很多方式,Pull和SAX解析是常用的兩種。
修改MainActivity:
...
public void onClick(View v) {
if (v.getId() == R.id.send_request){
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").build();//http://10.0.2.2/對於模擬器來說是電腦本機IP地址**
Response response = client.newCall(request).execute();
String responseData = response.body().string();
**parseXMLWithPull(responseData);//解析伺服器返回的資料**
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private void **parseXMLWithPull**(String xmlData){
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = factory.newPullParser();
xmlPullParser.setInput(new StringReader(xmlData));
int eventType = xmlPullParser.getEventType();//得到當前解析事件
String id = "";
String name = "";
String version = "";
while (eventType != XmlPullParser.END_DOCUMENT){
//解析事件不為XmlPullParser.END_DOCUMENT,說明解析工作沒完成
String nodeName = xmlPullParser.getName();//獲取當前結點名字
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;
//完成解析某個節點
case XmlPullParser.END_TAG:
if ("app".equals(nodeName)){
//解析完一個app節點後列印獲取到的內容
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();
}
}
...
執行程式,點擊發送按鈕,檢視日誌:
成功解析。
(2)SAX解析方式
比Pull解析複雜一些,但予以更清楚。
通常會新建一個類繼承子DefaultHandler,並重寫父類5個方法。
新建ContentHandler類繼承自DefaultHandler(org.xml.sax.helpers):
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;
}
//在獲取節點內容時呼叫,會被呼叫多次
@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());//id\name\version中可能包含回車或換行符,需呼叫trim()方法除去
Log.d("ContentHandler", "name is " + name.toString().trim());
Log.d("ContentHandler", "version is " + version.toString().trim());
//最後要將StringBuilder清空,避免影響下一次內容讀取
id.setLength(0);
name.setLength(0);
version.setLength(0);
}
}
//完成整個XML解析時呼叫
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}
修改MainActivity:
...
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").build();//http://10.0.2.2/對於模擬器來說是電腦本機IP地址
Response response = client.newCall(request).execute();
String responseData = response.body().string();
**parseXMLWithSAX(responseData);//解析伺服器返回的資料**
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
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();
}
}
...
執行程式,點選按鈕,檢視日誌:
解析結果一樣。還有DOM解析方式可用。
4、解析JSON資料
JSON的體積比XML更小,網路傳輸更省流量,但語義性差不如XML直觀。
在…\Apache\htdocs目錄中新建一個get_data.json檔案:
[{"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"}]
(1)使用JSONObject
解析JSON資料也有很多方法,可使用官方的JSONObject,谷歌的開源庫GSON,或第三方的開源庫如Jackson、FastJSON等.
使用JSONObject,修改MainActivity:
...
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.json").build();//http://10.0.2.2/對於模擬器來說是電腦本機IP地址**
Response response = client.newCall(request).execute();
String responseData = response.body().string();
**parseJSONWithJSONObject(responseData);**
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private void parseJSONWithJSONObject(String jsonData){
try {
JSONArray jsonArray = new JSONArray(jsonData);//伺服器中定義的時JSON陣列
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");
Log.d("MainActivity", "id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
}
} catch (Exception e) {
e.printStackTrace();
}
}
...
執行程式,會看到日誌資訊。
(2)使用GSON
更簡單。由於其沒被新增到Android官方API中,所以要使用它必須在專案中新增GSON庫依賴:
compile 'com.google.code.gson:gson:2.7'
它主要可以將一段JSON格式的字串自動對映成一個物件(定義一個類對應),不需手動編寫程式碼解析。
如果需要解析的是一段JSON陣列,需要藉助TypeToken將期望解析成的資料型別傳入到fromJson()方法中,如:
List<Person> people = gson.fromJson(jsonData, new TypeToken<List<Person>>(){}.getType());
例,首先新建一個App類:
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:
...
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.json").build();//http://10.0.2.2/對於模擬器來說是電腦本機IP地址
Response response = client.newCall(request).execute();
String responseData = response.body().string();
**parsJSONWithGSON(responseData);**
} catch (Exception 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){
Log.d("MainActivity", "id is " + app.getId());
Log.d("MainActivity", "name is " + app.getName());
Log.d("MainActivity", "version is " + app.getVersion());
}
}
...
執行程式,效果一樣。
5、網路程式設計最佳實踐
應用程式很可能會在許多地方都使用網路功能,而傳送HTTP請求的程式碼基本相同,所以通常應該把通用的網路操作提取到一個公共類例,並提供一個靜態方法。考慮到這種耗時的操作需開啟子執行緒完成且能實時接收到返回的資料,這裡使用Java的回撥機制來完成資料的獲取:
定義一個HttpCallbackListener介面:
public interface HttpCallbackListener {
void onFinish(String response);//當伺服器成功響應請求時呼叫,引數為伺服器返回的資料
void onError(Exception e);//當進行網路操作出現錯誤時呼叫,引數記錄錯誤的詳細資訊
}
接著新建一個類(通用網路操作的公共類):
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介面引數)。
在呼叫時還需傳入HttpCallbackListener例項:
HttpUtil.sendHttpRequest(address, new HtpCallbackListener(){
@Override
public void onFinish(String response){
//在這裡根據返回內容執行具體的邏輯
}
@Override
public void onError(Exception e){
//在這裡對異常情況進行處理
}
});
利用回撥機制將響應資料成功返回給呼叫方。
如果使用OkHttp,在HttpUtil中加入一個sendOkHttpRequest()方法:
public class 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);//enqueue方法內部已經開好了子執行緒
}
}
呼叫sendOkHttpRequest()方法:
HttpUtil.sendOkHttpRequest("https://www.baidu.com", new okhttp3.Callback(){
@Override
public void onResponse(Call call, Response response) throws IOException{
//得到伺服器返回的具體資料
String reponseData = reponse.body().string();
}
@Override
public void onFailure(Call call, IOException e){
//在這裡對異常情況進行處理
}
});
不管使用HttpURLConnection還是OkHttp,最終的回撥介面都還在子執行緒中執行,因此不可以在這裡執行任何的UI操作,除非藉助runOnUiThread()方法進行執行緒轉換。