課程 2: HTTP 網絡

這節課是 Android 開發(入門)課程 的第三部分《訪問網絡》的第二節課,導師是 Chris Lei 和 Joe Lewis。因爲上節課的 JSON 是硬編碼的佔位符,並非真正從網絡獲取的數據,因此按照計劃的開發步驟,要實現從網絡獲取數據,這節課先經過一個叫做 Soonami 的示例應用 (Sample App) 來驗證網絡 (Networking) 相關的代碼。由於網絡命題的內容很龐大,因此課程中會從實用性出發,僅對用到的部分提供相應的資訊,不做深刻討論。html

Soonami App 一樣使用 USGS API 顯示是否有地震引發的海嘯預警,分四個步驟完成:java

  1. Form HTTP Request
  2. Send the Request
  3. Receive the Response and make sense of it
  4. Update the UI

關鍵詞:Android Permissions、Android System Architecture、Exception、try/catch/finally block、HTTP Request、URL Class、HttpURLConnection、HTTP Verb、HTTP Status Code、StringBuilder、InputStream、InputStreamReader、BufferedReader、Method Chainingnode

Android Permissions

在進行 Android 中的網絡操做前,先了解一下 Android 權限的相關知識。默認狀況下 Android 應用不具有任何權限,當應用須要使用設備的藍牙、網絡鏈接、指紋識別,或者訪問用戶的日曆、地址、聯繫人等操做時,應用就須要請求權限,完整的 Android 權限列表能夠到 Android Developers 網站 查看。android

Android 權限按保護等級分爲幾種類型,其中最重要的兩種是正常權限 (Normal Permissions) 和危險權限 (Dangerous Permissions)。git

  1. 正常權限:容許的操做對用戶信息和其它應用的數據無影響,例如使用設備的藍牙、網絡鏈接、指紋識別等,完整列表能夠到 Android Developers 網站 查看。當應用請求正常權限時,Android 會自動授予應用該權限,無需用戶介入。github

  2. 危險權限:容許訪問用戶的我的信息,可能會對其它應用的數據產生影響,例如訪問用戶的日曆、地址、聯繫人等。當應用請求危險權限時,須要由用戶手動處理該請求。危險權限是經過 權限組 (Permission Groups) 來管理的(正常權限也可能包含在權限組內,不過權限組對其無影響,因此無需考慮權限組內的正常權限)。
    (1)當設備運行在 Android 6.0 (API Level 23) 以及應用的 targetSdkVersion 爲 23 或以上時,Android 會在應用運行時 (Runtime),彈出對話框,顯示應用請求的危險權限所在的權限組。若是用戶拒絕權限請求,應用未能得到該權限,那麼它就沒法提供對應的功能,但仍能正常運行;若是用戶贊成該請求,就至關於授予應用整個權限組的權限。例如應用請求 READ_CONTACTS 權限時,這個權限屬於 CONTACTS 權限組,系統就會在應用運行時彈出對話框,顯示應用請求 CONTACTS 權限組,若是用戶贊成該請求,此時應用只得到 READ_CONTACTS 權限,可是在這個基礎上,若是應用再請求同一權限組的 WRITE_CONTACTS 權限,Android 會自動授予應用該權限。
    (2)當設備運行在 Android 5.1 (API Level 22) 或應用的 targetSdkVersion 爲 22 或如下時,Android 會在應用安裝時 (Install Time),彈出對話框,顯示應用請求的全部權限組列表,用戶必須贊成全部的權限請求,不然沒法安裝應用。json

爲應用請求 Android 權限的方法是在 AndroidManifest 中添加 <uses-permission> 標籤以及對應的屬性,注意標籤名不是 <user-permission>,例如在 Soonami App 中請求網絡訪問的權限:api

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.soonami">
    <uses-permission android:name="android.permission.INTERNET"/>
    ...
</manifest>
複製代碼

正如上面描述的,網絡訪問屬於 Android 的正常權限,系統會自動授予應用該權限,無需用戶介入。瀏覽器

Tips:
1. 雖然應用得到一個危險權限就意味着得到了整個權限組的權限,可是在未來的 Android SDK 中一些權限可能會從一個權限組移動到另外一個權限組,因此不該該根據權限組來假定應用是否得到某些權限,最佳作法 (Best Practice) 是在 AndroidManifest 中明確請求每一個權限。
2. 當應用要用到拍照、地圖等功能時,能夠經過 Intent 調用相應的應用來實現,從而避免請求過多的權限。過多的權限請求會引發用戶的懷疑,因此應用應該儘量少地請求權限,同時確保具備充分的理由向用戶解釋須要請求權限的緣由。bash

