Retrofit2 完全解析 探索與okhttp之間的關係
一、概述
之前寫了個okhttputils的工具類,然後有很多同學詢問這個工具類和retrofit
什麼區別,於是上了下官網,發現其底層對網路的訪問預設也是基於okhttp
,不過retrofit
非常適合於restful
url
格式的請求,更多使用註解的方式提供功能。
既然這樣,我們本篇博文首先研究其所提供的常用的用法:
- 一般的get、post請求
- 動態url,動態引數設定,各種註解的使用
- 上傳檔案(單檔案,多檔案上傳等)
- 下載檔案等(這個不推薦retrofit去做,具體看下文)
此外,由於其內部提供了ConverterFactory
用於對返回的requestBody
進行轉化和特殊的requestBody
的構造,所以本文也包含:
- 如何自定義
ConverterFactory
最後呢,因為其原始碼並不複雜,本文將對原始碼進行整體的介紹,即
- retrofit 原始碼分析
ok,說這麼多,既然需要restful url
,我只能撿起我那個半桶水的spring mvc
搭建一個服務端的小例子~~
最後本文使用版本:
- 1
- 1
主要是原始碼解析,自定義Converter.Factory
等一些細節的探索。
恩,寫完後,發現本文很長,中途請沒事站起來走兩步。
retrofit2官網地址:https://github.com/square/retrofit/
二、retrofit 用法示例
(1)一般的get請求
retrofit
在使用的過程中,需要定義一個介面物件,我們首先演示一個最簡單的get請求,介面如下所示:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
可以看到有一個getUsers()方法,通過@GET
註解標識為get請求,@GET
中所填寫的value和baseUrl
組成完整的路徑,baseUrl
在構造retrofit物件時給出。
下面看如何通過retrofit
完成上述的請求:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
依然是構造者模式,指定了baseUrl
和Converter.Factory
,該物件通過名稱可以看出是用於物件轉化的,本例因為伺服器返回的是json格式的陣列,所以這裡設定了GsonConverterFactory
完成物件的轉化。
ok,這裡可以看到很神奇,我們通過Retrofit.create
就可以拿到我們定義的IUserBiz
的例項,呼叫其方法即可拿到一個Call
物件,通過call.enqueue
即可完成非同步的請求。
具體retrofit怎麼得到我們介面的例項的,以及物件的返回結果是如何轉化的,我們後面具體分析。
這裡需要指出的是:
- 介面中的方法必須有返回值,且比如是
Call<T>
型別 -
.addConverterFactory(GsonConverterFactory.create())
這裡如果使用gson,需要額外匯入:compile 'com.squareup.retrofit2:converter-gson:2.0.2'
當然除了gson以外,還提供了以下的選擇:
Gson: com.squareup.retrofit2:converter-gson Jackson: com.squareup.retrofit2:converter-jackson Moshi: com.squareup.retrofit2:converter-moshi Protobuf: com.squareup.retrofit2:converter-protobuf Wire: com.squareup.retrofit2:converter-wire Simple XML: com.squareup.retrofit2:converter-simplexml Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars
當然也支援自定義,你可以選擇自己寫轉化器完成資料的轉化,這個後面將具體介紹。
-
既然
call.enqueue
是非同步的訪問資料,那麼同步的訪問方式為call.execute
,這一點非常類似okhttp的API,實際上預設情況下內部也是通過okhttp3.Call
實現。
那麼,通過這麼一個簡單的例子,應該對retrofit
已經有了一個直觀的認識,下面看更多其支援的特性。
(2)動態的url訪問@PATH
文章開頭提過,retrofit
非常適用於restful
url
的格式,那麼例如下面這樣的url:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
即通過不同的username訪問不同使用者的資訊,返回資料為json字串。
那麼可以通過retrofit提供的@PATH
註解非常方便的完成上述需求。
我們再定義一個方法:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
可以看到我們定義了一個getUser方法,方法接收一個username引數,並且我們的@GET
註解中使用{username}
宣告瞭訪問路徑,這裡你可以把{username}
當做佔位符,而實際執行中會通過@PATH("username")
所標註的引數進行替換。
那麼訪問的程式碼很類似:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
(3)查詢引數的設定@Query
看下面的url
- 1
- 2
- 1
- 2
即一般的傳參,我們可以通過@Query
註解方便的完成,我們再次在介面中新增一個方法:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
訪問的程式碼,其實沒什麼寫的:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
ok,這樣我們就完成了引數的指定,當然相同的方式也適用於POST,只需要把註解修改為@POST
即可。
對了,我能剛才學了@PATH
,那麼會不會有這樣嘗試的衝動,對於剛才的需求,我們這麼寫:
- 1
- 2
- 1
- 2
乍一看別說好像有點感覺,哈,實際上執行是不支援的~估計是@ Path
的定位就是用於url的路徑而不是引數,對於引數還是選擇通過@Query
來設定。
(4)POST請求體的方式向伺服器傳入json字串@Body
大家都清楚,我們app很多時候跟伺服器通訊,會選擇直接使用POST方式將json字串作為請求體傳送到伺服器,那麼我們看看這個需求使用retrofit
該如何實現。
再次新增一個方法:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
提交的程式碼其實基本都是一致的:
- 1
- 2
- 3
- 1
- 2
- 3
ok,可以看到其實就是使用@Body
這個註解標識我們的引數物件即可,那麼這裡需要考慮一個問題,retrofit是如何將user物件轉化為字串呢?下文將詳細解釋~
下面對應okhttp,還有兩種requestBody,一個是FormBody
,一個是MultipartBody
,前者以表單的方式傳遞簡單的鍵值對,後者以POST表單的方式上傳檔案可以攜帶引數,retrofit
也二者也有對應的註解,下面繼續~
(5)表單的方式傳遞鍵值對@FormUrlEncoded
這裡我們模擬一個登入的方法,新增一個方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
訪問的程式碼:
- 1
- 2
- 3
- 1
- 2
- 3
ok,看起來也很簡單,通過@POST
指明url,新增FormUrlEncoded
,然後通過@Field
新增引數即可。
(6)單檔案上傳@Multipart
下面看一下單檔案上傳,依然是再次新增個方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
這裡@MultiPart
的意思就是允許多個@Part
了,我們這裡使用了3個@Part
,第一個我們準備上傳個檔案,使用了MultipartBody.Part
型別,其餘兩個均為簡單的鍵值對。
使用:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
ok,這裡感覺略為麻煩。不過還是蠻好理解~~多個@Part
,每個Part對應一個RequestBody。
這裡插個實驗過程,其實我最初對於檔案,也是嘗試的@Part RequestBody
,因為@Part("key")
,然後傳入一個代表檔案的RequestBody
,我覺得更加容易理解,後來發現試驗無法成功,而且查了下issue,給出了一個很奇怪的解決方案,這裡可以參考:retrofit#1063。
給出了一個類似如下的方案:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
可以看到對於檔案的那個@Part
value竟然寫了這麼多奇怪的東西,而且filename竟然硬編碼了~~這個不好吧,我上傳的檔名竟然不能動態指定。
為了檔名不會被寫死,所以給出了最上面的上傳單檔案的方法,ps:上面這個方案經測試也是可以上傳成功的。
恩,這個奇怪方案,為什麼這麼做可行,下文會給出非常詳細的解釋。
最後看下多檔案上傳~
(7)多檔案上傳@PartMap
再新增一個方法~~~
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
這裡使用了一個新的註解@PartMap
,這個註解用於標識一個Map,Map的key為String型別,代表上傳的鍵值對的key(與伺服器接受的key對應),value即為RequestBody,有點類似@Part
的封裝版本。
執行的程式碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
可以看到,可以在Map中put進一個或多個檔案,鍵值對等,當然你也可以分開,單獨的鍵值對也可以使用@Part
,這裡又看到設定檔案的時候,相對應的key很奇怪,例如上例"photos\";
filename=\"icon.png"
,前面的photos就是與伺服器對應的key,後面filename是伺服器得到的檔名,ok,引數雖然奇怪,但是也可以動態的設定檔名,不太影響使用~~
(8)下載檔案
這個其實我覺得直接使用okhttp就好了,使用retrofit去做這個事情真的有點瞎用的感覺~~
增加一個方法:
- 1
- 2
- 1
- 2
呼叫:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
可以看到可以返回ResponseBody
,那麼很多事都能幹了~~
but,也看出這種方式下載感覺非常雞肋,並且onReponse回撥雖然在UI執行緒,但是你還是要處理io操作,也就是說你在這裡還要另外開執行緒操作,或者你可以考慮同步的方式下載。
最後還是建議使用okhttp去下載,例如使用okhttputils.
有人可能會問,使用okhttp,和使用retrofit會不會造成新建多個OkHttpClient
物件呢,其實是可設定的,參考下文。
ok,上面就是一些常用的方法,當然還涉及到一些沒有介紹的註解,但是通過上面這麼多方法的介紹,再多一二個註解的使用方式,相信大家能夠解決。
三、配置OkHttpClient
這個需要簡單提一下,很多時候,比如你使用retrofit需要統一的log管理,給每個請求新增統一的header等,這些都應該通過okhttpclient去操作,比如addInterceptor
例如:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
或許你需要更多的配置,你可以單獨寫一個OkhttpClient的單例生成類,在這個裡面完成你所需的所有的配置,然後將OkhttpClient
例項通過方法公佈出來,設定給retrofit。
設定方式:
- 1
- 2
- 3
- 1
- 2
- 3
callFactory
方法接受一個okhttp3.Call.Factory
物件,OkHttpClient
即為一個實現類。
四、retrofit 原始碼解析
ok,接下來我們隊retrofit的原始碼做簡單的分析,首先我們看retrofit如何為我們的介面實現例項;然後看整體的執行流程;最後再看詳細的細節;
(1)retrofit如何為我們的介面實現例項
通過上文的學習,我們發現使用retrofit需要去定義一個介面,然後可以通過呼叫retrofit.create(IUserBiz.class);
方法,得到一個介面的例項,最後通過該例項執行我們的操作,那麼retrofit如何實現我們指定介面的例項呢?
其實原理是:動態代理。但是不要被動態代理這幾個詞嚇唬到,Java中已經提供了非常簡單的API幫助我們來實現動態代理。
看原始碼前先看一個例子:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
輸出結果為:
- 1
- 2
- 3
- 1
- 2
- 3
可以看到我們通過Proxy.newProxyInstance
產生的代理類,當呼叫介面的任何方法時,都會呼叫InvocationHandler#invoke
方法,在這個方法中可以拿到傳入的引數,註解等。
試想,retrofit也可以通過同樣的方式,在invoke方法裡面,拿到所有的引數,註解資訊然後就可以去構造RequestBody
,再去構建Request
,得到Call
物件封裝後返回。
ok,下面看retrofit#create
的原始碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
哈,和上面對應。到這裡,你應該明白retrofit為我們介面生成例項物件並不神奇,僅僅是使用了Proxy
這個類的API而已,然後在invoke
方法裡面拿到足夠的資訊去構建最終返回的Call而已。
哈,其實真正的動態代理一般是有具體的實現類的,只是在這個類呼叫某個方法的前後去執行一些別的操作,比如開事務,打log等等。當然,本博文並不需要涉及這些詳細的內容,如果你希望詳細去了解,可以搜尋關鍵字:Proxy InvocationHandler
。
(2)retrofit整體實現流程
4.2.1 Retrofit的構建
這裡依然是通過構造者模式進行構建retrofit物件,好在其內部的成員變數比較少,我們直接看build()方法。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- baseUrl必須指定,這個是理所當然的;
- 然後可以看到如果不著急設定callFactory,則預設直接
new OkHttpClient()
,可見如果你需要對okhttpclient進行詳細的設定,需要構建OkHttpClient
物件,然後傳入; - 接下來是callbackExecutor,這個想一想大概是用來將回撥傳遞到UI執行緒了,當然這裡設計的比較巧妙,利用platform物件,對平臺進行判斷,判斷主要是利用
Class.forName("")
進行查詢,具體程式碼已經被放到文末,如果是Android平臺,會自定義一個Executor
物件,並且利用Looper.getMainLooper()
例項化一個handler物件,在Executor
內部通過handler.post(runnable)
,ok,整理憑大腦應該能構思出來,暫不貼程式碼了。 - 接下來是adapterFactories,這個物件主要用於對Call進行轉化,基本上不需要我們自己去自定義。
- 最後是converterFactories,該物件用於轉化資料,例如將返回的
responseBody
轉化為物件等;當然不僅僅是針對返回的資料,還能用於一般備註解的引數的轉化例如@Body
標識的物件做一些操作,後面遇到原始碼詳細再描述。
ok,總體就這幾個物件去構造retrofit,還算比較少的~~
4.2.2 具體Call構建流程
我們構造完成retrofit,就可以利用retrofit.create方法去構建介面的例項了,上面我們已經分析了這個環節利用了動態代理,而且我們也分析了具體的Call的構建流程在invoke方法中,下面看程式碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
主要也就三行程式碼,第一行是根據我們的method將其包裝成ServiceMethod,第二行是通過ServiceMethod和方法的引數構造retrofit2.OkHttpCall
物件,第三行是通過serviceMethod.callAdapter.adapt()
方法,將OkHttpCall
進行代理包裝;
下面一個一個介紹:
- ServiceMethod應該是最複雜的一個類了,包含了將一個method轉化為Call的所有的資訊。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
直接看build方法,首先拿到這個callAdapter最終拿到的是我們在構建retrofit裡面時adapterFactories時新增的,即為:new ExecutorCallbackCall<>(callbackExecutor, call)
,該ExecutorCallbackCall
唯一做的事情就是將原本call的回撥轉發至UI執行緒。
接下來通過callAdapter.responseType()
返回的是我們方法的實際型別,例如:Call<User>
,則返回User
型別,然後對該型別進行判斷。
接下來是createResponseConverter
拿到responseConverter物件,其當然也是根據我們構建retrofit時,addConverterFactory
新增的ConverterFactory物件來尋找一個合適的返回,尋找的依據主要看該converter能否處理你編寫方法的返回值型別,預設實現為BuiltInConverters
,僅僅支援返回值的實際型別為ResponseBody
和Void
,也就說明了預設情況下,是不支援Call<User>
這類型別的。
接下來就是對註解進行解析了,主要是對方法上的註解進行解析,那麼可以拿到httpMethod以及初步的url(包含佔位符)。
後面是對方法中引數中的註解進行解析,這一步會拿到很多的ParameterHandler
物件,該物件在toRequest()
構造Request的時候呼叫其apply方法。
ok,這裡我們並沒有去一行一行檢視程式碼,其實意義也不太大,只要知道ServiceMethod主要用於將我們介面中的方法
轉化為一個Request物件
,於是根據我們的介面返回值確定了responseConverter,解析我們方法上的註解拿到初步的url,解析我們引數上的註解拿到構建RequestBody所需的各種資訊,最終呼叫toRequest的方法完成Request的構建。
- 接下來看OkHttpCall的構建,建構函式僅僅是簡單的賦值
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
- 最後一步是
serviceMethod.callAdapter.adapt(okHttpCall)
我們已經確定這個callAdapter是ExecutorCallAdapterFactory.get()
對應程式碼為:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
可以看到adapt
返回的是ExecutorCallbackCall
物件,繼續往下看:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
可以看出ExecutorCallbackCall僅僅是對Call物件進行封裝,類似裝飾者模式,只不過將其執行時的回撥通過callbackExecutor進行回撥到UI執行緒中去了。
4.2.3 執行Call
在4.2.2我們已經拿到了經過封裝的ExecutorCallbackCall
型別的call物件,實際上就是我們實際在寫程式碼時拿到的call物件,那麼我們一般會執行enqueue
方法,看看原始碼是怎麼做的
首先是ExecutorCallbackCall.enqueue
方法,程式碼在4.2.2,可以看到除了將onResponse和onFailure回撥到UI執行緒,主要的操作還是delegate完成的,這個delegate實際上就是OkHttpCall物件,我們看它的enqueue方法
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
沒有任何神奇的地方,內部實際上就是okhttp的Call物件,也是呼叫okhttp3.Call.enqueue
方法。
中間對於okhttp3.Call
的建立程式碼為:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
可以看到,通過serviceMethod.toRequest
完成對request的構建,通過request去構造call物件,然後返回.
中間還涉及一個parseResponse
方法,如果順利的話,執行的程式碼如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
通過serviceMethod對ResponseBody進行轉化,然後返回,轉化實際上就是通過responseConverter
的convert方法。
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
ok,關於responseConverter
後面還會細說,不用擔心。
到這裡,我們整個原始碼的流程分析就差不多了,目的就掌握一個大體的原理和執行流程,瞭解下幾個核心的類。
那麼總結一下:
- 首先構造retrofit,幾個核心的引數呢,主要就是baseurl,callFactory(預設okhttpclient),converterFactories,adapterFactories,excallbackExecutor。
- 然後通過create方法拿到介面的實現類,這裡利用Java的
Proxy
類完成動態代理的相關代理 - 在invoke方法內部,拿到我們所宣告的註解以及實參等,構造ServiceMethod,ServiceMethod中解析了大量的資訊,最痛可以通過
toRequest
構造出okhttp3.Request
物件。有了okhttp3.Request
物件就可以很自然的構建出okhttp3.call
,最後calladapter對Call進行裝飾返回。 - 拿到Call就可以執行enqueue或者execute方法了
ok,瞭解這麼多足以。
下面呢,有幾個地方需要注意,一方面是一些特殊的細節;另一方面就是Converter
。
五、retrofit中的各類細節
(1)上傳檔案中使用的奇怪value值
第一個問題涉及到檔案上傳,還記得我們在單檔案上傳那裡所說的嗎?有種類似於hack的寫法,上傳檔案是這麼做的?
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
首先我們一點明確,因為這裡使用了@ Multipart
,那麼我們認為@Part
應當支援普通的key-value,以及檔案。
對於普通的key-value是沒問題的,只需要這樣@Part("username") String username
。
那麼對於檔案,為什麼需要這樣呢?@Part("file_key\"; filename=\"pp.png")
這個value設定的值不用看就會覺得特別奇怪,然而卻可以正常執行,原因是什麼呢?
原因是這樣的:
當上傳key-value的時候,實際上對應這樣的程式碼:
- 1
- 2
- 1
- 2
也就是說,我們的@Part
轉化為了
- 1
- 1
這麼一看,很隨意,只要把key放進去就可以了。
但是,retrofit2並沒有對檔案做特殊處理,檔案的對應的字串應該是這樣的
- 1
- 1
與鍵值對對應的字串相比,多了個;filename="filename.png
,就因為retrofit沒有做特殊處理,所以你現在看這些hack的做法
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
ok,到這裡我相信你已經理解了,為什麼要這麼做,而且為什麼這麼做可以成功!
恩,值得一提的事,因為這種方式檔名寫死了,我們上文使用的的是@Part MultipartBody.Part file
,可以滿足檔名動態設定,這個方式貌似也是2.0.1的時候支援的。
上述相關的原始碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
可以看到呢,並沒有對檔案做特殊處理,估計下個版本說不定@Part
會多個isFile=true|false
屬性,甚至修改對應形參,然後在這裡做簡單的處理。
ok,最後來到關鍵的ConverterFactory
了~
六、自定義Converter.Factory
(1)responseBodyConverter
關於Converter.Factory
,肯定是通過addConverterFactory
設定的
- 1
- 2
- 3
- 1
- 2
- 3
該方法接受的是一個Converter.Factory factory
物件
該物件呢,是一個抽象類,內部包含3個方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
可以看到呢,3個方法都是空方法而不是抽象的方法,也就表明了我們可以選擇去實現其中的1個或多個方法,一般只需要關注requestBodyConverter
和responseBodyConverter
就可以了。
ok,我們先看如何自定義,最後再看GsonConverterFactory.create
的原始碼。
先來個簡單的,實現responseBodyConverter
方法,看這個名字很好理解,就是將responseBody進行轉化就可以了。
ok,這裡呢,我們先看一下上述中我們使用的介面:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
不知不覺,方法還蠻多的,假設哈,我們這裡去掉retrofit構造時的GsonConverterFactory.create
,自己實現一個Converter.Factory
來做資料的轉化工作。
首先我們解決responseBodyConverter
,那麼程式碼很簡單,我們可以這麼寫:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
使用的時候呢,可以
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
ok,這樣的話,就可以完成我們的ReponseBody
到List<User>
或者User
的轉化了。
可以看出,我們這裡用的依然是Gson,那麼有些同學肯定不希望使用Gson就能實現,如果不使用Gson的話,一般需要針對具體的返回型別,比如我們針對返回List<User>
或者User
你可以這麼寫:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
這裡簡單讀取了一個屬性,大家肯定能看懂,這樣就能滿足返回值是Call<List<User>>
或者Call<User>
.
這裡鄭重提醒:如果你針對特定的型別去寫Converter
,一定要在UserConverterFactory#responseBodyConverter
中對型別進行檢查,發現不能處理的型別return
null
,這樣的話,可以交給後面的Converter.Factory
處理,比如本例我們可以按照下列方式檢查:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
好了,到這呢responseBodyConverter
方法告一段落了,謹記就是將reponseBody->返回值返回中的實際型別,例如Call<User>中的User
;還有對於該converter不能處理的型別一定要返回null。
(2)requestBodyConverter
ok,上面介面一大串方法呢,使用了我們的Converter之後,有個方法我們現在還是不支援的。
- 1
- 2
- 1
- 2
ok,這個@Body
需要用到這個方法,叫做requestBodyConverter
,根據引數轉化為RequestBody
,下面看下我們如何提供支援。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
然後在UserConverterFactory中複寫requestBodyConverter方法,返回即可:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
這裡偷了個懶,使用Gson將物件轉化為json字串了,如果你不喜歡使用框架,你可以選擇拼接字串,或者反射寫一個支援任何物件的,反正就是物件->json字串的轉化。最後構造一個RequestBody返回即可。
ok,到這裡,我相信如果你看的細緻,自定義Converter.Factory
是幹嘛的,但是我還是要總結下:
- responseBodyConverter 主要是對應
@Body
註解,完成ResponseBody到實際的返回型別的轉化,這個型別對應Call<XXX>
裡面的泛型XXX,其實@Part
等註解也會需要responseBodyConverter,只不過我們的引數型別都是RequestBody
,由預設的converter處理了。 - requestBodyConverter 完成物件到RequestBody的構造。
- 一定要注意,檢查type如果不是自己能處理的型別,記得return null (因為可以新增多個,你不能處理return null ,還會去遍歷後面的converter).
七、值得學習的API
其實一般情況下看原始碼呢,可以讓我們更好的去使用這個庫,當然在看的過程中如果發現了一些比較好的處理方式呢,是非常值得記錄的。如果每次看別人的原始碼都能吸取一定的精華,比你單純的去理解會好很多,因為你的記憶力再好,原始碼解析你也是會忘的,而你記錄下來並能夠使用的優越的程式碼,可能用久了就成為你的程式碼了。
我舉個例子:比如retrofit2中判斷當前執行的環境程式碼如下,如果下次你有這樣的需求,你也可以這麼寫,甚至原始碼中根據不同的執行環境還提供了不同的Executor
都很值得記錄:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
相關文章
- 探索“精益”與“智慧製造”之間的關係
- 前端之DOM解析和渲染與CSS、JS之間的關係前端CSSJS
- ODS與DW之間的關係
- TLS與SSL之間關係TLS
- ps 與 svmon之間關係
- 類與類之間的基本關係
- 【最佳化】引數SESSION_CACHED_CURSORS與解析之間的關係Session
- 思考 TPS 與 RT 之間的關係
- 談Ubuntu與FOSS之間的關係(轉)Ubuntu
- FAILGROUP和REDUNDANCY之間的關係關係!AI
- 類之間的關係
- Android Https相關完全解析 當OkHttp遇到HttpsAndroidHTTP
- 成員方法與const之間的關係
- 求引數遍歷疊加與結果之間強關係的探索測試思路
- 【java】類之間的關係Java
- dispaly、position、float之間的關係與相互作用
- ERP與精益生產之間的關係
- Window、WindowManager、View 之間的關係View
- UML中類之間的關係
- tablespace和datafile之間的關係
- 不同層之間的物件關係物件
- 大資料技術與Hadoop之間的關係大資料Hadoop
- 特殊特性與FMEA之間的關係是什麼?
- Window, WindowManager和WindowManagerService之間的關係
- git、github、gitlab之間的關係GithubGitlab
- UML類圖--類之間的關係
- Activity、View、Window之間關係的分析View
- QT中類之間的關係圖QT
- .Net Framework各版本之間的關係Framework
- 光敏電阻與光強之間什麼關係?
- 訊息佇列與快遞櫃之間的奇妙關係佇列
- 併發使用者數與TPS之間的關係
- Oracle 查詢表與表之間的 主外來鍵關係Oracle
- listener.ora檔案與tnsnames.ora之間的關係
- SAP FICO表關係圖解 BSEG與BSIS、BSAS、BSID、BSAD、BSIK、BSAK之間的關係圖解
- python與人工智慧之間有什麼關係?Python人工智慧
- 大資料與Hadoop之間是什麼關係?大資料Hadoop
- table/segment/extent/block之間關係BloC