觀戰Retrofit開發中的哪點事

又是一年中秋佳節,祝各位中秋節快樂。

今天我們來聊聊這個最近很火的網絡請求庫retrofit,在此基礎上會延伸出一些列的知識點。現在關於retrofit的文章很多,我之所以寫這篇文章的原因在於:8月份負責假設新客戶端底層的過程中首次嘗試使用該庫,並取得非常不錯的效果,不到20天的時間內實現新產品的快速開發。

另外因爲個人的原因,在寫完基礎框架及發佈兩個基礎版本之後,我也選擇的離職。現在,留給自己10天的時間,用來對2016年做個簡短的總結,也希望能幫助各位。首先簡單的說說會總結哪方面的,覺得感興趣的同學可以關注下:

  1. 網絡請求及優化,以retrofit作爲出發點,其中穿插源碼分析
  2. ANR原理及實現對ANR的監控
  3. App省點開發技巧
  4. apk混淆與逆向部分,其中拿一些市面上的app做示例。
  5. App快速開發框架部分,其中夾雜這一些從後端到移動端設計的敏感點,也會去整理出一個基本的app開發框架做演示
  6. 最後,就是一些雜七雜八點吧。對插件化之類的,目前市面上的文章已經非常不錯了。

暫時的計劃是這樣。話不多說,這就開始。


Retrofit是什麼?

Retrofit就是一個Http請求庫,和其它Http庫最大區別在於通過大範圍使用註解簡化Http請求。目前Retrofit 2.0底層是依賴OkHttp實現的,也就是說Retrofit本質上就是對OkHttp的更進一步封裝。那麼我們一定要使用它麼?當然不,目前Android領域內的各種網絡請求庫多不勝數,隨便哪一個都可以滿足你大部分的需求,所以沒必要糾結。不過從我個人的經驗來看,如果你的服務端是restful風格,並且你希望後面使用rxjava相關的技術,那麼Retrofit是你最好的選擇。


Retrofit實戰

添加依賴

要想使用retrofit,首先需要添加相關的依賴:

compile 'com.squareup.retrofit2:retrofit:2.0.2'

爲什麼要說這個呢?主要目的是告訴大家我們這裏是基於此版本的。接下來,我們正式開始講解。

常用註解

前面我們說道,retrofit通過使用註解來簡化請求,這裏我們先來認識一下常用的註解。retrofit中的註解大體分爲以下基類:用於標註請求方式的註解,用於標記請求頭的註解以及用於標記請求參數的註解。其實,任何一種Http庫都提供了相關的支持,無非在retrofit中是用註解來簡化。

請求方法註解

該類型的註解用於標註不同的http請求方式,主要有以下幾種:

註解 說明
@GET 表明這是get請求
@POST 表明這是post請求
@PUT 表明這是put請求
@DELETE 表明這是delete請求
@PATCH 表明這是一個patch請求,該請求是對put請求的補充,用於更新局部資源
@HEAD 表明這是一個head請求
@OPTIONS 表明這是一個option請求
@HTTP 通用註解,可以替換以上所有的註解,其擁有三個屬性:method,path,hasBody

我並不準備對其使用做什麼說明,官網的示例已經寫的非常不錯。但是我看國內的很多開發者只用get和post,這是件很悲傷的事情,實際上這些請求方法各自有各自的使用場景。
最容易混淆的是put,post,patch這三者,簡單的說,post表示新增,put可以理解爲完整替換,而patch則是更新資源。順便來看看官方定義:

  • POST to create a new resource when the client cannot predict the identity on the origin server (think a new order)
  • PUT to override the definition of a specified resource with what is passed in from the client
  • PATCH to override a portion of a specified resource in a predictable and effectively transactional way (if the entire patch cannot be performed, the server should not do any part of it)

接下來我們來重點說說@HTTP:
@HTTP註解我個人很少用,這裏用個簡單的例子來說明下:我們當前存在獲取驗證碼的請求:

