詳解如何魔改Retrofit實例
前言
Retrofit 是 Square 公司開源的網(wǎng)絡(luò)框架,在 Android 日常開發(fā)中被廣泛使用,開發(fā)者們對于 Retrofit 的原理、源碼都已經(jīng)有相當(dāng)深入的分析。
本文也是從一次簡單的性能優(yōu)化開始,挖掘了 Retrofit 的實現(xiàn)細(xì)節(jié),并在此基礎(chǔ)上,探索了對 Retrofit 的更多玩法。
因此,本文將主要講述從發(fā)現(xiàn)、優(yōu)化到探索這一完整的過程,以及過程的一些感悟。
Retrofit 的性能問題
問題源自一次 App 冷啟動優(yōu)化,常規(guī)啟動優(yōu)化的思路,一般是分析主線程耗時,然后把這些耗時操作打包丟到IO線程中執(zhí)行。短期來看這不失是一種見效最快的優(yōu)化方法,但站在長期優(yōu)化的角度,也是性價比最低的一種方法。因為就性能優(yōu)化而言,我們不能僅考慮主線程的執(zhí)行,更多還要考慮對整體資源分配的優(yōu)化,尤其在并發(fā)場景,還要考慮鎖的影響。而 Retrofit 的問題正屬于后者。
我們在排查啟動速度時發(fā)現(xiàn),首頁接口請求的耗時總是高于接口平均值,導(dǎo)致首屏數(shù)據(jù)加載很慢。針對這個問題,我們使用 systrace 進(jìn)行了具體的分析,其中一次結(jié)果如下圖,
可以看到,這一次請求中有大段耗時是在等鎖,并沒有真正執(zhí)行網(wǎng)絡(luò)請求;如果觀察同一時間段的其他請求,也能發(fā)現(xiàn)類似現(xiàn)象。
那么這里的請求是在等什么鎖?配合 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)的實現(xiàn)原理這里就不再贅述,簡而言之 loadServiceMethod
這個方法的作用是:通過請求 interface 的入?yún)ⅰ⒎祷刂?、注解等信息,生?Converter、CallAdapter,并包裝成一個 ServiceMethod
返回,之后會通過這個 ServiceMethod
來發(fā)起真正的網(wǎng)絡(luò)請求。
從上述源碼也可以看到,ServiceMethod
是有內(nèi)存緩存的,但問題也正在這里—— ServiceMethod
的生成是在鎖內(nèi)完成的。
因此問題就變成,生成 ServiceMethod
為什么會有耗時?以云音樂的項目為例,各個團(tuán)隊都是使用 moshi 進(jìn)行 json 解析,大部分 meta 類是通過 kotlin 實現(xiàn),但也存在一定 kotlin、 Java 混用的情況。
這部分耗時主要來自 moshi 生成 JsonAdapter
。生成 JsonAdapter
需要遞歸遍歷 meta 類中的所有 field,過程中除了 kotlin 反射本身的效率和受并發(fā)的影響,還涉及 kotlin 的 builtins 機(jī)制,以及冷啟動過程中,類加載的耗時。
上述提到的幾個耗時點,每一個都可以單開一篇文章討論,篇幅原因這里一言以蔽之——冷啟動過程中,moshi 生成 JsonAdapter
是一個非常耗時的過程(而且這個耗時,跟使用 moshi 解析框架本身也沒有必然聯(lián)系,使用其他 json 解析框架,或多或少也會遇到類似問題)。
鎖+不可避免的耗時,引發(fā)的必然結(jié)果是:在冷啟動過程中,通過 Retrofit 發(fā)起的網(wǎng)絡(luò)請求,會部分劣化成一個串行過程。因此出現(xiàn) systrace 中呈現(xiàn)的結(jié)果,請求大部分時間在等鎖,這里等待的是前一個請求生成 ServiceMethod
的耗時,并以此類推耗時不斷向后傳遞。
嘗試優(yōu)化
既然定位到了原因,我們可以嘗試優(yōu)化了。
首先可以從 JsonAdapter
的生成效率入手,比如 moshi 原生就支持 @JsonClass
注解,通過 apt 在編譯時生成 meta的 解析器,從而顯著減少反射耗時。
二來,還是嘗試從根本上解決問題。其實從發(fā)現(xiàn)這個問題開始,我們就一直在思考這種寫法的合理性:首先加鎖肯定是為了訪問 serviceMethodCache
時的線程安全;其次,生成 ServiceMethod
的過程時,確實有一些反射操作內(nèi)部是有緩存的,如果發(fā)生并發(fā)是有一定性能損耗的。
但就我們的實際項目而言,不同 Retrofit interface 之間,幾乎沒有重疊的部分,反射操作都是以 Class 為單位在進(jìn)行。以此為基礎(chǔ),我們可以嘗試優(yōu)化一下這里的寫法。
那么,在不修改 Retrofit 源碼的基礎(chǔ)上,有什么方法可以修改請求流程嗎?
在云音樂的項目中,對于創(chuàng)建 Retrofit 動態(tài)代理,是有統(tǒng)一封裝的。也就是說,項目中除個別特殊寫法,絕大多數(shù)請求的創(chuàng)建,都是通過同一段封裝。只要我們改寫了 Retrofit 創(chuàng)建動態(tài)代理的流程,是不是就可以優(yōu)化掉前面的問題?
先觀察一下 Retrofit.create
方法的內(nèi)部實現(xiàn),可以發(fā)現(xiàn)大部分方法的可見性都是包可見的。眾所周知,在 Java 的世界里,包可見就等于 public,所以我們可以自己實現(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
這個私有成員 ,其他方法都可以直接訪問。這里把耗時的 ServiceMethod.parseAnnotations
方法從鎖中移出,改為對 interface Class 加鎖。(當(dāng)然這里激進(jìn)一點,也可以完全不加鎖,需要根據(jù)實際項目的情況來定)
修改之后,在啟動過程中重新抓取 systrace,已經(jīng)看不到之前等鎖的耗時了,首頁請求速度也回落到正常區(qū)間內(nèi)。
或許從這也能看出 kotlin 為什么要約束包可見性和泛型的上下邊界—— Java 原有的約束太弱,雖然方便了 hook,但同樣也說明代碼邊界更容易被破壞;同時這里也說明了代碼規(guī)范的重要性,只要保證統(tǒng)一的編碼規(guī)范,即使不使用什么“黑科技”,也能對代碼運行效率實現(xiàn)有效的管控。
不是AOP的AOP
到這里,我們會突然發(fā)現(xiàn)一個問題:既然我們都自己來實現(xiàn) Retrofit 的動態(tài)代理了,那不是意味著我們可以獲取到每一次請求的結(jié)果,乃至控制每一次請求的流程?
我們知道,傳統(tǒng)的接口緩存,一般是基于網(wǎng)絡(luò)庫實現(xiàn)的,比如在 okhttp 中的 CacheInterceptor
。
這種網(wǎng)絡(luò)庫層級緩存的缺點是:網(wǎng)絡(luò)請求畢竟是一個IO過程,它很難是面向?qū)ο蟮模徊⑶?Response 的 body 也不能被多次 read,在 cache 過程中,一般需要把數(shù)據(jù)深拷貝一次,有一定性能損耗。
比如,CacheInterceptor
中就有如下緩存相關(guān)的邏輯,在 body 被 read 的同時,再 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 } ... }
但如果我們能整個控制 Retrofit 請求,在動態(tài)代理這一層取到的是真正請求結(jié)果的 meta 對象,如果把這個對象緩存起來,連 json 解析的過程都可以省去;而且拿到真實的返回對象后,基于對象對數(shù)據(jù)做一些 hook 操作,也更加容易。
當(dāng)然,直接緩存對象也有風(fēng)險風(fēng)險,比如如果 meta 本身不是 immutable 的,會破壞請求的冪等性,這也是需要在后續(xù)的封裝中注意的,避免能力被濫用。
那么我們能在動態(tài)代理層拿到 Retrofit 的請求結(jié)果嗎?答案是肯定的。
我們知道 ServiceMethod.invoke
這個方法返回的結(jié)果,取決于 CallAdapter
的實現(xiàn)。Retrofit 有兩種原生的 CallAdpater
,一種是基于 okhttp 原生的 RealCall,一種是基于 kotlin 的 suspend 方法。
也就是說我們在通過 Retrofit 發(fā)起網(wǎng)絡(luò)請求時,一般只有如下兩種寫法(各個寫法其實都還有幾個不同的小變種,這里就不展開了)。
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 定義的返回值,其實就是動態(tài)代理那里的返回值,
對于返回值為 Call 的寫法 ,hook 邏輯類似下面的寫法,只要對回調(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) } }
但對于 suspend 方法呢?調(diào)試一下會發(fā)現(xiàn),當(dāng)請求定義為 suspend 方法時,返回值如下,
這里的 COROUTINE_SUSPENDED
是什么?
獲取 suspend 方法的返回值
要解釋 COROUTINE_SUSPENDED
是什么,稍微涉及協(xié)程的實現(xiàn)原理。我們可以先看看 Retrofit 本身在生成動態(tài)代理時,是怎么適配 suspend 方法的。
Retrofit 中對于 suspend 方法的返回,是通過 SuspendForBody
和 SuspendForResponse
這兩個 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 方法在編譯時,會被編譯成一個普通的 Java 方法,除了返回值被改寫成 Object,它與普通 Java 方法的另一個區(qū)別是,編譯器會在方法末尾插入一個入?yún)?,這個入?yún)⒌念愋途褪?Continuation
。
可以看到,一個 suspend 方法,在編譯之后,多了一個入?yún)ⅰ?/p>
kotlin 協(xié)程正是借助 Continuation
來向下傳遞協(xié)程上下文,再向上返回結(jié)果的;所以 suspend 方法真正的返回結(jié)果,一般不是通過方法本身的返回值來返回的。
此時,我們只要根據(jù)協(xié)程狀態(tài),任意返回一個占位的返回值即可,比如在 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
這個返回結(jié)果的來源。
回到前面 Retrofit 橋接 suspend 的代碼,如果我們寫一段類似下面的測試代碼,會發(fā)現(xiàn)這里的 context 與入?yún)?continuation.getContext 返回的是同一個對象。
val ret = runBlocking { val context = coroutineContext // 上一級協(xié)程的上下文 val ret = api.getUserDetail(uid) ret }
而 Retrofit 中的 KotlinExtensions.await
方法的實現(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é)合前面對 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)雅實現(xiàn)回調(diào)的語法糖,無論是在它的設(shè)計目的上,還是實現(xiàn)原理上,都是這樣。
所以,根據(jù)這個原理,我們也可以按類似如下方式 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); }
緩存請求結(jié)果
到這里已經(jīng)距離成功很近了,既然我們能拿到每一種請求類型的返回結(jié)果,再加億點點細(xì)節(jié),就意味著我們可以實現(xiàn)基于 Retrofit 的預(yù)加載、緩存封裝了。
Cache 封裝大差不差,主要是處理以下這條邏輯鏈路:
Request -> Cache Key -> Store -> Cached Response
因為我們只做內(nèi)存緩存,所以也不需要考慮數(shù)據(jù)的持久化,直接使用Map來管理緩存即可。
- 先封裝入?yún)?,我們在動態(tài)代理層以此入?yún)闃?biāo)志,觸發(fā)預(yù)加載或緩存機(jī)制,
sealed class LoadInfo( val id: String = "", // 請求id,默認(rèn)不需要設(shè)置 val timeout: Long // 超時時間 ) // 用來寫緩存/預(yù)加載 class CacheWriter( id: String = "", timeout: Long = 10000 ) : LoadInfo(id, timeout) // 用來讀緩存 class CacheReader( id: String = "", timeout: Long = 10000, val asCache: Boolean = false // 未命中時,是否要產(chǎn)生一個新的緩存,可供下一次請求使用 ) : LoadInfo(id, timeout)
- 插入 hook 代碼,處理緩存讀寫邏輯,(這里還需要處理并發(fā),基于協(xié)程比較簡單,這里就不展開了)
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)) { // 找到緩存,并且請求參數(shù)一致,則直接返回 return cache?.result as? T } } // 正常發(fā)起請求 val result = invoke(args) if (loadInfo is CacheWriter) { // 存緩存 map[id] = Cache(id, result) } return result }
這里使用 map 緩存請求結(jié)果,豐富一下緩存超時邏輯和前文提到的并發(fā)處理,即可投入使用。
- 定義請求,
我們可以利用 Retrofit 中的 @Tag
注解來傳入 LoadInfo
參數(shù),這樣不會影響真正的網(wǎng)絡(luò)請求。
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)用,只會發(fā)起一次真正的網(wǎng)絡(luò)請求,并且兩次返回結(jié)果是同一個對象,跟我們的預(yù)期一致。
相比傳統(tǒng)網(wǎng)絡(luò)緩存,這種寫法的好處,除了前面提到的減少 IO 開銷之外,幾乎可以做到零侵入,相比常規(guī)網(wǎng)絡(luò)請求寫法,只是多了一個入?yún)ⅲ欢覍懛ǚ浅:啙?,常?guī)寫法可能用到的預(yù)加載、超時、并發(fā)等大量的膠水代碼,都被隱藏在 Retrofit 動態(tài)代理內(nèi)部,上層業(yè)務(wù)代碼并不需要感知。當(dāng)然 AOP 帶來的便利性,與動態(tài)代理寫法的優(yōu)勢也是相輔相成。
One more thing?
云音樂內(nèi)部一直在推動 Backend-for-Frontend (BFF) 的建設(shè),BFF 與 Android 時下新興的 MVI 框架非常契合,借助 BFF 可以讓 Model 層變的非常簡潔。
但 BFF 本身對于服務(wù)端是一個比較重的方案,特別對于大型項目,需要考慮 RPC 數(shù)據(jù)敏感性、接口性能、容災(zāi)降級等一系列工程化問題,并且 BFF 在大型項目里一般也只用在一些非 P0 場景上。特別對于團(tuán)隊規(guī)模比較小的業(yè)務(wù)來說,考慮到這些成本后,BFF 本身帶來的便利幾乎全被抵消了。
那么有什么辦法可以不借助其他端實現(xiàn)一個輕量級的 BFF 嗎?相信你已經(jīng)猜到了,我們已經(jīng) AOP 了 Retrofit,實現(xiàn)網(wǎng)絡(luò)緩存可以看作是小試牛刀,那么實現(xiàn) BFF 也不過是更進(jìn)一步。
與前文借助動態(tài)代理層實現(xiàn)網(wǎng)絡(luò)緩存的思路類似,我們也選擇把 BFF 層隱藏在動態(tài)代理層中。
可以先梳理一下大概的思路:
- 使用注解定位需要 BFF 的 Retrofit 請求;
- 使用 apt 生成 BFF 需要的膠水代碼,將多個普通 Retrofit 請求,合并成一個 BFF 請求;
- 通過 AGP Transform 收集所有 BFF 生成類,建立映射表;
- 在 Retrofit 動態(tài)代理層,借助映射表,把請求實現(xiàn)替換成生成好的 BFF 代碼。
實際上,目前主流的各種零入侵代碼框架(比如路由、埋點、數(shù)據(jù)庫、啟動框架、依賴注入等),都是用類似的思路實現(xiàn)的,我們觸類旁通即可。
這里為對此思路還不太熟悉的小伙伴,簡單過一遍整體設(shè)計流程,
首先,定義需要的注解,用 @BFF
來標(biāo)識需要進(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ù)據(jù)預(yù)處理的邏輯(在大部分簡單場景下,是不需要使用此注解的,因此把這部分拆分成一個單獨的注解,以降低學(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 請求一樣,只是方法上額外加一個 @BFF
注解作為 apt 的標(biāo)識,
@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
注解,與前面的請求做關(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請求,
@JvmSuppressWildcards interface BFFApi { @BFF @POST("path/all") // 在這個方案中,BFF api的path沒有實際意義 suspend fun getAll( @Field("position") position: Int, @Field("id") id: Int ): MyMeta }
通過上述注解,在編譯時生成膠水代碼如下,(這里生成代碼的邏輯其實跟依賴注入是完全一致的,囿于篇幅就不詳細(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 } }
在使用時,直接把 BFF api 當(dāng)作一個普通的接口調(diào)用即可,Retrofit 內(nèi)部會完成替換。
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è)計接口緩存封裝類似,可以做到零侵入、零膠水代碼,使用起來非常簡潔、直接。
總結(jié)
至此,我們回顧了對于 Retrofit 的性能問題,從發(fā)現(xiàn)問題到解決問題的過程,并簡單講解了我們是怎么進(jìn)一步開發(fā) Retrofit 的潛力,以及常用的低侵入框架的設(shè)計思路。文章涉及的基于 Retrofit 的緩存、BFF 設(shè)計,更多是拋磚引玉,而且不僅僅是 Retrofit,大家掌握類似的設(shè)計思路之后,可以把它們應(yīng)用在更多場景中,對于日常的開發(fā)、編碼效率提升和性能優(yōu)化,都會很有幫助,希望對各位能有所啟發(fā),更多關(guān)于魔改Retrofit實例的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android PullToRefreshLayout下拉刷新控件的終結(jié)者
這篇文章主要介紹了Android自定義控件實戰(zhàn)中下拉刷新控件終結(jié)者PullToRefreshLayout的實現(xiàn)方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-03-03Ubuntu中為Android系統(tǒng)上編寫Linux內(nèi)核驅(qū)動程序?qū)崿F(xiàn)方法
本文主要介紹在Ubuntu 上為Android系統(tǒng)編寫Linux內(nèi)核驅(qū)動程序, 這里對編寫驅(qū)動程序做了詳細(xì)的說明,對研究Android源碼和HAL都有巨大的幫助,有需要的小伙伴可以參考下2016-08-08詳解Retrofit2.0 公共參數(shù)(固定參數(shù))
這篇文章主要介紹了Retrofit2.0 公共參數(shù)(固定參數(shù)),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04android webview 簡單瀏覽器實現(xiàn)代碼
android webview 簡單瀏覽器實現(xiàn)代碼,需要的朋友可以參考一下2013-05-05Android使用動畫動態(tài)添加商品進(jìn)購物車
這篇文章主要為大家詳細(xì)介紹了Android使用動畫動態(tài)添加商品進(jìn)購物車,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-06-06詳解Android開發(fā)錄音和播放音頻的步驟(動態(tài)獲取權(quán)限)
這篇文章主要介紹了詳解Android開發(fā)錄音和播放音頻的步驟(動態(tài)獲取權(quán)限),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08