拆輪子系列:Retrofit2

拆輪子系列:Retrofit2

[TOC]

Retrofit本質上是對OkHttpClient網絡交互的封裝,它接管的是網絡請求前和網絡請求后,即HttpRequest封裝,HttpResponse處理,網絡請求過程則交給OkHttpClient。Retrofit采用了一套非常好的設計思想,使得其提供的各部分功能擴展性強,耦合度低。

Retrofit設計流程如下:


image

Retrofit詳細過程梳理如下:

定義網絡接口

ResultType 為原始的 retrofit2.Call:

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

ResultType為RxJava Observable:

public interface StudioApiService {
    @GET("/studio/doctors/{docId}/clinics/{clinicId}")
    Observable<UserClinic> getClinicItem(
            @Path("docId") String docId, @Path("clinicId") String clinicId);

    @PUT("/studio/doctors/{docId}/clinics/{clinicId}")
    Observable<UserClinic> putClinic(
            @Path("docId") String docId, @Path("clinicId") String clinicId,
            @Body UserClinic clinic);

    @DELETE("/studio/doctors/{docId}/clinics/{clinicId}")
    Observable<Void> deleteClinic(
            @Path("docId") String docId, @Path("clinicId") String clinicId);

    @POST("/studio/doctors/{docId}/clinics")
    Observable<UserClinic> postClinic(@Path("docId") String docId, @Body UserClinic clinic);
}

創建Retrofit實例

new Retrofit.Builder()
    .baseUrl(url)
    .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().setLenient().create()))
    .addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create(context))
    .client(setupClient())
    .build();

API 接口的調用

API接口的調用過程:

StudioAPiService apiService = retrofit.create(StudioAPiService.class);

Create過程采用的是動態代理方式,Retrofit為接口類生成一個動態代理,通過這種方式調用接口時Retrofit自動接管了調用的過程。代碼如下,其中Proxy.newProxyInstance就是一個標準的動態代理過程

public <T> T create(final Class<T> 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);
          }
        });
  }

調用過程中有關鍵的三個步驟。

