梳理Retrofit的知識體系

作者:RainyJiang

在學(xué)習(xí)Retrofit后,由于它本身就是OKHttp的封裝,面試中也經(jīng)常會被一起問到;單純的解析它的源碼學(xué)習(xí)難免會有點(diǎn)無從下手,往往讓人抓不住重點(diǎn),學(xué)習(xí)效率并不是很高,本文從提出幾個問題出發(fā),帶著問題去思考學(xué)習(xí)Retrofit源碼,從而快速理解它的核心知識點(diǎn)

下面我將從以下幾個問題來梳理Retrofit的知識體系,方便自己理解

  • Retrofitcreate為什么使用動態(tài)代理?
  • 談?wù)?code>Retrofit運(yùn)用的動態(tài)代理及反射?
  • Retrofit注解是怎么進(jìn)行解析的?
  • Retrofit如何將注解封裝成OKHttpCall?
  • Rretrofit是怎么完成線程切換和數(shù)據(jù)適配的?

Retrofitcreate為什么使用動態(tài)代理

我們首先可以看Retrofit代理實(shí)例創(chuàng)建過程,通過一個例子來說明

    val retrofit = Retrofit.Builder()
            .baseUrl("https://www.baidu.com")
            .build()
        val myInterface = retrofit.create(MyInterface::class.java)

創(chuàng)建了一個MyInterface接口類對象,create函數(shù)內(nèi)使用了動態(tài)代理來創(chuàng)建接口對象,這樣的設(shè)計(jì)可以讓所有的訪問請求都給被代理,這里我簡化了下它的create函數(shù),簡單來說它的作用就是創(chuàng)建了一個你傳入類型的接口實(shí)例

/**
     *
     * @param loader 需要代理執(zhí)行的接口類
     * @return 動態(tài)代理,運(yùn)行的時候生成一個loader對象類型的類,在調(diào)用它的時候走
     */
    @SuppressWarnings("unchecked")
    public <T> T create(final Class<T> loader) {
        return (T) Proxy.newProxyInstance(loader.getClassLoader(),
                new Class<?>[]{loader}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("具體方法調(diào)用前的準(zhǔn)備工作");
                        Object result = method.invoke(object, args);
                        System.out.println("具體方法調(diào)用后的善后事情");
                        return result;
                    }
                });
    }

那么這個函數(shù)為什么要使用動態(tài)代理呢,這樣有什么好處?

我們進(jìn)行Retrofit請求的時候構(gòu)建了許多接口,并且都要調(diào)用接口中的對象;接下來我們再調(diào)用這個對象中的getSharedList方法

val sharedListCall:Call<ShareListBean> = myService.getSharedList(2,1)

在調(diào)用它的時候,在動態(tài)代理里面,在運(yùn)行的時候會存在一個函數(shù)getSharedList,這個函數(shù)里面會調(diào)用invoke,這個invoke函數(shù)就是Retrofit里的invoke函數(shù);并且也形成了一個功能攔截,如下圖所示:

所以,相當(dāng)于動態(tài)代理可以代理所有的接口,讓所有的接口都走invoke函數(shù),這樣就可以攔截調(diào)用函數(shù)的值,相當(dāng)于獲取到所有的注解信息,也就是Request動態(tài)變化內(nèi)容,至此不就可以動態(tài)構(gòu)建帶有具體的請求的URL了么,從而就可以將網(wǎng)絡(luò)接口的參數(shù)配置歸一化

這樣也就解決了之前OKHttp存在的接口配置繁瑣問題,既然都是要構(gòu)建Request,為了自主動態(tài)的來完成,所以Retrofit使用了動態(tài)代理

談?wù)?code>Retrofit運(yùn)用的動態(tài)代理及反射

那么我們在讀Retrofit源碼的時候,是否都有這樣一個問題,為什么我寫個接口以及一些接口Api,我們就可以完成相應(yīng)的http請求呢?Retrofit到底在這其中做了什么工作?簡單來說,其核心就是通過反射+動態(tài)代理來解決的,那么動態(tài)代理和反射的原理是怎么樣的?

代理模式梳理

首先我們要明白代理模式到底是怎么樣的,這里我簡單梳理下

  • 代理類與委托類有著同樣的接口
  • 代理類主要為委托類預(yù)處理消息,過濾消息,然后把消息發(fā)送給委托類,以及事后處理消息等等
  • 一個代理類的對象與一個委托類的對象關(guān)聯(lián),代理類的對象本身并不真正實(shí)現(xiàn)服務(wù),而是通過調(diào)用委托類的對象的相關(guān)方法,為提供特定的服務(wù)

