又是一年中秋佳節,祝各位中秋節快樂。
今天我們來聊聊這個最近很火的網絡請求庫retrofit,在此基礎上會延伸出一些列的知識點。現在關於retrofit的文章很多,我之所以寫這篇文章的原因在於:8月份負責假設新客戶端底層的過程中首次嘗試使用該庫,並取得非常不錯的效果,不到20天的時間內實現新產品的快速開發。
另外因爲個人的原因,在寫完基礎框架及發佈兩個基礎版本之後,我也選擇的離職。現在,留給自己10天的時間,用來對2016年做個簡短的總結,也希望能幫助各位。首先簡單的說說會總結哪方面的,覺得感興趣的同學可以關注下:
- 網絡請求及優化,以retrofit作爲出發點,其中穿插源碼分析
- ANR原理及實現對ANR的監控
- App省點開發技巧
- apk混淆與逆向部分,其中拿一些市面上的app做示例。
- App快速開發框架部分,其中夾雜這一些從後端到移動端設計的敏感點,也會去整理出一個基本的app開發框架做演示
- 最後,就是一些雜七雜八點吧。對插件化之類的,目前市面上的文章已經非常不錯了。
暫時的計劃是這樣。話不多說,這就開始。
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,就是這麼簡單。
設置通用請求參數
在實際項目中,各個客戶端往往需要向服務端傳送一些固定的參數,通常來說有兩種方案:
- 可以將這個公共的請求參數放到請求Header中
- 也可以將其放在請求參數中
如何添加到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中可以通過兩種方式設置緩存:
- 通過添加 @Headers(“Cache-Control: max-age=120”) 進行設置。添加了Cache-Control 的請求,retrofit 會默認緩存該請求的返回數據。
- 通過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也就不成問題了,此時想必你的腦海中已經有了以下幾種解決方案:
- 通過響應攔截器從response取出cookie並保存到本地,通過請求攔截器從本地取出cookie並添加到請求中
- 自定義CookieJar,在saveFromResponse()中保存cookie到本地,在loadForRequest()從本地取出cookie。
以上兩種思路都能解決我們的問題。但是現在我並不急於向你展示如何持久化cookie。而是希望通過JavaNetCookieJar背後的原理來向大家展示另一種思路,也就是第三種思路。關於這方面,我會在另外一文中進行深入的分析。