Retrofit出來(lái)之后,就曾學(xué)習(xí)過(guò)它的使用方法,也做過(guò)簡(jiǎn)單Demo。但是這次在項(xiàng)目中應(yīng)用Retrofit2.0的時(shí)候,還是發(fā)現(xiàn)一些新問(wèn)題。趁著項(xiàng)目封包上線(xiàn),特來(lái)全面梳理總結(jié),從最簡(jiǎn)單的請(qǐng)求網(wǎng)絡(luò)一步步的豐富功能打造一套完善可行的網(wǎng)絡(luò)請(qǐng)求框架,以及自己在項(xiàng)目應(yīng)用中遇到的問(wèn)題和解決辦法:
先附上官方介紹:Retrofit、Github源碼地址
如何使用Retrofit進(jìn)行網(wǎng)絡(luò)請(qǐng)求
- 添加Retrofit依賴(lài)
- 創(chuàng)建 用于描述網(wǎng)絡(luò)請(qǐng)求 的接口
- 創(chuàng)建 Retrofit 實(shí)例
- 創(chuàng)建 網(wǎng)絡(luò)請(qǐng)求接口實(shí)例 并 配置網(wǎng)絡(luò)請(qǐng)求參數(shù)
- 發(fā)送網(wǎng)絡(luò)請(qǐng)求
1.添加Retrofit及相關(guān)庫(kù)的依賴(lài)
compile 'com.squareup.retrofit2:retrofit:2.2.0'
這里我們看見(jiàn)只添加Retrofit的依賴(lài),但是大家不要誤會(huì),Retrofit只是RESTful 的 HTTP 網(wǎng)絡(luò)請(qǐng)求框架的封裝??梢栽赗etrofit 2.0源碼的maven配置里發(fā)現(xiàn)添加了對(duì)OkHttp的依賴(lài)。網(wǎng)絡(luò)請(qǐng)求本質(zhì)上是 OkHttp 完成,而 Retrofit 僅負(fù)責(zé) 網(wǎng)絡(luò)請(qǐng)求接口的封裝。

App應(yīng)用程序通過(guò) Retrofit 請(qǐng)求網(wǎng)絡(luò),實(shí)際上是使用 Retrofit 接口層封裝請(qǐng)求參數(shù)、Header、Url 等信息,之后由 OkHttp 完成后續(xù)的請(qǐng)求操作;
在服務(wù)端返回?cái)?shù)據(jù)之后,OkHttp 將原始的結(jié)果交給 Retrofit,Retrofit根據(jù)用戶(hù)的需求對(duì)結(jié)果進(jìn)行解析。
附:也可通過(guò)添加依賴(lài)來(lái)指定OkHttp版本:
compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.okhttp3:okhttp:3.3.1'
2.創(chuàng)建用于描述網(wǎng)絡(luò)請(qǐng)求的接口

- 這里定義的是interface;
- 這里@GET注解是定義網(wǎng)絡(luò)請(qǐng)求方式;
- 后面的括號(hào)里的參數(shù)是接口路徑名,我們知道無(wú)法只通過(guò)接口路徑定位到接口,先帶著疑問(wèn)往下看;
- 這里@Query注解是服務(wù)接口的方法參數(shù);
- 返回值這里先返回完整的響應(yīng)體,稍后會(huì)解釋怎么數(shù)據(jù)解析。
這里注解還有很多,這里暫不一一解釋?zhuān)笪臅?huì)給出。
3.創(chuàng)建Retrofit實(shí)例 & 4.創(chuàng)建網(wǎng)絡(luò)請(qǐng)求接口實(shí)例并配置網(wǎng)絡(luò)請(qǐng)求參數(shù)