@GET("mobile/capture")
Call<ResponseBody> getCapture(@Query("phone") String phone);

用@HTTP代替後:

@HTTP(method = "get", path = "mobile/capture", hasBody = false)
Call<ResponseBody> getCapture(@Query("phone") String phone);

請求頭註解

該類型的註解用於爲請求添加請求頭。

註解 說明
@Headers 用於添加固定請求頭,可以同時添加多個。通過該註解添加的請求頭不會相互覆蓋,而是共同存在
@Header 作爲方法的參數傳入,用於添加不固定值的Header,該註解會更新已有的請求頭

首先來看@Headers的示例:

//使用@Headers添加單個請求頭
@Headers("Cache-Control:public,max-age=120")
@GET("mobile/active")
Call<ResponseBody> getActive(@Query("id") int activeId);

//使用@Headers添加多個請求頭
@Headers({
    "User-Agent:android"
    "Cache-Control:public,max-age=120",
    })
@GET("mobile/active")
Call<ResponseBody> getActive(@Query("id") int activeId);

接下來來看@Header的示例:

@GET("mobile/active")
Call<ResponseBody> getActive(@Header("token") String token,@Query("id") int activeId);

可以看出@Header是以方法參數形勢傳入的,想必你現在能理解@Headers和@Header之間的區別了。

請求和響應格式註解

該類型的註解用於標註請求和響應的格式。

名稱 說明
@FormUrlEncoded 表示請求發送編碼表單數據,每個鍵值對需要使用@Field註解
@Multipart 表示請求發送multipart數據,需要配合使用@Part
@Streaming 表示響應用字節流的形式返回.如果沒使用該註解,默認會把數據全部載入到內存中.該註解在在下載大文件的特別有用

請求參數類註解

該類型的註解用來標註請求參數的格式,有些需要結合上面請求和響應格式的註解一起使用。

名稱 說明
@Body 多用於post請求發送非表單數據,比如想要以post方式傳遞json格式數據
@Filed 多用於post請求中表單字段,Filed和FieldMap需要FormUrlEncoded結合使用
@FiledMap 和@Filed作用一致,用於不確定表單參數
@Part 用於表單字段,Part和PartMap與Multipart註解結合使用,適合文件上傳的情況
@PartMap 用於表單字段,默認接受的類型是Map<String,REquestBody>,可用於實現多文件上傳
@Path 用於url中的佔位符
@Query 用於Get中指定參數
@QueryMap 和Query使用類似
@Url 指定請求路徑

我們依次對着註解進行舉例說明。

@Headers

@Headers("Cache-Control: max-age=64000")
@GET("active/list")
Call<List<Active>> ActiveList();

當然@Header也支持同時設置多個:

@Headers({
    "version:1.0.0"
    "Cache-Control: max-age=64000"
})
@GET("active/list")
Call<List<Active>> ActiveList();

@Header

@GET("user")
Call<User> getUserInfo(@Header("token") token)

@Body
根據轉換方式將實例對象轉換爲相應的字符串作爲請求參數傳遞。比如在很多情況下,你可能需要以post的方式上傳json格式的數據。那麼該怎麼來做呢?
我們以一個登錄接口爲例,該接口接受以下格式的json數據:

{"password":"abc123456","username":"18611990521"}

首先建立請求實體,爲了區別其他實體,通常來說約定以Post爲後綴.

public class LoginPost {
    private String username;
    private String password;

    public LoginPost(String username, String password) {
        this.username = username;
        this.password = password;
    }

}

然後定義該請求api:

@POST("mobile/login")
Call<ResponseBody> login(@Body LoginPost post);

retrofit默認採用json轉化器,因此在我們發送數據的時候會將LogintPost對象映射成json數據,這樣發送出的數據就是json格式的。另外,如果你不確定這種轉化行爲,可以強制指定retrofit使用Gson轉換器:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://www.test.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build();

