別人家SDK中的設計模式--Android Retrofit庫源碼解讀

我們在日常編寫代碼中免不了會用到各種各樣第三方庫,網(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)別人巧妙的設計無處不在。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末躯肌,一起剝皮案震驚了整個濱河市者春,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌清女,老刑警劉巖钱烟,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嫡丙,居然都是意外死亡拴袭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門曙博,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拥刻,“玉大人,你說我怎么就攤上這事羊瘩√┘眩” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵尘吗,是天一觀的道長逝她。 經(jīng)常有香客問我,道長睬捶,這世上最難降的妖魔是什么黔宛? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮擒贸,結(jié)果婚禮上臀晃,老公的妹妹穿的比我還像新娘觉渴。我一直安慰自己,他們只是感情好徽惋,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布案淋。 她就那樣靜靜地躺著,像睡著了一般险绘。 火紅的嫁衣襯著肌膚如雪踢京。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天宦棺,我揣著相機與錄音瓣距,去河邊找鬼。 笑死代咸,一個胖子當著我的面吹牛蹈丸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播呐芥,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼逻杖,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贩耐?” 一聲冷哼從身側(cè)響起弧腥,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎潮太,沒想到半個月后管搪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡铡买,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年更鲁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奇钞。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡澡为,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出景埃,到底是詐尸還是另有隱情媒至,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布谷徙,位于F島的核電站拒啰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏完慧。R本人自食惡果不足惜谋旦,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧册着,春花似錦拴孤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至司顿,卻和暖如春绽媒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背免猾。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留囤热,地道東北人猎提。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像旁蔼,于是被迫代替她去往敵國和親锨苏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容