Android 網路框架 Retrofit 原始碼解析

WngShhng發表於2019-03-03

在之前的文章 《Andriod 網路框架 OkHttp 原始碼解析》 中我們分析了 OkHttp 的原始碼。現在我們就來分析一下 OkHttp 的兄弟框架 Retrofit。關於 Retrofit 的註解的使用,可以參考其官方文件:square.github.io/retrofit/

Retrofit 也是 Square 釋出的一個開源的庫,它是一個型別安全的 Http 客戶端,適用於 Android 和 Java。本質上,Retrofit 使用了 Java 的動態代理,內部使用 OkHttp 來進行網路訪問,並且可以通過指定 “請求介面卡” 和 “型別轉換器” 來完成:請求的適配,方法引數到 OkHttp 請求的轉換,以及響應到 Java 型別的轉換。

1、基本使用

Retrofit 設計的一個好的地方就是它把我們上面提到的 “請求介面卡” 和 “型別轉換器” 使用策略模式解耦出來。使用者可以根據自己的需求通過實現指定的介面來自定義自己的型別轉換器。所以,當我們使用 Gson 進行轉換和 RxJava2 進行適配的時候,就需要指定下面三個依賴:

api `com.squareup.retrofit2:retrofit:2.4.0`
api `com.squareup.retrofit2:converter-gson:2.4.0`
api `com.squareup.retrofit2:adapter-rxjava2:2.4.0`
複製程式碼

然後,我們需要根據自己的 API 介面的資訊,在程式碼裡用一個介面來對該 API 進行宣告:

public interface WXInfoService {
    @GET("/sns/userinfo")
    Observable<WXUserInfo> getWXUserInfo(
        @Query("access_token") String accessToken, @Query("openid") String openId);
}
複製程式碼

這裡的 WXUserInfo 是由該 API 介面返回的 Json 生成的 Java 物件。然後,我們可以像下面這樣獲取一個該介面的代理物件:

WXInfoService wXInfoService = new Retrofit.Builder()
    .baseUrl("https://api.weixin.qq.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .client(okHttpClient)
    .build().create(WXInfoService.class);
複製程式碼

然後,我們就可以使用該物件並呼叫其方法來獲取介面返回的資訊了:

Disposable disposable = wxInfoService.getWXUserInfo(accessToken, openId)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(wxUserInfo -> { /*...拿到結果之後進行處理...*/ });
複製程式碼

上面我們只使用了 Retrofit 最基礎的 GET 介面。當然,Retrofit 本身的功能遠比這要豐富得多,關於其更多的使用,可以參考其官方的文件。

2、動態代理:魔力發生的地方

上面我們使用 Retrofit 進行網路請求,實際其內部使用 OkHttp 來完成網路請求的。僅定義了一個介面並呼叫了該介面的方法,就拿到了請求的結果,這看上去非常簡潔,而這其中的功不可沒的就是動態代理

當我們使用 Retrofit.Buildercreate() 方法獲取一個 WXInfoService 例項的時候,實際返回的是經過代理之後的物件。該方法內部會呼叫 Proxy 的靜態方法 newProxyInstance() 來得到一個代理之後的例項。為了說明這個方法的作用,我們寫了一個例子:

public static void main(String...args) {
    Service service = getProxy(Service.class);
    String aJson = service.getAInfo();
    System.out.println(aJson);
    String bJson = service.getBInfo();
    System.out.println(bJson);
}

private static <T> T getProxy(final Class<T> service) {
    InvocationHandler h = (proxy, method, args) -> {
        String json = "{}";
        if (method.getName().equals("getAInfo")) {
            json = "{A請求的結果}";
        } else if (method.getName().equals("getBInfo")) {
            json = "{B請求的結果}";
        }
        return json;
    };
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, h);
}

該程式的輸出結果:

 {A請求的結果}
 {B請求的結果}
複製程式碼

在上面的這個例子中,我們先使用 getProxy() 獲取一個代理之後的例項,然後依次呼叫它的 getAInfo()getBInfo() 方法,來模擬呼叫 A 介面和 B 介面的情形,並依次得到了 A 請求的結果和 B 請求的結果。