更詳細的內容參照[retrofit官網][1],另外關於retrofit的轉換器我會在另一節中進行詳細的分析。

@Filed & @FiledMap
@Filed通常多用於Post請求中以表單的形勢上傳數據,這對任何開發者來說應該都是很常見的。

@POST("mobile/register")
Call<ResponseBody> registerDevice(@Field("id") String registerid);

@FileMap和@Filed的用途相似,但是它用於不確定表單參數個數的情況下。

@Part & @PartMap
多用於Post請求實現文件上傳功能。關於這兩者的具體使用參考下文的文件上傳。
在這裏我們來解釋一下@Filed和@Part的區別。
兩者都可以用於Post提交,但是最大的不同在於@Part標誌上文的內容可以是富媒體形勢,比如上傳一張圖片,上傳一段音樂,即它多用於字節流傳輸。而@Filed則相對簡單些,通常是字符串鍵值對。

@Path
關於@Path沒什麼好說,官網解釋已經足夠清楚了。這裏着重提示:

{佔位符}和PATH只用在URL的path部分,url中的參數使用Query和QueryMap 代替,保證接口定義的簡潔

異步VS同步

任何一個任務都可以被分爲異步任務或者同步任務,和其它大多數的請求框架一樣,retrofit也分爲同步請求和異步請求。在retrofit是實現這兩者非常簡單:

同步調用

同步請求需要藉助retrofit提供的execute()方法實現。

  public void get() throws IOException {
        Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();
        GitHubApi api = retrofit.create(GitHubApi.class);
        Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);
        Response<ResponseBody> response = call.execute();
        if (response.isSuccessful()) {
            ResponseBody responseBody = response.body();
            //處理成功請求
        }else{
            //處理失敗請求
        }
    }

異步調用

異步請求需要藉助retrofit提供的enqueue()方法實現。(從這個方法名中你可以看出之該方法實現的是將請求加入請求隊列)。想async-http一樣,同樣你需要在enqueue(()方法中爲其最終結果提供相應的回調,以實現結果的處理。

    public void get() {
        Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();
        GitHubApi api = retrofit.create(GitHubApi.class);

        Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);
        call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                //處理請求成功

            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                //處理請求失敗
            }
        });
    }

不難發現retrofit中實現同步和異步是如此的方便,僅僅通過提供請求的不同執行方法(execute()和enqueue())便成功的實現的請求執行方式和請求類型的解耦,實在是棒極了。

到目前爲止,使用retrofit的多是在Android上,此時我們關注多事異步請求,畢竟Android中並不允許你在主線程去做一些耗時任務。

無論是同步請求還是異步請求,我們都希望這兩種請求是可控的,通常來說是分爲三個方面:開始請求,結束請求以及查詢請求的執行狀態。上面的同步請求和異步請求屬於開始請求這方面,那麼結束請求和查詢請求呢?

我們發現無論是同步請求還是異步請求,返回給我們的都是Call接口的實例。我們稍微看一下該接口:

public interface Call<T> extends Cloneable {
  //執行同步請求
  Response<T> execute() throws IOException;

  //執行異步請求
  void enqueue(Callback<T> callback);

  //該請求是否在執行過程中
  boolean isExecuted();

  //取消當前執行的請求
  void cancel();

  //該請求是否被執行
  boolean isCanceled();

  //和java中的clone()方法含義一樣,通常利用該請求實現重複請求
  Call<T> clone();

  //獲取原始請求
  Request request();
}

通過上面的代碼不難看出,Call接口提供了我們上面所說的執行請求,查詢請求狀態以及結束請求。

移除請求

看完上面的Call對象之後,我們知道要想取消一個請求(無論異步還是同步),則只需要在響應的Call對象上調用其cancel()對象即可。

public void cancle(){
            Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();
        GitHubApi api = retrofit.create(GitHubApi.class);
        Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);
        Response<ResponseBody> response = call.execute();
        if (response.isSuccessful()) {
            ResponseBody responseBody = response.body();
            //處理成功請求
        }else{
            //處理失敗請求
        }

        ...

