主頁 > 移動端開發 > 《第一行代碼:Android篇》學習筆記(九)

《第一行代碼:Android篇》學習筆記(九)

2022-05-12 08:31:03 移動端開發

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了,
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以后閱讀和查閱,最后,感激感激郭霖先生提供這么好的書籍,

第9章 看看精彩的世界——使用網路技術

現在早已不是玩單機的時代了,無論是PC、手機、平板,還是電視,幾乎都會具備上網的功能,在可預見的未來,手表、眼鏡、汽車等設備也會逐個加入到這個行列,21世紀的確是互聯網的時代,

當然,Android手機肯定也是可以上網的,所以作為開發者,我們就需要考慮如何利用網路來撰寫出更加出色的應用程式,像QQ、微博、微信等常見的應用都會大量使用網路技術,

本章主要會講述如何在手機端使用HTTP協議和服務器端進行網路互動,并對服務器回傳的資料進行決議,這也是Android中最常使用到的網路技術,下面就讓我們一起來學習一下吧,

9.1 WebView的用法

比如說,要求在應用程式里展示一些網頁,加載和顯示網頁通常都是瀏覽器的任務,但是需求里又明確指出,不允許打開系統瀏覽器,而我們當然也不可能自己去撰寫一個瀏覽器出來,這時應該怎么辦呢?

Android早就已經考慮到了,并提供了一個WebView控制元件,借助它就可以在自己的應用程式里嵌入一個瀏覽器,從而非常輕松地展示各種各樣的網頁,WebView的用法也是相當簡單,下面我們就通過一個例子來學習一下吧,新建一個WebViewTest專案,然后修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_height="match_parent">
    <WebView
        android:layout_
        android:layout_height="match_parent"
        android:id="@+id/web_view"/>
</LinearLayout>

在布局檔案中使用到了一個新的控制元件:WebView,這個控制元件用來顯示網頁的,給它設定了一個id,并讓它充滿整個螢屏,然后修改MainActivity中的代碼,如下所示:

package com.zhouzhou.webviewtest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

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("https://www.cnblogs.com/1693977889zz/");
    }
}

MainActivity中,首先使用findViewById()方法獲取到了WebView的實體,然后呼叫WebView的getSettings()方法可以去設定一些瀏覽器的屬性,這里只是呼叫了setJavaScriptEnabled()方法來讓WebView支持JavaScript腳本,

接下來,呼叫了WebView的setWebViewClient()方法,并傳入了一個WebViewClient的實體,這段代碼的作用是,當需要從一個網頁跳轉到另一個網頁時,我們希望目標網頁仍然在當前WebView中顯示,而不是打開系統瀏覽器,

最后一步,呼叫WebView的loadUrl()方法,并將網址傳入,即可展示相應網頁的內容,

另外還需要注意,由于本程式使用到了網路功能,而訪問網路是需要宣告權限的,因此我們還得修改AndroidManifest.xml檔案,并加入權限宣告,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.webviewtest">
    <uses-permission android:name="android.permission.INTERNET"/>
    ...
</manifest>

在開始運行之前,首先需要保證你的手機或模擬器是聯網的,如果你使用的是模擬器,只需保證電腦能正常上網即可,然后就可以運行一下程式了,效果如圖:

image

可以看到,WebViewTest這個程式現在已經具備了一個簡易瀏覽器的功能,不僅成功將我的博客展示了出來,還可以通過點擊鏈接瀏覽更多的網頁,

當然,WebView還有很多更加高級的使用技巧,就不再繼續進行探討了,這里先介紹了一下WebView的用法,只是希望你能對HTTP協議的使用有一個最基本的認識,接下來就要利用這個協議來做一些真正的網路開發作業,

9.2 使用HTTP協議訪問網路

如果說真的要去深入分析HTTP協議,可能需要花費整整一本書的篇幅,這里你只需要稍微了解一些就足夠了,它的作業原理特別簡單,就是客戶端向服務器發出一條HTTP請求,服務器收到請求之后會回傳一些資料給客戶端,然后客戶端再對這些資料進行決議和處理就可以了,

比如,上一節中使用到的WebView控制元件,其實也就是我們向服務器發起了一條HTTP請求,接著服務器分析出我們想要訪問的頁面,于是會把該網頁的HTML代碼進行回傳,然后WebView再呼叫手機瀏覽器的內核對回傳的HTML代碼進行決議,最終將頁面展示出來,

簡單來說,WebView已經在后臺幫我們處理好了發送HTTP請求、接收服務回應、決議回傳資料,以及最終的頁面展示這幾步作業,不過由于它封裝得實在是太好了,反而使得我們不能那么直觀地看出HTTP協議到底是如何作業的,因此,接下來就讓我們通過手動發送HTTP請求的方式,來更加深入地理解一下這個程序,

