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);
}
複製程式碼
這個方法挺長的,刪了些無關緊要的程式碼還是很長。首先一開始先獲取幾個重要物件:callAdapter
、responseType
和responseConverter
,這三個物件都跟最後的結果有關,我們先不管。
看到一個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;
}
複製程式碼
看到核心程式碼是converter
的stringConverter(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
裡面!我們一個一個看吧:
Request request()方法:
enqueue(final Callback callback)方法
Response execute()的方法
很明顯上面三個方法都是retrofit的發起網路請求的方式,分別是同步請求和非同步請求。我們的示例中在最後一步就是呼叫了
request
方法和enqueue
方法發起網路請求。至此我們已經疏通了retrofit是如何進行網路請求的了。總結:當我們呼叫Retrofit的網路請求方式的時候,就會呼叫okhttp的網路請求方式,引數使用的是實現介面的方法的時候拿到的資訊構建的
RequestBuilder
物件,然後在build
方法中構建okhttp的Request
,最終發起網路請求
六.總結
至此retrofit的流程講完了,文章很長,程式碼很多,讀者最好下載程式碼匯入IDE,跟著文章一起看程式碼。
Retrofit主要是在create
方法中採用動態代理模式實現介面方法,這個過程構建了一個ServiceMethod物件,根據方法註解獲取請求方式,引數型別和引數註解拼接請求的連結,當一切都準備好之後會把資料新增到Retrofit的RequestBuilder
中。然後當我們主動發起網路請求的時候會呼叫okhttp發起網路請求,okhttp的配置包括請求方式,URL等在Retrofit的RequestBuilder
的build()
方法中實現,併發起真正的網路請求。
Retrofit封裝了okhttp框架,讓我們的網路請求更加簡潔,同時也能有更高的擴充套件性。當然我們只是窺探了Retrofit原始碼的一部分,他還有更復雜更強大的地方等待我們去探索包括返回值轉換工廠,攔截器等,這些都屬於比較難的地方,我們需要循序漸進的去學習,當我們一點一點的看透框架的本質之後,我們使用起來才會熟能生巧。大神的程式碼,對於Android想要進階的同學來說很有好處,不僅教會我們如何設計程式碼更多的是解決思想。
喜歡本篇文章的話點個讚的哦
如果喜歡我的文章,想與一群資深開發者一起交流學習的話,歡迎加入我的合作群Android Senior Engineer技術交流群。有flutter—效能優化—移動架構—資深UI工程師 —NDK相關專業人員和視訊教學資料
後續會整理出有關Retrofit相關資料分享
群號:925019412