- 解惑:在創(chuàng)建Retrofit實(shí)例的時(shí)候設(shè)置了baseUrl,Retrofit把 網(wǎng)絡(luò)請(qǐng)求的URL 分成了兩部分設(shè)置。網(wǎng)絡(luò)請(qǐng)求的完整 Url =在創(chuàng)建Retrofit實(shí)例時(shí)通過(guò).baseUrl()設(shè)置 +網(wǎng)絡(luò)請(qǐng)求接口的注解設(shè)置;
- 創(chuàng)建網(wǎng)絡(luò)請(qǐng)求接口的實(shí)例,把Retrofit實(shí)例跟描述網(wǎng)絡(luò)請(qǐng)求的接口關(guān)聯(lián)起來(lái);
- 添加方法調(diào)用網(wǎng)絡(luò)請(qǐng)求。
5.發(fā)送網(wǎng)絡(luò)請(qǐng)求
通過(guò)上面的工作,我們的Retrofit就已經(jīng)做好了網(wǎng)絡(luò)請(qǐng)求的準(zhǔn)備了。下面正式發(fā)起請(qǐng)求,來(lái)驗(yàn)證流程是否正確。
注意申請(qǐng)網(wǎng)絡(luò)權(quán)限
<uses-permission android:name="android.permission.INTERNET"/>


- 通過(guò)獲取Retrofit實(shí)例發(fā)起請(qǐng)求傳遞參數(shù);
- 在enqueue()里面設(shè)置請(qǐng)求響應(yīng)的回調(diào)處理,我們可以看見(jiàn),在回調(diào)里面能直接更新UI,說(shuō)明這里回調(diào)是在主線(xiàn)程里,但具體是怎么操作的,后文去閱讀源碼再做介紹。
關(guān)于Retrofit那些注解
Retrofit的注解我們上面已經(jīng)接觸過(guò)了@GET、@Query,現(xiàn)在我來(lái)更全面的了解這些注解。其實(shí)Retrofit提供三類(lèi)一共22個(gè)注解,幫助我們?cè)谑褂眠^(guò)程中配置網(wǎng)絡(luò)。
1.HTTP請(qǐng)求方法類(lèi)注解

表格中的除HTTP以外都對(duì)應(yīng)了HTTP標(biāo)準(zhǔn)中的請(qǐng)求方法,而HTTP注解則可以代替以上方法中的任意一個(gè)注解,有3個(gè)屬性:method、path、hasBody。下面我們?cè)囍聾HTTP替代我們上面接口里面的定義:

2.標(biāo)記類(lèi)注解



- @FormUrlEncoded 表示發(fā)送form-encoded的數(shù)據(jù),每個(gè)鍵值對(duì)需要用@Filed來(lái)注解鍵名,隨后的對(duì)象需要提供值;
- @Multipart 表示發(fā)送form-encoded的數(shù)據(jù)(適用于 有文件 上傳的場(chǎng)景),每個(gè)鍵值對(duì)需要用@Part來(lái)注解鍵名,隨后的對(duì)象需要提供值。
3.網(wǎng)絡(luò)請(qǐng)求參數(shù)類(lèi)注解