9.2.1 使用HttpURLConnection

在過去,Android上發送HTTP請求一般有兩種方式:HttpURLConnection和HttpClient,不過由于HttpClient存在API數量過多、擴展困難等缺點,Android團隊越來越不建議我們使用這種方式,

終于在Android 6.0系統中,HttpClient的功能被完全移除了,標志著此功能被正式棄用,因此本小節我們就學習一下現在官方建議使用的HttpURLConnection的用法,

首先需要獲取到HttpURLConnection的實體,一般只需new出一個URL物件,并傳入目標的網路地址,然后呼叫一下openConnection()方法即可,如下所示:

URL url = new URL("http://baidu.com");
HttpURLConnection connection = (HttpURLConnection)url.openConnection();

在得到了HttpURLConnection的實體之后,可以設定一下HTTP請求所使用的方法,常用的方法主要有兩個:GET和POST,GET表示希望從服務器那里獲取資料,而POST則表示希望提交資料給服務器,寫法如下:

connection.setRequestMethod("GET");

接下來,就可以進行一些自由的定制了,比如設定連接超時、讀取超時的毫秒數,以及服務器希望得到的一些訊息頭等,這部分內容根據自己的實際情況進行撰寫,示例寫法如下:

connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);

之后再呼叫getInputStream()方法就可以獲取到服務器回傳的輸入流了,剩下的任務就是對輸入流進行讀取,如下所示:

InputStream in = connection.getInputStream();

最后,可以呼叫disconnect()方法將這個HTTP連接關閉掉,如下所示:

connection.disconnect();

下面通過一個具體的例子來真正體驗一下HttpURLConnection的用法,新建一個NetworkTest專案,首先修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/send_request"
        android:text="Send Request"/>
    <ScrollView
        android:layout_
        android:layout_height="match_parent">
        <TextView
            android:layout_
            android:layout_height="wrap_content"
            android:id="@+id/response_text"/>
    </ScrollView>
</LinearLayout>

使用了一個新的控制元件:ScrollView,它是用來做什么的呢?

由于手機螢屏的空間一般都比較小,有些時候過多的內容一屏是顯示不下的,借助ScrollView控制元件的話,我們就可以以滾動的形式查看螢屏外的那部分內容,另外,布局中還放置了一個Button和一個TextView, Button用于發送HTTP請求,TextView用于將服務器回傳的資料顯示出來,接著修改MainActivity中的代碼,如下所示:

package com.zhouzhou.networktest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

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 view) {
        if (view.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://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面對獲取到的輸入流進行讀取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在這里進行Ui操作,將結果顯示到界面
                responseText.setText(response);
            }
        });
    }
}

可以看到,在Send Request按鈕的點擊事件里呼叫了sendRequestWithHttpURLConnection()方法,在這個方法中先是開啟了一個子執行緒,然后在子執行緒里使用HttpURLConnection發出一條HTTP請求,請求的目標地址就是百度的首頁,

接著利用BufferedReader對服務器回傳的流進行讀取,并將結果傳入到了showResponse()方法中,而在showResponse()方法里則是呼叫了一個runOnUiThread()方法,然后在這個方法的匿名類引數中進行操作,將回傳的資料顯示到界面上,

那么這里為什么要用這個runOnUiThread()方法呢?這是因為Android是不允許在子執行緒中進行UI操作的,我們需要通過這個方法將執行緒切換到主執行緒,然后再更新UI元素,

關于這部分內容,我們將會在下一章中進行詳細講解,現在你只需要記得必須這么寫就可以了,完整的一套流程就是這樣,不過在開始運行之前,仍然別忘了要宣告網路權限,修改AndroidManifest.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.networktest">
    <uses-permission android:name="android.permission.INTERNET"/>
    ...
</manifest>

好了,現在運行一下程式,并點擊Send Request按鈕,結果如圖:

image

服務器回傳給我們的就是這種HTML代碼,只是通常情況下瀏覽器都會將這些代碼決議成漂亮的網頁后再展示出來,

那么如果是想要提交資料給服務器應該怎么辦呢?只需要將HTTP請求的方法改成POST,并在獲取輸入流之前把要提交的資料寫出即可,注意每條資料都要以鍵值對的形式存在,資料與資料之間用“&”符號隔開,比如說我們想要向服務器提交用戶名和密碼,就可以這樣寫:

connection.setRequestMethod("POST");
DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()) ;
outputStream.writeBytes("username=admin&&password=123456");

9.2.2 使用OkHttp

