Retrofit 2.3.0 源碼解析

前言

Retrofit A type-safe HTTP client for Android and Java

Retrofit,是一個基于http請求庫二次封裝的HTTP客戶端,將 REST API 轉換為 Java 接口。

基于注解,進一步解放了生產力,使得http請求就像調用方法一樣簡單,如絲般順滑。

結構概覽

architecture.png

項目結構整體分四個部分,Builder -> Proxy -> Invocation -> RawCall
這里我們把基于Retrofit的HTTP通信比做是郵遞信件。

郵遞信件

  • 信封:當我們準備好信件之后,要在信封上寫郵寄地址,收件人,可能還要備注勿折(是的,我暴露了我的年齡,如今很多人可能都沒有過寫信寄信的體驗)。
  • 郵遞員:然后我們親自去送信嗎?No,我們把信投入郵箱,交給郵遞員代為送信就行了。
  • 郵局:然后郵遞員會根據信封上的信息對信件進行分揀,寄信或收信均經由郵局統一處理
  • 郵寄方式:最后就是交給運送單位送信了,空運或是陸運等。

基于Retrofit的HTTP通信

  • Builder:當我們準備好數據之后,要指定服務端的通信地址,處理接口地址,請求方法,可能還要備注是否有body、是否是multipart。
  • Proxy:然后通信的事交給代理去做,代理會幫你做好一系列的工作,比如注解解析,Call適配,以及請求調度等
  • Invocation:這里負責調度同步或異步請求,請求裝配和響應解析
  • RawCall:這里就是具體的通信工具了,可選Okhttp等框架來做具體的Http通信。

來看看寄信和Retrofit之間的對比:

arch_flow.png

大概過程就是這樣,郵遞員會把信送出去,并在適合的時機把對方的回信取回來送給你,當然如果你的信件是表白情書,那也很可能會收不到回信的,畢竟表白成功的概率要看人品的。不要傷心,HTTP通信也會有時候收不到服務端的回信噢。

目錄概覽

官方 Javadoc

│  BuiltInConverters.java               # 內建Converter
│  Call.java                            # 發送請求接收響應的retrofit方法調用
│  CallAdapter.java                     # 適配Call的響應類型,將默認響應類型R轉換為類型T
│  Callback.java                        # 返回服務端或離線請求的響應體
│  Converter.java                       # HTTP交互中,轉換對象為數據 或 從數據轉換為對象
│  DefaultCallAdapterFactory.java       # 默認CallAdapter工廠
│  ExecutorCallAdapterFactory.java      # http請求執行器工廠
│  HttpException.java                   # 非2xx HTTP響應的異常處理
│  OkHttpCall.java                      # 真正調用OkHttp3發送Http請求的類
│  package-info.java                    # 包描述
│  ParameterHandler.java                # 參數注解解析器
│  Platform.java                        # 平臺適配(Java/Android)
│  RequestBuilder.java                  # 請求拼裝
│  Response.java                        # 原汁原味的HTTP 響應體,所謂 T body
│  Retrofit.java                        # 組裝工廠,基于建造者模式拼裝自定義HTTP交互所需的組件,并作為總調度暴露接口
│  ServiceMethod.java                   # 框架核心處理類,注解解析器調度,生成請求(包含api url、path、http請求方法、請
                                        # 求頭、是否是multipart等等),并返回用于發起http請求的Call對象