以上是普通代理模式(靜態(tài)代理)它是有一個具體的代理類來實(shí)現(xiàn)的

動態(tài)代理+反射

那么Retrofit用到的動態(tài)代理呢?答案不言而喻,所謂動態(tài)就是沒有一個具體的代理類,我們看到Retrofitcreate函數(shù)中,它是可以為委托類對象生成代理類, 代理類可以將所有的方法調(diào)用分派到委托對象上反射執(zhí)行,大致如下

  • 接口的classLoader
  • 只包含接口的class數(shù)組
  • 自定義的InvocationHandler()對象, 該對象實(shí)現(xiàn)了invoke() 函數(shù), 通常在該函數(shù)中實(shí)現(xiàn)對委托類函數(shù)的訪問

這就是在create函數(shù)中所使用的動態(tài)代理及反射

擴(kuò)展:通過這些文章,了解更多動態(tài)代理與反射

反射,動態(tài)代理在Retrofit中的運(yùn)用
Retrofit的代理模式解析

Retrofit注解是怎么進(jìn)行解析的?

在使用Retrofit的時候,或者定義接口的時候,在接口方法中加入相應(yīng)的注解(@GET,@POST,@Query@FormUrlEncoded等),然后我們就可以調(diào)用這些方法進(jìn)行網(wǎng)絡(luò)請求了;那么就有了問題,為什么注解可以完整的覆蓋網(wǎng)絡(luò)請求?我們知道,注解大致分為三類,通過請求方法、請求體編碼格式、請求參數(shù),大致有22種注解,它們基本完整的覆蓋了HTTP請求方案 ;通過它們我們確定了網(wǎng)絡(luò)請求request的具體方案;

此時,就拋出了開始的問題,Retrofit注解是怎么被解析的呢?這里就要熟悉Retrofit中的ServiceMethod類了,總的來說,它首先選擇Retrofit里提供的工具(數(shù)據(jù)轉(zhuǎn)換器converter,請求適配器adapter),相當(dāng)于就是具體請求Request的一個封裝,封裝完成之后將注解進(jìn)行解析;

下面通過官方提供例子來說明下ServiceMethod組成的部分

其中 @GET("users/{user}/repos"是由parseMethodAnnotation負(fù)責(zé)解析的;@Path參數(shù)注解就由對應(yīng)ParameterHandler進(jìn)行處理,剩下的Call<List<Repo>毫無疑問就是使用CallAdapter將這個Call 類型適配為用戶定義的 service method 的返回類型。

那么ServiceMethod是怎么對注解進(jìn)行解析的呢,來簡單梳理下它的源碼

  • 首先,在loadService方法中進(jìn)行檢測,禁止靜態(tài)方法,這里Retrofit筆者使用的是2.9.0版本,不再是直接調(diào)用ServiceMethod.Builder(),而是使用緩存的方式調(diào)用ServiceMethod.parseAnnotations(this, method),將它轉(zhuǎn)為RequestFactory對象,其實(shí)本地大同小異,原理是差不多的
final class RequestFactory {
  static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
    return new Builder(retrofit, method).build();
  }
  ...
  • 同樣在RequestFactory中也是使用Builder模式,其實(shí)就是封裝了一層,傳入retrofit-method兩個參數(shù),在這里面我們調(diào)用了Method類獲取了它的注解數(shù)組methodAnnotations,型參的類型數(shù)組parameterTypes,型參的注解數(shù)組parameterAnnotationsArray
 Builder(Retrofit retrofit, Method method) {
      this.retrofit = retrofit;
      this.method = method;
      this.methodAnnotations = method.getAnnotations();
      this.parameterTypes = method.getGenericParameterTypes();
      this.parameterAnnotationsArray = method.getParameterAnnotations();
    }
  • 然后在build方法中,它會創(chuàng)建一個ReqeustFactory對象,最終解析它通過HttpServiceMethod又轉(zhuǎn)換成ServiceMethod實(shí)例,這個方法里主要就是針對注解的解析過程,由于源碼非常長,感興趣的同學(xué)可以去詳細(xì)閱讀下,這里大概概括下幾個重要的解析方法
  1. parseMethodAnnotation

    該方法就是確定網(wǎng)絡(luò)的傳輸方式,判斷加了哪些注解,下面借用一張網(wǎng)絡(luò)上的圖表達(dá)會更直觀點(diǎn)

  1. parseHttpMethodAndPath,parseHeaders

    我們通過上圖也可以看到,其實(shí)就是解析httpMethodheaders,它們都是在parseMethodAnnotation方法中被調(diào)用的,從而進(jìn)行細(xì)化。前者確定的是請求方法(get,post,delete等),后者顧名思義確定的是headers頭部;前者會檢測httpMethod,它不允許有多個方法注解,會使用正則表達(dá)式進(jìn)行判斷,url中的參數(shù)必須用括號占位,最終提取出了真實(shí)的urlurl中攜帶的參數(shù)名稱;后者就是解析當(dāng)前Http請求的頭部信息

  • 經(jīng)過以上方法注解處理以及驗(yàn)證,在build方法中還要對參數(shù)注解進(jìn)行處理
int parameterCount = parameterAnnotationsArray.length;
      parameterHandlers = new ParameterHandler<?>[parameterCount];
      for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
        parameterHandlers[p] =
            parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
      }