- @Header 添加請(qǐng)求頭
- @Headers 添加不固定的請(qǐng)求頭
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)
// @Headers
@Headers("Authorization: authorization")
@GET("user")
Call<User> getUser()
// 以上的效果是一致的。
// 區(qū)別在于使用場(chǎng)景和使用方式
// 1. 使用場(chǎng)景:@Header用于添加不固定的請(qǐng)求頭,@Headers用于添加固定的請(qǐng)求頭
// 2. 使用方式:@Header作用于方法的參數(shù);@Headers作用于方法
- @Body 以 Post方式 傳遞 自定義數(shù)據(jù)類(lèi)型 給服務(wù)器;
特別注意:如果提交的是一個(gè)Map,那么作用相當(dāng)于 @Field。
Map要經(jīng)過(guò) FormBody.Builder 類(lèi)處理成為符合 Okhttp 格式的表單,如:
FormBody.Builder builder = new FormBody.Builder();
builder.add("key","value");
- @Field & @FieldMap 發(fā)送 Post請(qǐng)求 時(shí)提交請(qǐng)求的表單字段
與 @FormUrlEncoded 注解配合使用
public interface GetRequest_Interface {
/**
*表明是一個(gè)表單格式的請(qǐng)求(Content-Type:application/x-www-form-urlencoded)
* <code>Field("username")</code> 表示將后面的 <code>String name</code> 中name的取值作為 username 的值
*/
@POST("/form")
@FormUrlEncoded
Call<ResponseBody> testFormUrlEncoded1(@Field("username") String name, @Field("age") int age);
/**
* Map的key作為表單的鍵
*/
@POST("/form")
@FormUrlEncoded
Call<ResponseBody> testFormUrlEncoded2(@FieldMap Map<String, Object> map);
}
// 具體使用
// @Field
Call<ResponseBody> call1 = service.testFormUrlEncoded1("Carson", 24);
// @FieldMap
// 實(shí)現(xiàn)的效果與上面相同,但要傳入Map
Map<String, Object> map = new HashMap<>();
map.put("username", "Carson");
map.put("age", 24);
Call<ResponseBody> call2 = service.testFormUrlEncoded2(map);
- @Part & @PartMap 發(fā)送 Post請(qǐng)求 時(shí)提交請(qǐng)求的表單字段
與@Field的區(qū)別:功能相同,但攜帶的參數(shù)類(lèi)型更加豐富,包括數(shù)據(jù)流,所以適用于有文件上傳的場(chǎng)景
與 @Multipart 注解配合使用
public interface GetRequest_Interface {
/**
* {@link Part} 后面支持三種類(lèi)型,{@link RequestBody}、{@link okhttp3.MultipartBody.Part} 、任意類(lèi)型
* 除 {@link okhttp3.MultipartBody.Part} 以外,其它類(lèi)型都必須帶上表單字段({@link okhttp3.MultipartBody.Part} 中已經(jīng)包含了表單字段的信息),
*/
@POST("/form")
@Multipart
Call<ResponseBody> testFileUpload1(@Part("name") RequestBody name, @Part("age") RequestBody age, @Part MultipartBody.Part file);
/**
* PartMap 注解支持一個(gè)Map作為參數(shù),支持 {@link RequestBody } 類(lèi)型,
* 如果有其它的類(lèi)型,會(huì)被{@link retrofit2.Converter}轉(zhuǎn)換,如后面會(huì)介紹的 使用{@link com.google.gson.Gson} 的 {@link retrofit2.converter.gson.GsonRequestBodyConverter}
* 所以{@link MultipartBody.Part} 就不適用了,所以文件只能用<b> @Part MultipartBody.Part </b>
*/
@POST("/form")
@Multipart
Call<ResponseBody> testFileUpload2(@PartMap Map<String, RequestBody> args, @Part MultipartBody.Part file);
@POST("/form")
@Multipart
Call<ResponseBody> testFileUpload3(@PartMap Map<String, RequestBody> args);
}
// 具體使用
MediaType textType = MediaType.parse("text/plain");
RequestBody name = RequestBody.create(textType, "Carson");
RequestBody age = RequestBody.create(textType, "24");
RequestBody file = RequestBody.create(MediaType.parse("application/octet-stream"), "這里是模擬文件的內(nèi)容");
// @Part
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", "test.txt", file);
Call<ResponseBody> call3 = service.testFileUpload1(name, age, filePart);
ResponseBodyPrinter.printResponseBody(call3);
// @PartMap
// 實(shí)現(xiàn)和上面同樣的效果
Map<String, RequestBody> fileUpload2Args = new HashMap<>();
fileUpload2Args.put("name", name);
fileUpload2Args.put("age", age);
//這里并不會(huì)被當(dāng)成文件,因?yàn)闆](méi)有文件名(包含在Content-Disposition請(qǐng)求頭中),但上面的 filePart 有
//fileUpload2Args.put("file", file);
Call<ResponseBody> call4 = service.testFileUpload2(fileUpload2Args, filePart); //單獨(dú)處理文件
ResponseBodyPrinter.printResponseBody(call4);
}
- @Query和@QueryMap 用于 @GET 方法的查詢(xún)參數(shù)(URL請(qǐng)求里?后面的鍵值對(duì))
配置時(shí)只需要在接口方法中增加一個(gè)參數(shù)即可
@GET("bookcontent/")
Call<ResponseBody> getBookInfo(@Query("bookid") String bookId);
- @Path URL地址的缺省值
可用于配置動(dòng)態(tài)的URL地址
@GET("users/{user}/repos")
Call<ResponseBody> getBlog(@Path("user") String user );
// 訪(fǎng)問(wèn)的API是:https://api.github.com/users/{user}/repos
// 在發(fā)起請(qǐng)求時(shí), {user} 會(huì)被替換為方法的第一個(gè)參數(shù) user(被@Path注解作用)
- @Url 直接傳入一個(gè)請(qǐng)求的 URL變量 用于URL設(shè)置
@GET
Call<ResponseBody> testUrlAndQuery(@Url String url, @Query("showAll") boolean showAll);
// 當(dāng)有URL注解時(shí),@GET傳入的URL就可以省略
// 當(dāng)GET、POST...HTTP等方法中沒(méi)有設(shè)置Url時(shí),則必須使用 {@link Url}提供
一般在Android開(kāi)發(fā)中,設(shè)計(jì)網(wǎng)絡(luò)請(qǐng)求框架的時(shí)候我們都要考慮三個(gè)功能:網(wǎng)絡(luò)請(qǐng)求、請(qǐng)求結(jié)果數(shù)據(jù)解析、異步響應(yīng)(因?yàn)榭紤]到要在主線(xiàn)程刷新UI)。在上文中我們關(guān)于Retrofit網(wǎng)絡(luò)請(qǐng)求模塊的使用已經(jīng)有了完整的認(rèn)識(shí),下面我們繼續(xù)介紹數(shù)據(jù)解析、和異步響應(yīng)的知識(shí)點(diǎn)。
Retrofit數(shù)據(jù)解析器(Converter)
在前面的例子中我們請(qǐng)求中都是直接返回的Call<ResponseBody> ,這里并沒(méi)有做響應(yīng)結(jié)果的解析,我們還需要自己去響應(yīng)體里提取和解析數(shù)據(jù)。其實(shí),這些需求Retrofit的設(shè)計(jì)者們都已經(jīng)給我們考慮到了,Retrofit支持包括常見(jiàn)的Json在內(nèi)的,多種數(shù)據(jù)解析方式。以Json為例具體如下使用:
1.使用時(shí)需要在Gradle添加依賴(lài)
| 數(shù)據(jù)解析器 | Gradle依賴(lài) |
|---|---|
| Gson | com.squareup.retrofit2:converter-gson:2.0.2 |
| Jackson | com.squareup.retrofit2:converter-jackson:2.0.2 |
| Simple XML | com.squareup.retrofit2:converter-simplexml:2.0.2 |
| Moshi | com.squareup.retrofit2:converter-moshi:2.0.2 |
| Wire | com.squareup.retrofit2:converter-wire:2.0.2 |
| Scalars | com.squareup.retrofit2:converter-scalars:2.0.2 |
// 在build.gradle中添加依賴(lài)
compile 'com.squareup.retrofit2:converter-gson:2.2.0'
2.創(chuàng)建Retrofit實(shí)例時(shí),添加ConverterFactory