//取消相關請求
call.cancel();

}

多次請求

個別情況下我們可能需要一個請求執行多次。但是我們在retrofit中,call對象只能被調用一次,這時候該怎麼辦?
這時候我們可以利用Call接口中提供的clone()方法實現多次請求。

 public void multi_async_get() {
        Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();
        GitHubApi api = retrofit.create(GitHubApi.class);
        Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);
        call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                //請求成功
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                //請求失敗
            }
        });

        call.clone().enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                //請求成功
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                //請求失敗
            }
        });
    }

關於請求

上面我們簡單的介紹了retrofit中註解,但是我並不準備像入門教程一樣去舉例說明。這裏我們只對大家經常有困惑的兩點做說明:

提交json格式數據

很多情況下,我們需要上傳json格式的數據。比如當我們註冊新用戶的時候,因爲用戶註冊時的數據相對較多,並可能以後會變化,這時候,服務端可能要求我們上傳json格式的數據。此時就要@Body註解來實現。

首先定義請求實體RegisterPost:

public class RegisterPost {
   private String username;
   private int age;

   ...
}

接下來定義請求方法:

    @POST("mobile/register")
    Call register1(@Body RegisterPost post);

這樣我們就能夠上傳json格式的數據了。

上傳文件

retrofit中的實現文件上傳也是非常簡單的。這裏我們以圖片上傳爲例。

單張圖片上傳
retrofit 2.0的上傳和以前略有不同,需要藉助@Multipart註解、@Part和MultipartBody實現。
首先定義上傳接口

@Multipart
@POST("mobile/upload")
Call<ResponseBody> upload(@Part MultipartBody.Part file);

然後來看看如何調用該方法。和調用其他請求稍有不同,這裏我們需要構建MultipartBody對象:

File file = new File(url);
//構建requestbody
RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file);
//將resquestbody封裝爲MultipartBody.Part對象
MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);

這樣,我們就可以方便的進行上傳圖片了。

多張圖片上傳
如果有很多張圖片要上傳,我們總不能一張一張的來吧?好吧,我們來看看如果進行多文件(圖片)上傳。
在retrofit中提供了@PartMap註解,藉助該對象,我們可以實現多文件的上傳。同樣我們來看看具體文件的定義

@Multipart
@POST("upload/upload")
Call<ResponseBody> upload(@PartMap Map<String, MultipartBody.Part> map);

和單文件上傳的唯一區別就是將@Part註解換成了@PartMap註解。這意味我們可以以Map的形式進行多文件上傳。具體如何調用相信你已經明白。

圖文混傳
無論是多文件上傳還是單文件上傳,本質上個都是藉助@Multipart註解和MultipartBody來實現的。
這和其他網絡請求框架的實現原理並無本質區別,但retrofit在圖文上傳方面得天獨厚的優勢。比如我們在註冊時候既要傳用戶文本信息又要上傳圖片,結合上面的用戶註冊來做說明:

@Multipart
@POST("")
Call<ResponseBody> register(@Body RegisterPost post,@Part MultipartBody.Part image);

該註冊接口實現了用戶註冊信息和用戶頭像的同時上傳,其調用無非就是結合我們上文提到的json數據上傳以及單張圖上傳。

文件下載

很多時候,我們可能需要暫時下載文件,但是又不希望引入其他的下載庫,那麼如何retrofit實現下載呢?同樣,我們還是以下載圖片爲例

首先定義api接口如下:

@GET
Call<ResponseBody> downloadPicture(@Url String fileUrl);

關鍵就是獲取到ResponseBody對象。我們來看獲取到ResponseBody之後的處理:

 InputStream is = responseBody.byteStream();
 String[] urlArr = url.split("/");
 File filesDir = Environment.getExternalStorageDirectory();
 File file = new File(filesDir, urlArr[urlArr.length - 1]);
 if (file.exists()) file.delete();