當然,我們并不是只能使用HttpURLConnection,完全沒有任何其他選擇,事實上在開源盛行的今天,有許多出色的網路通信庫都可以替代原生的HttpURLConnection,而其中OkHttp無疑是做得最出色的一個,

OkHttp是由鼎鼎大名的Square公司開發的,這個公司在開源事業上面貢獻良多,除了OkHttp之外,還開發了像Picasso、Retrofit等著名的開源專案,

OkHttp不僅在介面封裝上面做得簡單易用,就連在底層實作上也是自成一派,比起原生的HttpURLConnection,可以說是有過之而無不及,現在已經成了廣大Android開發者首選的網路通信庫,

那么本小節我們就來學習一下OkHttp的用法,OkHttp的專案主頁地址是:https://github.com/square/okhttp,

See the project website for documentation and APIs.

HTTP is the way modern applications network. It’s how we exchange data & media. Doing HTTP efficiently makes your stuff load faster and saves bandwidth.

OkHttp is an HTTP client that’s efficient by default:

  • HTTP/2 support allows all requests to the same host to share a socket.
  • Connection pooling reduces request latency (if HTTP/2 isn’t available).
  • Transparent GZIP shrinks download sizes.
  • Response caching avoids the network completely for repeat requests.

OkHttp perseveres when the network is troublesome: it will silently recover from common connection problems. If your service has multiple IP addresses, OkHttp will attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6 and services hosted in redundant data centers. OkHttp supports modern TLS features (TLS 1.3, ALPN, certificate pinning). It can be configured to fall back for broad connectivity.

Using OkHttp is easy. Its request/response API is designed with fluent builders and immutability. It supports both synchronous blocking calls and async calls with callbacks.

在使用OkHttp之前,我們需要先在專案中添加OkHttp庫的依賴,編輯app/build.gradle檔案,在dependencies閉包中添加如下內容:

    dependencies {
       implementation("com.squareup.okhttp3:okhttp:4.9.3")
    }

添加上述依賴會自動下載兩個庫,一個是OkHttp庫,一個是Okio庫,后者是前者的通信基礎,

image

下面我們來看一下OkHttp的具體用法,首先需要創建一個OkHttpClient的實體,如下所示:

OkHttpClient client = new OkHttpClient();

接下來,如果想要發起一條HTTP請求,就需要創建一個Request物件:

Request request = new Request.Builder().build();

當然,上述代碼只是創建了一個空的Request物件,并沒有什么實際作用,我們可以在最終的build()方法之前連綴很多其他方法來豐富這個Request物件,比如可以通過url()方法來設定目標的網路地址,如下所示:

Request request = new Request.Builder()
      .url("https://www.baidu.com")
      .build();

之后,呼叫OkHttpClient的newCall()方法來創建一個Call物件,并呼叫它的execute()方法來發送請求并獲取服務器回傳的資料,寫法如下:

Response response = client.newCall(request).execute();

其中,Response物件就是服務器回傳的資料了,我們可以使用如下寫法來得到回傳的具體內容:

String responseData = https://www.cnblogs.com/1693977889zz/p/response.body().string();

如果是發起一條POST請求會比GET請求稍微復雜一點,我們需要先構建出一個RequestBody物件來存放待提交的引數,如下所示:

RequestBody requestBody = new FormBody.Builder()
      .add("username","admin")
      .add("password","123456")
      .build();

然后,在Request.Builder中呼叫一下post()方法,并將RequestBody物件傳入:

Request request = new Request.Builder()
      .url("https://www.baidu.com")
      .post(requestBody)
      .build();

接下來的操作就和GET請求一樣了,呼叫execute()方法來發送請求并獲取服務器回傳的資料即可,