Android System Architecture

之因此 Android 有系統權限的概念,是由於 Android 是一種權限分離 (privilege-separated) 的操做系統,應用以惟一的身份標識運行。也就是說,每一個 Android 應用都運行在一個進程沙盒 (Process Sandbox) 中,應用須要明確請求沙盒外的資源和數據。這種模式是由 Android 系統框架決定的,應用與設備之間的交互經過一系列的層抽象 (Layer Abstraction) 實現,每一層實現一部分功能,越底層實現的功能越小。

上圖是簡化的 Android 系統框架,完整的圖表能夠到 Android Developers 網站 查看。

  1. 頂層是應用層,開發者寫的全部應用程序都在這一層。App 經過調用下一層的 Android Framework class,如 TextView、Activity,使應用僅用幾行代碼就完成不少複雜的工做,如顯示文本、打開一個新頁面。
  2. 次層是框架層,這一層提供了許多 Android Framework class,它們經過調用下一層的代碼來避免不少重複的複雜工做,最終達到控制設備硬件的效果。框架層是鏈接應用和設備的橋樑。
  3. 次底層是系統層,這一層有一套複雜的控制設備硬件的代碼,用來規範應用和系統進程如何訪問硬件資源,從而實現設備上的多個應用共享同一套硬件。
  4. 底層是物理層,指的是設備硬件,如 Wi-Fi、藍牙,以及 CPU、GPU、內存等電子器件。

在 Soonami App 中,應用經過 Android Framework 的 HttpURLCOnnection 類使用設備上的蜂窩或 Wi-Fi 硬件設備,以從網絡上獲取數據,而不是直接操做 Android 系統,更不是直接控制設備硬件。

Exception

若是 Soonami App 在沒有得到網絡訪問權限的狀況下進行相關的操做,應用會產生 SecurityException 致使應用崩潰。事實上,一些應用崩潰的緣由每每是沒有正確處理 Exception(例外/異常)。Exception 是 Throwable class 的一個擴展類(另外一個是 Error),當代碼運行失敗或遇到意外狀態時會觸發異常(Throw an Exception),稱爲異常事件 (Exception Event)。Exception class 的子類定義了許多異常事件的類型,例如 IllegalStateException 表示有 method 被非法狀態下調用;NullPointerException 表示對空對象進行非法操做。因此異常能夠理解爲錯誤 (error),但它能夠被捕獲 (catch) 處理或包含到 Exception 類的實例中;與其它類同樣,開發者也能夠建立自定義的 Exception class,例以下面的 InvalidPurchaseException。

public void completePurchase() throws InvalidPurchaseException {
    ...
    ...
    throw new InvalidPurchaseException();
    ...
}
複製代碼
  1. 在任何地方觸發異常時都要用到 Java 關鍵字 throw
  2. 異常觸發後下面的代碼不會執行。

異常可分爲檢查異常和非檢查異常 (Checked and Unchecked Exception)。

  • 全部非 RuntimeException(Exception 的子類)的異常都是檢查異常,都必須在方法簽名 (Method Signature) 中聲明,表示調用該 method 時必須處理異常,例如調用上面的 completePurchase() method 時必須處理 InvalidPurchaseException 異常。這種作法在一個類內的輔助方法 (Helper Method) 很經常使用,能夠將異常處理轉移到調用 method 的地方。
  • 全部 RuntimeException 都是非檢查異常,編譯器不會強制 (check) 代碼處理異常。雖然 Java 有使用異常的標準框架,但這並不意味着必定要在發生錯誤時觸發異常。理論上,在出現錯誤或意外狀況時,應該在代碼中提供一些合理的默認行爲,儘量地使代碼繼續運行,這種作法叫靜默失敗 (Failing Silently);可是若是錯誤會對接下來的代碼形成影響,那麼就要觸發異常以通知此錯誤。開發者須要根據實際狀況進行權衡。

處理 Exception 的方法一般是將可能觸發異常的 method 放到 try/catch/finally 區塊中,例以下面的 openFile method:

public void openFile() {
    FileReader reader = null;
    try {
        // constructor may throw FileNotFoundException
        reader = new FileReader("someFile");
        int i = 0;
        while (i != -1) {
            //reader.read() may throw IOException
            i = reader.read();
            System.out.println((char) i);
        }
    } catch (FileNotFoundException e) {
        Log.e(LOG_TAG, "Problem reading the file.", e);
    } catch (IOException e) {
        Log.e(LOG_TAG, "Problem opening the file.", e);
    } finally {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                Log.e(LOG_TAG, "Problem closing the file.", e);
            }
        }
        System.out.println("--- File End ---");
    }
}
複製代碼
  1. FileReader()reader.read() 兩個可能觸發異常的 method 放進 try 區塊,並用兩個 catch 區塊分別處理不一樣的異常,在這裏是經過 Log 日誌記錄錯誤信息。
  2. 若是 FileReader() 觸發異常,那麼就再也不執行 try 區塊內的代碼,而是跳到 catch (FileNotFoundException e) 處理異常,而後跳到 finally 執行該區塊內的代碼,最後跳出 try/catch 區塊,從上至下繼續執行下面的代碼。若是 reader.read() 觸發異常,則會跳到 catch (IOException e) 處理異常,接下來的步驟與上面相同。所以,try 區塊內的代碼沒法保證老是會執行,代碼不會同時進入兩個 catch 區塊。
  3. 不管是否觸發異常,finally 區塊內的代碼都會執行。
  4. 注意變量的做用域,例如這裏的 reader 變量是在 try/catch 區塊外聲明的,若是在 try 區塊內聲明變量,那麼變量的做用域僅在 try/catch 區塊內。

Tips:
1. 在 Android Studio 中打開 Java 文件,選中左側的 "7:Structure" 標籤,能夠按照嵌套結構清晰地選擇瀏覽文件中的 Java 變量、類、對象。
2. 在 Android Studio 中選中被識別出錯誤的代碼(有波浪下劃線)使用快捷鍵 opt(alt)+enter 能夠選擇 Android Studio 提供的解決方案。例如選擇 "Surround with try/catch" 能夠快速添加 try/catch 區塊,在 catch 區塊內還會自動添加 e.printStackTrace(); 表示打印錯誤堆棧。

Networking

網絡是計算機(包括手機、筆記本電腦、服務器等)之間交換信息的概念,它的基本原理是一臺計算機向另外一臺計算機發送 HTTP 請求,發送端一般稱爲客戶端,接收端爲 Web 服務器;服務器做出響應後,客戶端可以獲取響應並從中提取信息。例如使用瀏覽器打開 Google 搜索主頁時,瀏覽器做爲客戶端向 Google 服務器發送 HTTP 請求,瀏覽器接收到 Google 服務器的響應後解析數據,最後刷新頁面顯示一個完整的網頁。利用 Chrome 瀏覽器的開發者工具(在空白處右鍵選擇 Inspect),在 Network 界面能夠看到瀏覽器已加載的資源 (HTML, CSS, JavaScript),選中其中一項,在 Headers 標籤頁下能夠看到瀏覽器向 Google 服務器發送的請求的相關信息,如 URL、method、響應代碼等。

HTTP 請求 (HTTP Request) 是網絡交換信息的基礎部分,HTTP(超文本傳輸協議,Hypertext Transfer Protocol)是其中的核心技術。相似在餐廳點披薩,顧客須要明確告訴服務員披薩的尺寸和配料等信息,客戶端發送 HTTP 請求也須要明確指出請求內容以及提供方式,其中一項重要指標是 URL(統一資源定位器,Uniform Resource Locator),它決定了數據源的地址或位置,在 API 中稱爲端點 (Endpoints)。一個 URL 示例以下,它包含了五個基本元素:

https://example.com/animal/mammal/primate?diet=omnivore&active=night#tarsier
複製代碼
  1. 傳送協議 (Protocol/Scheme):一般爲 http 或 https,後接 // 標記符。
  2. 服務器 (Host/Domain/Authority):Web 資源的主體,一般是域名,如 google.com,有時是 IP 地址,如 192.168.0.1。後面能夠接網絡端口號(數字,若爲 HTTP 的默認值 ":80" 可省略)。
  3. 資源路徑 (Resource Path):相似目錄結構,表示資源在服務器中的位置。
  4. 查詢 (Query):可選,以 ? 爲開始,每一個參數用 & 分隔。
  5. 片斷 (Fragment):可選,以 # 爲開始,指頁面中的某些資源 ID,表示頁面會從該資源開始顯示。

在 Android 中利用 URL class 來生成訪問 API 端點的 URL,例如在 Soonami App 中新建一個名爲 createUrl 的 URL 對象,在 try/catch 區塊內經過字符串構造 URL 對象,同時可捕獲 MalformedURLException 並經過 Log 日誌記錄錯誤信息。

/**
 * Returns new URL object from the given string URL.
 */
private URL createUrl(String stringUrl) {
    URL url = null;
    try {
        url = new URL(stringUrl);
    } catch (MalformedURLException exception) {
        Log.e(LOG_TAG, "Error making the HTTP request.", exception);
        return null;
    }
    return url;
}
複製代碼