上面的效果近似於我們使用 Retrofit 訪問介面的過程。為了說明這個過程中發生了什麼,我們需要先了解一下這裡的 newProxyInstance() 方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {
    // ...
}
複製程式碼

該方法接收三個引數:

  1. 第一個是類載入器;
  2. 第二個是介面的 Class 型別;
  3. 第三個是一個處理器,你可以將其看作一個用於回撥的介面。當我們的代理例項觸發了某個方法的時候,就會觸發該回撥介面的方法。

InvocationHandler 是一個介面,它內部定義了一個方法如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
複製程式碼

該方法也接收三個引數,第一個是觸發該方法的代理例項;第二個是觸發的方法;第三個是觸發的方法的引數。invoke() 方法的返回結果會作為代理類的方法執行的結果。

所以,當了解了 newProxyInstance() 方法的定義之後,我們可以做如下總結:當我們使用 newProxyInstance() 方法獲取了一個代理例項 service 並呼叫其 getAInfo() 方法之後,該方法的資訊和引數資訊會分別通過 methodargs 傳入到 hinvoke() 中。所以,最終的效果就是,當我們呼叫 servicegetAInfo() 時候會觸發 hinvoke()。在 invoke() 方法中我們根據 method 得知觸發的方法是 getAInfo。於是,我們把它對應的請求從 invoke() 方法中返回,並作為 service.getAInfo() 的返回結果。

所以,我們可以總結 Retrofit 的大致工作流程:當我們獲取了介面的代理例項,並呼叫它的 getWXUserInfo() 方法之後,該方法的請求引數會傳遞到代理類的 InvocationHandler.invoke() 方法中。然後在該方法中,我們將這些資訊轉換成 OkHttp 的 Request 並使用 OkHttp 進行訪問。從網路中拿到結果之後,我們使用 “轉換器” 將響應轉換成介面指定的 Java 型別。

上面是 Retrofit 請求處理的基本流程,下面我們看一下 Retrofit 的代理方法內部究竟發生了什麼。

3、Retrofit 的原始碼解析

3.1 建立 Retrofit

根據上面的例子,當使用 Retrofit 的時候,首先我們需要使用 Retrofit 的構建者來建立 Retrofit 的例項。這裡的構建者有幾個重要的方法需要提及一下:

3.1.1 addConverterFactory 方法

該方法用來向 Retrofit 中新增一個 Converter.FactoryConverter.Factory,顧名思義是一種工廠模式。它是一個介面需要,實現兩個重要的方法。每個方法需要返回一個轉換器:某種資料型別到請求體的轉換器,響應體到我們需要的資料型別的轉換器。當我們使用 Gson 來完成這個轉換,那麼我們就需要使用 GsonConverterFactory.create() 來得到一個適用於 Gson 的 Converter.Factory

public Builder addConverterFactory(Converter.Factory factory) {
  converterFactories.add(checkNotNull(factory, "factory == null"));
  return this;
}
複製程式碼

3.1.2 addCallAdapterFactory 方法

CallAdapter.Factory 用於獲取 CallAdapter 物件, CallAdapter 物件用於把原生的 OkHttp 的 Call 轉換成我們指定的請求型別。比如,上面的例子中,我們用來將其轉換成 Observable<WXUserInfo>。下面是該方法的定義:

public Builder addCallAdapterFactory(CallAdapter.Factory factory) {
  callAdapterFactories.add(checkNotNull(factory, "factory == null"));
  return this;
}
複製程式碼

3.1.3 build 方法

當根據使用者的自定義設定完了引數之後,就可以呼叫 build() 方法,來獲取一個 Retrofit 的例項。在該方法中會將上述方法傳入的 “介面卡” 和 “轉換器” 新增到各自的列表中,然後 new 出一個 Retrofit 的例項並返回。

3.1.4 小結

為了說明介面卡 CallAdapter 和轉換器 Converter 的作用,我們繪製了下圖:

Retrofit類圖

從上面我們看出,CallAdapter 主要用來將某個請求轉換成我們指定的型別。比如,在我們最開始的例子中,要將請求轉換成 Observable<WXUserInfo>。如果轉換之後的請求是 Observable 型別的,那麼當我們對轉換後的請求進行訂閱的時候,就啟動了 OkHttp 的網路請求過程。