書中后面所有網路相關的功能我們都將會使用OkHttp來實作,到時候再進行進一步的學習,那么現在我們先把NetworkTest這個專案改用OkHttp的方式再實作一遍吧,由于布區域分完全不用改動,所以現在直接修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    ...
    @Override
    public void onClick(View view) {
        if (view.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://www.baidu.com")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = https://www.cnblogs.com/1693977889zz/p/response.body().string();
                    showResponse(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
	...
}

這里并沒有做太多的改動,只是添加了一個sendRequestWithOkHttp()方法,并在Send Request按鈕的點擊事件里去呼叫這個方法,在這個方法中同樣還是先開啟了一個子執行緒,然后在子執行緒里使用OkHttp發出一條HTTP請求,請求的目標地址還是百度的首頁,OkHttp的用法也正如前面所介紹的一樣,

最后仍然還是呼叫了showResponse()方法來將服務器回傳的資料顯示到界面上,重新運行一下程式了,點擊SendRequest按鈕后,測驗結果OK,由此證明,使用OkHttp來發送HTTP請求的功能也已經成功實作了,

9.3 決議XML格式資料

通常情況下,每個需要訪問網路的應用程式都會有一個自己的服務器,我們可以向服務器提交資料,也可以從服務器上獲取資料,

不過這個時候就出現了一個問題,這些資料到底要以什么樣的格式在網路上傳輸呢?隨便傳遞一段文本肯定是不行的,因為另一方根本就不會知道這段文本的用途是什么,因此,一般我們都會在網路上傳輸一些格式化后的資料,這種資料會有一定的結構規格和語意,當另一方收到資料訊息之后就可以按照相同的結構規格進行決議,從而取出他想要的那部分內容,在網路上傳輸資料時最常用的格式有兩種:XML和JSON,下面我們就來一個一個地進行學習,本節首先學習一下如何決議XML格式的資料,

在開始之前我們還需要先解決一個問題,就是從哪兒才能獲取一段XML格式的資料呢?

這里我準備教你搭建一個最簡單的Web服務器,在這個服務器上提供一段XML文本,然后我們在程式里去訪問這個服務器,再對得到的XML文本進行決議,

搭建Web服務器其實非常簡單,有很多的服務器型別可供選擇,這里我準備使用Apache服務器,首先你需要去下載一個Apache服務器的安裝包,官方下載地址是:http://httpd.apache.org/download.cgi,如果你在這個網址中找不到Windows版的安裝包,也可以直接在百度上搜索“Apache服務器下載”,將會找到很多下載鏈接,

下面簡介,官方下載步驟:

  1. 點擊鏈接 a number of third party vendors.:

image

  1. 找到Downloading Apache for Windows,點擊ApacheHaus鏈接:

image

  1. 點擊紅框中的圖示即可開始下載,x86是32位的,x64是64位的,根據自己的作業系統選擇下載:

image

  1. 點擊之后跳轉到下載頁面,Your download will start shortly...

image

  1. 經常會超時哦~所以,我在網上下載的【Apache HTTP Server 官方版 v2.4.17】,保存到想要保存的位置(我保存到了:F:\ApacheServer),解壓壓縮包,打開httpd.conf檔案(我在F:\ApacheServer\conf),修改Apache安裝目錄,(其中“${SRVROOT}”指定義的SRVROOT路徑變數):

image

  1. 若80埠被占用(可在cmd下用命令netstat -an -o | findstr 80),則將80埠改為別的保存:

image

  1. 以管理員身份運行cmd,復制輸入"F:\ApacheServer\bin\httpd.exe" -k install -n apache,該命令意思是安裝這個目錄(F:\ApacheServer\bin\)下的Apache服務,并將該服務命名為“apache”,

    也可以,以管理員身份運行cmd,進入到F:\ApacheServer\bin目錄下輸入 httpd.exe -k install -n apache安裝apache服務

image

  1. 雙擊運行bin目錄下的ApacheMonitor.exe,也可通過服務打開:

image

  1. 雙擊打開視窗界面,會看到如圖所示:

image
10. 點擊 start,測驗Apache服務器是否可用,在瀏覽器中輸入http://localhost:8099,(80埠被占用,修改成了8099)出現下面頁面,表示安裝配置成功!

image

接下來進入到F:\ApacheServer\htdocs目錄下,在這里新建一個名為get_data.xml的檔案,然后編輯這個檔案,并加入如下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:8099/get_data.xml這個網址,就應該出現如圖:

image

準備作業到此結束,接下來就讓我們在Android程式里去獲取并決議這段XML資料吧,

9.3.1 Pull決議方式

決議XML格式的資料其實也有挺多種方式的,本節中我們學習比較常用的兩種,Pull決議和SAX決議,

那么簡單起見,這里仍然是在NetworkTest專案的基礎上繼續開發,這樣我們就可以重用之前網路通信部分的代碼,從而把作業的重心放在XML資料決議上,

既然XML格式的資料已經提供好了,現在要做的就是從中決議出我們想要得到的那部分內容,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.networktest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

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 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 view) {
        if (view.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("http://127.0.0.1:8099/get_data.xml")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = https://www.cnblogs.com/1693977889zz/p/response.body().string();
                    //showResponse(responseData);
                    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) {
                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)) {
                                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 sendRequestWithHttpURLConnection() {
        //開啟執行緒來發起網路請求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面對獲取到的輸入流進行讀取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在這里進行Ui操作,將結果顯示到界面
                responseText.setText(response);
            }
        });
    }
}

可以看到,這里首先是將HTTP請求的地址改成了http://10.0.2.2:8099/get_data.xml,

