詳解如何魔改Retrofit實(shí)例
前言
Retrofit 是 Square 公司開源的網(wǎng)絡(luò)框架,在 Android 日常開發(fā)中被廣泛使用,開發(fā)者們對(duì)于 Retrofit 的原理、源碼都已經(jīng)有相當(dāng)深入的分析。
本文也是從一次簡(jiǎn)單的性能優(yōu)化開始,挖掘了 Retrofit 的實(shí)現(xiàn)細(xì)節(jié),并在此基礎(chǔ)上,探索了對(duì) Retrofit 的更多玩法。
因此,本文將主要講述從發(fā)現(xiàn)、優(yōu)化到探索這一完整的過程,以及過程的一些感悟。
Retrofit 的性能問題
問題源自一次 App 冷啟動(dòng)優(yōu)化,常規(guī)啟動(dòng)優(yōu)化的思路,一般是分析主線程耗時(shí),然后把這些耗時(shí)操作打包丟到IO線程中執(zhí)行。短期來看這不失是一種見效最快的優(yōu)化方法,但站在長(zhǎng)期優(yōu)化的角度,也是性價(jià)比最低的一種方法。因?yàn)榫托阅軆?yōu)化而言,我們不能僅考慮主線程的執(zhí)行,更多還要考慮對(duì)整體資源分配的優(yōu)化,尤其在并發(fā)場(chǎng)景,還要考慮鎖的影響。而 Retrofit 的問題正屬于后者。
我們?cè)谂挪閱?dòng)速度時(shí)發(fā)現(xiàn),首頁接口請(qǐng)求的耗時(shí)總是高于接口平均值,導(dǎo)致首屏數(shù)據(jù)加載很慢。針對(duì)這個(gè)問題,我們使用 systrace 進(jìn)行了具體的分析,其中一次結(jié)果如下圖,
可以看到,這一次請(qǐng)求中有大段耗時(shí)是在等鎖,并沒有真正執(zhí)行網(wǎng)絡(luò)請(qǐng)求;如果觀察同一時(shí)間段的其他請(qǐng)求,也能發(fā)現(xiàn)類似現(xiàn)象。
那么這里的請(qǐng)求是在等什么鎖?配合 systrace 可以在 Retrofit 源碼(下文相關(guān)源碼都是基于 Retrofit 2.7.x 版本,不同版本邏輯可能略有出入)中定位到,是如下的一把鎖,
// retrofit2/Retrofit.java public <T> T create(final Class<T> service) { validateServiceInterface(service); return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { @Override public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { ... return loadServiceMethod(method).invoke(args != null ? args : emptyArgs); } }); } ServiceMethod<?> loadServiceMethod(Method method) { ServiceMethod<?> result = serviceMethodCache.get(method); if (result != null) return result; synchronized (serviceMethodCache) { // 等待的鎖 result = serviceMethodCache.get(method); if (result == null) { result = ServiceMethod.parseAnnotations(this, method); serviceMethodCache.put(method, result); } } return result; }
Retrofit 相關(guān)的實(shí)現(xiàn)原理這里就不再贅述,簡(jiǎn)而言之 loadServiceMethod
這個(gè)方法的作用是:通過請(qǐng)求 interface 的入?yún)?、返回值、注解等信息,生?Converter、CallAdapter,并包裝成一個(gè) ServiceMethod
返回,之后會(huì)通過這個(gè) ServiceMethod
來發(fā)起真正的網(wǎng)絡(luò)請(qǐng)求。
從上述源碼也可以看到,ServiceMethod
是有內(nèi)存緩存的,但問題也正在這里—— ServiceMethod
的生成是在鎖內(nèi)完成的。
因此問題就變成,生成 ServiceMethod
為什么會(huì)有耗時(shí)?以云音樂的項(xiàng)目為例,各個(gè)團(tuán)隊(duì)都是使用 moshi 進(jìn)行 json 解析,大部分 meta 類是通過 kotlin 實(shí)現(xiàn),但也存在一定 kotlin、 Java 混用的情況。
這部分耗時(shí)主要來自 moshi 生成 JsonAdapter
。生成 JsonAdapter
需要遞歸遍歷 meta 類中的所有 field,過程中除了 kotlin 反射本身的效率和受并發(fā)的影響,還涉及 kotlin 的 builtins 機(jī)制,以及冷啟動(dòng)過程中,類加載的耗時(shí)。
上述提到的幾個(gè)耗時(shí)點(diǎn),每一個(gè)都可以單開一篇文章討論,篇幅原因這里一言以蔽之——冷啟動(dòng)過程中,moshi 生成 JsonAdapter
是一個(gè)非常耗時(shí)的過程(而且這個(gè)耗時(shí),跟使用 moshi 解析框架本身也沒有必然聯(lián)系,使用其他 json 解析框架,或多或少也會(huì)遇到類似問題)。
鎖+不可避免的耗時(shí),引發(fā)的必然結(jié)果是:在冷啟動(dòng)過程中,通過 Retrofit 發(fā)起的網(wǎng)絡(luò)請(qǐng)求,會(huì)部分劣化成一個(gè)串行過程。因此出現(xiàn) systrace 中呈現(xiàn)的結(jié)果,請(qǐng)求大部分時(shí)間在等鎖,這里等待的是前一個(gè)請(qǐng)求生成 ServiceMethod
的耗時(shí),并以此類推耗時(shí)不斷向后傳遞。
嘗試優(yōu)化
既然定位到了原因,我們可以嘗試優(yōu)化了。
首先可以從 JsonAdapter
的生成效率入手,比如 moshi 原生就支持 @JsonClass
注解,通過 apt 在編譯時(shí)生成 meta的 解析器,從而顯著減少反射耗時(shí)。
二來,還是嘗試從根本上解決問題。其實(shí)從發(fā)現(xiàn)這個(gè)問題開始,我們就一直在思考這種寫法的合理性:首先加鎖肯定是為了訪問 serviceMethodCache
時(shí)的線程安全;其次,生成 ServiceMethod
的過程時(shí),確實(shí)有一些反射操作內(nèi)部是有緩存的,如果發(fā)生并發(fā)是有一定性能損耗的。
但就我們的實(shí)際項(xiàng)目而言,不同 Retrofit interface 之間,幾乎沒有重疊的部分,反射操作都是以 Class 為單位在進(jìn)行。以此為基礎(chǔ),我們可以嘗試優(yōu)化一下這里的寫法。
那么,在不修改 Retrofit 源碼的基礎(chǔ)上,有什么方法可以修改請(qǐng)求流程嗎?
在云音樂的項(xiàng)目中,對(duì)于創(chuàng)建 Retrofit 動(dòng)態(tài)代理,是有統(tǒng)一封裝的。也就是說,項(xiàng)目中除個(gè)別特殊寫法,絕大多數(shù)請(qǐng)求的創(chuàng)建,都是通過同一段封裝。只要我們改寫了 Retrofit 創(chuàng)建動(dòng)態(tài)代理的流程,是不是就可以優(yōu)化掉前面的問題?
先觀察一下 Retrofit.create
方法的內(nèi)部實(shí)現(xiàn),可以發(fā)現(xiàn)大部分方法的可見性都是包可見的。眾所周知,在 Java 的世界里,包可見就等于 public,所以我們可以自己實(shí)現(xiàn) Retrofit.create
方法,寫法大概如下,
private ServiceMethod<?> loadServiceMethod(Method method) { // 反射取到Retrofit內(nèi)部的緩存 Map<Method, ServiceMethod<?>> serviceMethodCache = null; try { serviceMethodCache = cacheField != null ? (Map<Method, ServiceMethod<?>>) cacheField.get(retrofit) : null; } catch (IllegalAccessException e) { e.printStackTrace(); } if (serviceMethodCache == null) { return retrofit.loadServiceMethod(method); } ServiceMethod<?> result = serviceMethodCache.get(method); if (result != null) return result; synchronized (serviceMethodCache) { result = serviceMethodCache.get(method); if (result != null) return result; } synchronized (service) { // 這里替換成類鎖 result = ServiceMethod.parseAnnotations(retrofit, method); } synchronized (serviceMethodCache) { serviceMethodCache.put(method, result); } return result; }
可以看到,除了需要反射獲取 serviceMethodCache
這個(gè)私有成員 ,其他方法都可以直接訪問。這里把耗時(shí)的 ServiceMethod.parseAnnotations
方法從鎖中移出,改為對(duì) interface Class 加鎖。(當(dāng)然這里激進(jìn)一點(diǎn),也可以完全不加鎖,需要根據(jù)實(shí)際項(xiàng)目的情況來定)
修改之后,在啟動(dòng)過程中重新抓取 systrace,已經(jīng)看不到之前等鎖的耗時(shí)了,首頁請(qǐng)求速度也回落到正常區(qū)間內(nèi)。
或許從這也能看出 kotlin 為什么要約束包可見性和泛型的上下邊界—— Java 原有的約束太弱,雖然方便了 hook,但同樣也說明代碼邊界更容易被破壞;同時(shí)這里也說明了代碼規(guī)范的重要性,只要保證統(tǒng)一的編碼規(guī)范,即使不使用什么“黑科技”,也能對(duì)代碼運(yùn)行效率實(shí)現(xiàn)有效的管控。
不是AOP的AOP
到這里,我們會(huì)突然發(fā)現(xiàn)一個(gè)問題:既然我們都自己來實(shí)現(xiàn) Retrofit 的動(dòng)態(tài)代理了,那不是意味著我們可以獲取到每一次請(qǐng)求的結(jié)果,乃至控制每一次請(qǐng)求的流程?
我們知道,傳統(tǒng)的接口緩存,一般是基于網(wǎng)絡(luò)庫實(shí)現(xiàn)的,比如在 okhttp 中的 CacheInterceptor
。
這種網(wǎng)絡(luò)庫層級(jí)緩存的缺點(diǎn)是:網(wǎng)絡(luò)請(qǐng)求畢竟是一個(gè)IO過程,它很難是面向?qū)ο蟮模徊⑶?Response 的 body 也不能被多次 read,在 cache 過程中,一般需要把數(shù)據(jù)深拷貝一次,有一定性能損耗。
比如,CacheInterceptor
中就有如下緩存相關(guān)的邏輯,在 body 被 read 的同時(shí),再 copy一份到 cache 中。
val cacheWritingSource = object : Source { var cacheRequestClosed: Boolean = false @Throws(IOException::class) override fun read(sink: Buffer, byteCount: Long): Long { val bytesRead: Long try { bytesRead = source.read(sink, byteCount) } catch (e: IOException) { if (!cacheRequestClosed) { cacheRequestClosed = true cacheRequest.abort() // Failed to write a complete cache response. } throw e } if (bytesRead == -1L) { if (!cacheRequestClosed) { cacheRequestClosed = true cacheBody.close() // The cache response is complete! } return -1 } sink.copyTo(cacheBody.buffer, sink.size - bytesRead, bytesRead) cacheBody.emitCompleteSegments() return bytesRead } ... }
但如果我們能整個(gè)控制 Retrofit 請(qǐng)求,在動(dòng)態(tài)代理這一層取到的是真正請(qǐng)求結(jié)果的 meta 對(duì)象,如果把這個(gè)對(duì)象緩存起來,連 json 解析的過程都可以省去;而且拿到真實(shí)的返回對(duì)象后,基于對(duì)象對(duì)數(shù)據(jù)做一些 hook 操作,也更加容易。
當(dāng)然,直接緩存對(duì)象也有風(fēng)險(xiǎn)風(fēng)險(xiǎn),比如如果 meta 本身不是 immutable 的,會(huì)破壞請(qǐng)求的冪等性,這也是需要在后續(xù)的封裝中注意的,避免能力被濫用。
那么我們能在動(dòng)態(tài)代理層拿到 Retrofit 的請(qǐng)求結(jié)果嗎?答案是肯定的。
我們知道 ServiceMethod.invoke
這個(gè)方法返回的結(jié)果,取決于 CallAdapter
的實(shí)現(xiàn)。Retrofit 有兩種原生的 CallAdpater
,一種是基于 okhttp 原生的 RealCall,一種是基于 kotlin 的 suspend 方法。
也就是說我們?cè)谕ㄟ^ Retrofit 發(fā)起網(wǎng)絡(luò)請(qǐng)求時(shí),一般只有如下兩種寫法(各個(gè)寫法其實(shí)都還有幾個(gè)不同的小變種,這里就不展開了)。
interface Api { @FormUrlEncoded @POST("somePath") suspend fun get1(@Field("field") field: String): Result @FormUrlEncoded @POST("somePath") fun get2(@Field("field") field: String): Call<Result> }
這里 intreface 定義的返回值,其實(shí)就是動(dòng)態(tài)代理那里的返回值,
對(duì)于返回值為 Call 的寫法 ,hook 邏輯類似下面的寫法,只要對(duì)回調(diào)使用裝飾器包裝一下,就能拿到返回結(jié)果或者異常。
class WrapperCallback<T>(private val cb : Callback<T>) : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { val result = response.body() // 這里response.body()就是返回的meta cb.onResponse(call, response) } }
但對(duì)于 suspend 方法呢?調(diào)試一下會(huì)發(fā)現(xiàn),當(dāng)請(qǐng)求定義為 suspend 方法時(shí),返回值如下,
這里的 COROUTINE_SUSPENDED
是什么?
獲取 suspend 方法的返回值
要解釋 COROUTINE_SUSPENDED
是什么,稍微涉及協(xié)程的實(shí)現(xiàn)原理。我們可以先看看 Retrofit 本身在生成動(dòng)態(tài)代理時(shí),是怎么適配 suspend 方法的。
Retrofit 中對(duì)于 suspend 方法的返回,是通過 SuspendForBody
和 SuspendForResponse
這兩個(gè) ServiceMethod
來封裝的。兩者邏輯類似,我們以 SuspendForBody
為例,
static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> { ... @Override protected Object adapt(Call<ResponseT> call, Object[] args) { call = callAdapter.adapt(call); //noinspection unchecked Checked by reflection inside RequestFactory. Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1]; ... try { return isNullable ? KotlinExtensions.awaitNullable(call, continuation) : KotlinExtensions.await(call, continuation); } catch (Exception e) { return KotlinExtensions.suspendAndThrow(e, continuation); } } }
首先,代碼中的 Continuation
是什么? Continuation
可理解為掛起方法的回調(diào)。我們知道,suspend 方法在編譯時(shí),會(huì)被編譯成一個(gè)普通的 Java 方法,除了返回值被改寫成 Object,它與普通 Java 方法的另一個(gè)區(qū)別是,編譯器會(huì)在方法末尾插入一個(gè)入?yún)?,這個(gè)入?yún)⒌念愋途褪?Continuation
。
可以看到,一個(gè) suspend 方法,在編譯之后,多了一個(gè)入?yún)ⅰ?/p>
kotlin 協(xié)程正是借助 Continuation
來向下傳遞協(xié)程上下文,再向上返回結(jié)果的;所以 suspend 方法真正的返回結(jié)果,一般不是通過方法本身的返回值來返回的。
此時(shí),我們只要根據(jù)協(xié)程狀態(tài),任意返回一個(gè)占位的返回值即可,比如在 suspendCancellableCoroutine
閉包中,
// CancellableContinuationImpl.kt @PublishedApi internal fun getResult(): Any? { setupCancellation() if (trySuspend()) return COROUTINE_SUSPENDED // otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state val state = this.state if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this) ... return getSuccessfulResult(state) }
這也就是前文 COROUTINE_SUSPENDED
這個(gè)返回結(jié)果的來源。
回到前面 Retrofit 橋接 suspend 的代碼,如果我們寫一段類似下面的測(cè)試代碼,會(huì)發(fā)現(xiàn)這里的 context 與入?yún)?continuation.getContext 返回的是同一個(gè)對(duì)象。
val ret = runBlocking { val context = coroutineContext // 上一級(jí)協(xié)程的上下文 val ret = api.getUserDetail(uid) ret }
而 Retrofit 中的 KotlinExtensions.await
方法的實(shí)現(xiàn)如下,
suspend fun <T : Any> Call<T>.await(): T { return suspendCancellableCoroutine { continuation -> continuation.invokeOnCancellation { cancel() } enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { if (response.isSuccessful) { val body = response.body() if (body == null) { ... continuation.resumeWithException(e) } else { continuation.resume(body) } } else { continuation.resumeWithException(HttpException(response)) } } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } }) } }
結(jié)合前面對(duì) Continuation
的了解,把這段代碼翻譯成 Java 偽代碼,大概是這樣的,
public Object await(Call<T> call, Object[] args, Continuation<T> continuation) { call.enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { continuation.resumeWith(Result.success(response.body)); } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWith(Result.failure(t)); } }) return COROUTINE_SUSPENDED; }
可以看到,suspend 方法是一種更優(yōu)雅實(shí)現(xiàn)回調(diào)的語法糖,無論是在它的設(shè)計(jì)目的上,還是實(shí)現(xiàn)原理上,都是這樣。
所以,根據(jù)這個(gè)原理,我們也可以按類似如下方式 hook suspend 方法,從而獲得返回值。
@Nullable public T hookSuspend(Method method, Object[] args) { Continuation<T> realContinuation = (Continuation<T>) args[args.length - 1]; Continuation<T> hookedContinuation = new Continuation<T>() { @NonNull @Override public CoroutineContext getContext() { return realContinuation.getContext(); } @Overrid public void resumeWith(@NonNull Object o) { realContinuation.resumeWith(o); // 這里的object就是返回結(jié)果 } }; args[args.length - 1] = hookedContinuation; return method.invoke(args); }
緩存請(qǐng)求結(jié)果
到這里已經(jīng)距離成功很近了,既然我們能拿到每一種請(qǐng)求類型的返回結(jié)果,再加億點(diǎn)點(diǎn)細(xì)節(jié),就意味著我們可以實(shí)現(xiàn)基于 Retrofit 的預(yù)加載、緩存封裝了。
Cache 封裝大差不差,主要是處理以下這條邏輯鏈路:
Request -> Cache Key -> Store -> Cached Response
因?yàn)槲覀冎蛔鰞?nèi)存緩存,所以也不需要考慮數(shù)據(jù)的持久化,直接使用Map來管理緩存即可。
- 先封裝入?yún)?,我們?cè)趧?dòng)態(tài)代理層以此入?yún)闃?biāo)志,觸發(fā)預(yù)加載或緩存機(jī)制,
sealed class LoadInfo( val id: String = "", // 請(qǐng)求id,默認(rèn)不需要設(shè)置 val timeout: Long // 超時(shí)時(shí)間 ) // 用來寫緩存/預(yù)加載 class CacheWriter( id: String = "", timeout: Long = 10000 ) : LoadInfo(id, timeout) // 用來讀緩存 class CacheReader( id: String = "", timeout: Long = 10000, val asCache: Boolean = false // 未命中時(shí),是否要產(chǎn)生一個(gè)新的緩存,可供下一次請(qǐng)求使用 ) : LoadInfo(id, timeout)
- 插入 hook 代碼,處理緩存讀寫邏輯,(這里還需要處理并發(fā),基于協(xié)程比較簡(jiǎn)單,這里就不展開了)
fun <T> ServiceMethod<T>.hookInvoke(args: Array<Any?>): T? { val loadInfo = args.find { it is LoadInfo } as? LoadInfo // 這里我們可以用方法簽名做緩存key,方法簽名肯定是唯一的 val id = method.toString() if (loadInfo is CacheReader) { // 嘗試找緩存 val cache = map[id] if (isSameRequest(cache?.args, args)) { // 找到緩存,并且請(qǐng)求參數(shù)一致,則直接返回 return cache?.result as? T } } // 正常發(fā)起請(qǐng)求 val result = invoke(args) if (loadInfo is CacheWriter) { // 存緩存 map[id] = Cache(id, result) } return result }
這里使用 map 緩存請(qǐng)求結(jié)果,豐富一下緩存超時(shí)邏輯和前文提到的并發(fā)處理,即可投入使用。
- 定義請(qǐng)求,
我們可以利用 Retrofit 中的 @Tag
注解來傳入 LoadInfo
參數(shù),這樣不會(huì)影響真正的網(wǎng)絡(luò)請(qǐng)求。
interface TestApi { @FormUrlEncoded @POST("moyi/user/center/detail") suspend fun getUserDetail( @Field("userId") userId: String, @Tag loadInfo: LoadInfo // 緩存配置 ): UserDetail }
- have a try,
suspend fun preload(preload: Boolean) { launch { // 預(yù)加載 api.getUserDetail("123", CacheWriter(timeout = 5000)) } delay(3000) // 讀預(yù)加載的結(jié)果 api.getUserDetail("123", CacheReader()) // 讀到上一次的緩存 }
執(zhí)行代碼可以看到,兩次 api 調(diào)用,只會(huì)發(fā)起一次真正的網(wǎng)絡(luò)請(qǐng)求,并且兩次返回結(jié)果是同一個(gè)對(duì)象,跟我們的預(yù)期一致。
相比傳統(tǒng)網(wǎng)絡(luò)緩存,這種寫法的好處,除了前面提到的減少 IO 開銷之外,幾乎可以做到零侵入,相比常規(guī)網(wǎng)絡(luò)請(qǐng)求寫法,只是多了一個(gè)入?yún)?;而且寫法非常?jiǎn)潔,常規(guī)寫法可能用到的預(yù)加載、超時(shí)、并發(fā)等大量的膠水代碼,都被隱藏在 Retrofit 動(dòng)態(tài)代理內(nèi)部,上層業(yè)務(wù)代碼并不需要感知。當(dāng)然 AOP 帶來的便利性,與動(dòng)態(tài)代理寫法的優(yōu)勢(shì)也是相輔相成。
One more thing?
云音樂內(nèi)部一直在推動(dòng) Backend-for-Frontend (BFF) 的建設(shè),BFF 與 Android 時(shí)下新興的 MVI 框架非常契合,借助 BFF 可以讓 Model 層變的非常簡(jiǎn)潔。
但 BFF 本身對(duì)于服務(wù)端是一個(gè)比較重的方案,特別對(duì)于大型項(xiàng)目,需要考慮 RPC 數(shù)據(jù)敏感性、接口性能、容災(zāi)降級(jí)等一系列工程化問題,并且 BFF 在大型項(xiàng)目里一般也只用在一些非 P0 場(chǎng)景上。特別對(duì)于團(tuán)隊(duì)規(guī)模比較小的業(yè)務(wù)來說,考慮到這些成本后,BFF 本身帶來的便利幾乎全被抵消了。
那么有什么辦法可以不借助其他端實(shí)現(xiàn)一個(gè)輕量級(jí)的 BFF 嗎?相信你已經(jīng)猜到了,我們已經(jīng) AOP 了 Retrofit,實(shí)現(xiàn)網(wǎng)絡(luò)緩存可以看作是小試牛刀,那么實(shí)現(xiàn) BFF 也不過是更進(jìn)一步。
與前文借助動(dòng)態(tài)代理層實(shí)現(xiàn)網(wǎng)絡(luò)緩存的思路類似,我們也選擇把 BFF 層隱藏在動(dòng)態(tài)代理層中。
可以先梳理一下大概的思路:
- 使用注解定位需要 BFF 的 Retrofit 請(qǐng)求;
- 使用 apt 生成 BFF 需要的膠水代碼,將多個(gè)普通 Retrofit 請(qǐng)求,合并成一個(gè) BFF 請(qǐng)求;
- 通過 AGP Transform 收集所有 BFF 生成類,建立映射表;
- 在 Retrofit 動(dòng)態(tài)代理層,借助映射表,把請(qǐng)求實(shí)現(xiàn)替換成生成好的 BFF 代碼。
實(shí)際上,目前主流的各種零入侵代碼框架(比如路由、埋點(diǎn)、數(shù)據(jù)庫、啟動(dòng)框架、依賴注入等),都是用類似的思路實(shí)現(xiàn)的,我們觸類旁通即可。
這里為對(duì)此思路還不太熟悉的小伙伴,簡(jiǎn)單過一遍整體設(shè)計(jì)流程,
首先,定義需要的注解,用 @BFF
來標(biāo)識(shí)需要進(jìn)行 BFF 操作的 meta 類或接口,
@Retention(RetentionPolicy.CLASS) @Target({ElementType.FIELD, ElementType.METHOD}) public @interface BFF { String source() default ""; // 數(shù)據(jù)源信息,默認(rèn)不需要 boolean primary() default false; // 是否為必要數(shù)據(jù) }
用 @BFFSource
注解來標(biāo)識(shí)數(shù)據(jù)預(yù)處理的邏輯(在大部分簡(jiǎn)單場(chǎng)景下,是不需要使用此注解的,因此把這部分拆分成一個(gè)單獨(dú)的注解,以降低學(xué)習(xí)成本),
@Retention(RetentionPolicy.CLASS) @Target({ElementType.FIELD}) public @interface BFFSource { Class clazz() default String.class; // 目前數(shù)據(jù) String name() default ""; // 別名 String logic() default ""; // 預(yù)處理邏輯 }
定義數(shù)據(jù)源,數(shù)據(jù)源的寫法跟普通 Retrofit 請(qǐng)求一樣,只是方法上額外加一個(gè) @BFF
注解作為 apt 的標(biāo)識(shí),
@JvmSuppressWildcards interface TestApi { @BFF @FormUrlEncoded @POST("path/one") suspend fun getPartOne(@Field("position") position: Int): PartOne @BFF @FormUrlEncoded @POST("path/two") suspend fun getPartTwo(@Field("id") id: Int): PartTwo }
定義目標(biāo)數(shù)據(jù)結(jié)構(gòu),這里依然通過 @BFF
注解,與前面的請(qǐng)求做關(guān)聯(lián),
data class MyMeta( @BFF(primary = true) val one: PartOne, @BFF val two: PartTwo? ) { @BFFSource(clazz = PartOne::class, logic = "total > 0") var valid: Boolean = false }
定義BFF請(qǐng)求,
@JvmSuppressWildcards interface BFFApi { @BFF @POST("path/all") // 在這個(gè)方案中,BFF api的path沒有實(shí)際意義 suspend fun getAll( @Field("position") position: Int, @Field("id") id: Int ): MyMeta }
通過上述注解,在編譯時(shí)生成膠水代碼如下,(這里生成代碼的邏輯其實(shí)跟依賴注入是完全一致的,囿于篇幅就不詳細(xì)討論了)
public class GetAllBFF( private val creator: RetrofitCreate, scope: CoroutineScope ) : BFFSource(scope) { private val testApi: TestApi by lazy { creator.create(UserApi::class.java) } public suspend fun getAll( position: Int, id: Int ): MyMeta { val getPartOneDeferred = loadAsync { testApi.getPartOne(position) } val getPartTwoDeferred = loadAsync { testApi.getPartTwo(id) } val getPartOneResult = getPartOneDeferred.await() val getPartTwoResult = getPartTwoDeferred.await() val result = MyMeta(getPartOneResult!!, getPartTwoResult) result.valid = getPartOneResult!!.total > 0 return result } }
在使用時(shí),直接把 BFF api 當(dāng)作一個(gè)普通的接口調(diào)用即可,Retrofit 內(nèi)部會(huì)完成替換。
private val bffApi by lazy { creator.create(BFFApi::class.java) } public suspend fun getAllMeta( position: Int, id: Int ): MyMeta { return bffApi.getAll(position, id) // 直接返回BFF合成好的結(jié)果 }
可以看到,與前文設(shè)計(jì)接口緩存封裝類似,可以做到零侵入、零膠水代碼,使用起來非常簡(jiǎn)潔、直接。
總結(jié)
至此,我們回顧了對(duì)于 Retrofit 的性能問題,從發(fā)現(xiàn)問題到解決問題的過程,并簡(jiǎn)單講解了我們是怎么進(jìn)一步開發(fā) Retrofit 的潛力,以及常用的低侵入框架的設(shè)計(jì)思路。文章涉及的基于 Retrofit 的緩存、BFF 設(shè)計(jì),更多是拋磚引玉,而且不僅僅是 Retrofit,大家掌握類似的設(shè)計(jì)思路之后,可以把它們應(yīng)用在更多場(chǎng)景中,對(duì)于日常的開發(fā)、編碼效率提升和性能優(yōu)化,都會(huì)很有幫助,希望對(duì)各位能有所啟發(fā),更多關(guān)于魔改Retrofit實(shí)例的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android PullToRefreshLayout下拉刷新控件的終結(jié)者
這篇文章主要介紹了Android自定義控件實(shí)戰(zhàn)中下拉刷新控件終結(jié)者PullToRefreshLayout的實(shí)現(xiàn)方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-03-03Ubuntu中為Android系統(tǒng)上編寫Linux內(nèi)核驅(qū)動(dòng)程序?qū)崿F(xiàn)方法
本文主要介紹在Ubuntu 上為Android系統(tǒng)編寫Linux內(nèi)核驅(qū)動(dòng)程序, 這里對(duì)編寫驅(qū)動(dòng)程序做了詳細(xì)的說明,對(duì)研究Android源碼和HAL都有巨大的幫助,有需要的小伙伴可以參考下2016-08-08android實(shí)現(xiàn)滾動(dòng)文本效果
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)滾動(dòng)文本效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05詳解Retrofit2.0 公共參數(shù)(固定參數(shù))
這篇文章主要介紹了Retrofit2.0 公共參數(shù)(固定參數(shù)),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04android webview 簡(jiǎn)單瀏覽器實(shí)現(xiàn)代碼
android webview 簡(jiǎn)單瀏覽器實(shí)現(xiàn)代碼,需要的朋友可以參考一下2013-05-05Android使用動(dòng)畫動(dòng)態(tài)添加商品進(jìn)購(gòu)物車
這篇文章主要為大家詳細(xì)介紹了Android使用動(dòng)畫動(dòng)態(tài)添加商品進(jìn)購(gòu)物車,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06詳解Android開發(fā)錄音和播放音頻的步驟(動(dòng)態(tài)獲取權(quán)限)
這篇文章主要介紹了詳解Android開發(fā)錄音和播放音頻的步驟(動(dòng)態(tài)獲取權(quán)限),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08