在進行網路請求之前會先使用 Converter 將請求的引數轉換成一個 RequestBody。這裡將其作為一個介面的好處是便於解耦。比如,上面我們用 Gson 來完成轉換過程,你也可以通過自定義轉換器來使用其他的框架,比如 Moshi 等。當拿到了響應之後,我們又會再次使用 Converter 來將響應體 ResponseBody 轉換成我們要求的型別。比如 WXUserInfo

從上面我們看出,Retrofit 設計的非常妙的地方就在於上面的兩個過程的解耦(策略模式+工廠模式+介面卡模式)。一次是將請求轉換成 Observable 的過程,一次是將請求體和響應體轉換成 OkHttp 要求的 RequestBodyResponseBody 的過程。對於前者,不論我們使用的是 RxJava 1 還是 RxJava 2,只要傳入一個 CallAdapter 即可。對於後者,不論我們使用哪種 Json 轉換框架,只要實現了 Converter 介面皆可。

3.2 獲取代理例項

3.2.1 劃分平臺:Platform

建立了 Retrofit 的例項之後,我們就可以使用它的 create() 方法來獲取代理之後的服務例項。下面是這個方法的定義。在這裡,我們會先根據 validateEagerly 變數來判斷是否立即對傳入的服務介面的方法進行解析。然後,我們使用 Proxy 的靜態方法獲取一個代理例項。

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    // 這裡的 validateEagerly 在 Retrofit 構建的時候設定
    if (validateEagerly) {
        // 是否立即對 Service 方法的內容進行解析
        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, @Nullable Object[] args)
                        throws Throwable {
                    // 該方法是 Object 的方法,直接觸發該方法
                    if (method.getDeclaringClass() == Object.class) {
                        return method.invoke(this, args);
                    }
                    // 如果是 default 方法,那麼使用該 Java8 平臺的方法執行
                    if (platform.isDefaultMethod(method)) {
                        return platform.invokeDefaultMethod(method, service, proxy, args);
                    }
                    // 獲取服務方法的資訊,並將其包裝成 ServiceMethod
                    ServiceMethod<Object, Object> serviceMethod =
                            (ServiceMethod<Object, Object>) loadServiceMethod(method);
                    OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
                    return serviceMethod.adapt(okHttpCall);
                }
            });
}
複製程式碼

這裡的 eagerlyValidateMethods() 方法定義如下:

private void eagerlyValidateMethods(Class<?> service) {
    // 獲取程式當前執行的平臺
    Platform platform = Platform.get();
    for (Method method : service.getDeclaredMethods()) {
        // 判斷該方法是否是 default 方法
        if (!platform.isDefaultMethod(method)) {
            loadServiceMethod(method);
        }
    }
}
複製程式碼

它的作用是立即對服務介面的方法進行解析,並將解析之後的結果放進一個快取中。這樣,當這個服務方法被觸發的時候,直接從快取當中獲取解析之後的 ServiceMethod 來使用即可。該方法會先會根據當前程式執行的平臺來決定是否應該載入服務的方法。因為,Java 8 之後,我們可以為介面增加 default 型別的方法,所以,如果是 default 型別的話,我們不會呼叫 loadServiceMethod() 進行解析,而是呼叫 Java8 平臺的 invokeDefaultMethod() 來處理。在 invokeDefaultMethod() 中,會根據傳入的資訊建立一個例項並使用反射觸發它的方法。此時,就間接地觸發了該 default 方法。

判斷平臺的時候,使用瞭如下這段程式碼:

platform.isDefaultMethod(Method)
複製程式碼

這裡的 platform 是呼叫 Platform.get() 的時候得到的。它會在 get() 方法中嘗試使用反射去獲取一個只有 Java8 平臺才具有的類,以此來判斷是否是 Java8 的環境。在 Retrofit 中,提供了 Java8Android 兩個類來區分所在的平臺,並會根據執行環境來決定返回哪個例項。

所以,Platform 應用了策略模式,以對不同的平臺做不同的處理。在當前的版本中,它的主要作用是對 default 型別的方法進行處理。

3.2.2 解析服務方法:ServiceMethod

