Android進階:七、Retrofit2.0原理解析之最簡流程

Android丶SE開發發表於2019-04-27

retrofit 已經流行很久了,它是Square開源的一款優秀的網路框架,這個框架對okhttp進行了封裝,讓我們使用okhttp做網路請求更加簡單。但是光學會使用只是讓我們多了一個技能,學習其原始碼才能讓我們更好的成長。

本篇文章是在分析retrofit的原始碼流程,有大量的程式碼,讀者最好把原始碼下載下來匯入IDE,然後跟著一起看,效果會更好

一.retrofit入門

  • 定義網路請求的API介面:
interface GithubApiService {
        @GET("users/{name}/repos")
        Call<ResponseBody> searchRepoInfo(@Path("name") String name);
    }
複製程式碼

使用了註解表明請求方式,和引數型別,這是retrofit的特性,也正是簡化了我們的網路請求過程的地方!

  • 初始化一個retrofit的例項:
Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .build();
複製程式碼

retrofit的例項化很簡單,採用鏈式呼叫的設計,把需要的引數傳進去即可,複雜的引數我們這裡就不舉例了。

  • 生成介面實現類:
GithubApiService githubService = retrofit.create(service)
Call<ResponseBody> call = githubService.searchRepoInfo("changmu175");
複製程式碼

我們呼叫retrofit的create方法就可以把我們定義的介面轉化成實現類,我們可以直接呼叫我們定義的方法進行網路請求,但是我們只定義了一個介面方法,也沒有方法體,請求方式和引數型別都是註解,create是如何幫我們整理引數,實現方法體的呢?一會我們通過原始碼解析再去了解。

  • 發起網路請求
//同步請求方式
 call.request();
 //非同步請求方式
 call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                //請求成功回撥
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                //請求與失敗回撥
            }
        });
複製程式碼

至此,retrofit的一次網路請求示例已經結束,基於對okhttp的封裝,讓網路請求已經簡化了很多。當然retrofit最適合的還是REST API型別的介面,方便簡潔。

下面我們就看看retrofit的核心工作是如何完成的!

二.retrofit初始化

retrofit的初始化採用了鏈式呼叫的設計

Retrofit retrofit = new Retrofit.Builder()
                       .baseUrl("https://api.github.com/")
                       .build();
複製程式碼

很明顯這個方法是在傳一些需要的引數,我們簡單的跟蹤一下:

首先看看Builder()的原始碼:

public Builder() {
      this(Platform.get());
    }
複製程式碼

這句程式碼很簡單就是呼叫了自己的另一個建構函式:

Builder(Platform platform) {
      this.platform = platform;
    }
複製程式碼

這個建構函式也很簡單,就是一個賦值,我們把之前的Platform.get()點開,看看裡面做在什麼:

private static final Platform PLATFORM = findPlatform();

static Platform get() {
    return PLATFORM;
  }
複製程式碼

我們發現這裡使用使用了一個餓漢式單例,使用Platform.get()返回一個例項,這樣寫的好處是簡單,執行緒安全,效率高,不會生成多個例項!

我們再看看findPlatform() 裡做了什麼:

 private static Platform findPlatform() {
    try {
      Class.forName("android.os.Build");
      if (Build.VERSION.SDK_INT != 0) {
        return new Android();
      }
    } catch (ClassNotFoundException ignored) {
    }

    ....省略部分程式碼...
 }
複製程式碼

所以是判斷了一下系統,然後根據系統例項化一個物件。這裡面應該做了一些和Android平臺相關的事情,屬於細節,我們追究,感興趣的可以只看看。
再看看baseUrl(url)的原始碼

public Builder baseUrl(String baseUrl) {
      checkNotNull(baseUrl, "baseUrl == null");
      HttpUrl httpUrl = HttpUrl.parse(baseUrl);
      ....
      return baseUrl(httpUrl);
    }

public Builder baseUrl(HttpUrl baseUrl) {
      checkNotNull(baseUrl, "baseUrl == null");
      ....
      this.baseUrl = baseUrl;
      return this;
    }
複製程式碼