不難發現這裏的關鍵就是通過ResponseBody對象獲取字節流,最後將其保存下來即可。實現下載就是這麼簡單。

這裏需要注意的是如果下載的文件較大,比如在10m以上,那麼強烈建議你使用@Streaming進行註解,否則將會出現IO異常.

@Streaming
@GET
Observable<ResponseBody> downloadPicture(@Url String fileUrl);

攔截器Interceptors使用

熟悉OkHttp的童鞋對Interceptors一定不會陌生。而Retrofit 2.0 底層強制依賴okHttp,所以可以使用okHttp的攔截器Interceptors 來對所有請求進行再處理。同樣來說,我們經常使用攔截器實現以下功能:

  • 設置通用Header
  • 設置通用請求參數
  • 攔截響應
  • 統一輸出日誌
  • 實現緩存

下面我們以上各自使用的場景給出相應的代碼說明:

設置通用Header

在app api接口設計中,我們往往需要客戶端在請求方法時,攜帶appid,appkey,timestamp,signature及version等header。你可能會問前邊不提到的@Headers不也同樣可以做到這事情麼?在方法很少的情況下,或者個別請求方法需要的情況下使用@Headers來添加當然可以,但是如果要爲所有請求方法都添加還是藉助攔截器使用更爲方便。直接看代碼:

   public static Interceptor getRequestHeader() {
        Interceptor headerInterceptor = new Interceptor() {

            @Override
            public Response intercept(Chain chain) throws IOException {
                Request originalRequest = chain.request();
                Request.Builder builder = originalRequest.newBuilder();
                builder.header("appid", "1");
                builder.header("timestamp", System.currentTimeMillis() + "");
                builder.header("appkey", "zRc9bBpQvZYmpqkwOo");
                builder.header("signature", "dsljdljflajsnxdsd");

                Request.Builder requestBuilder =builder.method(originalRequest.method(), originalRequest.body());
                Request request = requestBuilder.build();
                return chain.proceed(request);
            }

        };

        return headerInterceptor;
    }

你會發現在設置header的時候,我們有兩種方法可選擇:addHeader()和header()。切莫混淆兩者之間的區別:

使用addHeader()不會覆蓋之前設置的header,若使用header()則會覆蓋之前的header

統一輸出請求日誌

在早期開發調試階段,我們希望看到每個請求的詳細信息,在發佈時關閉這些消息。
得益於retrofit和okhttp的良好設計,可以方便的通過添加Log攔截器來實現,這裏我們使用到OkHttp中的HttpLoggingInterceptor攔截器。

在retrofit 2.0中要使用日誌攔截器,首先添加依賴:

compile 'com.squareup.okhttp3:logging-interceptor:3.1.2'

然後創建日誌攔截器

  public static HttpLoggingInterceptor getHttpLoggingInterceptor() {
        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        return loggingInterceptor;
    }

攔截服務器響應

通常來說,我們多利用攔截器來實現對請求的攔截。但是在很多的情況下我們需要從響應中獲取響應的Headers中獲取指定的header,比如在有些功能中我們需要服務端會給出我們某個活動的起始時間,需要我們客戶端來判斷當然活動是否可以執行。這時候,我們顯然不能利用客戶端本地的時間(有條原則叫做永遠不要相信客戶端的時間),這時候就需要服務端在將服務器的時間傳給我們。爲了方便,通常時間服務器的時間戳放在每個響應Header當中。

那麼我們該怎麼拿到這個時間戳呢?攔截器可以非常容易的幫助我們解決這個問題。這裏我們假設服務器在任何一個響應的Header中都添加了time,我們要做的就是通過攔截器來獲取到Header,具體見代碼:

    public static Interceptor getResponseHeader() {
        Interceptor interceptor = new Interceptor() {

            @Override
            public Response intercept(Chain chain) throws IOException {
                Response response = chain.proceed(chain.request());
                String timestamp = response.header("time");
                if (timestamp != null) {
                    //獲取到響應header中的time
                }
                return response;
            }
        };
        return interceptor;
    }

