亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

詳解如何魔改Retrofit實(shí)例

 更新時(shí)間:2022年11月15日 15:41:53   作者:lizongjun  
這篇文章主要為大家介紹了詳解如何魔改Retrofit實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

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 方法的返回,是通過 SuspendForBodySuspendForResponse 這兩個(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)文章

最新評(píng)論