這兩段程式碼也很簡單,校驗URL,生成httpUrl物件,然後賦值給baseUrl

看看build() 方法在做什麼
引數基本設定完了,最後就要看看build() 這個方法在做什麼:

 public Retrofit build() {
      if (baseUrl == null) {
        throw new IllegalStateException("Base URL required.");
      }

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

      return new Retrofit(callFactory, baseUrl, unmodifiableList(converterFactories),
          unmodifiableList(callAdapterFactories), callbackExecutor, validateEagerly);
    }
  }
}
複製程式碼

程式碼中有大量的引數校驗,有些複雜的引數我們沒有傳,所以我就把那些程式碼刪除了。簡單看一下也能知道,這段程式碼就是做一些引數校驗,baseUrl不能為空否則會拋異常,至於其他的引數如果為null則會建立預設的物件。其中callFactory就是okhttp的工廠例項,用於網路請求的。
最後我們看到,這個方法最終返回的是一個Retrofit的物件,初始化完成。

三.生成介面實現類

剛才我們就講過retrofit.create這個方法很重要,它幫我們生成了介面實現類,並完成了方法體的建立,省去了我們很多工作量。那我們來看看它是如何幫我們實現介面的。

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.adapt(okHttpCall);
          }
        });
  }
複製程式碼

這段程式碼實際上是使用了動態代理的設計模式,而且這個方法封裝的非常好,我們只需要呼叫 方法就可以獲得我們需要的實現類,遵循了迪米特法則(最少知道原則)。

瞭解動態代理的人都知道我們要重寫Object invoke(Object proxy, Method method,[@Nullable](https://xiaozhuanlan.com/u/undefined) Object[] args) 方法,這個方法會傳入我們需要的實現的方法,和引數,並返回我們需要的返回值。

retrofit在重寫這個方法的時候做了三件事:

  • 先判斷了這個方法的類是不是一個Object.class),就直接返回方法原有的返回值。
  • 判斷這個方法是不是DefaultMethod,大家都知道這個方法是Java 8出來的新屬性,表示介面的方法體。
  • 構建一個ServiceMethod<Object, Object>物件和OkHttpCall<Object>物件,並呼叫
    serviceMethod.adapt(okHttpCall)方法將二者繫結。

我們看看這個方法的原始碼:

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

這個callAdapter我們在初始化retrofit的時候沒有使用:
addCallAdapterFactory(CallAdapterFactory)傳值,所以這裡是預設的DefaultCallAdapterFactory

那我們再看看DefaultCallAdapterFactory裡的adapt(call)方法:

@Override public Call<Object> adapt(Call<Object> call) {
        return call;
      }
複製程式碼

直接返回引數,也就是OkHttpCall<Object>的物件。所以如果沒有自定義callAdapter的時候,我們定義介面的時候返回值型別應該是個Call型別的。
那麼,至此這個create方法已經幫我們實現了我們定義的介面,並返回我們需要的值。

四.請求引數整理

我們定義的介面已經被實現,但是我們還是不知道我們註解的請求方式,引數型別等是如何發起網路請求的呢?
這時我們可能應該關注一下ServiceMethod<Object, Object>物件的構建了

  ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
複製程式碼

主要的邏輯都在這個loadServiceMethod(method)裡面,我們看看方法體:

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

    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = new ServiceMethod.Builder<>(this, method).build();
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }
複製程式碼

邏輯很簡單,就是先從一個 serviceMethodCache中取ServiceMethod<?, ?>物件,如果沒有,則構建ServiceMethod<?, ?>物件,然後放進去serviceMethodCache中,這個serviceMethodCache是一個HashMap:

private final Map<Method, ServiceMethod<?, ?>> serviceMethodCache = new ConcurrentHashMap<
>();
複製程式碼

所以構建ServiceMethod<?, ?>物件的主要邏輯還不在這個方法裡,應該在new ServiceMethod.Builder<>(this, method).build();裡面。這也是個鏈式呼叫,一般都是引數賦值,我們先看看Builder<>(this, method)方法:

Builder(Retrofit retrofit, Method method) {
      this.retrofit = retrofit;
      this.method = method;
      this.methodAnnotations = method.getAnnotations();
      this.parameterTypes = method.getGenericParameterTypes();
      this.parameterAnnotationsArray = method.getParameterAnnotations();
    }
複製程式碼

果然,這裡獲取了幾個重要的引數:

  • retrofit例項
  • method,介面方法
  • 介面方法的註解methodAnnotations,在retrofit裡一般為請求方式
  • 引數型別parameterTypes
  • 引數註解陣列parameterAnnotationsArray,一個引數可能有多個註解

我們再看看build()的方法:

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

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

      if (httpMethod == null) {
        throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).");
      }

      int parameterCount = parameterAnnotationsArray.length;
      parameterHandlers = new ParameterHandler<?>[parameterCount];
      for (int p = 0; p < parameterCount; p++) {
        Type parameterType = parameterTypes[p];
        if (Utils.hasUnresolvableType(parameterType)) {
          throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",
              parameterType);
        }

        Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
        if (parameterAnnotations == null) {
          throw parameterError(p, "No Retrofit annotation found.");
        }

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

      return new ServiceMethod<>(this);
    }
複製程式碼

這個方法挺長的,刪了些無關緊要的程式碼還是很長。首先一開始先獲取幾個重要物件:callAdapterresponseTyperesponseConverter,這三個物件都跟最後的結果有關,我們先不管。

看到一個for迴圈,遍歷方法的註解,然後解析:

for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
      }
複製程式碼

private void parseMethodAnnotation(Annotation annotation) {
      if (annotation instanceof DELETE) {
        parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
      } else if (annotation instanceof GET) {
        parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
      } 

        ....
複製程式碼

這個方法的方法體我刪掉了後面的一部分,因為邏輯都是一樣,根據不同的方法註解作不同的解析,得到網路請求的方式httpMethod。但是主要的方法體還是if裡面的方法:

private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) {

      ....

      // Get the relative URL path and existing query string, if present.
      int question = value.indexOf('?');
      if (question != -1 && question < value.length() - 1) {
        // Ensure the query string does not have any named parameters.
        String queryParams = value.substring(question + 1);
        Matcher queryParamMatcher = PARAM_URL_REGEX.matcher(queryParams);
        if (queryParamMatcher.find()) {
          throw methodError("URL query string \"%s\" must not have replace block. "
              + "For dynamic query parameters use @Query.", queryParams);
        }
      }

      this.relativeUrl = value;
      this.relativeUrlParamNames = parsePathParameters(value);
    }
複製程式碼

邏輯不復雜,就是校驗這個value的值 是否合法,規則就是不能有“?”如果有則需要使用@Query註解。最後this.relativeUrl = value;。這個relativeUrl就相當於省略域名的URL,一般走到這裡我們能得到的是:users/{name}/repos這樣的。裡面的“{name}”是一會我們需要賦值的變數。

我們繼續看剛才的build()方法:
解析完方法的註解之後,需要解析引數的註解陣列,這裡例項化了一個一維陣列:

parameterHandlers = new ParameterHandler<?>[parameterCount];
複製程式碼

然後遍歷取出引數的型別:

Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
複製程式碼

然後把引數型別、引數註解都放在一起進行解析,解析的結果放到剛才例項化的陣列parameterHandlers裡面:

parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
複製程式碼

那我們再看看這個方法裡做了什麼:

private ParameterHandler<?> parseParameter(int p, Type parameterType, Annotation[] annotations) {
      ParameterHandler<?> result = null;
      for (Annotation annotation : annotations) {
        ParameterHandler<?> annotationAction = parseParameterAnnotation(
            p, parameterType, annotations, annotation);
      }

    }
複製程式碼