ServiceMethod<Object, Object> serviceMethod = (ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

這是Retrofit調用的關鍵三步:

  1. 創建ServiceMethod:根據傳入的method創建,這一步涉及到Annotation的解析等,后面會詳解;
  2. 創建OkHttpCall:根據ServiceMethod創建;
  3. 執行OkHttpCall進行網絡請求,涉及到CallAdapter切換

ServiceMethod

看看 ServiceMethod 的構造函數:

ServiceMethod(Builder<R, T> builder) {
  this.callFactory = builder.retrofit.callFactory();
  this.callAdapter = builder.callAdapter;
  this.baseUrl = builder.retrofit.baseUrl();
  this.responseConverter = builder.responseConverter;
  this.httpMethod = builder.httpMethod;
  this.relativeUrl = builder.relativeUrl;
  this.headers = builder.headers;
  this.contentType = builder.contentType;
  this.hasBody = builder.hasBody;
  this.isFormEncoded = builder.isFormEncoded;
  this.isMultipart = builder.isMultipart;
  this.parameterHandlers = builder.parameterHandlers;
}

成員很多,但這里我們重點關注四個成員:callFactory,callAdapter, responseConverter 和 parameterHandlers。

  1. callFactory 負責創建 HTTP 請求,HTTP 請求被抽象為了 okhttp3.Call 類,它表示一個已經準備好,可以隨時執行的 HTTP 請求;
  2. callAdapter 把 retrofit2.Call<T> 轉為 T(注意和 okhttp3.Call 區分開來,retrofit2.Call<T> 表示的是對一個 Retrofit 方法的調用),這個過程會發送一個 HTTP 請求,拿到服務器返回的數據(通過 okhttp3.Call 實現),并把數據轉換為聲明的 T 類型對象(通過 Converter<F, T> 實現);
  3. responseConverter 是 Converter<ResponseBody, T> 類型,負責把服務器返回的數據(JSON、XML、二進制或者其他格式,由 ResponseBody 封裝)轉化為 T 類型的對象;
  4. parameterHandlers 則負責解析 API 定義時每個方法的參數,并在構造 HTTP 請求時設置參數;

callFactory

this.callFactory = builder.retrofit.callFactory(),所以 callFactory 實際上由 Retrofit 類提供,而我們在構造 Retrofit 對象時,可以指定 callFactory,如果不指定,將默認設置為一個 okhttp3.OkHttpClient。

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

callFactory具體在什么時候使用呢?

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

serviceMethod.callFactory.newCall(request);這就是她的作用,用來創建一個 okhttp3.Call對象,然后通過這個call對象發起網絡請求。

callAdapter

關于Retrofit中的call,涉及到以下三個:

retrofit2.Call接口如下:

public interface Call<T> extends Cloneable {
  
  Response<T> execute() throws IOException;

  void enqueue(Callback<T> callback);

  boolean isExecuted();

  void cancel();

  boolean isCanceled();

  Call<T> clone();

  Request request();
}

retrofit2.OkHttpCall類,這是對retrofit2.Call的實現。Retrofit下的網絡請求會被轉換成retrofit2.OkHttpCall,其背后則是轉換成okhttp3.Call,執行,這里就把具體的網絡請求委托給了OkHttpClient執行了。

final class OkHttpCall<T> implements Call<T> {

    @Override public void enqueue(final Callback<T> callback) {
    checkNotNull(callback, "callback == null");

    okhttp3.Call call;
    Throwable failure;

    ...
    call.enqueue(new okhttp3.Callback() {
      @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
          throws IOException {
        Response<T> response;
        try {
          response = parseResponse(rawResponse);
        } catch (Throwable e) {
          callFailure(e);
          return;
        }
        callSuccess(response);
      }

      @Override public void onFailure(okhttp3.Call call, IOException e) {
        try {
          callback.onFailure(OkHttpCall.this, e);
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }

      private void callFailure(Throwable e) {
        try {
          callback.onFailure(OkHttpCall.this, e);
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }

      private void callSuccess(Response<T> response) {
        try {
          callback.onResponse(OkHttpCall.this, response);
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }
    });
  }
}

下面再來講講serviceMethod.callAdapter.adapt(okHttpCall),以RxJavaCallAdapterFactory中的RxJavaCallAdapter,前者是后者的工廠類。

下來看一下RxJavaCallAdapterFactory的構造方法,注意到isAsync,默認是false,即網絡請求默認是同步的。

private RxJavaCallAdapterFactory(@Nullable Scheduler scheduler, boolean isAsync) {
  this.scheduler = scheduler;
  this.isAsync = isAsync;
}

再來看一下RxJavaCallAdapter究竟如何實現Call對象的adapter(), 很簡單,為call對象創建一個RxJava的Observable。其中CallEnqueueOnSubscribe、CallExecuteOnSubscribe最終調用的又是OkHttpCall中的相應的enqueue()、execute()方法

@Override public Object adapt(Call<R> call) {
    OnSubscribe<Response<R>> callFunc = isAsync
        ? new CallEnqueueOnSubscribe<>(call)
        : new CallExecuteOnSubscribe<>(call);

    OnSubscribe<?> func;
    if (isResult) {
      func = new ResultOnSubscribe<>(callFunc);
    } else if (isBody) {
      func = new BodyOnSubscribe<>(callFunc);
    } else {
      func = callFunc;
    }
    Observable<?> observable = Observable.create(func);

    if (scheduler != null) {
      observable = observable.subscribeOn(scheduler);
    }

    if (isSingle) {
      return observable.toSingle();
    }
    if (isCompletable) {
      return observable.toCompletable();
    }
    return observable;
  }

responseConverter

這個很好理解,對Response的解析,將Response轉換成最終對象,跟蹤代碼可以看到。在OkHttpCall中拿到最終的response之后,會調用

T body = serviceMethod.toResponse(catchingBody);

R toResponse(ResponseBody body) throws IOException {
  return responseConverter.convert(body);
}

以GsonConverterFactory提供的GsonResponseBodyConverter為例:

@Override public T convert(ResponseBody value) throws IOException {
  JsonReader jsonReader = gson.newJsonReader(value.charStream());
  try {
    return adapter.read(jsonReader);
  } finally {
    value.close();
  }
}

parameterHandlers

每個參數都會有一個 ParameterHandler,由 ServiceMethod#parseParameter 方法負責創建,其主要內容就是解析每個參數使用的注解類型(諸如 Path,Query,Field 等),對每種類型進行單獨的處理。構造 HTTP 請求時,我們傳遞的參數都是字符串,那 Retrofit 是如何把我們傳遞的各種參數都轉化為 String 的呢?還是由 Retrofit 類提供 converter!

其關系流程為:

API Method--(ParameterHandler)--> retrofit2.RequestBuilder --> okhttp3.Request

先來看看ServiceMethod中的toRequest方法:

Request toRequest(@Nullable Object... args) throws IOException {
    RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
    contentType, hasBody, isFormEncoded, isMultipart);

    @SuppressWarnings("unchecked") // It is an error to invoke a method with the wrong arg types.
    ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;

    int argumentCount = args != null ? args.length : 0;
    if (argumentCount != handlers.length) {
      throw new IllegalArgumentException("Argument count (" + argumentCount
          + ") doesn't match expected count (" + handlers.length + ")");
    }

    for (int p = 0; p < argumentCount; p++) {
      handlers[p].apply(requestBuilder, args[p]);
    }

    return requestBuilder.build();
  }

再來看看RequestBuilder.build() 方法:

Request build() {
    HttpUrl url;
    HttpUrl.Builder urlBuilder = this.urlBuilder;
    if (urlBuilder != null) {
      url = urlBuilder.build();
    } else {
      // No query parameters triggered builder creation, just combine the relative URL and base URL.
      //noinspection ConstantConditions Non-null if urlBuilder is null.
      url = baseUrl.resolve(relativeUrl);
      if (url == null) {
        throw new IllegalArgumentException(
            "Malformed URL. Base: " + baseUrl + ", Relative: " + relativeUrl);
      }
    }

    RequestBody body = this.body;
    if (body == null) {
      // Try to pull from one of the builders.
      if (formBuilder != null) {
        body = formBuilder.build();
      } else if (multipartBuilder != null) {
        body = multipartBuilder.build();
      } else if (hasBody) {
        // Body is absent, make an empty body.
        body = RequestBody.create(null, new byte[0]);
      }
    }

    MediaType contentType = this.contentType;
    if (contentType != null) {
      if (body != null) {
        body = new ContentTypeOverridingRequestBody(body, contentType);
      } else {
        requestBuilder.addHeader("Content-Type", contentType.toString());
      }
    }

    return requestBuilder
        .url(url)
        .method(method, body)
        .build();
  }

Multipart 處理

@Multipart 是Retrofit2對多文件上傳處理的封裝,本質上是對multipart/form-data的封裝,這個封裝依賴于OkHttp中提供的MultipartBody。先來看一下@Multipart的使用方式:

public interface UploadService {

    /**
     * @Multipart 這個標記很重要,Retrofit會判斷是否有這個標記來對參數重新封裝
     *
     * 針對接口2,參數為MultipartBody, 它本身就是一個RequestBody,Retrofit據此判斷不進行再次封裝
     */
    @Multipart
    @POST("/upload")
    Call<Void> uploadImages(@Part() List<MultipartBody.Part> parts);

    @Multipart
    @POST("/upload")
    Call<Void> uploadImages(@Part() MultipartBody.Part part, @Part("description") RequestBody description);

    @POST("/upload")
    Call<Void> uploadImages(@Body MultipartBody body);
}

其中,可以將MultipartBody.Part、MultipartBody的創建封裝成通用的兩個方法,它們的創建方式都是由OKHttp提供的,目的都是將之轉換成OKHttp中的MultipartBody。如下:

public class FileToHttpBody {

    public static MultipartBody filesToMultiBody(List<File> files) {
        MultipartBody.Builder builder = new MultipartBody.Builder();
        for (File file: files) {
            RequestBody requestBody = RequestBody.create(MediaType.parse("image/png"), file);
            builder.addFormDataPart("file", file.getName(), requestBody);
        }
        builder.setType(MultipartBody.FORM);
        MultipartBody multipartBody = builder.build();
        return multipartBody;
    }


    public static List<MultipartBody.Part> filesToMultiParts(List<File> files) {
        List<MultipartBody.Part> parts = new ArrayList<>();
        for (File file: files) {
            RequestBody requestBody = RequestBody.create(MediaType.parse("image/png"), file);
            parts.add(MultipartBody.Part.createFormData("file", file.getName(), requestBody));
        }
        return parts;
    }
}

下面我們再來看一下Retrofit對@Multipart、@Part這兩個標簽分別干了什么。

@Multipart

private void parseMethodAnnotation(Annotation annotation) {
     ...
      
     } else if (annotation instanceof Multipart) {
        if (isFormEncoded) {
          throw methodError("Only one encoding annotation is allowed.");
        }
        isMultipart = true;
     } 

    ...
}

Retrofit根據@Multipart打上標記isMultipart,后續依此封裝成MultipartBody。

@Part

private ParameterHandler<?> parseParameterAnnotation(
        int p, Type type, Annotation[] annotations, Annotation annotation) {
     if (annotation instanceof Part) {
        if (!isMultipart) {
          throw parameterError(p, "@Part parameters can only be used with multipart encoding.");
        }
        Part part = (Part) annotation;
        gotPart = true;

        String partName = part.value();
        Class<?> rawParameterType = Utils.getRawType(type);
        if (partName.isEmpty()) {
          if (Iterable.class.isAssignableFrom(rawParameterType)) {

            ...
            else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) {
            return ParameterHandler.RawPart.INSTANCE;
          } else {
            throw parameterError(p,
                "@Part annotation must supply a name or use MultipartBody.Part parameter type.");
          }
        } else {
          Headers headers =
              Headers.of("Content-Disposition", "form-data; name=\"" + partName + "\"",
                  "Content-Transfer-Encoding", part.encoding());

            ...
            Converter<?, RequestBody> converter =
                retrofit.requestBodyConverter(type, annotations, methodAnnotations);
            return new ParameterHandler.Part<>(headers, converter);
          }
        }
    }
}

這里,提到了兩個ParameterHandler:ParameterHandler.RawPart, ParameterHandler.Part。這兩個ParameterHandler直接調用了MultipartBody
中的addPart()方法,其源碼如下:

static final class RawPart extends ParameterHandler<MultipartBody.Part> {
  static final RawPart INSTANCE = new RawPart();

  private RawPart() {
  }

  @Override void apply(RequestBuilder builder, @Nullable MultipartBody.Part value)
      throws IOException {
    if (value != null) { // Skip null values.
      builder.addPart(value);
    }
  }
}
static final class Part<T> extends ParameterHandler<T> {
  private final Headers headers;
  private final Converter<T, RequestBody> converter;

  Part(Headers headers, Converter<T, RequestBody> converter) {
    this.headers = headers;
    this.converter = converter;
  }

  @Override void apply(RequestBuilder builder, @Nullable T value) {
    if (value == null) return; // Skip null values.

    RequestBody body;
    try {
      body = converter.convert(value);
    } catch (IOException e) {
      throw new RuntimeException("Unable to convert " + value + " to RequestBody", e);
    }
    builder.addPart(headers, body);
  }
}

總結

retrofit2一個將工廠模式發揮到極致的優秀庫,值得細細體味。它的整個過程可以總結為以下幾個步驟:

  1. 對網絡請求方式的封裝,直接以接口呈現;
  2. 對接口的解析,轉換成RequestBuilder,最終轉成為okhttp3.Request;
  3. 將Request構造成retrofit2.OkHttpCall,轉換成okhttp3.Call執行;
  4. 通過CallAdapter接管Call執行回調,回調中涉及到Response的解析轉換等
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容

  • 目錄介紹 1.首先回顧Retrofit簡單使用方法 2.Retrofit的創建流程源碼分析2.1 Retrofit...
    楊充211閱讀 1,078評論 0 16
  • 整體Retrofit內容如下: 1、Retrofit解析1之前哨站——理解RESTful2、Retrofit解析2...
    隔壁老李頭閱讀 4,005評論 8 19
  • 簡介 剛接觸Retrofit的時候,就寫了一篇簡單的使用介紹:Retrofit 2.0基本使用方法,算是對Retr...
    Whyn閱讀 2,872評論 4 24
  • 多想忘記你 卻發現回憶總在折磨自己 明明討厭烏云 偏偏都是陰雨天氣 有時不得已 強迫自己逃離 我的影子卻在你的身后...
    三葉江風閱讀 317評論 3 2
  • 我是一個特別喜歡感受生活的人,總想在生活點滴當中收獲一些東西,也想通過紙筆記錄下來。如今每一天都是忙碌的,作為一名...
    16839da0bb42閱讀 175評論 0 0