3.創(chuàng)建bean類(lèi)并設(shè)置接口返回類(lèi)型

Retrofit網(wǎng)絡(luò)請(qǐng)求適配器(CallAdapter)
Retrofit支持多種網(wǎng)絡(luò)請(qǐng)求適配器方式:guava、Java8和Rxjava ,使用時(shí)如使用的是 默認(rèn)的 CallAdapter,則不需要添加網(wǎng)絡(luò)請(qǐng)求適配器的依賴(lài),否則則需要按照需求進(jìn)行添加,以Rxjava為例具體如下使用:
1.使用時(shí)需要在Gradle添加依賴(lài)
| 網(wǎng)絡(luò)請(qǐng)求適配器 | Gradle依賴(lài) |
|---|---|
| guava | com.squareup.retrofit2:adapter-guava:2.0.2 |
| Java8 | com.squareup.retrofit2:adapter-java8:2.0.2 |
| rxjava | com.squareup.retrofit2:adapter-rxjava:2.0.2 |
這里有一個(gè)很坑的地方是, com.squareup.retrofit2:adapter-rxjava:2.0.2,用的是RxJava1.X,現(xiàn)在所以很多Rx2的新特性都沒(méi)有,所以在這里為了支持Rx2.0,并不建議使用square的適配器。我用的是jakewharton大神出的一款適配器,應(yīng)用如下:
// 引入請(qǐng)求適配器
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0-RC3'
// 引入RxAndroid適應(yīng)Android開(kāi)發(fā)需求
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
2.創(chuàng)建Retrofit實(shí)例時(shí),添加CallAdapterFactory