這個方法的主要程式碼也很簡單,解析引數註解,得到一個ParameterHandler<?> annotationAction物件。
那我繼續看方法裡面的程式碼。當我們點進parseParameterAnnotation( p, parameterType, annotations, annotation);的原始碼裡面去之後發現這個方法的程式碼接近500行!但是大部分邏輯類似,都是通過if else判斷引數的註解,我們取一段我們剛才的例子相關的程式碼出來:

 if (annotation instanceof Path) {
        if (gotQuery) {
          throw parameterError(p, "A @Path parameter must not come after a @Query.");
        }
        if (gotUrl) {
          throw parameterError(p, "@Path parameters may not be used with @Url.");
        }
        if (relativeUrl == null) {
          throw parameterError(p, "@Path can only be used with relative url on @%s", httpMethod);
        }
        gotPath = true;

        Path path = (Path) annotation;
        String name = path.value();
        validatePathName(p, name);

        Converter<?, String> converter = retrofit.stringConverter(type, annotations);
        return new ParameterHandler.Path<>(name, converter, path.encoded());

      }
複製程式碼

前面做了一些校驗,後面取出註解的名字:name,然後用正則表達校驗這個name是否合法。然後構建一個Converter<?, String>物件

 Converter<?, String> converter = retrofit.stringConverter(type, annotations);
複製程式碼

點選去看看:

public <T> Converter<T, String> stringConverter(Type type, Annotation[] annotations) {
      ....
    for (int i = 0, count = converterFactories.size(); i < count; i++) {
      Converter<?, String> converter =
          converterFactories.get(i).stringConverter(type, annotations, this);
      if (converter != null) {
        //noinspection unchecked
        return (Converter<T, String>) converter;
      }
    }
    return (Converter<T, String>) BuiltInConverters.ToStringConverter.INSTANCE;
  }
複製程式碼

看到核心程式碼是converterstringConverter(type, annotations, this)方法:
因為我們剛才的示例中被沒有通過:addConverterFactory(ConverterFactory)新增一個ConverterFactory,所以這裡會返回一個空:

 public @Nullable Converter<?, String> stringConverter(Type type, Annotation[] annotations,
        Retrofit retrofit) {
      return null;
    }
複製程式碼

所以最後會執行最後一句程式碼:
return (Converter<T, String>) BuiltInConverters.ToStringConverter.INSTANCE;
我們點進去看看這個INSTANCE

static final ToStringConverter INSTANCE = new ToStringConverter();
複製程式碼

BuiltInConverters內的內部類ToStringConverter的單例。所以這裡我們得到的就
BuiltInConverters.ToStringConverter的例項。

最後用這個物件構建一個Path(因為示例中的引數型別是path,所以我們看這個程式碼):

new ParameterHandler.Path<>(name, converter, path.encoded());
複製程式碼

我們看看這個Path類的建構函式:

Path(String name, Converter<T, String> valueConverter, boolean encoded) {
      this.name = checkNotNull(name, "name == null");
      this.valueConverter = valueConverter;
      this.encoded = encoded;
    }
複製程式碼

只是賦值,並且我們看到這個類繼承自:ParameterHandler<T>,所以我們回到剛才的build()方法,發現把引數型別,引數註解放在一起解析之後儲存到了這個ParameterHandler<T>陣列中,中間主要做了多種合法性校驗,並根據註解的型別,生成不同的
ParameterHandler<T>子類,如註解是Url則生成ParameterHandler.RelativeUrl()物件,如果註解是Path,則生成:
ParameterHandler.Path<>(name, converter, path.encoded())物件等等。
我們檢視了ParameterHandler<T>類,發現它有一個抽象方法:

abstract void apply(RequestBuilder builder, @Nullable T value) throws IOException;
複製程式碼

這個方法每個子類都必須複寫,那我們看看Path裡面怎麼複寫的:

  @Override 
    void apply(RequestBuilder builder, @Nullable T value) throws IOException {
         builder.addPathParam(name, valueConverter.convert(value), encoded);
    }
複製程式碼

就是把value被新增到RequestBuilder中,我們看一下這個addPathParam方法:

void addPathParam(String name, String value, boolean encoded) {

    relativeUrl = relativeUrl.replace("{" + name + "}", canonicalizeForPath(value, encoded));
  }
複製程式碼

這個方法把我們傳進來的值value按照編碼格式轉換,然後替換relativeUrl中的{name},構成一個有效的省略域名的URL。至此,URL的拼接已經完成!