上面我們提到過 loadServiceMethod() 方法,它的主要作用:首先會嘗試從快取當中獲取該方法對應的 ServiceMethod 例項,如果取到的話,就將其返回;否則,就使用構建者模式建立一個並放進快取中,然後將其返回。

ServiceMethod<?, ?> loadServiceMethod(Method method) {
    // 從快取中進行獲取
    ServiceMethod<?, ?> result = serviceMethodCache.get(method);
    if (result != null) return result;
    synchronized (serviceMethodCache) {
        result = serviceMethodCache.get(method);
        if (result == null) {
            // 建立ServiceMethod例項
            result = new ServiceMethod.Builder<>(this, method).build();
            serviceMethodCache.put(method, result);
        }
    }
    return result;
}
複製程式碼

ServiceMethod 的構建過程比較簡單,只需要把當前的 Retrofit 例項和服務方法 method 傳入進去,然後呼叫它的 build() 方法就完成了整個建立過程。在 build() 方法中,會完成對 method 的解析,比如根據註解判斷是什麼型別的請求,根據方法的引數來解析請求的請求體等等。

所以,ServiceMethod 的作用是快取服務方法對應的請求資訊,這樣下次我們就不需要再次解析了。同時,它提供了以下幾個方法。它們的主要作用是用來從 ServiceMethod 中獲取請求相關的資訊:

toCall() 用來獲取用於 OkHttp 請求的 Call 物件:

okhttp3.Call toCall(@Nullable Object... args) throws IOException {
    RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
            contentType, hasBody, isFormEncoded, isMultipart);
    // ...
    return callFactory.newCall(requestBuilder.build());
}
複製程式碼

toResponse(ResponseBody) 用來把 OkHttp 得到的響應體轉換成 Java 物件等(在示例中是WXUserInfo):

R toResponse(ResponseBody body) throws IOException {
    return responseConverter.convert(body);
}
複製程式碼

adapt(Call<R>) 用來將 OkHttp 的請求轉換成我們的服務方法的返回型別(在示例中是Observable<WXUserInfo>):

T adapt(Call<R> call) {
    return callAdapter.adapt(call);
}
複製程式碼

3.2.3 請求封裝:OkHttpCall

解析完畢服務方法之後,我們得到了 ServiceMethod 例項。然後,我們使用它來建立 OkHttpCall 例項。這裡的 OkHttpCall 實現了 Retrofit 中定義的 Call 介面,會在方法內呼叫 ServiceMethodtoCall() 方法來獲取 OkHttp 中的 Call 物件,然後使用它進行網路訪問。當拿到了請求的結果之後又使用 ServiceMethodtoResponse() 把響應轉換成我們指定的型別。下面是該類中的幾個比較重要的方法:

  1. execute() 方法,用來同步執行網路請求:
    @Override
    public Response<T> execute() throws IOException {
        okhttp3.Call call;
        synchronized (this) {
            // ...
            call = rawCall;
            if (call == null) {
                try {
                    // 建立 OkHttp 的 Call 例項
                    call = rawCall = createRawCall();
                } catch (IOException | RuntimeException | Error e) {
                    throwIfFatal(e);
                    creationFailure = e;
                    throw e;
                }
            }
        }
        if (canceled) {
            call.cancel();
        }
        // 同步執行請求,並解析結果
        return parseResponse(call.execute());
    }
複製程式碼
  1. createRawCall() 用來建立 OkHttp 的 Call 例項:
    // 使用 serviceMethod 的 toCall 方法獲取 OkHttp 的 Call 例項
    private okhttp3.Call createRawCall() throws IOException {
        okhttp3.Call call = serviceMethod.toCall(args);
        if (call == null) {
            throw new NullPointerException("Call.Factory returned null.");
        }
        return call;
    }
複製程式碼
  1. parseResponse() 用來將 OkHttp 的響應轉換成我們介面中定義的型別。比如,在我們的例子中,返回的是 Observable<WXUserInfo>:
    // 使用 serviceMethod 的 toResponse 方法獲取 OkHttp 的 Response 例項
    Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
        ResponseBody rawBody = rawResponse.body();
        rawResponse = rawResponse.newBuilder()
                .body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
                .build();
        // ...
        ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);
        try {
            // 使用 serviceMethod 的 toResponse 方法獲取 OkHttp 的 Response 例項
            T body = serviceMethod.toResponse(catchingBody);
            return Response.success(body, rawResponse);
        } catch (RuntimeException e) {
            catchingBody.throwIfCaught();
            throw e;
        }
    }