10.0.2.2對于模擬器來說就是電腦本機的IP地址,如果用:http://127.0.0.1:8099/get_data.xml或者http://localhost:8099/get_data.xml 的話,報出的錯誤資訊是:java.net.ConnectException: failed to connect to /127.0.0.1 (port 8099) after 2500ms: isConnected failed: ECONNREFUSED (Connection refused)

image

另外,可能報錯:W/System.err: java.net.UnknownServiceException:
CLEARTEXT communication 你的域名 not permitted by network security policy.

image

此時的報錯原因是,網路安全策略不允許:

為保證用戶資料和設備的安全,Google針對下一代 Android 系統(Android P) 的應用程式,將要求默認使用加密連接,這意味著 Android P 將禁止 App 使用所有未加密的連接,因此運行 Android P 系統的安卓設備無論是接識訓者發送流量,未來都不能明碼傳輸,需要使用下一代(Transport Layer Security)傳輸層安全協議,而 Android Nougat 和 Oreo 則不受影響,

在Android P系統的設備上,如果應用使用的是非加密的明文流量的http網路請求,則會導致該應用無法進行網路請求,https則不會受影響,同樣地,如果應用嵌套了webview,webview也只能使用https請求,

解決辦法:

(1)APP改用https請求

(2)targetSdkVersion 降到27以下(此問題發生在 API>=27 的新專案工程)

(3)在 application 元素中添加:

android:usesCleartextTraffic="true"

更規范一點是:

在res目錄下新建xml目錄,在xml目錄下新增 network_security_config.xml 檔案,內容:

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <base-config cleartextTrafficPermitted="true" />
    </network-security-config>

再在 AndroidManifest.xml 的 Application 標簽中新增屬性:

android:networkSecurityConfig="@xml/network_security_config" 

報錯全部解決,程式運行OK,繼續,

在得到了服務器回傳的資料后,我們并不再直接將其展示,而是呼叫了parseXMLWithPull()方法來決議服務器回傳的資料,parseXMLWithPull()方法中的代碼,首先要獲取到一個XmlPullParserFactory的實體,并借助這個實體得到XmlPullParser物件,然后呼叫XmlPullParser的setInput()方法將服務器回傳的XML資料設定進去就可以開始決議了,決議的程序也非常簡單,通過getEventType()可以得到當前的決議事件,然后在一個while回圈中不斷地進行決議,如果當前的決議事件不等于XmlPullParser.END_DOCUMENT,說明決議作業還沒完成,呼叫next()方法后可以獲取下一個決議事件,

在while回圈中,我們通過getName()方法得到當前節點的名字,如果發現節點名等于id、name或version,就呼叫nextText()方法來獲取節點內具體的內容,每當決議完一個app節點后就將獲取到的內容列印出來,好了

整體的程序就是這么簡單,下面就讓我們來測驗一下吧,運行NetworkTest專案,然后點擊Send Request按鈕,觀察logcat中的列印日志,如圖:

image

9.3.2 SAX決議方式

Pull決議方式雖然非常好用,但它并不是我們唯一的選擇,SAX決議也是一種特別常用的XML決議方式,雖然它的用法比Pull決議要復雜一些,但在語意方面會更加清楚,通常情況下我們都會新建一個類繼承自DefaultHandler,并重寫父類的5個方法,如下所示:

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()方法會在開始XML決議的時候呼叫;

startElement()方法會在開始決議某個節點的時候呼叫;

characters()方法會在獲取節點中內容的時候呼叫;

endElement()方法會在完成決議某個節點的時候呼叫;

endDocument()方法會在完成整個XML決議的時候呼叫,

其中,startElement()、characters()和endElement()這3個方法是有引數的,從XML中決議出的資料就會以引數的形式傳入到這些方法中,

需要注意的是,在獲取節點中的內容時,characters()方法可能會被呼叫多次,一些換行符也被當作內容決議出來,我們需要針對這種情況在代碼中做好控制,

那么下面就讓我們嘗試用SAX決議的方式來實作和上一小節中同樣的功能吧,新建一個ContentHandler類繼承自DefaultHandler,并重寫父類的5個方法,如下所示:

package com.zhouzhou.networktest;

