入木三分:從設計者角度看Retrofit原理

語言: CN / TW / HK

作者:Bezier

連結:https://juejin.cn/post/6963202606676049957

前言

通常我不喜歡去寫分析原始碼類的文章,流水線式的分析 枯燥乏味,但讀完Retrofit原始碼後讓我有了改變這種想法的衝動~~

一般來講讀原始碼的好處有兩點:

  • 熟悉程式碼設計流程,使用過程碰到問題可以更快速解決。說實話僅這一點無法激起我讀原始碼的興趣,畢竟以正確的姿態使用一個優秀的框架不應該出現這種問題。

  • 一個優秀的框架必須要保證易用性、擴充套件性,所以作者定會引入大量的思考進行設計,如若我們能吸收一二,那何嘗不是與作者進行了一次心靈互動呢!

今天我將帶著我的理解,嘗試從設計者的角度分析Retrofit原理,相信你認真讀完再加以思考,當再被面試官問Retrofit時你的答覆或許會讓他眼前一亮

提示:Retrofit基於2.9.0。文中貼的原始碼可能會有部分缺失,這是我刻意為之,目的在於篩選掉無用資訊增強可讀性

什麼是REST ful API?

一句話概括REST ful API:在我們使用HTTP協議做資料傳輸時應當遵守HTTP的規矩,包括請求方法、資源型別、Uri格式等等..

不久前在群裡看到某小夥伴提出一個問題:“應後端要求需要在GET請求加入Body但Retrofit 中GET 請求新增Body會報錯,如何解決?”  一時間討論的好不熱鬧,有讓把Body塞到Header裡的,有讓自定義攔截器、也有人直接慫恿改原始碼...但問題的本質不是後端先違反規則在先嗎?兩個人打架總不能把捱打的抓起來吧。

俗話說無規矩不成方圓,面對以上這種情況應當讓錯誤方去修改,因為所有人都知道GET沒有Body,否則一旦其他人接手你的程式碼很容易被搞懵。

Retrofit對REST ful API的相容做的很優秀,不符合規範直接給你報錯,強行規範你的程式碼。所以你們公司正在使用REST ful API而Retrofit將是你的不二選擇

為什麼將請求設定為(介面+註解)形式?

迪米特法則和門面模式

  • 迪米特法則:也稱之為最小知道原則,即模組之間儘量減少不必要的依賴,即降低模組間的耦合性。

  • 門面模式:基於迪米特法則拓展出來的一種設計模式,旨在將複雜的模組/系統訪問入口控制的更加單一。 舉個例子:現要做一個獲取圖片功能,優先從本地快取獲取,沒有快取從網路獲取隨後再加入到本地快取,假如不做任何處理,那每獲取一張圖片都要寫一遍快取邏輯,寫的越多出錯的可能就越高,其實呼叫者只是想獲取一張圖片而已,具體如何獲取他不需要關心。此時可以通過門面模式將快取功能做一個封裝,只暴露出一個獲取圖片入口,這樣呼叫者使用起來更加方便而且安全性更高。其實函數語言程式設計也是門面模式的產物

為什麼通過門面模式設計ApiService?

用Retrofit做一次請求大致流程如下:

interface ApiService {
    /**
     * 獲取首頁資料
     */
    @GET("/article/list/{page}/json")
    suspend fun getHomeList(@Path("page") pageNo: Int)
    : ApiResponse<ArticleBean>
}

//構建Retrofit
val retrofit = Retrofit.Builder().build()

//建立ApiService例項
val apiService =retrofit.create(ApiService::class.java)

//發起請求(這裡用的是suspend會自動發起請求,Java中可通過返回的call請求)
apiService.getHomeList(1)

然後通過Retrofit建立ApiService型別例項呼叫對應方法即可發起請求。乍一看感覺很普通,但實際上Retrofit通過這種模式(門面模式)幫我們過濾掉了很多無用資訊

tips:我們都知道Retrofit只不過是對OkHttp做了封裝。

