我們在日常編寫代碼中免不了會用到各種各樣第三方庫,網(wǎng)絡請求耿导、圖片加載疫赎、數(shù)據(jù)庫等等。有些lib接入可能方便到幾行代碼搞定碎节,有些lib可能從demo、文檔到測試都是坑(比如lib嵌套lib導致資源沖突抵卫、lib中定義的類無法擴展狮荔、兼容性差導致大量崩潰等),相信接過第三方庫的童鞋不會沒有過這樣的吐槽介粘。筆者也是在最近修改一個第三方lib的bug過程中翻看了一些源碼殖氏,發(fā)現(xiàn)其中存在點設計技巧,于是結(jié)合最近看的設計模式姻采,來討論一下在SDK中如何使用雅采,與大家相互交流,也為本人之后SDK的開發(fā)工作做點鋪墊。
這個第三方lib叫做Retrofit婚瓜,是個用在Java中支持restful的網(wǎng)絡庫宝鼓。Retrofit是在基于OkHttp3的基礎上,用動態(tài)代理和annotation實現(xiàn)了restful標準的規(guī)范巴刻,令開發(fā)者使用起來異常方便愚铡。Retrofit當然也實現(xiàn)了網(wǎng)絡請求的異步處理,并且用工廠模式給開發(fā)者預留了很大的擴展空間胡陪,可以與ReactiveX結(jié)合沥寥,也可以由開發(fā)者定義自己的同步或異步請求、回調(diào)方式柠座。
為了方便講解設計模式的實現(xiàn)邑雅,我們先來看看代碼中如何使用Retrofit。引用官方文檔的介紹妈经,只需要這樣聲明好你的api接口:
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
在初始化時傳入這個接口的class:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
調(diào)用接口時只需兩行代碼即可:
Call<List<Repo>> repos = service.listRepos("octocat”);//獲取網(wǎng)絡請求實例
repos.excute();//執(zhí)行請求淮野,異步請求用repos.enqueue(callback);
其中List<Repo>是對請求返回數(shù)據(jù)的定義,repos是執(zhí)行請求的實例(實現(xiàn)了Call接口狂塘,后面會詳細介紹)录煤。
從以上代碼可以看到,我們做的僅僅是聲明了一個接口荞胡,涵蓋所需的api接口妈踊,Retrofit就自動幫我們創(chuàng)建了一個實現(xiàn)這個api接口的實例,我們只需坐享其成調(diào)用實例的方法即可完成網(wǎng)絡請求泪漂。Retrofit的這種“智能”是如何實現(xiàn)的呢廊营?那就是接下來要談的動態(tài)代理模式。
Retrofit中的代理模式
為什么需要代理呢萝勤?代理其實就是我們想做一件事的時候不親自動手露筒,也就是“創(chuàng)建網(wǎng)絡請求實例”這件事,交給一個代理去創(chuàng)建敌卓,這樣不管它內(nèi)部怎樣實現(xiàn)慎式,只要能幫我們創(chuàng)建出一個可用的實例就可以了,通常這個實例也是實現(xiàn)了某個接口的(比如文中的Call接口)趟径,所以即使底層的實現(xiàn)改變瘪吏,或者創(chuàng)建過程改變,使用者的代碼是不需要調(diào)整的蜗巧。就像我們在攜程掌眠、去哪兒上買機票,我們也不關心他們到底是從航空公司官方買票幕屹,還是從中間商手中買票蓝丙,只要最終我們能拿到票就行了(所以也會買到用里程數(shù)換來的機票级遭,噗…)。
言歸正傳渺尘,Retrofit用到的動態(tài)代理挫鸽,類圖如下:
籃框中的就是代理部分,代理了用戶定義接口(即開頭實例中的GitHubService)中的所有函數(shù)沧烈,返回一個Call對象掠兄,代理實例通過這句代碼來產(chǎn)生:
GitHubService service = retrofit.create(GitHubService.class);
進去看create函數(shù)源碼會發(fā)現(xiàn)這里是通過反射實現(xiàn)的,直接返回了java.lang.reflect.Proxy中的方法newProxyInstance:
public <T> T create(final Class<T> service) {
...
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
@Override public Object invoke(Object proxy, Method method, Object... args)
throws Throwable {
//這里有判斷method是否為Object類聲明的方法
...
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
這個代理實例可以將接口(也就是我們定義的GitHubService)指定的所有方法都指派到invocationHandler中去锌雀,當調(diào)用service的接口方法時蚂夕,就會執(zhí)行InvocationHandler中的Invoke方法,可以看到Retrofit就是在這里創(chuàng)建一個網(wǎng)絡請求實例OkHttpCall腋逆,將其返回(其實返回的是callAdapter.adapt(okHttpCall)婿牍,將okHttpCall適配轉(zhuǎn)換過的對象,詳見后面適配器模式)惩歉,我們就可以利用此實例進行網(wǎng)絡請求了等脂。這里invoke方法三個參數(shù)中proxy就是代理對象,method表示要調(diào)用的方法撑蚌,args是對method方法傳入的參數(shù)上遥。
Retrofit中的適配器模式
適配器模式是將一個類的接口,轉(zhuǎn)換成客戶期望的另一個接口争涌,讓原本接口不兼容的類可以合作無間粉楚。比如生活中的電源適配器,將220v電壓轉(zhuǎn)換成電子設備需要的輸入電壓亮垫,比如Android中的ListView模软,Adapter將各種各樣的數(shù)據(jù)轉(zhuǎn)換后傳給ListView用來顯示。Retrofit中的Adapter是用來轉(zhuǎn)換網(wǎng)絡請求Call接口的饮潦,而這里的Adapter可以由使用者自定義燃异,從而轉(zhuǎn)換成使用者希望的類,具有很強的擴展性继蜡,見類圖:
圖中綠色部分就是適配器模式回俐。這個適配器是怎樣運作的呢?
在剛才的代理模式中Retrofit已經(jīng)幫我們智能創(chuàng)建了網(wǎng)絡請求實例Call稀并,Call是對網(wǎng)絡請求定義的接口鲫剿。Retrofit實際默認new的對象是OkHttpCall(一個封裝了okhttp3.Call的類),我們并不在意它具體是什么類稻轨,能按照Call接口的定義來使用就夠了。但用起來才發(fā)現(xiàn)我們會有很多額外需求雕凹,比如OkHttpCall的回調(diào)函數(shù)是在工作線程調(diào)用的殴俱,而網(wǎng)絡回調(diào)函數(shù)我們通常要更新UI政冻,再用handler轉(zhuǎn)到主線程?對使用者來說太麻煩了线欲。于是適配器華麗登場明场,CallAdapter可以將默認生成的OkHttpCall轉(zhuǎn)換成你想要的任何類型。
比如Retrofit默認提供的Adapter李丰,就是這樣將OkHttpCall適配成ExecutorCallbackCall:
new CallAdapter<Call<?>>() {
...
@Override public <R> Call<R> adapt(Call<R> call) {
return new ExecutorCallbackCall<>(callbackExecutor, call);
}
}
static final class ExecutorCallbackCall<T> implements Call<T> {
...
@Override public void enqueue(final Callback<T> callback) {
delegate.enqueue(new Callback<T>() {
@Override public void onResponse(Call<T> call, final Response<T> response) {
callbackExecutor.execute(new Runnable() {
...
});
}
});
}
}
可以看到ExecutorCallbackCall在enqueue方法中苦锨,添加了一層回調(diào),用自定義線程(通常就是主線程)執(zhí)行器執(zhí)行外部callback趴泌,而在CallAdapter.adapt函數(shù)直接返回ExecutorCallbackCall的新實例就可以了舟舒,也就是動態(tài)代理中提到的這句:
return serviceMethod.callAdapter.adapt(okHttpCall);
這樣在適配器的幫助下既可以增強擴展添加新功能,又不會增加使用者代碼量嗜憔。比如你希望在網(wǎng)絡回調(diào)時統(tǒng)一處理一些錯誤碼秃励,或者希望與RxJava結(jié)合使用,又或者希望單獨處理cancel函數(shù)等等吉捶。這些都可以通過適配器來將Retrofit返回的Call適配成你想要的類夺鲜。
然而還存在個問題,適配器的adapt方法是在Retrofit內(nèi)部調(diào)用的呐舔,它怎么知道使用者要用哪個或哪幾個適配器呢币励?使用者如何設置自己的適配器呢?這就引出了下面要介紹的工廠模式珊拼。
Retrofit中的工廠模式
工廠模式分為簡單工廠模式食呻、工廠方法模式和抽象工廠模式。應用場景大部分是需要根據(jù)不同類型來生成不同對象時使用杆麸。剛接觸工廠模式時搁进,以為這三種模式一個比一個高級,是層層遞進的關系昔头。然而并不是饼问,簡單工廠模式的確是最簡單的一種,但工廠方法模式和抽象工廠模式應該屬于平級揭斧,只是為了解決不同維度的問題而存在莱革。
簡單工廠模式就是依據(jù)變化封裝的原則,將生產(chǎn)對象的部分封裝在工廠內(nèi)部讹开,根據(jù)不同需求返回不同類型實例盅视,結(jié)構簡單但擴展起來麻煩,需要對工廠類進行修改旦万。因此生產(chǎn)的類型一旦變多闹击,就需要工廠方法模式了,將工廠定義成一個接口(或抽象類)成艘,每新增一類產(chǎn)品就新增一個工廠實例即可赏半,完全符合開放關閉原則贺归,滿足大多數(shù)情況的需求。而抽象工廠模式適用于多個產(chǎn)品樹的情況断箫,比如原本工廠方法模式可以生產(chǎn)轎車拂酣、越野車和跑車,但這時候新增了一個產(chǎn)品樹:電動轎車仲义、電動越野車和電動跑車婶熬,就需要用到抽象工廠模式了,但這種模式對新增產(chǎn)品族埃撵,比如新增了商務車赵颅,修改起來較復雜。
上面談了適配器adapter的作用盯另,而適配器的產(chǎn)生就是由工廠模式來完成的性含,見類圖:
圖中紅框就是工廠方法模式,CallAdapter的生產(chǎn)由CallAdapter.Factory這個接口定義鸳惯,包含了一個get函數(shù)商蕴,會返回一個CallAdapter,至于是個什么樣的CallAdapter則由子類來實現(xiàn)芝发。比如上面講適配器時提到的將OkHttpCall轉(zhuǎn)換成ExecutorCallbackCall的適配器绪商,就是由這個ExecutorCallAdapterFacotry生產(chǎn)的。工廠方法模式重點就在于將方法抽象為接口或父類辅鲸,利用繼承關系和子類的差異化創(chuàng)建不同的Adapter格郁,從而將默認生成的OkHttpCall轉(zhuǎn)換成你所需要的各種類型。
談了這么多還是感覺不到這些設計模式的作用嗎独悴?沒關系例书,來看下我們拓展后的類圖:
圖中灰色的就是默認的和擴展的工廠模塊。除了Retrofit默認提供的ExecutorCallAdapterFactory和ExecutorCallbackCall刻炒,我們可以擴展出自己的Call和Factory决采,比如圖中的GACall和GACallAdapterFacotry,我這里擴展的GACall修改了cancel()的行為坟奥,調(diào)用cancel()之后就會切除callback在IO線程中的引用树瞭,不再收到回調(diào),從而方便處理頁面銷毀后網(wǎng)絡請求才收到返回的情形爱谁。當然你還能擴展出其他Factory晒喷、Call和Callback(比如RxJava對Retrofit專門實現(xiàn)了一個Factory,直接拿來用就行了)访敌,只要記得將你的Factory添加到Retrofit類的adapterFactories列表中就行凉敲。
但用戶添加了這么多工廠,真正生產(chǎn)網(wǎng)絡請求實例時,要用哪個工廠呢荡陷?仔細看工廠接口的get方法:
public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit);
第一個參數(shù)是returnType雨效,也就是網(wǎng)絡請求返回的數(shù)據(jù)類型:
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
@GET("users/{user}/repos")
GACall<List<Repo>> listRepos2(@Path("user") String user);
}
上面請求聲明的返回類型分別是Call和GACall,工廠會根據(jù)傳入的returnType來分辨是否屬于自己的生產(chǎn)范圍废赞,于是returnType為Call就會由Retrofit默認的工廠生產(chǎn)Adapter,returnType為用戶自定義的類型(如GACall)叮姑,則由用戶定義的工廠(如GACallAdapterFacotry)生產(chǎn)Adapter唉地。
以上就是本人在修改內(nèi)存泄露導致崩潰的bug時,碰巧看到Retrofit源碼比較有趣传透,分析了一遍拿來和大家分享耘沼。大體思路就是先用反射代理幫用戶生產(chǎn)請求實例,再由適配器轉(zhuǎn)換成用戶期望的類型朱盐,而這個適配器是通過工廠方法模式讓用戶無限擴展和自定義的群嗤。其實深究下去里面還有很多設計模式的體現(xiàn),這次就先挑這三種具有代表性的好了兵琳。只要我們留意身邊的源代碼狂秘,就會發(fā)現(xiàn)別人巧妙的設計無處不在。