它循環(huán)進(jìn)行參數(shù)的驗(yàn)證處理,通過parseParameter方法最后一個參數(shù)判斷是否繼續(xù),參考下網(wǎng)絡(luò)上的圖示

簡單說下ParameterHandler是怎么處理這個參數(shù)注解的?它會通過進(jìn)行兩項(xiàng)校驗(yàn),分別是對不確定的類型合法校驗(yàn)路徑名稱的校驗(yàn),然后就是一堆參數(shù)注解的處理,分析源碼后可以看到ParameterHandler最終都組裝成了一個RequestBuilder,那么它是用來干神馬的?答案是生成OKHttpRequest,網(wǎng)絡(luò)請求還是交給OKHttp來完成

以上簡單分析了下Retrofit注解的解析過程,需要深入了解的同學(xué)請自行探索。

如果同學(xué)對注解不太熟悉,想要了解Java注解的相關(guān)知識點(diǎn)可以閱讀這篇文章--->(Retrofit注解)

Retrofit如何將注解封裝成OKHttpcall

上個問題已經(jīng)知道了Retrofit中的ServiceMethod對會注解進(jìn)行解析封裝,這時候各種網(wǎng)絡(luò)請求適配器,請求頭,參數(shù),轉(zhuǎn)換器等等都準(zhǔn)備好了,最終它會將ServiceMethod轉(zhuǎn)為Retrofit提供的OkHttpCall,這個就是對okhttp3.Call的封裝,答案已經(jīng)呼之欲出了。

 @Override
  final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);
  }

換種說法,相當(dāng)于在ServiceMethod中已經(jīng)將注解轉(zhuǎn)變?yōu)?code>url +請求頭+參數(shù)等等格式,對照OKHttp請求流程,是不是已經(jīng)完成了構(gòu)建Request請求了,它最終要變成Okhttp3.call才能進(jìn)行網(wǎng)絡(luò)請求,所以OkHttpCall基本上就是做了這么一件事情,下面有張圖可以直觀看下ServiceMethod大概做了哪些事情

接著我們看下okhttpCall中的enqueue方法,它會去創(chuàng)建一個真實(shí)的Call,這個其實(shí)就是OKHttp中的call,接下來的網(wǎng)絡(luò)請求工作就交給OKHttp來完成

private okhttp3.Call createRawCall() throws IOException {
    okhttp3.Call call = callFactory.newCall(requestFactory.create(args));
    if (call == null) {
      throw new NullPointerException("Call.Factory returned null.");
    }
    return call;
  }

到這里,說白了Retrofit其實(shí)沒有任何與網(wǎng)絡(luò)請求相關(guān)的東西,它最終還是通過統(tǒng)一解析注解Request去構(gòu)建OkhttpCall執(zhí)行,通過設(shè)計(jì)模式去封裝執(zhí)行OkHttp

Rretrofit是怎么完成線程切換和數(shù)據(jù)適配的

Retrofit在網(wǎng)絡(luò)請求完成后所做的就只有兩件事,自動線程切換和數(shù)據(jù)適配;那么它是如何完成這些操作的呢?

關(guān)于數(shù)據(jù)適配