建立 URL 對象後,經過調用 url.openConnection() 建立一個 HttpURLConnection 對象,經過調用其中的 method 就能夠在 Android 中生成 HTTP 請求了。這種模式是有 Android 系統框架決定的,經過層抽象使 App 僅用幾行代碼就可以完成複雜的工做。例如在這裏就經過 Android Framework 的 HttpURLCOnnection 類使用設備上的蜂窩或 Wi-Fi 硬件設備,以從網絡上獲取數據,而不是直接操做 Android 系統,更不是直接控制設備硬件。

Tip: OkHttp 是一個開源的 HTTP 客戶端第三方庫,它也能夠實現 Android 的網絡操做。

/**
 * Make an HTTP request to the given URL and return a String as the response.
 */
private String makeHttpRequest(URL url) throws IOException {
    String jsonResponse = "";

    // If the URL is null, then return early.
    if (url == null) {
        return jsonResponse;
    }
    HttpURLConnection urlConnection = null;
    InputStream inputStream = null;
    try {
        urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setRequestMethod("GET");
        urlConnection.setReadTimeout(10000 /* milliseconds */);
        urlConnection.setConnectTimeout(15000 /* milliseconds */);
        urlConnection.connect();

        // If the request is successful (response code 200),
        // then read the input stream and parse the response.
        if (urlConnection.getResponseCode() == 200) {
            inputStream = urlConnection.getInputStream();
            jsonResponse = readFromStream(inputStream);
        } else {
            Log.e(LOG_TAG, "Error response code: " + urlConnection.getResponseCode());
        }
    } catch (IOException e) {
        Log.e(LOG_TAG, "Problem retrieving the earthquake JSON results.", e);
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
        if (inputStream != null) {
            // function must handle java.io.IOException here
            inputStream.close();
        }
    }
    return jsonResponse;
}
複製代碼
  1. urlConnection = (HttpURLConnection) url.openConnection();
    經過 url.openConnection() 建立一個 HttpURLConnection 對象,默認返回的數據類型爲 URLConnection,不過 HttpURLConnection 是 URLConnection 的子類,因此這裏能夠轉換數據類型。
  2. urlConnection.setRequestMethod("GET");
    調用 HttpURLConnection 的 setRequestMethod method 來設置 HTTP 動詞。

HTTP 方法或動詞 (Method/Verb) 是客戶端發送 HTTP 請求的另外一項重要指標,經過它來完成客戶端與服務器之間的交互,一般是四種操做,建立 (Create)、讀取 (Read)、更新 (Update)、刪除 (Delete),簡寫 CRUD)。經常使用的 HTTP 動詞有:
(1)GET: 客戶端從服務器獲取或檢索數據。
(2)POST: 客戶端向服務器發送一些數據。
(3)PUT: 客戶端更新服務器上的數據。
(4)DELETE: 客戶端刪除服務器上的數據。

在 Soonami App 中,客戶端要從服務器中獲取地震信息,屬於讀取操做,因此這裏設置 HTTP 動詞爲 GET。HTTP 動詞的詳細信息能夠到這個網站查看,裏面詳細敘述了每一個 HTTP 動詞的用法,以及對應的服務器響應。儘管 HTTP 動詞的用法遵循必定的規則,可是對於不一樣 API 而言會有差別,最終應用要以 API 文檔爲準。

  1. urlConnection.setReadTimeout(10000 /* milliseconds */);
    調用 HttpURLConnection 的 setReadTimeout method 來設置讀取數據的延時爲 10000 毫秒。

  2. urlConnection.setConnectTimeout(15000 /* milliseconds */);
    調用 HttpURLConnection 的 setConnectTimeout method 來設置鏈接延時爲 15000 毫秒。

  3. urlConnection.connect();
    打包 HTTP 請求並將其發送到服務器。這行代碼是客戶端與服務器創建 HTTP 鏈接的位置,在此以前的內容屬於設置 HTTP 請求,在此以後的屬於接收響應並解析數據的內容。

  4. urlConnection.getResponseCode() == 200
    調用 HttpURLConnection 的 getResponseCode method 來獲取 HTTP 響應代碼。

