文章來源:騰訊Bugly
Android 開發中,從原生的
HttpUrlConnection
到經典的 Apache 的
HttpClient
,再到對前面這些網絡基礎框架的封裝,比如
Volley
、Async
Http Client
,Http 相關開源框架的選擇還是很多的,其中由著名的 Square 公司開源的
Retrofit
更是以其簡易的接口配置、強大的擴展支持、優雅的代碼結構受到大家的追捧。也正是由於 Square 家的框架一如既往的簡潔優雅,所以我一直在想,Square 公司是不是隻招處女座的程序員?
1、初識 Retrofit
單從
Retrofit
這個單詞,你似乎看不出它究竟是幹嘛的,當然,我也看不出來 :)逃。。
Retrofitting refers to the addition of new technology or features to older systems.
—From Wikipedia
於是我們就明白了,冠以
Retrofit
這個名字的這個傢伙,應該是某某某的 『Plus』 版本了。
1.1 Retrofit 概覽
Retrofit
是一個 RESTful 的 HTTP 網絡請求框架的封裝。注意這裏並沒有說它是網絡請求框架,主要原因在於網絡請求的工作並不是
Retrofit
來完成的。Retrofit
2.0 開始內置
OkHttp
,前者專注於接口的封裝,後者專注於網絡請求的高效,二者分工協作,宛如古人的『你耕地來我織布』,小日子別提多幸福了。
我們的應用程序通過
Retrofit
請求網絡,實際上是使用
Retrofit
接口層封裝請求參數、Header、Url 等信息,之後由
OkHttp
完成後續的請求操作,在服務端返回數據之後,OkHttp
將原始的結果交給
Retrofit
,後者根據用戶的需求對結果進行解析的過程。
講到這裏,你就會發現所謂
Retrofit
,其實就是
Retrofitting OkHttp 了。
1.2 Hello Retrofit
多說無益,不要來段代碼陶醉一下。使用
Retrofit
非常簡單,首先你需要在你的 build.gradle 中添加依賴:
compile 'com.squareup.retrofit2:retrofit:2.0.2'
你一定是想要訪問 GitHub 的 api 對吧,那麼我們就定義一個接口:
public interface GitHubService {
@GET("users/{user}/repos")
Call> listRepos(@Path("user") String user);
}
接口當中的
listRepos
方法,就是我們想要訪問的 api 了:
其中,在發起請求時,
{user}
會被替換爲方法的第一個參數
user
。
好,現在接口有了,我們要構造
Retrofit
了:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
這裏的
service
就好比我們的快遞哥,還是往返的那種哈~
Call> repos = service.listRepos("octocat");
發請求的代碼就像前面這一句,返回的
repos
其實並不是真正的數據結果,它更像一條指令,你可以在合適的時機去執行它:
啥感覺?有沒有突然覺得請求接口就好像訪問自家的方法一樣簡單?吶,前面我們看到的,就是
Retrofit
官方的 demo 了。你以爲這就夠了?噗~怎麼可能。。
1.3 Url 配置
Retrofit
支持的協議包括
GET
/POST
/PUT
/DELETE
/HEAD
/PATCH
,當然你也可以直接用
HTTP
來自定義請求。這些協議均以註解的形式進行配置,比如我們已經見過
GET
的用法:
@GET("users/{user}/repos")
Call> listRepos(@Path("user") String user);
這些註解都有一個參數 value,用來配置其路徑,比如示例中的
users/{user}/repos
,我們還注意到在構造
Retrofit
之時我們還傳入了一個
baseUrl("https://api.github.com/")
,請求的完整 Url 就是通過
baseUrl
與註解的
value
(下面稱 “path
“
) 整合起來的,具體整合的規則如下:
-
path
是絕對路徑的形式:
path = "/apath"
,baseUrl = "http://host:port/a/b"
Url = "http://host:port/apath"
-
path
是相對路徑,baseUrl
是目錄形式:
path = "apath"
,baseUrl = "http://host:port/a/b/"
Url = "http://host:port/a/b/apath"
-
path
是相對路徑,baseUrl
是文件形式:
path = "apath"
,baseUrl = "http://host:port/a/b"
Url = "http://host:port/a/apath"
-
path
是完整的 Url:
path = "http://host:port/aa/apath"
,baseUrl = "http://host:port/a/b"
Url = "http://host:port/aa/apath"
建議採用第二種方式來配置,並儘量使用同一種路徑形式。如果你在代碼裏面混合採用了多種配置形式,恰好趕上你哪天頭暈眼花,信不信分分鐘寫一堆 bug 啊哈哈。
1.4 參數類型
發請求時,需要傳入參數,Retrofit
通過註解的形式令 Http 請求的參數變得更加直接,而且類型安全。
1.4.1 Query & QueryMap
@GET("/list")
Call list(@Query("page") int page);
Query
其實就是 Url 中 ‘?’ 後面的 key-value,比如:
這裏的
cate=android
就是一個
Query
,而我們在配置它的時候只需要在接口方法中增加一個參數,即可:
interface PrintlnServer{
@GET("/")
Call cate(@Query("cate") String cate);
}
這時候你肯定想,如果我有很多個
Query
,這麼一個個寫豈不是很累?而且根據不同的情況,有些字段可能不傳,這與方法的參數要求顯然也不相符。於是,打羣架版本的
QueryMap
橫空出世了,使用方法很簡單,我就不多說了。
1.4.2 Field & FieldMap
其實我們用
POST
的場景相對較多,絕大多數的服務端接口都需要做加密、鑑權和校驗,GET
顯然不能很好的滿足這個需求。使用
POST
提交表單的場景就更是剛需了,怎麼提呢?
@FormUrlEncoded
@POST("/")
Call example(
@Field("name") String name,
@Field("occupation") String occupation);
其實也很簡單,我們只需要定義上面的接口就可以了,我們用
Field
聲明瞭表單的項,這樣提交表單就跟普通的函數調用一樣簡單直接了。
等等,你說你的表單項不確定個數?還是說有很多項你懶得寫?Field
同樣有個打羣架的版本——FieldMap
,趕緊試試吧~~
1.4.3 Part & PartMap
這個是用來上傳文件的。話說當年用
HttpClient
上傳個文件老費勁了,一會兒編碼不對,一會兒參數錯誤(也怪那時段位太低吧TT)。。。可是現在不同了,自從有了
Retrofit
,媽媽再也不用擔心文件上傳費勁了~~~
public interface FileUploadService {
@Multipart
@POST("upload")
Call upload(@Part("description") RequestBody description,
@Part MultipartBody.Part file);
}
如果你需要上傳文件,和我們前面的做法類似,定義一個接口方法,需要注意的是,這個方法不再有
@FormUrlEncoded
這個註解,而換成了
@Multipart
,後面只需要在參數中增加
Part
就可以了。也許你會問,這裏的
Part
和
Field
究竟有什麼區別,其實從功能上講,無非就是客戶端向服務端發起請求攜帶參數的方式不同,並且前者可以攜帶的參數類型更加豐富,包括數據流。也正是因爲這一點,我們可以通過這種方式來上傳文件,下面我們就給出這個接口的使用方法:
在實驗時,我上傳了一個只包含一行文字的文件:
Visit me: http://www.println.net
那麼我們去服務端看下我們的請求是什麼樣的:
HEADERS
FORM/POST PARAMETERS
description: This is a description
RAW BODY
我們看到,我們上傳的文件的內容出現在請求當中了。如果你需要上傳多個文件,就聲明多個
Part
參數,或者試試
PartMap
。
1.5 Converter,讓你的入參和返回類型豐富起來
1.5.1 RequestBodyConverter
1.4.3 當中,我爲大家展示瞭如何用
Retrofit
上傳文件,這個上傳的過程其實。。還是有那麼點兒不夠簡練,我們只是要提供一個文件用於上傳,可我們前後構造了三個對象:
天哪,肯定是哪裏出了問題。實際上,Retrofit
允許我們自己定義入參和返回的類型,不過,如果這些類型比較特別,我們還需要準備相應的 Converter,也正是因爲 Converter 的存在,
Retrofit
在入參和返回類型上表現得非常靈活。
下面我們把剛纔的 Service 代碼稍作修改:
public interface FileUploadService {
@Multipart
@POST("upload")
Call upload(@Part("description") RequestBody description,
//注意這裏的參數 "aFile" 之前是在創建 MultipartBody.Part 的時候傳入的
@Part("aFile") File file);
}
現在我們把入參類型改成了我們熟悉的
File
,如果你就這麼拿去發請求,服務端收到的結果會讓你哭了的。。。
RAW BODY
服務端收到了一個文件的路徑,它肯定會覺得
好了,不鬧了,這明顯是
Retrofit
在發現自己收到的實際入參是個
File
時,不知道該怎麼辦,情急之下給
toString
了,而且還是個
JsonString
(後來查證原來是使用了 GsonRequestBodyConverter。。)。
接下來我們就自己實現一個
FileRequestBodyConverter
,
static class FileRequestBodyConverterFactory extends Converter.Factory {
@Override
public Converter requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return new FileRequestBodyConverter();
}
}
static class FileRequestBodyConverter implements Converter<File, RequestBody> {
@Override
public RequestBody convert(File file) throws IOException {
return RequestBody.create(MediaType.parse("application/otcet-stream"), file);
}
}
在創建
Retrofit
的時候記得配置上它:
addConverterFactory(new FileRequestBodyConverterFactory())
這樣,我們的文件內容就能上傳了。來,看下結果吧:
RAW BODY
文件內容成功上傳了,當然其中還存在一些問題,這個目前直接使用
Retrofit
的 Converter 還做不到,原因主要在於我們沒有辦法通過 Converter 直接將
File
轉換爲
MultiPartBody.Part
,如果想要做到這一點,我們可以對
Retrofit
的源碼稍作修改,這個我們後面再談。
1.5.2 ResponseBodyConverter
前面我們爲大家簡單示例瞭如何自定義
RequestBodyConverter
,對應的,Retrofit
也支持自定義
ResponseBodyConverter
。
我們再來看下我們定義的接口:
public interface GitHubService {
@GET("users/{user}/repos")
Call> listRepos(@Path("user") String user);
}
返回值的類型爲
List
,而我們直接拿到的原始返回肯定就是字符串(或者字節流),那麼這個返回值類型是怎麼來的呢?首先說明的一點是,GitHub 的這個 api 返回的是 Json 字符串,也就是說,我們需要使用 Json 反序列化得到
List
,這其中用到的其實是
GsonResponseBodyConverter
。
問題來了,如果請求得到的 Json 字符串與返回值類型不對應,比如:
接口返回的 Json 字符串:
{"err":0, "content":"This is a content.", "message":"OK"}
返回值類型
class Result{
int code;//等價於 err
String body;//等價於 content
String msg;//等價於 message
}
哇,這時候肯定有人想說,你是不是腦殘,偏偏跟服務端對着幹?哈哈,我只是示例嘛,而且在生產環境中,你敢保證這種情況不會發生??
這種情況下,
Gson
就是再牛逼,也只能默默無語倆眼淚了,它哪兒知道字段的映射關係怎麼這麼任性啊。好,現在讓我們自定義一個 Converter 來解決這個問題吧!
當然,別忘了在構造
Retrofit
的時候添加這個 Converter,這樣我們就能夠愉快的讓接口返回
Result
對象了。
注意!!
Retrofit
在選擇合適的 Converter 時,主要依賴於需要轉換的對象類型,在添加 Converter 時,注意 Converter 支持的類型的包含關係以及其順序。
2、Retrofit 原理剖析
前一個小節我們把
Retrofit
的基本用法和概念介紹了一下,如果你的目標是學會如何使用它,那麼下面的內容你可以不用看了。
不過呢,我就知道你不是那種淺嘗輒止的人!這一節我們主要把注意力放在
Retrofit
背後的魔法上面~~
2.1 是誰實際上完成了接口請求的處理?
前面講了這麼久,我們始終只看到了我們自己定義的接口,比如:
public interface GitHubService {
@GET("users/{user}/repos")
Call> listRepos(@Path("user") String user);
}
而真正我使用的時候肯定不能是接口啊,這個神祕的傢伙究竟是誰?其實它是
Retrofit
創建的一個代理對象了,這裏涉及點兒 Java 的動態代理的知識,直接來看代碼:
簡單的說,在我們調用
GitHubService.listRepos
時,實際上調用的是這裏的
InvocationHandler.invoke
方法~~
2.2 來一發完整的請求處理流程
前面我們已經看到
Retrofit
爲我們構造了一個
OkHttpCall
,實際上每一個
OkHttpCall
都對應於一個請求,它主要完成最基礎的網絡請求,而我們在接口的返回中看到的 Call 默認情況下就是
OkHttpCall
了,如果我們添加了自定義的
callAdapter
,那麼它就會將
OkHttp
適配成我們需要的返回值,並返回給我們。
先來看下 Call 的接口:
public interface Call<T> extends Cloneable {
//同步發起請求
Response execute() throws IOException;
//異步發起請求,結果通過回調返回
void enqueue(Callback callback);
boolean isExecuted();
void cancel();
boolean isCanceled();
Call clone();
//返回原始請求
Request request();
}
我們在使用接口時,大家肯定還記得這一句:
Call> repos = service.listRepos("octocat");
List data = repos.execute();
這個
repos
其實就是一個
OkHttpCall
實例,execute
就是要發起網絡請求。
OkHttpCall.execute
我們看到
OkHttpCall
其實也是封裝了
okhttp3.Call
,在這個方法中,我們通過
okhttp3.Call
發起了進攻,額,發起了請求。有關
OkHttp
的內容,我在這裏就不再展開了。
parseResponse
主要完成了由
okhttp3.Response
向
retrofit.Response
的轉換,同時也處理了對原始返回的解析:
Response parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
//略掉一些代碼
try {
//在這裏完成了原始 Response 的解析,T 就是我們想要的結果,比如 GitHubService.listRepos 的 List
T body = serviceMethod.toResponse(catchingBody);
return Response.success(body, rawResponse);
} catch (RuntimeException e) {
// If the underlying source threw an exception, propagate that rather than indicating it was
// a runtime exception.
catchingBody.throwIfCaught();
throw e;
}
}
至此,我們就拿到了我們想要的數據~~
2.3 結果適配,你是不是想用 RxJava?
前面我們已經提到過
CallAdapter
的事兒,默認情況下,它並不會對
OkHttpCall
實例做任何處理:
final class DefaultCallAdapterFactory extends CallAdapter.Factory {
static final CallAdapter.Factory INSTANCE = new DefaultCallAdapterFactory();
@Override
public CallAdapter get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
... 毫不留情的省略一些代碼 ...
return new CallAdapter>() {
... 省略一些代碼 ...
@Override public Call adapt(Call call) {
//看這裏,直接把傳入的 call 返回了
return call;
}
};
}
}
現在的需求是,我想要接入 RxJava,讓接口的返回結果改爲
Observable
:
public interface GitHub {
@GET("/repos/{owner}/{repo}/contributors")
Observable> contributors(
@Path("owner") String owner,
@Path("repo") String repo);
}
可不可以呢?當然是可以的,只需要提供一個 Adapter,將
OkHttpCall
轉換爲
Observable
即可呀!Retrofit
的開發者們早就想到了這個問題,並且爲我們提供了相應的 Adapter:
RxJavaCallAdapterFactory
我們只需要在構造
Retrofit
時,添加它:
addCallAdapterFactory(RxJavaCallAdapterFactory.create())
這樣我們的接口就可以以
RxJava
的方式工作了。
好,歇會兒,抽一袋煙。。。
接着我們搞清楚
RxJavaCallAdapterFactory
是怎麼工作的,首先讓我們來看下
CallAdapter
的接口:
代碼中做了較爲詳細的註釋,簡單來說,我們只需要實現
CallAdapter
類來提供具體的適配邏輯,並實現相應的
Factory
,用來將當前的
CallAdapter
註冊到
Retrofit
當中,並在
Factory.get
方法中根據類型來返回當前的
CallAdapter
即可。知道了這些,我們再來看
RxJavaCallAdapterFactory
:
RxJavaCallAdapterFactory
提供了不止一種 Adapter,但原理大同小異,有興趣的讀者可以自行參閱其源碼。
至此,我們已經對
CallAdapter
的機制有了一個清晰的認識了。
3、幾個進階玩法
前面我們已經介紹了很多東西了。。可,挖掘機專業的同學們,你們覺得這就夠了麼?當然是不夠!
3.1 繼續簡化文件上傳的接口
在 1.5.1 當中我們曾試圖簡化文件上傳接口的使用,儘管我們已經給出了相應的
File -> RequestBody
的
Converter
,不過基於
Retrofit
本身的限制,我們還是不能像直接構造
MultiPartBody.Part
那樣來獲得更多的靈活性。這時候該怎麼辦?當然是 Hack~~
首先明確我們的需求:
-
文件的 Content-Type 需要更多的靈活性,不應該寫死在 Converter 當中,可以的話,最好可以根據文件的擴展名來映射出來對應的 Content-Type, 比如 image.png -> image/png;
-
在請求的數據中,能夠正常攜帶 filename 這個字段。
爲此,我增加了一套完整的參數解析方案:
1. 增加任意類型轉換的 Converter,這一步主要是滿足後續我們直接將入參類型轉換爲
MultiPartBody.Part
類型:
public interface Converter<F, T> {
...
abstract class Factory {
...
//返回一個滿足條件的不限制類型的 Converter
public Converter arbitraryConverter(Type originalType,
Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit){
return null;
}
}
}
需要注意的是,Retrofit
類當中也需要增加相應的方法:
public Converter arbitraryConverter(
Type orignalType,Type convertedType,
Annotation[] parameterAnnotations,
Annotation[] methodAnnotations) {
return nextArbitraryConverter(null, orignalType, convertedType, parameterAnnotations, methodAnnotations);
}
public Converter nextArbitraryConverter(Converter.Factory skipPast,
Type type, Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {
checkNotNull(type, "type == null");
checkNotNull(parameterAnnotations, "parameterAnnotations == null");
checkNotNull(methodAnnotations, "methodAnnotations == null");
int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter.Factory factory = converterFactories.get(i);
Converter converter =
factory.arbitraryConverter(type, convertedType, parameterAnnotations, methodAnnotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter) converter;
}
}
return null;
}
2. 再給出
arbitraryConverter
的具體實現:
3. 在聲明接口時,@Part
不要傳入參數,這樣
Retrofit
在
ServiceMethod.Builder.parseParameterAnnotation
方法中解析
Part
時,就會認爲我們傳入的參數爲
MultiPartBody.Part
類型(實際上我們將在後面自己轉換)。那麼解析的時候,我們拿到前面定義好的
Converter
,構造一個
ParameterHandler
:
...
} else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) {
return ParameterHandler.RawPart.INSTANCE;
} else {
Converter converter =
retrofit.arbitraryConverter(type, MultipartBody.Part.class, annotations, methodAnnotations);
if(converter == null) {
throw parameterError(p,
"@Part annotation must supply a name or use MultipartBody.Part parameter type.");
} return new ParameterHandler.TypedFileHandler((Converter) converter);
}
...
static final class TypedFileHandler extends ParameterHandler{
private final Converter converter;
TypedFileHandler(Converter converter) {
this.converter = converter;
}
@Override
void apply(RequestBuilder builder, TypedFile value) throws IOException {
if(value != null){
builder.addPart(converter.convert(value));
}
}
}
4. 這時候再看我們的接口聲明:
public interface FileUploadService {
@Multipart
@POST("upload")
Call upload(@Part("description") RequestBody description,
@Part TypedFile typedFile);
}
以及使用方法:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.println.net/")
.addConverterFactory(new TypedFileMultiPartBodyConverterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build();
FileUploadService service = retrofit.create(FileUploadService.class);
TypedFile typedFile = new TypedFile("aFile", filename);
String descriptionString = "This is a description";
RequestBody description =
RequestBody.create(
MediaType.parse("multipart/form-data"), descriptionString);
Call call = service.upload(description, typedFile);
call.enqueue(...);
至此,我們已經通過自己的雙手,讓
Retrofit
的點亮了自定義上傳文件的技能,風騷等級更上一層樓!
3.1.2 Mock Server
我們在開發過程中,經常遇到服務端不穩定的情況,測試開發環境,這是難免的。於是我們需要能夠模擬網絡請求來調試我們的客戶端邏輯,Retrofit
自然是支持這個功能的。
真是太貼心,Retrofit
提供了一個
MockServer
的功能,可以在幾乎不改動客戶端原有代碼的前提下,實現接口數據返回的自定義,我們在自己的工程中增加下面的依賴:
compile 'com.squareup.retrofit2:retrofit-mock:2.0.2
還是先讓我們來看看官方 demo,首先定義了一個 GituHb api,好熟悉的感覺:
public interface GitHub {
@GET("/repos/{owner}/{repo}/contributors")
Call> contributors(
@Path("owner") String owner,
@Path("repo") String repo);
}
這就是我們要請求的接口了,怎麼 Mock 呢?
1. 定義一個接口實現類
MockGitHub
,我們可以看到,所有我們需要請求的接口都在這裏得到了實現,也就是說,我們待會兒調用 GitHub 的 api 時,實際上是訪問
MockGitHub
的方法:
2. 構建
Mock Server
對象:
// Create a very simple Retrofit adapter which points the GitHub API.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(SimpleService.API_URL)
.build();
// Create a MockRetrofit object with a NetworkBehavior which manages the fake behavior of calls.
NetworkBehavior behavior = NetworkBehavior.create();
MockRetrofit mockRetrofit = new MockRetrofit.Builder(retrofit)
.networkBehavior(behavior)
.build();
BehaviorDelegate delegate = mockRetrofit.create(GitHub.class);
MockGitHub gitHub = new MockGitHub(delegate);
3. 使用
Mock Server
:
Call> contributors = gitHub.contributors(owner, repo);
...
也就是說,我們完全可以自己造一個假的數據源,通過
Mock Server
來返回這些寫數據。
那麼問題來了,這其實並沒有完全模擬網絡請求的解析流程,如果我只能提供原始的
json
字符串,怎麼通過
Retrofit
來實現
Mock Server
?
時間已經不早啦,我就不猥瑣發育了,直接推塔~
本文前面一直專注於介紹
Retrofit
,很少提及
OkHttp
,殊不知
OkHttp
有一套攔截器的機制,也就是說,我們可以任性的檢查
Retrofit
即將發出或者正在發出的所有請求,並且篡改它。所以我們只需要找到我們想要的接口,定製自己的返回結果就好了,下面是一段示例:
這樣,我們就會攔截
contributors
這個
api
並定製其返回了。
4、小結
Retrofit
是非常強大的,本文通過豐富的示例和對源碼的挖掘,向大家展示了
Retrofit
自身強大的功能以及擴展性,就算它本身功能不能滿足你的需求,你也可以很容易的進行改造,畢竟人家的代碼真是寫的漂亮啊。
另外,我之前也寫過兩篇文章介紹我對
Retrofit
的 Hack,歡迎賞光~
-
Android 下午茶:Hack Retrofit 之 增強參數(http://www.println.net/post/Android-Hack-Retrofit 請複製此鏈接到瀏覽器打開)
-
Android 下午茶:Hack Retrofit (2) 之 Mock Server(http://www.println.net/post/Android-Hack-Retrofit-Mock-Server 打開方式同上)
文中 Hack 之後的
Retrofit
代碼見
GitHub(https://github.com/enbandari/HackRetrofit))。