在開發(fā)Android APP時,肯定會使用到Http請求與服務(wù)器通信,上傳或下載數(shù)據(jù)等功能。目前開源的Http請求工具也有很多,比如Google開發(fā)的Volley,loopj的Android Async Http,Square開源的OkHttp或者Retrofit等?,F(xiàn)在比較流行的網(wǎng)絡(luò)庫是:Retrofit2.0+RxJava2.0+oKhttp3.0。
我覺得Retrofit 無疑是這幾個當(dāng)中最好用的一個,設(shè)計這個庫的思路很特別而且巧妙。Retrofit的代碼很少,花點(diǎn)時間讀它的源碼肯定會收獲很多。
本文的源碼分析基于Retrofit 2,和Retrofit 1.0的Api有較大的不同, 本文主要分為幾部分:
0、Retrofit 是什么
1、Retrofit怎么用
2、Retrofit的原理是什么
3、我的心得與看法
- Retrofit是什么
來自Retrofit官網(wǎng)的介紹:
A type-safe HTTP client for Android and Java
簡單的說它是一個基于OkHttp的RESTFUL Api請求工具,從功能上來說和Google的Volley功能上很相似,但是使用上很不相似。
Volley使用上更加原始而且符合使用者的直覺,當(dāng)App要發(fā)送一個Http請求時,你需要先創(chuàng)建一個Request對象,指定這個Request用的是GET、POST或其他方法,一個api 地址,一個處理response的回調(diào),如果是一個POST請求,那么你還需要給這個Request對象設(shè)置一個body,有時候你還需要自定義添加Header什么的,然后將這個Request對象添加到RequestQueue中,接下去檢查Cache以及發(fā)送Http請求的事情,Volley會幫你處理。如果一個App中api不同的api請求很多,這樣代碼就會很難看。
而Retrofit可以讓你簡單到調(diào)用一個Java方法的方式去請求一個api,這樣App中的代碼就會很簡潔方便閱讀
- Retrofit怎么用
雖然Retrofit官網(wǎng)已經(jīng)說明了,我還是要按照我的思路說一下它的使用方法
比如你要請求這么一個api,查看知乎專欄的某個作者信息:
https://zhuanlan.zhihu.com/api/columns/{user}
首先,你需要創(chuàng)建一個Retrofit對象,并且指定api的域名:
public static final String API_URL = "https://zhuanlan.zhihu.com";
Create a very simple REST adapter which points the Zhuanlan API.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
其次,你要根據(jù)api新建一個Java接口,用Java注解來描述這個api
public interface ZhuanLanApi {
@GET("/api/columns/{user} ")
Call<ZhuanLanAuthor> getAuthor(@Path("user") String user)
}
再用這個retrofit對象創(chuàng)建一個ZhuanLanApi對象:
ZhuanLanApi api = retrofit.create(ZhuanLanApi.class);
Call<ZhuanLanAuthor> call = api.getAuthor("qinchao");
這樣就表示你要請求的api是https://zhuanlan.zhihu.com/api/columns/qinchao
最后你就可以用這個call對象獲得數(shù)據(jù)了,enqueue方法是異步發(fā)送http請求的,如果你想用同步的方式發(fā)送可以使用execute()方法,call對象還提供cancel()、isCancel()等方法獲取這個Http請求的狀態(tài)
// 請求數(shù)據(jù),并且處理response
call.enqueue(new Callback<ZhuanLanAuthor>() {
@Override
public void onResponse(Response<ZhuanLanAuthor> author) {
System.out.println("name: " + author.getName());
}
@Override
public void onFailure(Throwable t) {
}
});
看到?jīng)],Retrofit只要創(chuàng)建一個接口來描述Http請求,然后可以讓我們可以像調(diào)用Java方法一樣請求一個Api,是不是覺得很神奇,很不可思議??!
- Retrofit的原理
從上面Retrofit的使用來看,Retrofit就是充當(dāng)了一個適配器(Adapter)的角色:將一個Java接口翻譯成一個Http請求,然后用OkHttp去發(fā)送這個請求
Volley描述一個HTTP請求是需要創(chuàng)建一個Request對象,而執(zhí)行這個請求呢,就是把這個請求對象放到一個隊列中,在網(wǎng)絡(luò)線程中用HttpUrlConnection去請求
問題來了:Retrofit是怎么做的呢?
答案很簡單,就是:Java的動態(tài)代理
動態(tài)代理
我剛開始看Retrofit的代碼,我對下面這句代碼感到很困惑:
ZhuanLanApi api = retrofit.create(ZhuanLanApi.class);
我給Retrofit對象傳了一個ZhuanLanApi接口的Class對象,怎么又返回一個ZhuanLanApi對象呢?進(jìn)入create方法一看,沒幾行代碼,但是我覺得這幾行代碼就是Retrofit的精妙的地方:
/** Create an implementation of the API defined by the {@code service} interface. */
public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, Object... args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
create方法就是返回了一個Proxy.newProxyInstance動態(tài)代理對象。那么問題來了...
動態(tài)代理是個什么東西?
看Retrofit代碼之前我知道Java動態(tài)代理是一個很重要的東西,比如在Spring框架里大量的用到,但是它有什么用呢?
Java動態(tài)代理就是給了程序員一種可能:當(dāng)你要調(diào)用某個Class的方法前或后,插入你想要執(zhí)行的代碼
比如你要執(zhí)行某個操作前,你必須要判斷這個用戶是否登錄,或者你在付款前,你需要判斷這個人的賬戶中存在這么多錢。這么簡單的一句話,我相信可以把一個不懂技術(shù)的人也講明白Java動態(tài)代理是什么東西了。
為什么要使用動態(tài)代理
你看上面代碼,獲取數(shù)據(jù)的代碼就是這句:
Call<ZhuanLanAuthor> call = api.getAuthor("qinchao");
上面api對象其實(shí)是一個動態(tài)代理對象,并不是一個真正的ZhuanLanApi接口的implements產(chǎn)生的對象,當(dāng)api對象調(diào)用getAuthor方法時會被動態(tài)代理攔截,然后調(diào)用Proxy.newProxyInstance方法中的InvocationHandler對象,它的invoke方法會傳入3個參數(shù):
- Object proxy: 代理對象,不關(guān)心這個
- Method method:調(diào)用的方法,就是getAuthor方法
- Object... args:方法的參數(shù),就是"qinchao"
而Retrofit關(guān)心的就是method和它的參數(shù)args,接下去Retrofit就會用Java反射獲取到getAuthor方法的注解信息,配合args參數(shù),創(chuàng)建一個ServiceMethod對象
ServiceMethod就像是一個中央處理器,傳入Retrofit對象和Method對象,調(diào)用各個接口和解析器,最終生成一個Request,包含api 的域名、path、http請求方法、請求頭、是否有body、是否是multipart等等。最后返回一個Call對象,Retrofit2中Call接口的默認(rèn)實(shí)現(xiàn)是OkHttpCall,它默認(rèn)使用OkHttp3作為底層http請求client
使用Java動態(tài)代理的目的就要攔截被調(diào)用的Java方法,然后解析這個Java方法的注解,最后生成Request由OkHttp發(fā)送
-
Retrofit的源碼分析
想要弄清楚Retrofit的細(xì)節(jié),先來看一下Retrofit源碼的組成:- 一個retrofit2.http包,里面全部是定義HTTP請求的Java注解,比如GET、POST、PUT、DELETE、Headers、Path、Query等等
- 余下的retrofit2包中幾個類和接口就是全部retrofit的代碼了,代碼真的很少,很簡單,因為retrofit把網(wǎng)絡(luò)請求這部分功能全部交給了OkHttp了
-
Retrofit接口
Retrofit的設(shè)計非常插件化而且輕量級,真的是非常高內(nèi)聚而且低耦合,這個和它的接口設(shè)計有關(guān)。Retrofit中定義了4個接口:- Callback<T> 這個接口就是retrofit請求數(shù)據(jù)返回的接口,只有兩個方法
void onResponse(Response<T> response); void onFailure(Throwable t);Converter<F, T>
這個接口主要的作用就是將HTTP返回的數(shù)據(jù)解析成Java對象,主要有Xml、Gson、protobuf等等,你可以在創(chuàng)建Retrofit對象時添加你需要使用的Converter實(shí)現(xiàn)(看上面創(chuàng)建Retrofit對象的代碼)Call<T>
這個接口主要的作用就是發(fā)送一個HTTP請求,Retrofit默認(rèn)的實(shí)現(xiàn)是OkHttpCall<T>,你可以根據(jù)實(shí)際情況實(shí)現(xiàn)你自己的Call類,這個設(shè)計和Volley的HttpStack接口設(shè)計的思想非常相似,子類可以實(shí)現(xiàn)基于HttpClient或HttpUrlConnetction的HTTP請求工具,這種設(shè)計非常的插件化,而且靈活CallAdapter<T>
上面說到過,CallAdapter中屬性只有responseType一個,還有一個<R> T adapt(Call<R> call)方法,這個接口的實(shí)現(xiàn)類也只有一個,DefaultCallAdapter。這個方法的主要作用就是將Call對象轉(zhuǎn)換成另一個對象,可能是為了支持RxJava才設(shè)計這個類的吧
-
Retrofit的運(yùn)行過程
上面講到ZhuanLanApi api = retrofit.create(ZhuanLanApi.class);代碼返回了一個動態(tài)代理對象,而執(zhí)行Call<ZhuanLanAuthor> call = api.getAuthor("qinchao");代碼時返回了一個OkHttpCall對象,拿到這個Call對象才能執(zhí)行HTTP請求。
上面api對象其實(shí)是一個動態(tài)代理對象,并不是一個真正的ZhuanLanApi接口的implements產(chǎn)生的對象,當(dāng)api對象調(diào)用getAuthor方法時會被動態(tài)代理攔截,然后調(diào)用Proxy.newProxyInstance方法中的InvocationHandler對象, 創(chuàng)建一個ServiceMethod對象ServiceMethod serviceMethod = loadServiceMethod(method); OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); return serviceMethod.callAdapter.adapt(okHttpCall); 創(chuàng)建ServiceMethod
剛才說到,ServiceMethod就像是一個中央處理器,具體來看一下創(chuàng)建這個ServiceMethod的過程是怎么樣的
第一步,獲取到上面說到的3個接口對象:
callAdapter = createCallAdapter();
responseType = callAdapter.responseType();
responseConverter = createResponseConverter();
第二步,解析Method的注解,主要就是獲取Http請求的方法,比如是GET還是POST還是其他形式,如果沒有,程序就會報錯,還會做一系列的檢查,比如如果在方法上注解了@Multipart,但是Http請求方法是GET,同樣也會報錯。因此,在注解Java方法是需要嚴(yán)謹(jǐn)
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
if (httpMethod == null) {
throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).");
}
第三步,比如上面api中帶有一個參數(shù){user},這是一個占位符,而真實(shí)的參數(shù)值在Java方法中傳入,那么Retrofit會使用一個ParameterHandler來進(jìn)行替換:
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
最后,ServiceMethod會做其他的檢查,比如用了@FormUrlEncoded注解,那么方法參數(shù)中必須至少有一個@Field或@FieldMap
- 執(zhí)行Http請求
之前講到,OkHttpCall是實(shí)現(xiàn)了Call接口的,并且是真正調(diào)用OkHttp3發(fā)送Http請求的類。OkHttp3發(fā)送一個Http請求需要一個Request對象,而這個Request對象就是從ServiceMethod的toRequest返回的。
總的來說,OkHttpCall就是調(diào)用ServiceMethod獲得一個可以執(zhí)行的Request對象,然后等到Http請求返回后,再將response body傳入ServiceMethod中,ServiceMethod就可以調(diào)用Converter接口將response body轉(zhuǎn)成一個Java對象。
結(jié)合上面說的就可以看出,ServiceMethod中幾乎保存了一個api請求所有需要的數(shù)據(jù),OkHttpCall需要從ServiceMethod中獲得一個Request對象,然后得到response后,還需要傳入ServiceMethod用Converter轉(zhuǎn)換成Java對象。
你可能會覺得我只要發(fā)送一個HTTP請求,你要做這么多事情不會很“慢”嗎?不會很浪費(fèi)性能嗎?
我覺得,首先現(xiàn)在手機(jī)處理器主頻非常高了,解析這個接口可能就花1ms可能更少的時間(我沒有測試過),面對一個HTTP本來就需要幾百ms,甚至幾千ms來說不值得一提;而且Retrofit會對解析過的請求進(jìn)行緩存,就在Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>();這個對象中。
- 如何在Retrofit中使用RxJava
由于Retrofit設(shè)計的擴(kuò)展性非常強(qiáng),你只需要添加一個CallAdapter就可以了
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(ProtoConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
上面代碼創(chuàng)建了一個Retrofit對象,支持Proto和Gson兩種數(shù)據(jù)格式,并且還支持RxJava
- 最后
Retrofit非常巧妙的用注解來描述一個HTTP請求,將一個HTTP請求抽象成一個Java接口,然后用了Java動態(tài)代理的方式,動態(tài)的將這個接口的注解“翻譯”成一個HTTP請求,最后再執(zhí)行這個HTTP請求
Retrofit的功能非常多的依賴Java反射,代碼中其實(shí)還有很多細(xì)節(jié),比如異常的捕獲、拋出和處理,大量的Factory設(shè)計模式(為什么要這么多使用Factory模式?)
Retrofit中接口設(shè)計的恰到好處,在你創(chuàng)建Retrofit
對象時,讓你有更多更靈活的方式去處理你的需求,比如使用不同的Converter、使用不同的CallAdapter,這也就提供了你使用RxJava來調(diào)用Retrofit的可能.
我也慢慢看了Picasso和Retrofit的代碼了,收獲還是很多的,也更加深入的理解面向接口的編程方法,這個寫代碼就是好的代碼就是依賴接口而不是實(shí)現(xiàn)最好的例子
好感謝開源的世界,讓我能讀到大牛的代碼。我一直覺得一個人如果沒有讀過好的代碼是不太可能寫出好代碼的。什么是好的代碼?像Picasso和Retrofit這樣的就是好的代碼,擴(kuò)展性強(qiáng)、低耦合、插件化.