1. 程式人生 > >【轉】Android OkHttp3簡介和使用詳解

【轉】Android OkHttp3簡介和使用詳解

一 OKHttp簡介

OKHttp是一個處理網路請求的開源專案,Android 當前最火熱網路框架,由移動支付Square公司貢獻,用於替代HttpUrlConnection和Apache HttpClient(android API23 6.0裡已移除HttpClient)。 
OKHttpGitHub地址

OKHttp優點

  1. 支援HTTP2/SPDY(SPDY是Google開發的基於TCP的傳輸層協議,用以最小化網路延遲,提升網路速度,優化使用者的網路使用體驗。)
  2. socket自動選擇最好路線,並支援自動重連,擁有自動維護的socket連線池,減少握手次數,減少了請求延遲,共享Socket,減少對伺服器的請求次數。
  3. 基於Headers的快取策略減少重複的網路請求。
  4. 擁有Interceptors輕鬆處理請求與響應(自動處理GZip壓縮)。

OKHttp的功能

  1. PUT,DELETE,POST,GET等請求
  2. 檔案的上傳下載
  3. 載入圖片(內部會圖片大小自動壓縮)
  4. 支援請求回撥,直接返回物件、物件集合
  5. 支援session的保持

二 OkHttp3使用

主要介紹 OkHttp3 的 Get 請求、 Post 請求、 上傳下載檔案 、 上傳下載圖片等功能 。

新增OkHttp3的依賴

  1. compile  'com.squareup.okhttp3:okhttp:3.7.0'
  2. compile  'com.squareup.okio:okio:1.12.0'

新增網路許可權

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

新增請求頭
複製程式碼
    private Request.Builder addHeaders() {
        Request.Builder builder = new Request.Builder()
                //addHeader,可新增多個請求頭  header,唯一,會覆蓋
                .addHeader("Connection", "keep-alive")
                .addHeader("platform", "2")
                .addHeader("phoneModel", Build.MODEL)
                .addHeader("systemVersion", Build.VERSION.RELEASE)
                .addHeader("appVersion", "3.2.0")
                .header("sid", "eyJhZGRDaGFubmVsIjoiYXBwIiwiYWRkUHJvZHVjdCI6InFia3BsdXMiLCJhZGRUaW1lIjoxNTAzOTk1NDQxOTEzLCJyb2xlIjoiUk9MRV9VU0VSIiwidXBkYXRlVGltZSI6MTUwMzk5NTQ0MTkxMywidXNlcklkIjoxNjQxMTQ3fQ==.b0e5fd6266ab475919ee810a82028c0ddce3f5a0e1faf5b5e423fb2aaf05ffbf");
        return builder;
    }
複製程式碼

 1.非同步GET請求

複製程式碼
        //1.建立OkHttpClient物件
        OkHttpClient okHttpClient = new OkHttpClient();
        //2.建立Request物件,設定一個url地址(百度地址),設定請求方式。
        Request request = new Request.Builder().url("http://www.baidu.com").method("GET",null).build();
        //3.建立一個call物件,引數就是Request請求物件
        Call call = okHttpClient.newCall(request);
        //4.請求加入排程,重寫回調方法
        call.enqueue(new Callback() {
            //請求失敗執行的方法
            @Override
            public void onFailure(Call call, IOException e) {
            }
            //請求成功執行的方法
            @Override
            public void onResponse(Call call, Response response) throws IOException {
            }
        });
複製程式碼

 

上面就是傳送一個非同步GET請求的4個步驟:

  1. 建立OkHttpClient物件
  2. 通過Builder模式建立Request物件,引數必須有個url引數,可以通過Request.Builder設定更多的引數比如:header、method等
  3. 通過request的物件去構造得到一個Call物件,Call物件有execute()和cancel()等方法。
  4. 以非同步的方式去執行請求,呼叫的是call.enqueue,將call加入排程佇列,任務執行完成會在Callback中得到結果。

