天天看點

每天學習一個Android中的常用架構——4.OkIo1.簡介2.特性3.示範4.源碼位址

文章目錄

  • 1.簡介
  • 2.特性
  • 3.示範
    • 3.1 內建
    • 3.2 配置
    • 3.3 布局檔案和URL封裝
    • 3.4 POST請求送出鍵值對
    • 3.5 POST請求送出字元串
    • 3.6 POST請求送出檔案
    • 3.7 POST請求送出表單
  • 4.源碼位址

1.簡介

在上一篇部落格中,我們介紹了OkHttp的快速內建和使用,其中提到了有關于使用

post

請求送出

字元串/檔案/表單

等需要使用到OkHttp中的底層傳輸架構OkIo。鑒于架構的功能劃分,這篇部落格專門講解OkIo的常用api和應用場景。當然,考慮到OkIo和OkHttp一般是搭配使用的,是以這裡的示範更多是網絡傳輸場景。若想使用OkIo做為一個專門用來流傳輸的架構,可以參考官方文檔:OkIo,其中有更詳細的使用方法。

OkIo庫是一個由square公司開發的,它補充了java.io和java.nio的不足,以便能夠更加友善,快速的通路、存儲和處理你的資料。而OkHttp的底層也使用該庫作為支援。而在開發中,使用該庫可以大大給你帶來友善。

2.特性

  1. 緊湊的封裝:是對Java IO/NIO 的一個非常優秀的封裝,絕對的“短小精焊”,不僅支援檔案讀寫,也支援Socket通信的讀寫。
  2. 使用簡單:不用區分字元流或者位元組流,也不用記住各種不同的輸入/輸出流,統統隻有一個輸入流Source和一個輸出流Sink,它們之間所包含的主要api如圖所示:
    每天學習一個Android中的常用架構——4.OkIo1.簡介2.特性3.示範4.源碼位址
  3. API豐富:其封裝了大量的API接口用于讀/寫位元組或者一行文本
  4. 讀寫速度快:這得益于其優秀的緩沖機制和處理記憶體的技巧,使I/O在緩沖區得到更高的複用處理,進而盡量減少I/O的實際發生。
  5. 強大的支撐機制:與這些特性相比,就是其有強大的保障機制保駕護航
    1. 逾時機制:在讀/寫時增加了逾時機制,且有同步與異步之分。
    2. 緩沖機制:讀/寫都是基于緩沖來來實作,盡量減少實際的I/O。
    3. 壓縮機制:寫資料時,會對緩沖的每個Segment進行壓縮,避免空間的浪費。當然,這是其内部的優化技巧,提高記憶體使用率。
    4. 共享機制:主要是針對 Segment 而言的,對于不同的 buffer 可以共享同一個 Segment。這也是其内部的優化技巧。

然而,在正式分析之前有兩個核心基礎類

ByteString

Buffer

和兩個核心API類需要提前了解一下,因為大量的API都是以這兩個類為基礎來實作的。了解它們,以便有助于後面的分析。

  • ByteString:是一個不可變的位元組序列。對于字元資料,String是基礎,ByteString則是String失散多年的好兄弟。其可以很容易地将二進制資料視為一個值來處理。如用十六進制,base64,和UTF-8來進行編碼和解碼。
  • Buffer:是一個可變的位元組序列。就像ArrayList一樣,可以進行靈活的通路,插入與移除,完全不需要自己去動手管理。

這兩個類也可以看作是上面機制的實作,正是上面機制的實作,才使得該庫以最少的實際IO來實作快速的IO需求。

理論的知識就說明到這裡,接下來正式進入示範環節。OkHttp的請求分為異步/同步,這裡的示範統一使用異步請求,友善讀者參考。

3.示範

3.1 內建

在使用任何架構之前,內建都是第一步。由于在使用OkIo的同時還會使用到OkHttp,是以這裡要導入兩個依賴,修改module下的build.gradle,代碼如下:

implementation("com.squareup.okhttp3:okhttp:4.6.0")
implementation("com.squareup.okio:okio:1.11.0")
           

修改完成後Sync一下,確定OkHttp和OkIo都內建到了你的項目中。

3.2 配置

既然你內建了OkHttp,就不可不免地要進行與網絡有關的操作。另外,由于涉及到檔案上傳的操作,需要存儲卡讀寫權限,在清單檔案下聲明需要的權限,即:

<uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
           

另外:如果你的Android版本為Android P(即targetSdkVersion 27),在運作該項目時可能會報有關網絡的錯誤,如圖所示:

每天學習一個Android中的常用架構——4.OkIo1.簡介2.特性3.示範4.源碼位址

原因:在Android P系統的裝置上,如果應用使用的是非加密的明文流量的http網絡請求,則會導緻該應用無法進行網絡請求,https則不會受影響,同理若應用内使用WebView加載網頁 則加載網頁也需要是https請求。

解決方法:

  • APP整體網絡請求改用https
  • 将targetSdkVersion 版本下調至27以下
  • 更改項目網絡安全配置

三種方法亦可,這裡主要介紹一下第三種解決方法,以拓寬解決思路

  1. 在res目錄下建立xml檔案夾 在xml檔案夾内建立名為

    network_config(名字非固定)的xml

    ,代碼如下:
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
           
  1. 然後,在AndroidManifest中增加

    android:networkSecurityConfig

    屬性,代碼如下:
<application
    ...
     android:networkSecurityConfig="@xml/network_security_config"
    ...
        />
           
  1. 以上兩個步驟就完成了網絡安全配置,如果實在不行的話可以嘗試另外兩種方法或者百度

3.3 布局檔案和URL封裝

接下來,我們直接開始布局檔案activity_main.xml的編寫。該布局很簡單,僅有四個按鈕,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_post_keyvalue"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="發送post請求——送出鍵值對"/>

    <Button
        android:id="@+id/btn_post_string"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="發送post請求——送出字元串"/>
        
    <Button
        android:id="@+id/btn_post_file"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="發送post請求——送出檔案"/>

    <Button
        android:id="@+id/btn_post_form"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="發送post請求——送出表單"/>

</LinearLayout>
           

之後,我們簡單用字元串封裝一下要請求的URL。與上一篇部落格不同的是,為了模拟出網絡傳輸的效果,這裡架設一個本地伺服器,然後将相應URL進行封裝,代碼如下(伺服器URL不固定,根據自己的伺服器路徑名進行相應修改):

另外:如果你的Android版本為Android M(即targetSdkVersion 23),在運作該項目時可能會報有關網絡的錯誤,如圖所示:

每天學習一個Android中的常用架構——4.OkIo1.簡介2.特性3.示範4.源碼位址

原因:這是因為新的保護機制對于僅使用安全通信的應用,Android 6.0 Marshmallow(API 級别 23)引入了兩種機制來解決回退到明文通信的問題:(1) 在生産/安裝庫中,禁止明文通信,以及 (2) 在開發/QA 期間,在遇到任何非 TLS/SSL 通信時,予以記錄或者觸發崩潰。

解決方法:如果一定要使用明文通信的話,則可以打開AndroidManifest.xml 檔案,在 application 元素中添加:

android:usesCleartextTraffic=”true” 
           

3.4 POST請求送出鍵值對

異步POST請求送出鍵值對的步驟跟上一篇步驟提到的一樣,大抵分為:

  • 構造OkHttpClient對象;
  • 構造FormBody對象(鍵值對);
  • 構造Request對象;
  • 通過前兩步中的對象建構Call對象;
  • 通過call.newCall()方法來送出異步請求。

代碼如下:

private void postKeyValue() {
        btn_post_keyvalue.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.建構Client對象
                OkHttpClient client = new OkHttpClient();
                // 2.采用建造者模式和鍊式調用建構鍵值對對象
                FormBody formBody = new FormBody.Builder()
                        .add("username", "admin")
                        .add("password", "123456")
                        .build();
                // 3.采用建造者模式和鍊式調用建構Request對象
                final Request request = new Request.Builder()
                        .url(URL) // 請求URL
                        .post(formBody) // 預設就是get請求,可以不寫
                        .build();
                // 4.通過1和3産生的Client和Request對象生成Call對象
                Call call = client.newCall(request);
                // 5.調用Call對象的enqueue()方法,并且實作一個回調實作類
                call.enqueue(new Callback() {
                    @Override
                    public void onFailure(@NotNull Call call, @NotNull IOException e) {
                        Log.d(TAG, "發送post請求鍵值對失敗!");
                        e.printStackTrace();
                    }

                    @Override
                    public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                        Log.d(TAG, "發送post請求鍵值對成功!請求到的資訊為:" + response.body().string());
                    }
                });
            }
        });
    }
           