如果直接使用OkHttp,當在構造Request時要做很多繁瑣的工作,最要命的是Request可能在多處被構造(ViewModel、Repository...),寫的越分散出錯時排查的難度就越高。而Retrofit通過註解的形式將Request需要的必要資訊全依附在方法上(還是個抽象方法,儘量撇除一切多餘資訊),作為使用者只需要呼叫對應方法即可實現請求。至於如何解析、構造、發起請求 Retrofit內部會做處理,呼叫者不想也不需要知道,

所以Retrofit通過門面模式幫呼叫者遮蔽了一些無用資訊,只暴露出唯一入口,讓呼叫者更專注於業務開發。像我們常用的Room、GreenDao也使用了這種模式

動態代理其實不是工具

看過很多Retrofit相關的文章,都喜歡上來就拋動態代理,關於為什麼用隻字不提,搞的Retrofit動態代理像是一個工具(框架)一樣,殊不知它只是代理模式思想層面的一個產物而已。本小結會透過Retrofit看動態代理本質,幫你解除對它的誤解

Retrofit構建

Retrofit構建如下所示:

Retrofit.Builder()
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .baseUrl(ApiConstants.BASE_URL)
    .build()

很典型的構建者模式,可以配置OkHttp、Gson、RxJava等等,最後通過build()做構建操作,跟一下build()程式碼:

#Retrofit.class

public Retrofit build() {

        //1.CallAdapter工廠集合
        List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
        callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

        //2.Converter工廠集合
        List<Converter.Factory> converterFactories =
                new ArrayList<>(
                        1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());
        converterFactories.add(new BuiltInConverters());
        converterFactories.addAll(this.converterFactories);
        converterFactories.addAll(platform.defaultConverterFactories());

        return new Retrofit(
              callFactory,
                baseUrl,
                unmodifiableList(converterFactories),
                unmodifiableList(callAdapterFactories),
                callbackExecutor,
                validateEagerly);
    }

將一些必要資訊注入到Retrofit並建立返回。註釋1、2處兩個集合非常重要,這裡先埋個伏筆後面我們再回來看

何為動態代理?

什麼是代理模式?

代理模式概念非常簡單,比如A想做一件事可以讓B幫他做,這樣做的好處是什麼?下面通過一個例子簡要說明。需求:每一次本地資料庫CRUD都要做一次上報

最簡單粗暴的方式就是每次CRUD時都單獨做一次記錄,程式碼如下

//業務層方法test1
fun test1{
    //資料庫插入操作
    dao.insert()
    //上報
    post()
}
//業務層方法test2
fun test2(){
    //資料庫更新操作
    dao.update()
    //上報
    post()
}

以上這種方式存在一個問題:

上報操作本身與具體業務無關,一旦需要對上報進行修改,那就可能影響到業務,進而可能造成不可預期的問題產生

面對以上問題可以通過代理模式完美規避,改造後的程式碼如下:

class DaoProxy(){
    //資料庫插入操作
    fun insert(){
        dao.insert()
        //上報
        post()
    }

    //資料庫更新操作
    fun update(){
        dao.update()
        //上報
        post()
    }
}

//業務層方法test1
fun test1{
    //資料庫插入操作
    daoProxy.insert()
}
//業務層方法test2
fun test2(){
    //資料庫更新操作
    daoProxy.update()
}

新增一個代理類DaoProxy,將dao以及上報操作在代理類中執行,業務層直接操作代理物件,這樣就將上報從業務層抽離出來,從而避免業務層改動帶來的問題。實際使用代理模式時應遵守基於介面而非實現程式設計思想,但文章側重於傳授思想,規範上可能欠缺

此時還有一個問題,每次CRUD都會手動做一次上報操作,這顯然是模版程式碼,如何解決?下面來看動態代理:

什麼是動態代理?

java中的動態代理就是在執行時通過反射為目標物件做一些附加操作,程式碼如下:

class DaoProxy() {
    //建立代理類
    fun createProxy(): Any {
        //建立dao
        val proxyAny = Dao()
        val interfaces = proxyAny.javaClass.interfaces
        val handler = ProxyHandler(proxyAny)
        return Proxy.newProxyInstance(proxyAny::class.java.classLoader, interfaces, handler)
    }