│  Utils.java                           # 工具類
│  
└─http                              # http注解定義 (直接引用了Javadoc中的描述,均為提高生產力的注解)

        Body.java                       # control the request body of a POST/PUT request
        DELETE.java                     # Make a DELETE request
        Field.java                      # Named pair for a form-encoded request
        FieldMap.java                       # Named key/value pairs for a form-encoded request
        FormUrlEncoded.java                 # Denotes that the request body will use form URL encoding
        GET.java                        # Make a GET request
        HEAD.java                       # Make a HEAD request
        Header.java                     # Replaces the header with the value of its target
        HeaderMap.java                      # Adds headers specified in the Map
        Headers.java                        # Adds headers literally supplied in the value
        HTTP.java                       # Use a custom HTTP verb for a request
        Multipart.java                      # Denotes that the request body is multi-part
        OPTIONS.java                        # Make an OPTIONS request
        package-info.java                   # Package description
        Part.java                       # Denotes a single part of a multi-part request
        PartMap.java                        # Denotes name and value parts of a multi-part request
        PATCH.java                      # Make a PATCH request
        Path.java                       # Named replacement in a URL path segment
        POST.java                       # Make a POST request
        PUT.java                        # Make a PUT request
        Query.java                      # Query parameter appended to the URL
        QueryMap.java                       # Query parameter keys and values appended to the URL
        QueryName.java                      # Query parameter appended to the URL that has no value
        Streaming.java                      # Treat the response body on methods returning Response as is, i.e. 
                                # without converting body() to byte[]
        Url.java                        # URL resolved against the base URL

Retrofit的基本用法

讓我們從基本用法開始,先看如何使用,順著這個藤,摸摸如何實現的瓜。

用 Java 接口的方式定義一個HTTP API.

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

Retrofit 類生成一個 GitHubService 接口的實現實例.

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

Each Call from the created GitHubService can make a synchronous or asynchronous HTTP request to the remote webserver.
GitHubService實例的每一個方法調用都支持同步或異步HTTP請求.

Call<List<Repo>> repos = service.listRepos("octocat");

執行同步或異步HTTP請求,得到HTTP響應數據.

Response<List<Repo>> response = repos.execute();

Retrofit的源碼解析

首先我們心里要有個概念,Retrofit的核心關鍵詞:注解、動態代理、轉換器、適配器

Retrofit就是基于這四個關鍵詞搭建起來的充分解耦,靈活,可插拔的優秀框架。

下面我們結合Retrofit設計圖流程來解讀代碼。 還記得流程嗎? Builder -> Proxy -> Invocation -> RawCall.

Flow - Builder

Retrofit.Builder() .baseUrl("https://api.github.com/") ... .build();
Tips.設計模式之Builder模式

基于Builder模式,裝配一系列零部件,比如base請求地址,gson轉換器,Rxjava適配器,HTTP請求client(比如裝配OKHTTP)等。

// Retrofit.java -> class Builder

public Retrofit build() {
      
      ...

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

返回一個裝配了 callFactory,converterFactories,adapterFactories,callbackExecutor 和指定了 baseUrl 的 Retrofit 實例。
注:validateEagerly,用于指定是否預先解析注解,加速接口訪問效率。

Flow - Proxy

GitHubService service = retrofit.create(GitHubService.class);
我們知道,Java 接口是不可以直接 new 實例的,那么這個 create 方法看起來又像是返回了一個 GitHubService 接口類型的實現實例,這是怎么回事呢?我們來看下 create 的實現。

// Retrofit.java

  public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      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 {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

create方法主要就一個return,返回了一個Proxy.newProxyInstance生成的動態代理對象。原來這里是通過動態代理的方式生成了 GitHubService 接口的代理實例,那么后續 GitHubService 接口的方法都可以通過代理去調用了。
為什么用動態代理?
這是Retrofit設計的核心思路,基于動態代理,可以為后續在調用 GitHubService 接口的相關方法時先攔截下來,做完一系列工作后(即注解解析,請求轉換,適配等),再去完成方法本尊想要完成的工作,這就是動態代理的魅力。

Tips.動態代理

Call<List<Repo>> repos = service.listRepos("octocat");
通過代理對象 service 調用接口方法 listRepos ,會被動態代理攔截,調用Proxy.newProxyInstance方法中的InvocationHandler對象的 invoke 方法。

invoke中主要由ServiceMethod和CallAdapter完成了三件事:

  • 請求方法的注解解析
  • 創建OkHttpCall實例,為后續流程中的HTTP請求執行做準備,詳見 Flow - Invocation.
  • 適配Call的響應類型,將默認響應類型R轉換為類型T
ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);

ServiceMethod.java

// ServiceMethod.java

public ServiceMethod build() {
      callAdapter = createCallAdapter();
      responseType = callAdapter.responseType();
      
      ...

      responseConverter = createResponseConverter();

      for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
      }

      ...

      int parameterCount = parameterAnnotationsArray.length;
      parameterHandlers = new ParameterHandler<?>[parameterCount];
      for (int p = 0; p < parameterCount; p++) {
        Type parameterType = parameterTypes[p];
        
        ...

        Annotation[] parameterAnnotations = parameterAnnotationsArray[p];

        ...

        parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
      }

      ...

      return new ServiceMethod<>(this);
    }