通過上面的響應攔截器實現了從響應中獲取服務器返回的time,就是這麼簡單。

設置通用請求參數

在實際項目中,各個客戶端往往需要向服務端傳送一些固定的參數,通常來說有兩種方案:

  1. 可以將這個公共的請求參數放到請求Header中
  2. 也可以將其放在請求參數中

如何添加到header中我們已經介紹過了,現在來看看如何添加公共請求參數。添加公共請求參數和添加公共Header實現原理一致,都是藉助攔截器來實現,這裏我們同樣直接來看代碼:

private void commonParamsInterceptor() {
        Interceptor commonParams = new Interceptor() {
            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException {
                Request originRequest = chain.request();
                Request request;
//                String method = originRequest.method();
//                Headers headers = originRequest.headers();
                HttpUrl httpUrl = originRequest.url().newBuilder().addQueryParameter("paltform", "android").addQueryParameter("version", "1.0.0").build();
                request = originRequest.newBuilder().url(httpUrl).build();
                return chain.proceed(request);
            }
        };

        return commonParams;
    }

我個人強烈建議:如果需要添加統一的請求參數,最好將其放在請求頭當中。

使用攔截器

上面我們介紹在實際開發中4中常用的攔截器,可以發現有了這些攔截器,我們可以很容易處理公共聚焦點。至於攔截器的使用,就是直接將響應的攔截器設置給OkHttpClient客戶端即可,以添加日誌攔截器爲例:

HttpLoggingInterceptor logging = getHttpLoggingInterceptor();
//設置日誌攔截器
OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(logging).build();

Retrofit retofit=new Retrofit.Builder().baseUrl("http://www.demo.com").client(httpClient).build();    

客戶端請求策略

任何一個Http請求庫都少不了失敗重試及請求超時的設置。來看一下retrofit中如何設置:

失敗重試

retrofit通過okHttpClient來設置失敗時自動重試,其使用也非常簡單:

 public void setRetry(OkHttpClient.Builder builder) {
        builder.retryOnConnectionFailure(true);
    }

設置請求超時

當然,retrofit作爲一個完善的網絡請求框架也少不了這方面的設置。

public void setConnecTimeout(OkHttpClient.Builder builder) {
        builder.connectTimeout(10, TimeUnit.SECONDS);
        builder.readTimeout(20, TimeUnit.SECONDS);
        builder.writeTimeout(20, TimeUnit.SECONDS);

    }

添加緩存支持

在上面攔截器的使用中,我i門已經介紹了4中攔截器的使用,現在我們來介紹如何使用攔截器來實現HTTP緩存。Http緩存原理在本文中並不做重點解釋。
ok,現在來看看Retrofit中如何配置使用緩存。

設置緩存的的兩種方式
在retrofit中可以通過兩種方式設置緩存:

    1. 通過添加 @Headers(“Cache-Control: max-age=120”) 進行設置。添加了Cache-Control 的請求,retrofit 會默認緩存該請求的返回數據。
    1. 通過Interceptors實現緩存。

這兩者實現原理一致,但是適用場景不同。通常是使用Interceptors來設置通用緩存策略,而通過@Header針對某個請求單獨設置緩存策略。另外,一定要記住,retrofit 2.0底層依賴OkHttp實現,這也就意味着retrofit緩存的實現同樣是藉助OkHttp來的。另外,無論你是決定使用那種形勢的緩存,首先要爲OkHttpClient設置Cache,否則緩存不會生效(retrofit併爲設置默認緩存目錄),Cache的設置你將在下文看到。

下面我們來具體看看,如何通過@Headers爲某個方法設置緩存時間

@Headers("Cache-Control:public,max-age=120")
@GET("mobile/active")
Call<ResponseBody> getActive(@Query("id") int activeId);