3.5 POST請求送出字元串

異步POST請求送出字元串的步驟跟送出鍵值對類似,隻有第二步不同,大抵分為:

  • 構造OkHttpClient對象;
  • 構造RequestBody對象(FormBody是RequestBody的子類);
  • 構造Request對象;
  • 通過前兩步中的對象建構Call對象;
  • 通過call.newCall()方法來送出異步請求。

這種方式需要構造一個RequestBody對象,用它來攜帶我們要送出的資料。在構造 RequestBody 需要指定MediaType,用于描述請求/響應 body 的内容類型,關于 MediaType 的更多資訊可以檢視RFC 2045。

一般來說,送出字元串的應用場景多為:用戶端給伺服器發送一個json字元串,這種時候就需要送出字元串了。

代碼如下:

private void postString() {
        btn_post_string.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.建構Client對象
                OkHttpClient client = new OkHttpClient();
                // 2.構造RequestBody
                RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "{username:admin;password:123456}");
                // 3.采用建造者模式和鍊式調用建構Request對象
                final Request request = new Request.Builder()
                        .url(URL) // 請求URL
                        .post(requestBody) // 預設就是get請求,可以不寫
                        .build();
                // 4.通過1和3産生的Client和Request對象生成Call對象
                Call call = client.newCall(request);
                // 5.調用Call對象的enqueue()方法,并且實作一個回調實作類
                call.enqueue(new Callback() {
                    @Override
                    public void onFailure(@NotNull Call call, @NotNull IOException e) {
                        Log.d(TAG, "發送post請求字元串失敗!");
                        e.printStackTrace();
                    }

                    @Override
                    public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                        Log.d(TAG, "發送post請求字元串成功!請求到的資訊為:" + response.body().string());
                    }
                });
            }
        });

    }
           
上面的MediaType我們指定傳輸的是純文字,而且編碼方式是utf-8,通過上面的方式我們就可以向服務端發送json字元串了。

3.6 POST請求送出檔案

異步POST請求送出檔案的步驟跟送出其他資料類似,隻有第二步比較不同,就是要建構出一個檔案對象。這裡在存儲卡路徑下放置一個test.txt作為測試,大抵分為:

  • 構造OkHttpClient對象;
  • 構造RequestBody對象;
    • 構造File對象
    • 判斷File對象是否存在
    • 根據FIle對象來構造RequestBody對象
  • 構造Request對象;
  • 通過前兩步中的對象建構Call對象;
  • 通過call.newCall()方法來送出異步請求。
private void postFile() {
        btn_post_file.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.建構Client對象
                OkHttpClient client = new OkHttpClient();
                // 2.構造RequestBody
                // 2.1 構造檔案對象
                File file = new File(MainActivity.this.getExternalFilesDir(null), "test.txt");
                Log.i(TAG,Environment.getExternalStorageDirectory().toString());
                // 2.2 判斷檔案是否為空
                if (!file.exists()){
                    Log.i(TAG,"檔案為空,無法建立");
                }else{
                    // 2.3 通過檔案構造構造RequestBody對象
                    RequestBody requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
                    // 3.采用建造者模式和鍊式調用建構Request對象
                    final Request request = new Request.Builder()
                            .url(URL) // 請求URL
                            .post(requestBody) // 預設就是get請求,可以不寫
                            .build();
                    // 4.通過1和3産生的Client和Request對象生成Call對象
                    Call call = client.newCall(request);
                    // 5.調用Call對象的enqueue()方法,并且實作一個回調實作類
                    call.enqueue(new Callback() {
                        @Override
                        public void onFailure(@NotNull Call call, @NotNull IOException e) {
                            Log.d(TAG, "發送post請求檔案失敗!");
                            e.printStackTrace();
                        }

                        @Override
                        public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                            Log.d(TAG, "發送post請求檔案成功!請求到的資訊為:" + response.body().string());
                        }
                    });
                }
            }
        });
    }
           

另外:如果你的Android版本為Android X(即targetSdkVersion 29,萬惡的Android 10),若使用

Environment.getExternalStorageDirectory()

來擷取路徑名,則會報出傳輸權限的錯誤,如圖所示:

每天學習一個Android中的常用架構——4.OkIo1.簡介2.特性3.示範4.源碼位址

原因:Android長久以來都支援外置存儲空間這個功能,也就是我們常說的SD卡存儲。這個功能使用得極其廣泛,幾乎所有的App都喜歡在SD卡的根目錄下建立一個自己專屬的目錄,用來存放各類檔案和資料。

簡單來講,就是Android系統對SD卡的使用做了很大的限制。從Android 10開始,每個應用程式隻能有權在自己的外置存儲空間關聯目錄下讀取和建立檔案,這個特性也被叫做作用域存儲。是以使用之前的api來擷取存儲路徑的方式,已然行不通了。

解決方法:使用擷取該應用的關聯路徑的api來替代之前擷取路徑的api,代碼如下

路徑為:

/storage/emulated/0/Android/data/<包名>/files
           

該路徑下的概覽如圖所示:

每天學習一個Android中的常用架構——4.OkIo1.簡介2.特性3.示範4.源碼位址

3.7 POST請求送出表單

異步POST請求送出表單的步驟跟送出其他資料類似,隻有第二步比較不同,就是要建構出一個MuiltipartBody對象,大抵分為:

  • 構造OkHttpClient對象;
  • 構造MuiltipartBody(RequestBody)對象;
  • 構造Request對象;
  • 通過前兩步中的對象建構Call對象;
  • 通過call.newCall()方法來送出異步請求。

一般來說,送出字元串的應用場景多為:用戶端給伺服器發送一個攜帶有賬号、密碼、頭像等資訊的表單,這種時候就需要送出表單了。

這裡我們會用到一個OkIo包含的類:MuiltipartBody,這是RequestBody的一個子類,我們送出表單就是利用這個類來建構一個RequestBody,下面的代碼我們會發送一個包含使用者名、密碼、頭像的表單到服務端。

代碼如下:

private void postForm() {
        btn_post_form.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.建構Client對象
                OkHttpClient client = new OkHttpClient();
                // 2.構造RequestBody
                // 2.1 構造檔案對象
                File file = new File(MainActivity.this.getExternalFilesDir(null), "test.png");
                Log.i(TAG,Environment.getExternalStorageDirectory().toString());
                // 2.2 判斷檔案是否為空
                if (!file.exists()){
                    Log.i(TAG,"檔案為空,無法建立");
                }else{
                    // 2.3 通過表單構造構造RequestBody對象
                    RequestBody muiltipartBody = new MultipartBody.Builder()
                            //一定要設定這句
                            .setType(MultipartBody.FORM)
                            .addFormDataPart("username", "admin")//
                            .addFormDataPart("password", "admin")//
                            .addFormDataPart("myfile", "test.png", RequestBody.create(MediaType.parse("application/octet-stream"), file))
                            .build();
                    // 3.采用建造者模式和鍊式調用建構Request對象
                    final Request request = new Request.Builder()
                            .url(URL) // 請求URL
                            .post(muiltipartBody) // 預設就是get請求,可以不寫
                            .build();
                    // 4.通過1和3産生的Client和Request對象生成Call對象
                    Call call = client.newCall(request);
                    // 5.調用Call對象的enqueue()方法,并且實作一個回調實作類
                    call.enqueue(new Callback() {
                        @Override
                        public void onFailure(@NotNull Call call, @NotNull IOException e) {
                            Log.d(TAG, "發送post請求表單失敗!");
                            e.printStackTrace();
                        }

                        @Override
                        public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                            Log.d(TAG, "發送post請求表單成功!請求到的資訊為:" + response.body().string());
                        }
                    });
                }
            }
        });
    }
           

注意,在使用MuiltipartBody前,我們需要注意:

  1. 如果送出的是表單,一定要設定

    setType(MultipartBody.FORM)

    這一句
  2. 送出的檔案

    addFormDataPart()

    的第一個參數,就上面代碼中的myfile就是類似于鍵值對的鍵,是供服務端使用的,就類似于網頁表單裡面的name屬性,例如:
  3. 送出的檔案

    addFormDataPart()

    的第二個參數檔案的本地的名字,第三個參數是RequestBody,裡面包含了我們要上傳的檔案的路徑以及MidiaType
  4. 記得在AndroidManifest.xml檔案中添加存儲卡讀寫權限

4.源碼位址

AFL——Android架構學習

繼續閱讀