注意事項:

  1. 非同步呼叫的回撥函式是在子執行緒,我們不能在子執行緒更新UI,需要藉助於 runOnUiThread() 方法或者 Handler 來處理。
  2. onResponse回撥有一個引數是response,如果我們想獲得返回的是字串,可以通過response.body().string()獲取;如果希望獲得返回的二進位制位元組陣列,則呼叫response.body().bytes();如果你想拿到返回的inputStream,則調response.body().byteStream(),有inputStream我們就可以通過IO的方式寫檔案(後面會有例子)。

2.同步GET請求

複製程式碼
        //1.建立OkHttpClient物件
        OkHttpClient okHttpClient = new OkHttpClient();
        //2.建立Request物件,設定一個url地址(百度地址),設定請求方式。
        Request request = new Request.Builder().url("http://www.baidu.com").method("GET",null).build();
        //3.建立一個call物件,引數就是Request請求物件
        Call call = okHttpClient.newCall(request);
        //4.同步呼叫會阻塞主執行緒,這邊在子執行緒進行
        new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //同步呼叫,返回Response,會丟擲IO異常
                        Response response = call.execute();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
複製程式碼

 

同步GET請求和非同步GET請求基本一樣,不同地方是同步請求呼叫Call的execute()方法,而非同步請求呼叫call.enqueue()方法(具體2個方法的不同點我下一遍具體原始碼詳解再說)。

3.POST請求提交鍵值對

複製程式碼
        //1.建立OkHttpClient物件
        OkHttpClient  okHttpClient = new OkHttpClient();
        //2.通過new FormBody()呼叫build方法,建立一個RequestBody,可以用add新增鍵值對 
        RequestBody  requestBody = new FormBody.Builder().add("name","zhangqilu").add("age","25").build();
        //3.建立Request物件,設定URL地址,將RequestBody作為post方法的引數傳入
        Request request = new Request.Builder().url("url").post(requestBody).build();
        //4.建立一個call物件,引數就是Request請求物件
        Call call = okHttpClient.newCall(request);
        //5.請求加入排程,重寫回調方法
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }
 
            @Override
            public void onResponse(Call call, Response response) throws IOException {
            }
        });
複製程式碼

 

上面就是一個非同步POST請求提交鍵值對的5個步驟:

  1. 建立OkHttpClient物件。
  2. 通過new FormBody()呼叫build方法,建立一個RequestBody,可以用add新增鍵值對 ,FormBody 是 RequestBody 的子類。
  3. 建立Request物件,設定URL地址,將RequestBody作為post方法的引數傳入。
  4. 建立一個call物件,引數就是Request請求物件。
  5. 請求加入排程,重寫回調方法。

通過對比我們發現非同步的POST請求和GET請求步驟很相似。

4.非同步POST請求提交字串

POST請求提交字串和POST請求提交鍵值對非常相似,不同地方主要是RequestBody,下面我們來具體看一下。 
在有些情況下客戶端需要向服務端傳送字串,我們該怎麼做? 
我們需要用到另一種方式來構造一個 RequestBody 如下所示:

複製程式碼
        MediaType mediaType = MediaType.parse("application/json; charset=utf-8");//"型別,位元組碼"
        //字串
        String value = "{username:admin;password:admin}"; 
        //1.建立OkHttpClient物件
        OkHttpClient  okHttpClient = new OkHttpClient();
        //2.通過RequestBody.create 建立requestBody物件
        RequestBody requestBody =RequestBody.create(mediaType, value);
        //3.建立Request物件,設定URL地址,將RequestBody作為post方法的引數傳入
        Request request = new Request.Builder().url("url").post(requestBody).build();
        //4.建立一個call物件,引數就是Request請求物件
        Call call = okHttpClient.newCall(request);
        //5.請求加入排程,重寫回調方法
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }
 
            @Override
            public void onResponse(Call call, Response response) throws IOException {
            }
        });
複製程式碼

 

5.非同步POST請求上傳檔案

我們這裡舉一個上傳圖片的例子,也可以是其他檔案如,TXT文件等,不同地方主要是RequestBody,首先我們要新增儲存卡讀寫許可權,在 AndroidManifest.xml 檔案中新增如下程式碼:

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