獲取callAdapter、responseType、responseConverter接口對象

解析Method的注解

解析Method的參數注解

解析Method的參數中使用了依賴請求API的動態參數的注解,交由ParameterHandler處理

CallAdapter.java

public interface CallAdapter<R, T> {
 
  Type responseType();

  ...

  T adapt(Call<R> call);

  ...
  
  }

適配Call的響應類型,將默認響應類型R轉換為類型T.比如官方的RxJavaCallAdapter可以結合Rxjava特性對Call的響應做RxJava觀察者模式轉換,進一步解放生產力。

注:未在Builder階段指定CallAdapter(如 RxJavaCallAdapterFactory )的情況下,默認的 CallAdapter 不對Call做任何處理。
見 DefaultCallAdapterFactory:

final class DefaultCallAdapterFactory extends CallAdapter.Factory {
  static final CallAdapter.Factory INSTANCE = new DefaultCallAdapterFactory();

  ...
      ...

      @Override public Call<Object> adapt(Call<Object> call) {
        return call;
      }
  }
}

Flow - Invocation

Response<List<Repo>> response = repos.execute();

這一步開始基于同步的方式執行HTTP請求,并得到返回的HTTP響應數據.

本質上是執行了 OkHttpCall 的 execute方法.

// OkHttpCall.java

@Override public Response<T> execute() throws IOException {

    synchronized (this) {
     
     ...
        ...
          call = rawCall = createRawCall();

    }

    ...

    return parseResponse(call.execute());
  }

如你所見,這里創建了RawCall,即真正的去執行HTTP請求任務的對象。
這里還負責HTTP請求的響應數據解析。
我們看下createRawCall()干了什么。

// OkHttpCall.java

private okhttp3.Call createRawCall() throws IOException {
    Request request = serviceMethod.toRequest(args);
    okhttp3.Call call = serviceMethod.callFactory.newCall(request);
    ...
    return call;
  }

serviceMethod.toRequest()的功能:

// ServiceMethod.java

/** Builds an HTTP request from method arguments. */
  Request toRequest(@Nullable Object... args) throws IOException {
    RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
        contentType, hasBody, isFormEncoded, isMultipart);

    ...

    return requestBuilder.build();
  }

toRequest 方法通過 RequestBuilder 創建了 okhttp3 做 HTTP 請求時需要的 Request 對象。

serviceMethod.callFactory.newCall(request)的功能:
建立一個請求通道,為執行HTTP請求做準備。
這里callFactory可以由使用者指定,默認為 OkHttpClient,見:

// Retrofit.java

okhttp3.Call.Factory callFactory = this.callFactory;
      if (callFactory == null) {
        callFactory = new OkHttpClient();
      }

回頭看下 OkHttpCall 中 execute 方法最后一句: return parseResponse(call.execute());
這里調用真正的HTTP請求客戶端的請求執行方法。也就是來到了接下來的一個流程。

Flow - RawCall

上個 Flow 中最后一步, call.execute(),開啟了真正的HTTP請求,即通過 okhttp3 完成HTTP請求。
這個部分沒什么代碼可講,屬于面向接口開發的典范,要講就該去講 Okhttp 框架的源碼了。

這個部分引出了 Retrofit 的開源擁有者-Square 公司的另一個優秀的開源項目 Okhttp,是不是也很想一探究竟?

End

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容