服務器對 HTTP 請求的響應經過 HTTP 響應代碼 (HTTP Status Code) 表示,響應代碼爲三位數字,按首位數字分爲五類響應,完整的 HTTP 響應代碼列表能夠到 Wikipedia 查看。
(1)1xx Informational Responses
信息狀態碼,表示請求已被服務器接收,但仍需繼續處理。
(2)2xx Success
成功狀態碼,表示請求已成功被服務器接收、理解、並接受。常見 "200 OK" 表示請求已成功,請求的數據返回客戶端。
(3)3xx Redirection
重定向狀態碼,表示客戶端須要採起進一步的操做才能完成請求。常見 "301 Moved Permanently" 表示請求的資源已永久移動到新位置。
(4)4xx Client Errors
客戶端錯誤狀態碼,表示客戶端可能發生了錯誤,妨礙服務器的處理。常見 "400 Bad Request" 表示因爲明顯的客戶端錯誤(請求語法錯誤,欺騙性路由請求等),服務器不會處理該請求;"403 Forbidden" 表示服務器已經理解請求,可是拒絕執行它;"404 Not Found" 表示請求的資源未在服務器上找到。
(5)5xx Server Errors
服務器錯誤狀態碼,表示服務器在處理請求的過程當中有錯誤或者異常狀態發生,沒法完成有效的請求。常見 "502 Bad Gateway" 表示做爲網關或代理工做的服務器嘗試執行請求時,從上游服務器接收到無效的響應。

根據 HTTP 響應代碼,針對服務器的不一樣響應做出對應的處理方案,使代碼可以正常運行,同時增長代碼的魯棒性。例如在 Soonami App 中,USGS 服務器對 GET 動詞的響應多是 200 表示 OK,也多是 404 表示未找到資源,應用經過 if-else 語句實現僅在 USGS 服務器響應代碼爲 "200 OK" 時接收響應並解析數據,收到其它響應代碼時經過 Log 日誌記錄錯誤信息。

  1. inputStream = urlConnection.getInputStream();
    將服務器返回的數據存放在 InputStream 中。對於計算機而言,每一段數據,不管是文本仍是圖片,都是存放在字節大小的塊中,應用在接收數據時數據以數據流 (InputStream) 的形式輸入。數據流是抽象的,以二進制 (0/1) 保存。

  2. jsonResponse = readFromStream(inputStream); 經過 readFromStream 輔助方法來解析 InputStream 數據流,最終傳給 jsonResponse 字符串。因爲數據流是二進制 (0/1) 保存的原始數據,因此應用在使用前須要解析成有意義的內容。例如這裏須要將服務器返回的 GeoJSON 原始二進制數據轉換成字符串,readFromStream 輔助方法的代碼以下:

/**
 * Convert the {@link InputStream} into a String which contains the
 * whole JSON response from the server.
 */
private String readFromStream(InputStream inputStream) throws IOException {
    StringBuilder output = new StringBuilder();
    if (inputStream != null) {
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-8"));
        BufferedReader reader = new BufferedReader(inputStreamReader);
        String line = reader.readLine();
        while (line != null) {
            output.append(line);
            line = reader.readLine();
        }
    }
    return output.toString();
}
複製代碼
  1. 經過 InputStreamReader 將 InputStream 的二進制數據轉換爲字符。其中 Charset 指定了如何將原始數據的逐個字節轉換成字符,UTF-8 是一種普遍應用的 Unicode 字符編碼。
  2. 因爲 InputStreamReader 一次只能轉換一個字符,根據 InputStream 實際提供數據的不一樣方式,這可能會致使嚴重的性能問題,所以將 InputStreamReader 包裝到 BufferedReader 能夠避免這個問題。例如上面的 reader.readLine(); 使 BufferedReader 在收到對某個字符的請求後會讀取並保存該字符先後的一整行字符,當請求另外一個字符時就能利用 BufferedReader 提早讀取的字符來實現請求,無需再調用 InputStreamReader。
  3. 因爲數據解析過程當中會不斷生成新的字符,若是將解析的字符存入字符串,那麼就要不斷地對字符串從新賦值,其實是對 String 對象反覆進行刪除和建立操做,所以這裏引入一個新的 StringBuilder class。相對 String 而言,StringBuilder 是可變的 (Mutable),在改變字符時可以節省不少系統資源。
    (1)StringBuilder output = new StringBuilder();
    與其它類同樣,經過構造函數建立一個 StringBuilder 對象。
    (2)output.append(line);
    經過 append method 添加字符序列。append method 能夠在一行內屢次調用,如 output.append(line1).append(line2);,這種方法叫作方法鏈 (Method Chaining)。
    (3)StringBuilder 的其它一些經常使用 method 有 output.deleteCharAt(3) 表示刪除索引號 3 的字符;output.toString() 表示將 StringBuilder 保存到一個不可變 (Immutable) 的字符串中。
相關文章
相關標籤/搜索