下面我們具體看一下上傳檔案程式碼。

複製程式碼
        //1.建立OkHttpClient物件
        OkHttpClient  okHttpClient = new OkHttpClient();
        //上傳的圖片
        File file = new File(Environment.getExternalStorageDirectory(), "zhuangqilu.png");
        //2.通過RequestBody.create 建立requestBody物件,application/octet-stream 表示檔案是任意二進位制資料流
        RequestBody requestBody =RequestBody.create(MediaType.parse("application/octet-stream"), file);
        //3.建立Request物件,設定URL地址,將RequestBody作為post方法的引數傳入
        Request request = new Request.Builder().url("url").post(requestBody).build();
        //4.建立一個call物件,引數就是Request請求物件
        Call call = okHttpClient.newCall(request);
        //5.請求加入排程,重寫回調方法
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }
 
            @Override
            public void onResponse(Call call, Response response) throws IOException {
            }
        });
複製程式碼

 

6.非同步GET請求下載檔案

下載檔案也是我們經常用到的功能,我們就舉個下載圖片的例子吧

複製程式碼
        //1.建立OkHttpClient物件
        OkHttpClient okHttpClient = new OkHttpClient();
        //2.建立Request物件,設定一個url地址(百度地址),設定請求方式。
        Request request = new Request.Builder().url("https://www.baidu.com/img/bd_logo1.png").get().build();
        //3.建立一個call物件,引數就是Request請求物件
        Call call = okHttpClient.newCall(request);
        //4.請求加入排程,重寫回調方法
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.e(TAG, "onFailure: "+call.toString() );
            }
 
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                //拿到位元組流
                InputStream is = response.body().byteStream();
                int len = 0;
                //設定下載圖片儲存路徑和名稱
                File file = new File(Environment.getExternalStorageDirectory(),"baidu.png");
                FileOutputStream fos = new FileOutputStream(file);
                byte[] buf = new byte[128];
                while((len = is.read(buf))!= -1){
                    fos.write(buf,0,len);
                    Log.e(TAG, "onResponse: "+len );
                }
                fos.flush();
                fos.close();
                is.close();
            }
        });
複製程式碼

 

Get請求下載檔案還是比較簡單,設定下載地址,在回撥函式中拿到了圖片的位元組流,然後儲存為了本地的一張圖片。

從網路下載一張圖片並直接設定到ImageView中。

複製程式碼
@Override
public void onResponse(Call call, Response response) throws IOException {
    InputStream is = response.body().byteStream();
    //使用 BitmapFactory 的 decodeStream 將圖片的輸入流直接轉換為 Bitmap 
    final Bitmap bitmap = BitmapFactory.decodeStream(is);
    //在主執行緒中操作UI
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            //然後將Bitmap設定到 ImageView 中
            imageView.setImageBitmap(bitmap);
        }
    });
 
    is.close();
複製程式碼

 

主要註釋已在程式碼中了。

7.非同步POST請求上傳Multipart檔案

我們在有些情況下既要上傳檔案還要上傳其他型別欄位。比如在個人中心我們可以修改名字,年齡,修改影象,這其實就是一個表單。這裡我們用到MuiltipartBody ,它 是RequestBody 的一個子類,我們提交表單就是利用這個類來構建一個 RequestBody,我們來看一下具體程式碼。

複製程式碼
        //1.建立OkHttpClient物件
        OkHttpClient  okHttpClient = new OkHttpClient();
        //上傳的圖片
        File file = new File(Environment.getExternalStorageDirectory(), "zhuangqilu.png");
        //2.通過new MultipartBody build() 建立requestBody物件,
         RequestBody  requestBody = new MultipartBody.Builder()
                //設定型別是表單
                .setType(MultipartBody.FORM)
                //新增資料
                .addFormDataPart("username","zhangqilu")
                .addFormDataPart("age","25")
                .addFormDataPart("image","zhangqilu.png",
RequestBody.create(MediaType.parse("image/png"),file))
                .build();
        //3.建立Request物件,設定URL地址,將RequestBody作為post方法的引數傳入
        Request request = new Request.Builder().url("url").post(requestBody).build();
        //4.建立一個call物件,引數就是Request請求物件
        Call call = okHttpClient.newCall(request);
        //5.請求加入排程,重寫回調方法
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }
 
            @Override
            public void onResponse(Call call, Response response) throws IOException {
            }
        });