    //代理委託類
    class ProxyHandler(private val proxyObject:Any): InvocationHandler {
        //代理方法,p1為目標類方法、p2為目標類引數。呼叫proxyObject任一方法時都會執行invoke
        override fun invoke(p0: Any, p1: Method, p2: Array<out Any>): Any {
            //執行Dao各個方法(CRUD)
            val result = p1.invoke(proxyObject,p2)
            //上報
            post()
            return result
        }
    }
}
//此處規範上應該使用基於介面而非實現程式設計。如果要替換Dao通過介面程式設計可提高擴充套件性
val dao:Dao = DaoProxy().createProxy() as Dao
dao.insert()
dao.update()

其中Proxy是JDK中用於建立動態代理的類,InvocationHandler是一個委託類, 內部的invoke(代理方法)方法會隨著目標類(Dao)任一方法的呼叫而呼叫,所以在其內部實現上報操作即可消除大量模版程式碼。

動態代理與靜態代理核心思想一致,區別是動態代理可以在執行時通過反射動態建立一個切面(InvocationHandler#invoke),用來消除模板程式碼。喜歡思考的同學其實已經發現,代理模式符合面向切面程式設計(AOP)思想,而代理類就是切面

動態代理獲取ApiService

可以通過retrofit.create()建立ApiService,跟一下retrofit的create()

#Retrofit.class

public <T> T create(final Class<T> service) {
        //第一處
        validateServiceInterface(service);
        return (T) Proxy.newProxyInstance(
                        service.getClassLoader(),
                        new Class<?>[] {service},
                        new InvocationHandler() {
                            //第二處
                            @Override
                            public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                                    throws Throwable {
                                ...
                                return platform.isDefaultMethod(method)
                                        ? platform.invokeDefaultMethod(method, service, proxy, args)
                                        : loadServiceMethod(method).invoke(args);
                            }
                        });
    }

create()大致可以分為兩部分:

  • 第一部分為validateServiceInterface()內容,用來驗證ApiService合法性,比較簡單就不多描述,感興趣的同學可自行檢視。

  • 第二部分就是invoke(),通過3.2小節可知這是一個代理方法,可通過呼叫ApiService中的任一方法執行,其中引數method和args代表ApiService對應的方法和引數。返回值中有一個isDefaultMethod,這裡如果是Java8的預設方法直接執行,畢竟我們只需要代理ApiService中方法即可。經過反覆篩選最後重任落在了loadServiceMethod,這也是Retrofit中最核心的一個方法,下面我們來跟一下

#Retrofit.class

ServiceMethod<?> loadServiceMethod(Method method) {
    ServiceMethod<?> result = serviceMethodCache.get(method);
    if (result != null) return result;
    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = ServiceMethod.parseAnnotations(this, method);
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

大致就是對ServiceMethod做一個很常見的快取操作,這樣做的目的是為了提升執行效率,畢竟建立一個ServiceMethod會用到大量反射。建立ServiceMethod物件是通過其靜態方法parseAnnotations實現的,再跟一下這個方法:

#ServiceMethod.class

  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
        //第一步
        RequestFactory requestFactory =
            RequestFactory.parseAnnotations(retrofit, method);
        Type returnType = method.getGenericReturnType();
        ...
        //第二步
        return HttpServiceMethod.parseAnnotations(retrofit,
                method, requestFactory);
    }

第一步:

通過RequestFactory的parseAnnotations()解析method(ApiService的method)中的註解資訊,具體程式碼很簡單就不再貼了。不過需要注意這一步只是解析註解並儲存在RequestFactory工廠中,會在請求時再通過RequestFactory將請求資訊做拼裝。

第二步:

呼叫HttpServiceMethod的parseAnnotations建立ServiceMethod,這個方法很長並且資訊量很大,下一小節我再詳細描述,此處你只需知道它做了什麼即可。其實到這方法呼叫鏈已經很繞了,我先幫大家捋一下 HttpServiceMethod其實是ServiceMethod的子類,Retrofit動態代理裡面的loadServiceMethod就是HttpServiceMethod型別物件,最後來看一下它的invoke()方法。

#HttpServiceMethod.class

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

建立了一個OkHttpCall例項,它內部其實就是對OkHttp的一系列操作,這裡先按住不表後面我會再提到。把關注點切到返回值,返回的Call物件沒做任何操作,而是傳入到adapter()方法一併返回來,字面意思應該是一個適配操作,那究竟如何適配?這裡再埋一個伏筆與3.1結尾相呼應,下一小節我們再一一揭開。

動態代理講完了,那麼它解決了什麼問題?

  • 假如不使用代理模式,那關於ApiService中方法註解解析的操作勢必會浸入到業務當中,一旦對其修改就有可能影響到業務,其實也就是也違背了我們前面所說的門面模式和迪米特法則,通過代理模式做一個切面操作(AOP)可以完美規避了這一問題。可見這裡的門面模式和代理模式是相輔相成的

  • Retrofit事先都不知道ApiService方法數量,就算知道也避免不了逐一解析而產生大量的模版程式碼,此時可通過引入動態代理在執行時動態解析 從而解決這一問題。

ReturnT、ResponseT做一次適配的意義何在?

ResponseT、ReturnT是 Retrofit 對響應資料型別和返回值型別的簡稱

建立HttpServiceMethod

上一小節我們跟到了adapter(),這是一個抽象方法,其實現類是通過HttpServiceMethod的parseAnnotations建立的,繼續跟下去:

#HttpServiceMethod.class

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
            Retrofit retrofit, Method method, RequestFactory requestFactory) {
        boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
        boolean continuationWantsResponse = false;
        boolean continuationBodyNullable = false;

        Annotation[] annotations = method.getAnnotations();
        Type adapterType;
        //1.獲取adapterType,預設為method返回值型別
        if (isKotlinSuspendFunction) {
            Type[] parameterTypes = method.getGenericParameterTypes();
            Type responseType =
                    Utils.getParameterLowerBound(
                            0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
            if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
                // Unwrap the actual body type from Response<T>.
                responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
                continuationWantsResponse = true;
            } else {
            }
            adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
            annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
        } else {
            adapterType = method.getGenericReturnType();
        }
        //2.建立CallAdapter
        CallAdapter<ResponseT, ReturnT> callAdapter =
                createCallAdapter(retrofit, method, adapterType, annotations);
        Type responseType = callAdapter.responseType();
        //3.建立responseConverter
        Converter<ResponseBody, ResponseT> responseConverter =
                createResponseConverter(retrofit, method, responseType);

        okhttp3.Call.Factory callFactory = retrofit.callFactory;
        //4.建立HttpServiceMethod型別具體例項
        if (!isKotlinSuspendFunction) {
            return new HttpServiceMethod.CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
        }
        //相容kotlin suspend方法
        else if (continuationWantsResponse) {
            //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
            return (HttpServiceMethod<ResponseT, ReturnT>)
                    new HttpServiceMethod.SuspendForResponse<>(
                            requestFactory,
                            callFactory,
                            responseConverter,
                            (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
        } else {
            //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
            return (HttpServiceMethod<ResponseT, ReturnT>)
                    new HttpServiceMethod.SuspendForBody<>(
                            requestFactory,
                            callFactory,
                            responseConverter,
                            (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
                            continuationBodyNullable);
        }
    }
  • 註釋1:獲取adapterType,這裡的adapter指的是Retrofit構建時通過addCallAdapterFactory()新增的型別,如果新增的是RxJava那adapterType便是Observable。預設是method返回值,同時也會做kotlin suspend適配

  • 註釋2:建立callAdapter,暫時掠過,下面詳細描述

  • 註釋3:建立responseConverter,暫時掠過,下面詳細描述

  • 註釋4:這裡會建立具體的HttpServiceMethod型別例項,總共有三種類型CallAdapted、SuspendForResponse、SuspendForBody,第一種為預設型別,後兩種可相容kotlin suspend。內部主要做的事情其實很簡單,就是通過內部的adapter()呼叫callAdapter adapter(),具體程式碼就不貼了,感興趣的自行檢視

如何管理callAdapter、responseConverter?

建立建立callAdapter

#HttpServiceMethod.class

 private static <ResponseT, ReturnT> CallAdapter<ResponseT, ReturnT> createCallAdapter(
            Retrofit retrofit, Method method, Type returnType, Annotation[] annotations) {
        return (CallAdapter<ResponseT, ReturnT>) retrofit.callAdapter(returnType, annotations);
        ...
    }

通過retrofit#callAdapter()獲取CallAdapter,繼續跟

#Retrofit.class

public CallAdapter<?, ?> callAdapter(Type returnType, Annotation[] annotations) {
        return nextCallAdapter(null, returnType, annotations);
}

public CallAdapter<?, ?> nextCallAdapter(
            @Nullable CallAdapter.Factory skipPast, Type returnType, Annotation[] annotations) {
        int start = callAdapterFactories.indexOf(skipPast) + 1;
        for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
            //通過returnType在callAdapterFactories獲取adapter工廠,再get adapter
            CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
            if (adapter != null) {
                return adapter;
            }
        }
        ...
    }

先通過returnType在callAdapterFactories獲取adapter工廠,再通過工廠get()獲取CallAdapter例項。callAdapterFactories是3.1結尾build()中初始化的,通過platform新增預設型別,也可以通過addCallAdapterFactory()新增RxJava之類的介面卡型別。

這裡用到了兩個設計模式介面卡跟策略

介面卡模式

返回的CallAdapter其實就是Call 的介面卡,假如你想讓Retrofit配合RxJava使用,常規方式只能在業務中單獨建立Observable並與Call融合,關於Observable與Call融合(適配)其實是與業務無關的,此時可以引入介面卡模式將Call適配成Observable,將適配細節從業務層挪到Retrofit內部,符合迪米特法則

策略模式

通過ReturnT獲取對應的CallAdapter,如果ReturnT是Call 那獲取的是DefaultCallAdapterFactory建立的例項,如果是Observable 則獲取的是RxJava2CallAdapterFactory建立的例項。假如想新增一種介面卡只需明確ReturnT,建立對應工廠再通過addCallAdapterFactory新增即可,Retrofit會通過ReturnT自動尋找對應CallAdapter,符合開閉原則(擴充套件開放)

建立responseConverter

關於responseConverter其實是做資料轉換的,可以將ResponseT適配成我們想要的資料型別,比如Gson解析只需通過addConverterFactory新增GsonConverterFactory建立的Converter例項即可 具體新增、獲取流程與CallAdapter基本一致,感興趣的同學可自行檢視

發起請求

到上一小結我們已經建立了所有需要的內容,再回到HttpServiceMethod的invoke,這裡會將OkHttpCall傳入到adapt執行並返回,HttpServiceMethod的實現類的adapter會執行對應CallAdapter的adapter 我們就取預設的CallAdapter 即DefaultCallAdapterFactory通過get獲取的CallAdapter,程式碼如下:

DefaultCallAdapterFactory.class

public @Nullable CallAdapter<?, ?> get(
        return new CallAdapter<Object, Call<?>>() {
            @Override
            public Type responseType() {
                return responseType;
            }

            @Override
            public Call<Object> adapt(Call<Object> call) {
                return executor == null ? call : new DefaultCallAdapterFactory.ExecutorCallbackCall<>(executor, call);
            }
        };
    }

內部adapt即ApiService method最終返回的ExecutorCallbackCall是OkHttpCall裝飾類,最後可通過OkHttpCall的execute發起請求,程式碼如下:

#OkHttpCall.class

public Response<T> execute() throws IOException {
        okhttp3.Call call;
        ...
        return parseResponse(call.execute());
    }

OkHttp常規操作,再把關注點放到onResponse的parseResponse

#OkHttpCall.class

Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
        ...
        T body = responseConverter.convert(catchingBody);
        ...
        return Response.success(body, rawResponse);
    }

responseConverter會對Body做一個適配,如果addConverterFactory添加了GsonConvert那解析操作就會在此處進行

至此Retrofit全部流程分析完畢

綜上所述

  • Retrofit通過REST ful API從正規化層面約束程式碼

  • 通過門面模式設計ApiService可以讓開發者更專注於業務

  • 動態代理只是將功能程式碼從業務剝離,並解決了模板程式碼問題

  • ReturnT、ResponseT引入介面卡模式可以讓結果更加靈活

掃描二維碼

獲取更多精彩

Android補給站

點個 在看 你最好看