這樣我們就通過@Headers快速的爲該api添加了緩存控制。120s內,緩存都是生效狀態,即無論有網無網都讀取緩存。

現在我們再來看一下如何利用攔截器來實現緩存:
首先創建緩存攔截器:

    public static Interceptor getCacheInterceptor() {
        return new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                Response response = chain.proceed(request);
                return response.newBuilder().header("Cache-Control","public,max-age=120").build();
            }

        };
    }

和其它的攔截器使用一樣,將其設置到OkHttpClient即可,但此時設置緩存攔截器使用的addNetworkInterceptor()方法。凡是使用該設置了該緩存攔截器的OkHttpClient都具備了緩存功能,具體代碼如下:

//創建Cache
Cache cache = new Cache(AppContext.context().getCacheDir(), 10 * 1024 * 1024);

//設置攔截器和Cache
OkHttpClient httpClient = new OkHttpClient.Builder().addNetworkInterceptor(getCacheInterceptor()).cache(cache).build();

//設置OkHttpClient
Retrofit retofit=new Retrofit.Builder().baseUrl("http://www.demo.com").client(httpClient).build();   

實際開發中往往要求,有網的情況下直接從網絡中獲取數據,無網絡的情況下才走緩存,那麼此時上面的緩存攔截器就不是適用了,那這該怎麼做呢?

在解決這個問題之前首先我們解決大家的一個疑惑:通過addNetworkInterceptor()和通過addInterceptor()添加的攔截器有什麼不同呢?
簡單來說,addNetworkInterfacetor()添加的是網絡攔截器(Network Interfacetor),它會在request和response時分別被調用一次;addInterceptor()添加的是應用攔截器(Application Interceptor),他只會在response被調用一次。OkHttp中對此做了更加詳細的解釋[OkHttp攔截器詳解][2]

我們將上面的緩存問題再明確一下:在無網絡的情況下讀取緩存,有網絡的情況下根據緩存的過期時間重新請求,根據需求,我們創建以下攔截器:

    public static Interceptor getCacheInterceptor() {
        return new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                if (!TDevice.hasInternet()) {
                    //無網絡下強制使用緩存,無論緩存是否過期,此時該請求實際上不會被髮送出去。
                    request=request.newBuilder().cacheControl(CacheControl.FORCE_CACHE)
                            .build();
                }

                Response response = chain.proceed(request);
                if (TDevice.hasInternet()) {//有網絡情況下,根據請求接口的設置,配置緩存。
                //這樣在下次請求時,根據緩存決定是否真正發出請求。
                    String cacheControl = request.cacheControl().toString();
                    //當然如果你想在有網絡的情況下都直接走網絡,那麼只需要
                    //將其超時時間這是爲0即可:String cacheControl="Cache-Control:public,max-age=0"
                    return response.newBuilder().header("Cache-Control", cacheControl)
                            .removeHeader("Pragma")
                            .build();
                }else{//無網絡
                    return response.newBuilder().header("Cache-Control", "public,only-if-cached,max-stale=360000")
                            .removeHeader("Pragma")
                            .build();
                }

            }
        };
    }

接下來,將該請求設置到OkHttpClient,此時我們的網絡攔截器和應用攔截器都添加的是上面同一個攔截器:

//創建Cache
Cache cache = new Cache(AppContext.context().getCacheDir(), 10 * 1024 * 1024);

//設置攔截器和Cache
OkHttpClient httpClient = new OkHttpClient.Builder().addNetworkInterceptor(getCacheInterceptor()).cache(cache).addInterceptor(getCacheInterceptor()).build();

//設置OkHttpClient
Retrofit retofit=new Retrofit.Builder().baseUrl("http://www.demo.com").client(httpClient).build();   