複製程式碼

 

Post 表單

複製程式碼
public void postForm(View view) {
        OkHttpClient client = new OkHttpClient();
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("username", "葉應是葉")
                .addFormDataPart("password", "葉應是葉")
                .build();
        final Request request = new Request.Builder()
                .url("http://www.jianshu.com/")
                .post(requestBody)
                .build();
        Call call = client.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                ToastUtil.showToast(PostFormActivity.this, "Post Form 失敗");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String responseStr = response.body().string();
                ToastUtil.showToast(PostFormActivity.this, "Code:" + String.valueOf(response.code()));
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tv_result.setText(responseStr);
                    }
                });
            }
        });
    }
複製程式碼

 

Post 流

複製程式碼
public void postStreaming(View view) {
    final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");
    File file = new File("README.md");
    final FileInputStream fileInputStream1=new FileInputStream(file);
    RequestBody requestBody1=new RequestBody() {
        @Nullable
        @Override
        public MediaType contentType() {
            return MEDIA_TYPE_MARKDOWN;
        }

        @Override
        public void writeTo(BufferedSink sink) throws IOException {
            OutputStream outputStream=sink.outputStream();
            int length;
            byte[] buffer = new byte[1024];
            while ((length = fileInputStream1.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
        }
    };

    RequestBody requestBody2=new RequestBody() {
        @Nullable
        @Override
        public MediaType contentType() {
            return MEDIA_TYPE_MARKDOWN;
        }

        @Override
        public void writeTo(BufferedSink sink) throws IOException {
            int length;
            byte[] buffer = new byte[1024];
            while ((length = fileInputStream1.read(buffer)) != -1) {
                sink.write(buffer, 0, length);
            }
        }
    };

    Request request = new Request.Builder()
            .url(url)
            .post(requestBody1)
            .build();

    Call call = client.newCall(request);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            ToastUtil.showToast(PostStreamingActivity.this, "Post Streaming 失敗");
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            final String responseStr = response.body().string();
            ToastUtil.showToast(PostStreamingActivity.this, "Code:" + String.valueOf(response.code()));
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    tv_result.setText(responseStr);
                }
            });
        }
    });
}
複製程式碼

 

 

解析Json

這裡來通過Gson將response的內容解析為Java Bean
首先需要先去將Gson.jar檔案匯入工程

這裡來通過OkHttp訪問介面“http://news-at.zhihu.com/api/4/themes”,獲取Json資料然後將之解析為JavaBean實體

 

Response response = okHttpClient.newCall(request).execute();
        if (response.isSuccessful()){
        User user = new Gson().fromJson(response.body().charStream(), User.class);
        }

 

設定超時時間和快取

和OkHttp2.x有區別的是不能通過OkHttpClient直接設定超時時間和快取了,而是通過OkHttpClient.Builder來設定,通過builder配置好OkHttpClient後用builder.build()來返回OkHttpClient,所以我們通常不會呼叫new OkHttpClient()來得到OkHttpClient,而是通過builder.build():

複製程式碼
 File sdcache = getExternalCacheDir();
        int cacheSize = 10 * 1024 * 1024;
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(15, TimeUnit.SECONDS)
                .writeTimeout(20, TimeUnit.SECONDS)
                .readTimeout(20, TimeUnit.SECONDS)
                .cache(new Cache(sdcache.getAbsoluteFile(), cacheSize));
        OkHttpClient mOkHttpClient=builder.build();            
複製程式碼

 

關於取消請求和封裝