總結:Retrofit使用動態代理模式實現我們定義的網路請求介面,在重寫invoke方法的時候構建了一個ServiceMethod物件,在構建這個物件的過程中進行了方法的註解解析得到網路請求方式httpMethod,以及引數的註解分析,拼接成一個省略域名的URL

五.Retrofit網路請求

我們剛才解析了apply方法,我們看看apply方法是誰呼叫的呢?跟蹤一下就發先只有toCall(args);方法:

 okhttp3.Call toCall(@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 callFactory.newCall(requestBuilder.build());
  }
複製程式碼

這個方法一開始就構建了RequestBuilder,傳進去的引數包含:
httpMethod,baseUrl,relativeUrl,headers,contentType,hasBody,isFormEncoded,isMultipart

然後獲取了parameterHandlers,我們上邊分析的時候,知道這個陣列是存引數註解的解析結果的,並對其進行遍歷呼叫瞭如下方法:

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

把引數值傳進RequestBuilder中。
最後呼叫callFactory.newCall(requestBuilder.build())生成一個okhttp3.Call
我們看一下這個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();
  }
複製程式碼

可以看到okhttp的請求體在這裡構建,當所有的引數滿足的時候,則呼叫了

Request.Builder requestBuilder
        .url(url)
        .method(method, body)
        .build();
複製程式碼

這是發起okhttp的網路請求 。
那這個toCall(args);誰呼叫的呢?繼續往回跟!

 private okhttp3.Call createRawCall() throws IOException {
    okhttp3.Call call = serviceMethod.toCall(args);
    return call;
  }
複製程式碼

那誰呼叫了createRawCall()呢?繼續看誰呼叫了!於是發現呼叫方有三個地方,並且都是OkHttpCall裡面!我們一個一個看吧:

  1. Request request()方法:

  2. enqueue(final Callback callback)方法

  3. Response execute()的方法

    很明顯上面三個方法都是retrofit的發起網路請求的方式,分別是同步請求和非同步請求。我們的示例中在最後一步就是呼叫了request方法和enqueue方法發起網路請求。至此我們已經疏通了retrofit是如何進行網路請求的了。

    總結:當我們呼叫Retrofit的網路請求方式的時候,就會呼叫okhttp的網路請求方式,引數使用的是實現介面的方法的時候拿到的資訊構建的RequestBuilder物件,然後在build方法中構建okhttp的Request,最終發起網路請求

六.總結

至此retrofit的流程講完了,文章很長,程式碼很多,讀者最好下載程式碼匯入IDE,跟著文章一起看程式碼。

Retrofit主要是在create方法中採用動態代理模式實現介面方法,這個過程構建了一個ServiceMethod物件,根據方法註解獲取請求方式,引數型別和引數註解拼接請求的連結,當一切都準備好之後會把資料新增到Retrofit的RequestBuilder中。然後當我們主動發起網路請求的時候會呼叫okhttp發起網路請求,okhttp的配置包括請求方式,URL等在Retrofit的RequestBuilderbuild()方法中實現,併發起真正的網路請求。

Retrofit封裝了okhttp框架,讓我們的網路請求更加簡潔,同時也能有更高的擴充套件性。當然我們只是窺探了Retrofit原始碼的一部分,他還有更復雜更強大的地方等待我們去探索包括返回值轉換工廠,攔截器等,這些都屬於比較難的地方,我們需要循序漸進的去學習,當我們一點一點的看透框架的本質之後,我們使用起來才會熟能生巧。大神的程式碼,對於Android想要進階的同學來說很有好處,不僅教會我們如何設計程式碼更多的是解決思想。

喜歡本篇文章的話點個讚的哦

如果喜歡我的文章,想與一群資深開發者一起交流學習的話,歡迎加入我的合作群Android Senior Engineer技術交流群。有flutter—效能優化—移動架構—資深UI工程師 —NDK相關專業人員和視訊教學資料

後續會整理出有關Retrofit相關資料分享

群號:925019412


相關文章