import android.util.Log;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class ContentHandler extends DefaultHandler {
    private String nodeName;
    private StringBuilder id;
    private StringBuilder name;
    private StringBuilder version;
    
    // startDocument()方法會在開始XML決議的時候呼叫;
    @Override
    public void startDocument() throws SAXException {
        super.startDocument();
        id = new StringBuilder();
        name = new StringBuilder();
        version = new StringBuilder();
    }
    
    // startElement()方法會在開始決議某個節點的時候呼叫;
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        super.startElement(uri, localName, qName, attributes);
        //記錄當前節點名
        nodeName = localName;
    }
    
    // characters()方法會在獲取節點中內容的時候呼叫;
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        super.characters(ch, start, length);
        // 根據當前的節點名判斷將內容添加到哪一個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);
        }
    }
    
    // endElement()方法會在完成決議某個節點的時候呼叫;
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        super.endElement(uri, localName, qName);
        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清空掉
            id.setLength(0);
            name.setLength(0);
            version.setLength(0);
        }
    }
    
    // endDocument()方法會在完成整個XML決議的時候呼叫,
    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }
}

可以看到,我們首先給id、name和version節點分別定義了一個StringBuilder物件,并在startDocument()方法里對它們進行了初始化,

每當開始決議某個節點的時候,startElement()方法就會得到呼叫,其中localName引數記錄著當前節點的名字,這里我們把它記錄下來,

接著在決議節點中具體內容的時候就會呼叫characters()方法,我們會根據當前的節點名進行判斷,將決議出的內容添加到哪一個StringBuilder物件中,

最后在endElement()方法中進行判斷,如果app節點已經決議完成,就列印出id、name和version的內容,

需要注意的是,目前id、name和version中都可能是包括回車或換行符的,因此在列印之前我們還需要呼叫一下trim()方法,并且列印完成后還要將StringBuilder的內容清空掉,不然的話會影響下一次內容的讀取,接下來的作業就非常簡單了,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.networktest;

import androidx.appcompat.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 view) {
        if (view.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()
                            //指定訪問的服務器地址是本機電腦
                            // 模擬器默認把127.0.0.1和localhost當做本身了,在模擬器上可以用10.0.2.2代替127.0.0.1和localhost
                            .url("http://10.0.2.2:8099/get_data.xml")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = https://www.cnblogs.com/1693977889zz/p/response.body().string();
                    //showResponse(responseData);
                    //parseXMLWithPull(responseData);
                    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();
        }
    }

    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) {
                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)) {
                                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 sendRequestWithHttpURLConnection() {
        //開啟執行緒來發起網路請求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面對獲取到的輸入流進行讀取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在這里進行Ui操作,將結果顯示到界面
                responseText.setText(response);
            }
        });
    }
}

在得到了服務器回傳的資料后,我們這次去呼叫parseXMLWithSAX()方法來決議XML資料,parseXMLWithSAX()方法中先是創建了一個SAXParserFactory的物件,然后再獲取到XMLReader物件,接著將我們撰寫的ContentHandler的實體設定到XMLReader中,最后呼叫parse()方法開始執行決議就好了,

現在重新運行一下程式,點擊Send Request按鈕后觀察logcat中的列印日志,你會看到和上面一樣的結果,

image

除了Pull決議和SAX決議之外,其實還有一種DOM決議方式也算挺常用的,不過這里我們就不再展開進行講解了,感興趣的話你可以自己去查閱一下相關資料,

9.4 決議JSON格式資料

如何決議JSON格式的資料了,比起XML, JSON的主要優勢在于它的體積更小,在網路上傳輸的時候可以更省流量,但缺點在于,它的語意性較差,看起來不如XML直觀,

在開始之前,還需要在F:\ApacheServer\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:8099/get_data.json這個網址,就應該出現如圖:

image

把JSON格式的資料也準備好了,下面就開始學習如何在Android程式中決議這些資料吧,

9.4.1 使用JSONObject

類似地,決議JSON資料也有很多種方法,可以使用官方提供的JSONObject,也可以使用谷歌的開源庫GSON,另外,一些第三方的開源庫如Jackson、FastJSON等也非常不錯,本節中我們就來學習一下前兩種決議方式的用法,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.networktest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.json.JSONArray;
import org.json.JSONObject;
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 view) {
        if (view.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()
                            //指定訪問的服務器地址是本機電腦
                            // 模擬器默認把127.0.0.1和localhost當做本身了,在模擬器上可以用10.0.2.2代替127.0.0.1和localhost
                            .url("http://10.0.2.2:8099/get_data.json")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = https://www.cnblogs.com/1693977889zz/p/response.body().string();
                    //showResponse(responseData);
                    //parseXMLWithPull(responseData);
                    //parseXMLWithSAX(responseData);
                    parseJSONWithJSONObject(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void parseJSONWithJSONObject(String jsonData) {
        try {
            JSONArray jsonArray = new JSONArray(jsonData);
            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();
        }
    }

    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();
        }
    }

    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) {
                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)) {
                                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 sendRequestWithHttpURLConnection() {
        //開啟執行緒來發起網路請求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面對獲取到的輸入流進行讀取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在這里進行Ui操作,將結果顯示到界面
                responseText.setText(response);
            }
        });
    }
}