其實(shí)這個在上文注解解析問題中已經(jīng)回答了一部分了,這里我大概總結(jié)下流程,具體的數(shù)據(jù)解析適配過程細(xì)節(jié)需要大家私下去深入探討;在封裝的OkhttpCall調(diào)用OKHttp進(jìn)行網(wǎng)絡(luò)請求后會拿到接口響應(yīng)結(jié)果response,這時候就要進(jìn)行數(shù)據(jù)格式的解析適配了,會調(diào)用parsePerson方法,里面最終還是會調(diào)用RetrofitconverterFactories拿到數(shù)據(jù)轉(zhuǎn)換器工廠的集合其中的一個,所以當(dāng)我們創(chuàng)建Retrfoit中進(jìn)行addConvetFactory的時候,它保存在了Retrofit當(dāng)中,交給了responseConverter.convert(responsebody),從而完成了數(shù)據(jù)轉(zhuǎn)換適配的過程

關(guān)于線程切換

首先我們要知道線程切換是發(fā)生在什么時候?毫無疑問,肯定是在最后一步,當(dāng)網(wǎng)絡(luò)請求回來后,且進(jìn)行數(shù)據(jù)解析后,那這樣我們向上尋根,發(fā)現(xiàn)最終數(shù)據(jù)解析由HttpServiceMethod之后它會調(diào)用callAdapter.adapt()進(jìn)行適配

 protected Object adapt(Call<ResponseT> call, Object[] args) {
      call = callAdapter.adapt(call);
      ....
 }

這意味著callAdapter會去包裝OkhttpCall,那么這個callAdapter是來自哪里的,追本朔源,它其實(shí)在Retrofit中的build會去添加defaultCallAdapterFactories,這個方法里就調(diào)用了DefaultCallAdapterFactory,真正的線程切換就在這里

 Executor callbackExecutor = this.callbackExecutor;
      if (callbackExecutor == null) {
        callbackExecutor = platform.defaultCallbackExecutor();
      }     
// Make a defensive copy of the adapters and add the default Call adapter.
      List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
      callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

這個默認(rèn)的defaultCallAdapterFactories會傳入平臺的defaultCallbackExecutor(),由于我們平臺是Android,所以它里面存放的就是主線程的Executor,它里面就是一個Handler

    static final class MainThreadExecutor implements Executor {
      private final Handler handler = new Handler(Looper.getMainLooper());

      @Override
      public void execute(Runnable r) {
        handler.post(r);
      }
    }

到這來看下DefaultCallAdapterFactory中的enqueue(代理模式+裝飾模式), 這里面使用了一個代理類delegate,它其實(shí)就是Retrofit中的OkhttpCall,最終請求結(jié)果完成后使用callbackExecutor.execute()將線程變?yōu)橹骶€程,最終又回到了MainThreadExecutor當(dāng)中

callbackExecutor.execute(
                  () -> {
                    if (delegate.isCanceled()) {
                      // Emulate OkHttp's behavior of throwing/delivering an IOException on
                      // cancellation.
                      callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled"));
                    } else {
                      callback.onResponse(ExecutorCallbackCall.this, response);
                    }
                  });

所以總的來說,這其實(shí)是一個層層疊加的過程,Retrofit的線程切換原理本質(zhì)上就是Handler消息機(jī)制;到這關(guān)于數(shù)據(jù)適配和線程切換的回答就告一段落了,有很多細(xì)節(jié)的東西沒有提到,有時間的話,需要自己去補(bǔ)充,用一張草圖來展示下Retrofit對它們進(jìn)行的封裝

Android 學(xué)習(xí)筆錄

Android 性能優(yōu)化篇:https://qr18.cn/FVlo89
Android 車載篇:https://qr18.cn/F05ZCM
Android 逆向安全學(xué)習(xí)筆記:https://qr18.cn/CQ5TcL
Android Framework底層原理篇:https://qr18.cn/AQpN4J
Android 音視頻篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(內(nèi)含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源碼解析筆記:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知識體:https://qr18.cn/CyxarU
Android 核心筆記:https://qr21.cn/CaZQLo
Android 往年面試題錦:https://qr18.cn/CKV8OZ
2023年最新Android 面試題集https://qr18.cn/CgxrRy
Android 車載開發(fā)崗位面試習(xí)題:https://qr18.cn/FTlyCJ
音視頻面試題錦:https://qr18.cn/AcV6Ap

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評論 2 374

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