取消請求仍舊可以呼叫call.cancel(),這個沒有變化,不明白的可以檢視上一篇文章Android網路程式設計(五)OkHttp2.x用法全解析,這裡就不贅述了,封裝上一篇也講過仍舊推薦OkHttpFinal,它目前是基於OkHttp3來進行封裝的。

        call.cancel();//取消請求,不能取消已經準備完成的請求     
        okHttpClient.dispatcher().cancelAll();//取消所有請求

有時候網路條件不好的情況下,使用者會主動關閉頁面,這時候需要取消正在請求的http request, OkHttp提供了cancel方法,但是實際在使用過程中發現,如果呼叫cancel()方法,會回撥到CallBack裡面的 onFailure方法中,

 /**
   * Called when the request could not be executed due to cancellation, a connectivity problem or
   * timeout. Because networks can fail during an exchange, it is possible that the remote server
   * accepted the request before the failure.
   */
  void onFailure(Call call, IOException e); 

可以看到註釋,當取消一個請求,網路連線錯誤,或者超時都會回撥到這個方法中來,但是我想對取消請求做一下單獨處理,這個時候就需要區分不同的失敗型別了

解決思路

測試發現不同的失敗型別返回的IOException e 不一樣,所以可以通過e.toString 中的關鍵字來區分不同的錯誤型別

自己主動取消的錯誤的 java.net.SocketException: Socket closed 超時的錯誤是 java.net.SocketTimeoutException 網路出錯的錯誤是java.net.ConnectException: Failed to connect to xxxxx 

程式碼

直貼了部分程式碼

 call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) { if(e.toString().contains("closed")) { //如果是主動取消的情況下 }else{ //其他情況下 }

攔截器

 新增Interceptor
複製程式碼
// 配置一些資訊進入OkHttpClient
mOkHttpClient = new OkHttpClient().newBuilder()
        .connectTimeout(REQUEST_TIME, TimeUnit.SECONDS)
        .readTimeout(REQUEST_TIME, TimeUnit.SECONDS)
        .writeTimeout(REQUEST_TIME, TimeUnit.SECONDS)
        .addInterceptor(new LoggerInterceptor())
        .build();
複製程式碼

 

只要利用addInterceptor方法就可以新增攔截器,而自定義的攔截器只需要實現 Interceptor 介面就行了,可以使用攔截器方便的列印網路請求時,需要檢視的日誌。如下所示:

複製程式碼
public class LoggerInterceptor implements Interceptor {
 
  @Override
  public Response intercept(@NonNull Chain chain) throws IOException {
    // 攔截請求,獲取到該次請求的request
    Request request = chain.request();
    // 執行本次網路請求操作,返回response資訊
    Response response = chain.proceed(request);
    if (Configuration.DEBUG) {
      for (String key : request.headers().toMultimap().keySet()) {
        LogUtil.e("zp_test", "header: {" + key + " : " + request.headers().toMultimap().get(key) + "}");
      }
      LogUtil.e("zp_test", "url: " + request.url().uri().toString());
      ResponseBody responseBody = response.body();
 
      if (HttpHeaders.hasBody(response) && responseBody != null) {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(responseBody.byteStream(), "utf-8"));
        String result;
        while ((result = bufferedReader.readLine()) != null) {
          LogUtil.e("zp_test", "response: " + result);
        }
        // 測試程式碼
        responseBody.string();
      }
    }
    // 注意,這樣寫,等於重新建立Request,獲取新的Response,避免在執行以上程式碼時,
    // 呼叫了responseBody.string()而不能在返回體中再次呼叫。
    return response.newBuilder().build();
  }
 
}
複製程式碼

 

注意事項

  1. 如果提交的是表單,一定要設定表單型別, setType(MultipartBody.FORM)
  2. 提交檔案 addFormDataPart() 方法的第一個引數就是類似於鍵值對的鍵,是供服務端使用的,第二個引數是檔案的本地的名字,第三個引數是 RequestBody,裡面包含了我們要上傳的檔案的路徑以及 MidiaType。

  from:https://www.cnblogs.com/chenxibobo/p/9585760.html