現在重新運行一下程式,并點擊Send Request按鈕,結果如圖:

image

9.4.2 使用GSON

如果你認為使用JSONObject來決議JSON資料已經非常簡單了,那你就太容易滿足了,谷歌提供的GSON開源庫(https://github.com/google/gson/)可以讓決議JSON資料的作業簡單到讓你不敢想象的地步,那我們肯定是不能錯過這個學習機會的,

不過GSON并沒有被添加到Android官方的API中,因此如果想要使用這個功能的話,就必須要在專案中添加GSON庫的依賴,編輯app/build.gradle檔案,在dependencies閉包中添加如下內容:

dependencies {
  implementation 'com.google.code.gson:gson:2.9.0'
}

那么,GSON庫究竟是神奇在哪里呢?其實它主要就是可以將一段JSON格式的字串自動映射成一個物件,從而不需要我們再手動去撰寫代碼進行決議了,比如說一段JSON格式的資料如下所示:

{"name":"Tom","age":20}

那我們就可以定義一個Person類,并加入name和age這兩個欄位,然后只需簡單地呼叫如下代碼就可以將JSON資料自動決議成一個Person物件了:

Gson gson = new Gson();
Person person = gson.fromJson(jsonData,Person.class);

如果需要決議的是一段JSON陣列會稍微麻煩一點,我們需要借助TypeToken將期望決議成的資料型別傳入到fromJson()方法中,如下所示:

List<Person> appList = gson.fromJson(gsonData,new TypeToken<List<Person>>(){}.getType());

基本的用法就是這樣,下面就讓我們來真正地嘗試一下吧,首先新增一個App類,并加入id、name和version這3個欄位,如下所示:

package com.zhouzhou.networktest;

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中的代碼,如下所示:

package com.zhouzhou.networktest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import org.json.JSONArray;
import org.json.JSONObject;
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 java.util.List;

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 view) {
        if (view.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()
                            //指定訪問的服務器地址是本機電腦
                            // 模擬器默認把127.0.0.1和localhost當做本身了,在模擬器上可以用10.0.2.2代替127.0.0.1和localhost
                            .url("http://10.0.2.2:8099/get_data.json")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = https://www.cnblogs.com/1693977889zz/p/response.body().string();
                    //showResponse(responseData);
                    //parseXMLWithPull(responseData);
                    //parseXMLWithSAX(responseData);
                    //parseJSONWithJSONObject(responseData);
                    parseJSONWithGSON(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void parseJSONWithGSON(String gsonData) {
        Gson gson = new Gson();
        List appList = gson.fromJson(gsonData,new TypeToken>(){}.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());
        }
    }

    private void parseJSONWithJSONObject(String jsonData) {
        try {
            JSONArray jsonArray = new JSONArray(jsonData);
            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();
        }
    }

    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();
        }
    }

    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) {
                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)) {
                                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 sendRequestWithHttpURLConnection() {
        //開啟執行緒來發起網路請求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面對獲取到的輸入流進行讀取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在這里進行Ui操作,將結果顯示到界面
                responseText.setText(response);
            }
        });
    }
}

現在重新運行程式,點擊Send Request按鈕后觀察logcat中的列印日志,測驗OK!

9.5 網路編程的最佳實踐

也許你還沒有發現,之前我們的寫法其實是很有問題的,因為一個應用程式很可能會在許多地方都使用到網路功能,而發送HTTP請求的代碼基本都是相同的,如果我們每次都去撰寫一遍發送HTTP請求的代碼,這顯然是非常差勁的做法,

沒錯,通常情況下我們都應該將這些通用的網路操作提取到一個公共的類里,并提供一個靜態方法,當想要發起網路請求的時候,只需簡單地呼叫一下這個方法即可,比如使用如下的寫法:

package com.zhouzhou.networktest;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HttpUtil {
    public static String sendHttpRequest(String address) {
        HttpURLConnection connection = null;
        try {
            URL url = new URL(address);
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(8000);
            connection.setReadTimeout(8000);
            connection.setDoInput(true);
            connection.setDoOutput(true);
            InputStream inputStream = connection.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            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();
            }
        }
    }
}

以后每當需要發起一條HTTP請求的時候就可以這樣寫:

String address = "http://www.baidu.com"
String response = HttpUtil.sendHttpRequest(address);

在獲取到服務器回應的資料后,我們就可以對它進行決議和處理了,但是需要注意,網路請求通常都是屬于耗時操作,而sendHttpRequest()方法的內部并沒有開啟執行緒,這樣就有可能導致在呼叫sendHttpRequest()方法的時候使得主執行緒被阻塞住,