3.修改接口返回類(lèi)型

調(diào)用接口請(qǐng)求數(shù)據(jù):

至此關(guān)于在項(xiàng)目中引入Retrofit+OkHttp+RxJava的流程已經(jīng)簡(jiǎn)單的使用就已經(jīng)介紹完畢,但是自己在項(xiàng)目的使用過(guò)程中還是遇見(jiàn)了一些問(wèn)題,下面會(huì)做出總結(jié)。
在項(xiàng)目使用過(guò)程中遇見(jiàn)的問(wèn)題
1.OkHttp在4.4及以下不支持TLS協(xié)議的解決方法
這個(gè)bug最新是測(cè)試發(fā)現(xiàn)安裝包在某個(gè)機(jī)型上無(wú)法請(qǐng)求網(wǎng)絡(luò),然后找來(lái)測(cè)試機(jī)調(diào)試。發(fā)現(xiàn)在網(wǎng)絡(luò)請(qǐng)求的時(shí)候,會(huì)報(bào)異常,具體信息如下:
javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x79f145b0: Failure in SSL library, usually a protocol error
error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version
解決方案參考:# OkHttp在4.4及以下不支持TLS協(xié)議的解決方法
2.混淆問(wèn)題
這個(gè)問(wèn)題就有點(diǎn)坑了,測(cè)試用release包發(fā)現(xiàn)所有頁(yè)面都請(qǐng)求不到數(shù)據(jù)。當(dāng)時(shí)聽(tīng)描述就大概鎖定了是混淆問(wèn)題。但是自己多次對(duì)照官方文檔的混淆配置,發(fā)現(xiàn)自己的配置跟官方配置一樣。然后調(diào)試的時(shí)候看了一下異常信息,大概是說(shuō)解析的時(shí)候的問(wèn)題。然后我去掉了Retrofit的Gson數(shù)據(jù)解析器進(jìn)行測(cè)試,發(fā)現(xiàn)請(qǐng)求是能正常發(fā)送和響應(yīng)的。鎖定了問(wèn)題之后問(wèn)題之后開(kāi)始找解決辦法,由于converter-gson的資料很少。我想著其原理應(yīng)該是Gson類(lèi)似,于是這是找Gson有關(guān)的混淆問(wèn)題,最后解決辦法如下:

# Retrofit
-dontnote retrofit2.Platform
-dontnote retrofit2.Platform$IOS$MainThreadExecutor
-dontwarn retrofit2.Platform$Java8
-keepattributes Signature
-keepattributes Exceptions
# okhttp
-dontwarn okio.**
# converter-gson
-keep class cn.tianya.light.reader.model.bean.**{*;} # 自定義數(shù)據(jù)模型的bean目錄