複製程式碼

3.3 Retrofit 的工作過程

上面是 Retrofit 框架設計中幾個關鍵的部分的功能的解析。下面,我們再來具體看一下,從觸發代理類的方法到拿到響應的結果,這一整個過程中,都有哪些類的哪些方法參與,以及它們在什麼時候,扮演什麼樣的角色。這裡我們仍然使用最初的示例:

Retrofit的執行過程

上圖中,我們將 Retrofit 的請求的過程分成三個過程來進行說明:

  1. 建立代理例項的過程:在這個過程中主要是呼叫 Proxy.newProxyInstance() 來獲取一個代理例項。相關的主要引數是 validateEagerly,我們會使用它來決定是否立即對傳入的介面的方法進行解析。不論我們什麼時候進行解析,都會把解析的結果快取起來。
  2. 觸發代理方法的過程:觸發代理方法是整個請求的第二過程。這個時候,我們呼叫了 WXInfoService 代理例項的 getWXUserInfo() 方法。此時,會觸發 InvocationHandler.invoke() 方法。在該方法內部會呼叫 ServiceMethod 的構建者模式來建立 serviceMethod 例項。當呼叫構建者模式的 build() 方法的時候,會對方法 getWXUserInfo() 的資訊進行解析。然後,使用 serviceMethod 建立 okHttpCall。最後,呼叫 serviceMethod.adapt() 方法將 okHttpCall 例項轉換成 Observable<WXUserInfo>。在轉換的過程中會使用 CallAdapteradapt() 方法來完成適配。
  3. 執行網路請求的過程:拿到了 Observable<WXUserInfo> 之後,需要對其進行訂閱才能觸發網路請求。相關的邏輯在 CallAdapter 中完成。首先,它會根據你使用同步還是非同步的來決定使用哪個執行器。這裡存在兩個執行器,它們的區別是一個會在內部呼叫 OkHttpCallenqueue(),另一個會在執行器中呼叫 OkHttpCallexecute() 方法。不論呼叫 enqueue() 還是 execute(),都會先使用 OkHttpCalltoCall() 方法獲取一個 Call 請求。獲取請求的過程中會使用 Converter 來將某個例項轉換成請求體。拿到了請求之後,使用該請求來進行網路訪問。當從網路中拿到了響應之後,會使用 Converter 來將響應體轉換成物件。這樣,拿到了實際的結果之後,就會呼叫 ObserveronNext() 方法把結果通知給觀察者。

4、總結

在這篇文章中,我們先簡單介紹了 Retrofit 的使用,然後,因為 Retrofit 內部使用動態代理來實現的,所以,我們對動態代理相關內容進行了介紹。最後,我們對 Retrofit 的原始碼進行了分析,先從設計思路,後從各個環節的執行過程進行了說明。最後的最後,我們將兩者結合起來用一個時序圖做了說明。

從上文中可以看出來,Retrofit 設計的幾個值得我們借鑑的地方:

  1. 使用執行時註解和反射簡化請求描述,但是考慮到反射的效率比較低,所以將一次反射之後的結果快取起來,以便於下次使用。
  2. 動態代理:使用介面描述請求的好處是它簡潔,而且 “描述” 本來就是它的責任。但是,一般我們需要去實現介面才能使用。而這裡告訴我們,使用動態代理一樣可以使用接。
  3. 解耦:從我們上面的圖中也可以看出來,Retrofit 的設計的思路是比較清晰的。它將一個請求的幾個過程解耦出來。首先是我們 Observable 到請求的轉換,這裡使用介面卡來完成;然後是請求體和響應體的轉換,基本就是 Json 的轉換,使用轉換器來完成。這樣,不論你使用 RxJava 1 還是 RxJava 2,不論是 Gson 還是 FastXml 都可以和 Retrifut 配合使用。

以上就是我們對 Retrofit 的原始碼的分析。

相關文章