你可能會說,很簡單嘛,在sendHttpRequest()方法內部開啟一個執行緒不就解決這個問題了嗎?其實沒有你想象中的那么容易,因為如果我們在sendHttpRequest()方法中開啟了一個執行緒來發起HTTP請求,那么服務器回應的資料是無法進行回傳的,所有的耗時邏輯都是在子執行緒里進行的,sendHttpRequest()方法會在服務器還沒來得及回應的時候就執行結束了,當然也就無法回傳回應的資料了,

那么遇到這種情況時應該怎么辦呢?其實解決方法并不難,只需要使用Java的回呼機制就可以了,下面就讓我們來學習一下回呼機制到底是如何使用的,首先需要定義一個介面,比如將它命名成HttpCallbackListener,代碼如下所示:

package com.zhouzhou.networktest;

public interface HttpCallbackListener {
    void onFinish(String response);
    void one rror(Exception e);
}

可以看到,我們在介面中定義了兩個方法,onFinish()方法表示當服務器成功回應我們請求的時候呼叫,onError()表示當進行網路操作出現錯誤的時候呼叫,這兩個方法都帶有引數,onFinish()方法中的引數代表著服務器回傳的資料,而onError()方法中的引數記錄著錯誤的詳細資訊,接著修改HttpUtil中的代碼,如下所示:

package com.zhouzhou.networktest;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

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 inputStream = connection.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                    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) {
                    e.printStackTrace();
                    // 回呼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 one rror(Exception e) {
        //在這里對例外情況進行處理
    }
});

這樣的話,當服務器成功回應的時候,我們就可以在onFinish()方法里對回應資料進行處理了,類似地,如果出現了例外,就可以在onError()方法里對例外情況進行處理,

如此一來,我們就巧妙地利用回呼機制將回應資料成功回傳給呼叫方了,不過你會發現,上述使用HttpURLConnection的寫法總體來說還是比較復雜的,那么使用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);
    }
}

可以看到,sendOkHttpRequest()方法中有一個okhttp3.Callback引數,這個是OkHttp庫中自帶的一個回呼介面,類似于我們剛才自己撰寫的HttpCallbackListener,

然后在client. newCall()之后沒有像之前那樣一直呼叫execute()方法,而是呼叫了一個enqueue()方法,并把okhttp3.Callback引數傳入,OkHttp在enqueue()方法的內部已經幫我們開好子執行緒了,然后會在子執行緒中去執行HTTP請求,并將最終的請求結果回呼到okhttp3.Callback當中,那么我們在呼叫sendOkHttpRequest()方法的時候就可以這樣寫:

HttpUtil.sendOkHttpRequest("http://www.baidu.com",new okhttp3.Callback(){
    @Override
    public void onResponse(Call call,Response response) throws IOException {
        //得到服務器回傳的具體內容
        String responseData = https://www.cnblogs.com/1693977889zz/p/response.body().string();
    }
    @Override
    public void onFailure(Call call,IOException e) {
        //在這里對例外情況進行處理
    }
});

由此可以看出,OkHttp的介面設計得確實非常人性化,它將一些常用的功能進行了很好的封裝,使得我們只需撰寫少量的代碼就能完成較為復雜的網路操作,當然這并不是OkHttp的全部,后面我們還會繼續學習它的其他相關知識,

另外需要注意的是,不管是使用HttpURLConnection還是OkHttp,最終的回呼介面都還是在子執行緒中運行的,因此我們不可以在這里執行任何的UI操作,除非借助runOnUiThread()方法來進行執行緒轉換,至于具體的原因,我們很快就會在下一章中學習到了,

9.6 小結與點評

本章中主要學習了在Android中使用HTTP協議來進行網路互動的知識,雖然Android中支持的網路通信協議有很多種,但HTTP協議無疑是最常用的一種,通常我們有兩種方式來發送HTTP請求,分別是HttpURLConnection和OkHttp,

接著又學習了XML和JSON格式資料的決議方式,無論是XML還是JSON,它們各自又擁有多種決議方式,如果以后作業中還需要用到其他的決議方式,可以自行去學習,

最后,主要學習了如何利用Java的回呼機制來將服務器回應的資料進行回傳,其實除此之外,還有很多地方都可以使用到Java的回呼機制,希望能舉一反三,

個人學習筆記,針對本人在自學中遇到的問題,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472409.html

標籤:Android

上一篇:《第一行代碼:Android篇》學習筆記(八)

下一篇:《第一行代碼:Android篇》學習筆記(十)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more