實際上,緩存策略應該由服務器指定,但是在有些情況下服務器並不支持緩存策略,這就要求我們客戶端自行設置緩存策略。以上的代碼假設服務端不支持緩存策略,因此器緩存策略完全由客戶端通過重寫request和response來實現。

不出意外,在進行一些網絡請求後,我們就可以在緩存目前下看到許多的緩存文件。每一個請求的緩存文件都分爲兩部分,非別是以.0結尾的請求和以.1結尾的響應數據。到這裏,關於緩存的部門我們就說完了。我們可能會問,必須要基於retrofit來實現緩存麼?如果,以後我更換網絡框架(儘管可能性非常小),這豈不是要出大問題?如果你此顧慮,完全可以自行實現一套緩存框架,其原理本質上也非常相似:基於LRU算法。你可能不瞭解LRU算法,但是LRUCache和LRUDiskCache想必是耳熟能詳的,對此我不畫蛇添足了。

設置cookie

retrofit 2.0依賴OkHttp 3.0.而在OkHttp 3.0中,爲了方便開發者自定義cookie的管理策略,新增了Cookiejar和Cookie兩個類。

持久化cookie和非持久化cookie

cookie的管理又分爲持久化cookie和非持久化cookie。非持久化cookie存儲在內存中,也就意味着,其生命週期基本和app保持一致(如果你的Retrofit是單例),app關閉後,cookie丟失。而持久化cookie則是存儲在本地磁盤中,app關閉後不丟失。

首先來看非持久化cookie。對於OkHttp 3來說,要實現非持久化我們有兩種方案,一種是像以前一樣,利用傳統的JavaNetCookieJar來實現,另一種則是利用OkHttp 3給我們提供的CookieJar自實行實現。

非持久化cookie——利用JavaNetCookieJar

首先我們還是利用JavaNetCookieJar來實現非持久化cookie,它和以前的實現一樣,首先添加依賴

compile 'com.squareup.okhttp3:okhttp-urlconnection:3.2.0'

接下來在代碼中設置cookie

  public void setCookies(OkHttpClient.Builder builder) {
        CookieManager cookieManager = new CookieManager();
        cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
        builder.cookieJar(new JavaNetCookieJar(cookieManager));
    }

不難發現,它的使用非常簡單,主要依靠CookieManager和JavaNetCookieJar兩個類即可,關於其原理部分我們放在另外一文中進行解釋。

非持久化cookie——自定義CookieJar

通過CookieJar來實現非持久化cookie顯得更爲簡單,其代碼如下:

   builder.cookieJar(new CookieJar() {
            final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<HttpUrl, List<Cookie>>()
            @Override
            public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                cookieStore.put(url, cookies);//保存cookie
            }

            @Override
            public List<Cookie> loadForRequest(HttpUrl url) {
                List<Cookie> cookies = cookieStore.get(url);//取出cookie
                return cookies!=null?cookies:new ArrayList<Cookie>();
            }
        });

這樣,我們就實現了一個非常簡單的非持久化cookie。

因此要實現非持久化Cookie是如此的簡單,就不多做解釋了。

持久化cookie

上面的代碼中實現對cookie的管理並不是持久化在本地,而是在內存當中。如果想要實現cookie的持久化應該怎麼做呢?
很顯然,我們會考慮從response中取出cookie取出cookie保存在本地,在request的時候,從本地取出cookie添加到請求中即可.有了大體的思路之後,如何持久化cookie也就不成問題了,此時想必你的腦海中已經有了以下幾種解決方案:

  1. 通過響應攔截器從response取出cookie並保存到本地,通過請求攔截器從本地取出cookie並添加到請求中
  2. 自定義CookieJar,在saveFromResponse()中保存cookie到本地,在loadForRequest()從本地取出cookie。

以上兩種思路都能解決我們的問題。但是現在我並不急於向你展示如何持久化cookie。而是希望通過JavaNetCookieJar背後的原理來向大家展示另一種思路,也就是第三種思路。關於這方面,我會在另外一文中進行深入的分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章