这次看点不一样之Retrofit和OkHttp

3,333 阅读21分钟

导语

为什么会选择这样一个话题来展开讨论?其实从Square开源了这两个优秀的网络库到现在已经有很长时间了,各种源码解析和踩坑指南到处都是,而本篇的出发点是:通过几个有趣的实战问题开始,挖掘Retrofit和OkHttp的设计理念,并挑选一些代码进行阅读,相信读完这一篇,大家对Retrofit 和 Okhttp的细节会熟稔于心


2020-09-02 21:19:48补充:

考虑到写这么长的文章,而且代码很多,读起来会很累,又补充了Demo github.com/leobert-lan…; 如果您觉得这些内容还不错,烦请给文章和项目点个赞支持下,苦兮兮的。


如何为请求设置独立的超时时间

okhttp中提供了一些设置超时时间的API,例如:

后三个是严格遵守HTTP协议中的超时含义的,callTimeout是okhttp给客户端程序提供的一个全局超时时间,一个HTTP或者HTTPS请求,从DNS解析开始,到连接,到发请求体,到服务端响应,到服务端返回数据;以及可能出现的重定向,整个过程都需要在这个时间内完成,否则就算超时了,客户端会主动放弃或者断开连接。一般来说,我们不会去动态的约定这个时间(按照okhttp的默认设定是无限长),我们先看后三个。这三个超时具体的含义简单带过下:

  • 连接超时,获知目标ip(如果直接指定了ip那就不需要dns解析)后,按照三次握手建立TCP连接(SSL或者TLS先搁置不谈),如果这个连接握手环节超时了那就gg了,
  • 写超时,这个写是朝服务端写,即发送TCP报文,我们知道报文是封装成数据帧(frame)发送的,接收方接收后会有ack,这个写超时时间是对于每一帧而言的,虽然发送失败了会有重发机制,但超时了也会gg,
  • 读超时,和写超时类似,不过是对方(服务端)往自己(客户端)发送数据的时候。

因为业务中,业务的重要程度不同,以及服务端处理不同业务的复杂度不同,我们需要为不同级别的业务设定灵活的超时时间。

以往我们可能会看到一种不太负责任的做法,就是创建多个okhttp client实例和retrofitClient实例,对应到不同的超时时间,这个做法就不多评价了。

阅读过代码,我们发现retrofit的切面设计中,是可以为每个请求单独设置这三个时间的:

interface Chain {
    fun request(): Request

    fun withConnectTimeout(timeout: Int, unit: TimeUnit): Chain

    fun withReadTimeout(timeout: Int, unit: TimeUnit): Chain

    fun withWriteTimeout(timeout: Int, unit: TimeUnit): Chain
    
    //无关代码略去
  }

具体做法自然就是大家熟知的拦截器:

class OverrideTimeoutInterceptor : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()

        var newChain = chain

        originalRequest.header(Consts.OVERRIDE_CONNECT_TIMEOUT)?.toIntOrNull()?.let {
            if (it > 0)
                newChain = newChain.withConnectTimeout(it, TimeUnit.MILLISECONDS)
        }

        originalRequest.header(Consts.OVERRIDE_WRITE_TIMEOUT)?.toIntOrNull()?.let {
            if (it > 0)
                newChain = newChain.withWriteTimeout(it, TimeUnit.MILLISECONDS)
        }

        originalRequest.header(Consts.OVERRIDE_READ_TIMEOUT)?.toIntOrNull()?.let {
            if (it > 0)
                newChain = newChain.withReadTimeout(it, TimeUnit.MILLISECONDS)
        }

        return newChain.proceed(originalRequest)
    }
}

然后根据我们熟知的Retrofit工作原理,会通过反射方式创建动态代理,对我们定义的网络接口信息是一清二楚,并且在创建出可执行对象的过程中,提供了拦截器让我们对创建过程进行“干涉”,所以我们可以直接利用这个机制,将超时时间设置被Retrofit读取到,从上面我们看到,我们是将这些信息放在Header中的,如果没有特殊的要求,我们可以不用读取后移除,如果放在QueryString或者Field中,那还是要处理掉的,避免出现问题。

我们再往下看一看源码,调用链的类是 okhttp3.internal.http.RealInterceptorChain 设置的超时时间最终被应用在这几处:

  • okhttp3.internal.connection.RealConnection#newCodec
  • okhttp3.internal.connection.ExchangeFinder
  • okhttp3.internal.http2.Http2ExchangeCodec#writeRequestHeaders

那么前面提到的CallTimeout,如果铁了心非要干他丫的,源码是否给了我们这个机会呢,其实这个问题来自于我参与的Okhttp项目中一个issue的讨论:链接

这个全局超时的设计实现,在不同的Okhttp版本中可能有所不同,在4.3.1版本中,他是这样的:

class Transmitter(
  private val client: OkHttpClient,
  private val call: Call
) {
  private val connectionPool: RealConnectionPool = client.connectionPool.delegate
  private val eventListener: EventListener = client.eventListenerFactory.create(call)
  private val timeout = object : AsyncTimeout() {
    override fun timedOut() {
      cancel()
    }
  }.apply {
    timeout(client.callTimeoutMillis.toLong(), MILLISECONDS)
  }
  //...无关代码略去
}

而这个控制超时的控制器被RealCall所依赖:

internal class RealCall private constructor(
  val client: OkHttpClient,
  /** The application's original request unadulterated by redirects or auth headers. */
  val originalRequest: Request,
  val forWebSocket: Boolean
) : Call {
  /**
   * There is a cycle between the [Call] and [Transmitter] that makes this awkward.
   * This is set after immediately after creating the call instance.
   */
  private lateinit var transmitter: Transmitter
  //... 无关代码略去
}

这个RealCall就是Okhttp中描述请求的类。

阅读代码会发现,okhttp在编码上并没有留给我们直接修改RealCall的机会,哪怕是这个类对象的实例创建也是相对封闭的,哪怕继承OkhttpClient并重现实现工厂,也只能针对request做一下小修改,并不能大动干戈:

static class OkHttpClient2 extends OkHttpClient {

    public OkHttpClient2(@NotNull Builder builder) {
        super(builder);
    }
    
    // Prepares the [request] to be executed at some point in the future. 
    //
	//  override fun newCall(request: Request): Call {
    //		return RealCall.newRealCall(this, request, forWebSocket = false)
  	//	}

    @NotNull
    @Override
    public Call newCall(@NotNull Request request) {
    	//拷贝了父类是如何创建RealCall的,见上,因为internal修饰,
        //并不允许我们自行创建RealCall
        
        return super.newCall(request);
    }

    static OkHttpClient2 reBuilder(@NonNull OkHttpClient client) {
        return new OkHttpClient2(client.newBuilder());
    }
}

最终我们得出结论:OkHttp并没有提供一个正常的方式让我们去“动态的”修改这个全局的超时时间,也许这背离了他们的设计。

square的 Jesse Wilson也给了官方解释

Yep. We don’t have a mechanism to adjust a timeout that’s already started. By the time the interceptor runs we’re already subject to the call timeout.

并没有提供这样一个机制,可以在计时开始后动态调整计时。并且一定走进了拦截器逻辑,其实计时已经开始了。

这一点我们可以直接在RealCall的代码中得出结论:

 override fun execute(): Response {
    synchronized(this) {
      check(!executed) { "Already Executed" }
      executed = true
    }
    transmitter.timeoutEnter()
    transmitter.callStart()
    try {
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      client.dispatcher.finished(this)
    }
  }

  override fun enqueue(responseCallback: Callback) {
    synchronized(this) {
      check(!executed) { "Already Executed" }
      executed = true
    }
    transmitter.callStart()
    client.dispatcher.enqueue(AsyncCall(responseCallback))
  }

所以,如果非要修改,要么修改Okhttp的源码,要么像我在issue讨论中说的,用反射处理。

补,2020-10-28 09:18:20:

又重新阅读了一下部分代码和issue中的内容。之前存在一些误解,okhttp并不提供一个机制去修改一个已经开始处理的请求的"总超时",因为它是从开始就计算的。

而对于一个没有开始处理的请求,还是可以动态修改的。API中暴露了修改call的timeout的方式,我们只需要在请求开始处理前得到call对象和对call配置的timeout时长。目前有两个比较安全的做法:

callAdapter机制,可以直接根据注解处理,哪怕是自定义的注解

OkhttpClient生成call阶段处理,需要将timeout写入header之类

网络层优化的判断依据--更加精细的统计工具

相信经过上一个问题,大家又仔细温习了一下Okhttp的源码,这时候应该又一次记住了:okhttp中如何创建Request、如何创建RealCall、如果利用Chain去描述切面并做职责划分、封装Response,并如何对请求做管理的。

这时候我们遇到了一个新的问题:业务线需要从技术层面对网络层进行优化,口说无凭,我们需要有一套机制来收集:请求到底花了多少事件,自身的处理业务花了多少时间;而服务端也需要采集业务响应所耗费的时间。

于是我们需要一套机制去采集网络层花费时间的关键信息。

我们可以想象一下,从用户触发一个用户行为,到最终按照接口返回数据在UI层反馈给用户,大体会经历以下几步:

  • 前置业务:用户输入和场景校验
  • 创建API请求
  • 切换到工作线程执行API请求
  • 解析API返回结果并切换到UI线程反馈结果给用户

这里前置业务我们基本认为它和网络层没啥关系,也不算啥太耗时的业务,姑且认为它不需要针对时间做优化,也不做时长采集;

创建API请求这一块,如果忽略掉反射耗费的时间以及认为Retrofit是不可替换了话,那么我们也不需要太过于严苛的对待它;

切换工作线程并执行API请求,这里可能会遇到线程池等待,域名或者IP最大连接数量达到上限而等待;以及我们解析dns,创建连接,tls握手验证等环节的耗时也是需要关心的。

返回数据解析和结果反馈,是需要考量下json数据(应该不会有xml了吧)是否耗时较多,并且严格审视下是否有不恰当的主线程耗时行为(例如数据写入磁盘做缓存,主线程进行大量的对象创建和属性copy等)

我们先看和网络请求直接相关的部分

在上一个问题中,我们简单接触了一个类:Transmitter,并且发现他持有一个类的对象依赖:EventListener。直接看一下源码,篇幅较长,我把注释都删了:

abstract class EventListener {

  open fun callStart(call: Call) {
  }

  open fun proxySelectStart(call: Call,url: HttpUrl) {
  }

  open fun proxySelectEnd(call: Call,url: HttpUrl,proxies: List<@JvmSuppressWildcards Proxy>) {
  }

  open fun dnsStart(
    call: Call,
    domainName: String
  ) {
  }

  open fun dnsEnd(
    call: Call,
    domainName: String,
    inetAddressList: List<@JvmSuppressWildcards InetAddress>
  ) {
  }

  open fun connectStart(
    call: Call,
    inetSocketAddress: InetSocketAddress,
    proxy: Proxy
  ) {
  }

  open fun secureConnectStart(
    call: Call
  ) {
  }

  open fun secureConnectEnd(
    call: Call,
    handshake: Handshake?
  ) {
  }

  open fun connectEnd(
    call: Call,
    inetSocketAddress: InetSocketAddress,
    proxy: Proxy,
    protocol: Protocol?
  ) {
  }

  open fun connectFailed(
    call: Call,
    inetSocketAddress: InetSocketAddress,
    proxy: Proxy,
    protocol: Protocol?,
    ioe: IOException
  ) {
  }

  open fun connectionAcquired(
    call: Call,
    connection: Connection
  ) {
  }

  open fun connectionReleased(
    call: Call,
    connection: Connection
  ) {
  }

  open fun requestHeadersStart(
    call: Call
  ) {
  }

  open fun requestHeadersEnd(call: Call, request: Request) {
  }

  open fun requestBodyStart(
    call: Call
  ) {
  }

  open fun requestBodyEnd(
    call: Call,
    byteCount: Long
  ) {
  }

  open fun requestFailed(
    call: Call,
    ioe: IOException
  ) {
  }

 
  open fun responseHeadersStart(
    call: Call
  ) {
  }

  open fun responseHeadersEnd(
    call: Call,
    response: Response
  ) {
  }

  open fun responseBodyStart(
    call: Call
  ) {
  }

  open fun responseBodyEnd(
    call: Call,
    byteCount: Long
  ) {
  }

  open fun responseFailed(
    call: Call,
    ioe: IOException
  ) {
  }

  open fun callEnd(
    call: Call
  ) {
  }

  open fun callFailed(
    call: Call,
    ioe: IOException
  ) {
  }

  interface Factory {
    fun create(call: Call): EventListener
  }

  companion object {
    @JvmField
    val NONE: EventListener = object : EventListener() {
    }
  }
}

这里呢,我们先关注两个时间,一是EventListener对象的创建时间,二是callStart方法被调用的时间。

我们还有印象,Transmitter对象创建的时候,通过工厂创建了EventListener对象:

class Transmitter(private val client: OkHttpClient,private val call: Call) {
  private val eventListener: EventListener = client.eventListenerFactory.create(call)
  //...
}

在Transmitter#callStart时,调用了EventListener的callStart:

fun callStart() {
  this.callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()")
  eventListener.callStart(call)
}

我们前面也看到源码,他是RealCall被调用execute()或者enqueue()时被立即调用的,那么线程池等待时间、连接上限等待等时间只会发生在callStart之后,而dns解析等时间,可以从EventListener中计算得出。

而Transmitter是在RealCall创建时一同创建的,如果没有啥“意外”情况,EventListener创建的时间和callStart的时间应该非常接近,因为Retrofit的使用中,仅仅是做了一些简单的前置业务,创建RealCall之后随即调用了execute或者enqueue;如果这个时间差较大,就值得分析下是否存在“魔改”实现的业务。

而为请求添加自定义的EventListener,只需要实现工厂并利用API注册:

okhttp3.OkHttpClient.Builder#eventListenerFactory()

假定我们已经实现好了一个细致的EventListener,继续往下,假定一切顺利,我们得到了服务端的返回数据,业务处理事件如何优雅的处理呢,我们分三种情况简单看下

  • 仅使用Retrofit的Callback机制,这个没啥好说的,从基类动手脚
  • 使用了RxJava,可以从Adapter入手,从Observable切入
  • 使用了kotlin协程(Retrofit内部的支持),可以从Flow切入

这里我提供一下我们项目中使用RxJava的处理情况,改写了Square提供的Adapter,仅供参考一二: 注意 RetrofitClient.netTracker?.apiRequestInfoTrack(call.request())即可

internal class CallEnqueueObservable<T>(private val originalCall: Call<T>) : Observable<Response<T>?>() {
    override fun subscribeActual(observer: Observer<in Response<T>?>) {
        // Since Call is a one-shot type, clone it for each new observer.
        val call = originalCall.clone()
        val callback = CallCallback(call, observer)
        observer.onSubscribe(callback)
        if (!callback.isDisposed) {
            call.enqueue(callback)
        }
    }

    private class CallCallback<T> internal constructor(private val call: Call<*>, private val observer: Observer<in Response<T>?>)
        : Disposable, Callback<T> {

        @Volatile
        private var disposed = false
        var terminated = false
        override fun onResponse(call: Call<T>, response: Response<T>) {
            if (disposed) return
            try {
                val body = response.body()
                if (body is BaseData) {
                    body.callHolder = call
                }

                observer.onNext(response)
                if (!disposed) {
                    terminated = true
                    observer.onComplete()
                }
            } catch (t: Throwable) {
                if (terminated) {
                    RxJavaPlugins.onError(t)
                } else if (!disposed) {
                    try {
                        observer.onError(t)
                    } catch (inner: Throwable) {
                        Exceptions.throwIfFatal(inner)
                        RxJavaPlugins.onError(CompositeException(t, inner))
                    }
                }
            }

            try {
                RetrofitClient.netTracker?.apiRequestInfoTrack(call.request())
            } catch (e: Exception) {
                L.e(e)
            }
        }

        override fun onFailure(call: Call<T>, t: Throwable) {
            if (call.isCanceled) return
            try {
                observer.onError(t)
            } catch (inner: Throwable) {
                Exceptions.throwIfFatal(inner)
                RxJavaPlugins.onError(CompositeException(t, inner))
            }
        }

        override fun dispose() {
            disposed = true
            call.cancel()
        }

        override fun isDisposed(): Boolean {
            return disposed
        }
    }

}

internal class CallExecuteObservable<T>(private val originalCall: Call<T>) : Observable<Response<T>?>() {
    override fun subscribeActual(observer: Observer<in Response<T>?>) {
        // Since Call is a one-shot type, clone it for each new observer.
        val call = originalCall.clone()
        val disposable = CallDisposable(call)
        observer.onSubscribe(disposable)
        if (disposable.isDisposed) {
            return
        }
        var terminated = false
        try {
            val response = call.execute()
            if (!disposable.isDisposed) {
                val body = response.body()
                if (body is BaseData) {
                    body.callHolder = originalCall //这个originalCall在没有使用时内部的call和request都不会创建,
                                                   // 为了避免重新测原有业务,不做修改了,但是如果要使用它的request和call信息时注意其时效性
                }
                observer.onNext(response)
            }
            if (!disposable.isDisposed) {
                terminated = true
                observer.onComplete()
            }
        } catch (t: Throwable) {
            Exceptions.throwIfFatal(t)
            if (terminated) {
                RxJavaPlugins.onError(t)
            } else if (!disposable.isDisposed) {
                try {
                    observer.onError(t)
                } catch (inner: Throwable) {
                    Exceptions.throwIfFatal(inner)
                    RxJavaPlugins.onError(CompositeException(t, inner))
                }
            }
        }

        try {
            RetrofitClient.netTracker?.apiRequestInfoTrack(call.request())
        } catch (e: Exception) {
            L.e(e)
        }
    }

    private class CallDisposable internal constructor(private val call: Call<*>) : Disposable {

        @Volatile
        private var disposed = false
        override fun dispose() {
            disposed = true
            call.cancel()
        }

        override fun isDisposed(): Boolean {
            return disposed
        }
    }
}

ok,这时候我们思考一个问题:出于实际情况,我们被迫将业务耗时的采集和请求开始的时间已经分开了,如何将他们再结合起来呢?,直白点说,如何在执行时获知请求开始时间? RetrofitClient.netTracker?.apiRequestInfoTrack(call.request())

这又开始考验我们对源码的熟悉程度了。我们都使用过Square为Retrofit提供的日志拦截器吧,是不是很好奇,这个日志拦截器里面为什么可以打印出调用的方法原型,类似以下形式:

[Method:get] (url),tag=[package.UserApi#getUserInfo(String:uid)]

其实这里打印的是okhttp的Request信息,我们看下这个类:

class Request internal constructor(
  @get:JvmName("url") val url: HttpUrl,
  @get:JvmName("method") val method: String,
  @get:JvmName("headers") val headers: Headers,
  @get:JvmName("body") val body: RequestBody?,
  internal val tags: Map<Class<*>, Any>
) {

  private var lazyCacheControl: CacheControl? = null

  val isHttps: Boolean
    get() = url.isHttps

  fun header(name: String): String? = headers[name]

  fun headers(name: String): List<String> = headers.values(name)

  /**
   * Returns the tag attached with `Object.class` as a key, or null if no tag is attached with
   * that key.
   *
   * Prior to OkHttp 3.11, this method never returned null if no tag was attached. Instead it
   * returned either this request, or the request upon which this request was derived with
   * [newBuilder].
   */
  fun tag(): Any? = tag(Any::class.java)
  
  fun <T> tag(type: Class<out T>): T? = type.cast(tags[type])

//...略去无关部分
  override fun toString() = buildString {
    append("Request{method=")
    append(method)
    append(", url=")
    append(url)
    if (headers.size != 0) {
      append(", headers=[")
      headers.forEachIndexed { index, (name, value) ->
        if (index > 0) {
          append(", ")
        }
        append(name)
        append(':')
        append(value)
      }
      append(']')
    }
    if (tags.isNotEmpty()) {
      append(", tags=")
      append(tags)
    }
    append('}')
  }

没错,就是创建RealCall的那个Request,它可以设置一些tag,而Retrofit创建ok的Request时,往里面加入了tag信息,参考:retrofit2.RequestFactory#create(Object[] args),加入了一个Invocation的tag:

public final class Invocation {
  public static Invocation of(Method method, List<?> arguments) {
    checkNotNull(method, "method == null");
    checkNotNull(arguments, "arguments == null");
    return new Invocation(method, new ArrayList<>(arguments)); // Defensive copy.
  }

  private final Method method;
  private final List<?> arguments;

  /** Trusted constructor assumes ownership of {@code arguments}. */
  Invocation(Method method, List<?> arguments) {
    this.method = method;
    this.arguments = Collections.unmodifiableList(arguments);
  }

  public Method method() {
    return method;
  }

  public List<?> arguments() {
    return arguments;
  }

  @Override public String toString() {
    return String.format("%s.%s() %s",
        method.getDeclaringClass().getName(), method.getName(), arguments);
  }
}

不过可惜,retrofit的RequestFactory是Final修饰的,并且不是实现的接口,所以我们无法直接借助Retrofit添加我们需要的tag,回想一下,我们上面提到了okhttp的RealCall创建,他是有一个Factory的,而且OkHttpClient实现了这个工厂,我们覆写一下即可:

static class OkHttpClient2 extends OkHttpClient {

    public OkHttpClient2(@NotNull Builder builder) {
        super(builder);
    }

    @NotNull
    @Override
    public Call newCall(@NotNull Request request) {
        // 看了下代码没有享元模式, 这样改没啥风险,但是也没啥用,
        // 底层基本都是retrofit创建的Proxy模式的Call,真正使用时才通过RequestFactory创建request、继而创建ok的call,
        // 即取出来的tag应该都是null
        NetLoggingEventListener.RetrofitRequest tag = null;
        try {
            tag = request.tag(NetLoggingEventListener.RetrofitRequest.class);
        } catch (Exception e) {
            L.e(e);
        }

        if (tag == null)
            request = request.newBuilder().tag(NetLoggingEventListener.RetrofitRequest.class,
                    new NetLoggingEventListener.RetrofitRequest(System.currentTimeMillis())).build();
        return super.newCall(request);
    }

    static OkHttpClient2 reBuilder(@NonNull OkHttpClient client) {
        return new OkHttpClient2(client.newBuilder());
    }
}
class RetrofitRequest(val timeMillis: Long) {
    override fun toString(): String {
        return "RetrofitRequest(timeMillis=$timeMillis)"
    }
}

我们定义这样一个类来描述我们目前需要以及以后可能需要的信息,并将其作为tag写入okhttp的Request中。并且可以通过fun <T> tag(type: Class<out T>): T? = type.cast(tags[type])API将其取出。

其实看到这里,大家已经完全可以自己动手完成这样一个时长采集工具了

这里还想再补充一点,笔者当时没有注意的一个细节:

我们看过代码都知道,Retrofit中,又定义了Call接口、OkHttpCall实现类,这个OkHTTPCall的实现类可以认为是遵循Facade模式以及Proxy模式设计思想的,在没有真正用于请求时,不会创建OkHttp的call,创建的关键代码:

private okhttp3.Call createRawCall() throws IOException {
  okhttp3.Call call = callFactory.newCall(requestFactory.create(args));
  if (call == null) {
    throw new NullPointerException("Call.Factory returned null.");
  }
  return call;
}

而这个方法,仅在三处调用:获取Request实例,execute,enqueue; 后两个都是真实要发起请求的,第一处,如果在请求前调用,往往是打算做一些修改。

而配合RxJava使用时,我们注意这种情况:CallExecuteObservable,代码上面有贴过,这里贴关键的:

 // Since Call is a one-shot type, clone it for each new observer.
  val call = originalCall.clone()
  val disposable = CallDisposable(call)

担心流处理中可能存在的重试以及复用问题,对originalCall进行了clone,所以我们必须使用clone出来的call对象去获取Request再获取tag,否则,使用originalCall则会遇到一个问题,Request是真正使用到时才开始创建,并没有对应到请求开始的时间😂。

全栈支持人机校验,Square给我们提供的 RxJava&协程 Adapter丢弃了什么

这次我们又遇到了一个新问题:某些接口被单ip访问频次过高,可能是恶意行为,需要添加人机校验环节,并且要求在校验完成后携带参与重发原请求。

经过和服务端的一番商榷后,我们确定用统一的错误码来表达需要进行人机校验。

其实这个问题看起来很清晰,发现response了特定的错误码,即弹出人机校验,注册回调,完成后得到相关参数,添加到原先的Request中,重新发起请求。

在处理这个问题时,我们没有像上一个问题中那样,直接到Observable的处进行处理,因为这属于业务级,所以我们在请求的响应回调基类中处理,当然,如果你使用的是官方的Adapter,也并没有办法直接处理Observable。

此时我们先将这个问题搁置,回到标题,Square给我们提供的 RxJava&协程 Adapter丢弃了什么?

按照我们对RxJava和协程flow的了解,这两者都是基于流的响应式订阅处理框架,而流的源,其实都是Okhttp的Response,或者更直白的说,是将response的body解析成的对象。因为业务特性,绝大多数情况下我们只需要关心返回的结果,而不再需要关心请求本身。我们再看一次这段代码:

internal class CallExecuteObservable<T>(private val originalCall: Call<T>) : Observable<Response<T>?>() {
    override fun subscribeActual(observer: Observer<in Response<T>?>) {
        // Since Call is a one-shot type, clone it for each new observer.
        val call = originalCall.clone()
        val disposable = CallDisposable(call)
        observer.onSubscribe(disposable)
        if (disposable.isDisposed) {
            return
        }
        var terminated = false
        try {
            val response = call.execute()
            if (!disposable.isDisposed) {
                val body = response.body()
                if (body is BaseData) {
                    body.callHolder = originalCall //这个originalCall在没有使用时内部的call和request都不会创建,
                                                   // 为了避免重新测原有业务,不做修改了,但是如果要使用它的request和call信息时注意其时效性
                }
                observer.onNext(response)
            }
            if (!disposable.isDisposed) {
                terminated = true
                observer.onComplete()
            }
        } catch (t: Throwable) {
            Exceptions.throwIfFatal(t)
            if (terminated) {
                RxJavaPlugins.onError(t)
            } else if (!disposable.isDisposed) {
                try {
                    observer.onError(t)
                } catch (inner: Throwable) {
                    Exceptions.throwIfFatal(inner)
                    RxJavaPlugins.onError(CompositeException(t, inner))
                }
            }
        }

        try {
            RetrofitClient.netTracker?.apiRequestInfoTrack(call.request())
        } catch (e: Exception) {
            L.e(e)
        }
    }
}

如果和官方的Adapter对比一下的话,我们增加了一段内容:

if (body is BaseData) {
  body.callHolder = originalCall //这个originalCall在没有使用时内部的callrequest都不会创建,
                                 // 为了避免重新测原有业务,不做修改了,但是如果要使用它的requestcall信息时注意其时效性
}

附BaseData:

public class BaseData {

    /**
     * 因为配合RxJava使用,需要修改的底层过多,我们留一个引用来存储原来的Call
     */
    @Nullable
    public transient Call<?> callHolder;

    @SerializedName("code")
    public String code;

    @SerializedName(value = "msg")
    public String msg;

    @Nullable
    @SerializedName("ext")
    public JsonElement ext;
}

没错,这只是一个取巧的改法,我们没有改变源,毕竟一个已经上线的项目,对于底层修改的兼容性要求很高,如果是刚开始搭建的项目,倒是可以考虑直接修改事件源,当然,这会让后续的编码变得很糟糕

此时我们就可以在请求结果的流处理中获得原始的请求了。我们先忽略掉人机校验弹窗的事情,假设我们已经通过回调方式获得了人机校验的行为参数。

我们将:

  • 从原始的call得到原始okhttp3的Request对象
  • Request遵循Builder设计模式,我们可以很轻松的将Request对象转变为Builder并拷贝原始的对应属性,继而添加人机校验的参数。
  • 利用Retrofit的CallFactory重新创建Call,其实这个工厂就是OkHttpClient,我们上面提到过,他实现了Okhttp中CallFactory
  • 确保主线程中再次调用onStart,让UI层显得优雅一点,但是务必要注意原先的业务中onStart()是否有必须成对出现的逻辑
  • 重新发送请求并使用包装类,复用当前的请求回调

参考下面的代码:

 protected void onHumanCheck(@NonNull final retrofit2.Call oldCall, final int code, final Result<T> result) {
        humanCheckRef = new WeakReference<Function1<HumanCheckParam, Unit>>(new Function1<HumanCheckParam, Unit>() {
            @Override
            public Unit invoke(HumanCheckParam humanCheckParam) {
                if (isDisposed())
                    return null;

                if (humanCheckParam == null) {
                    onFailureCode(code, result);
                    return null;
                }

                try {
                    Request request = oldCall.request();

                    Call call = RetrofitClient.getRetrofit().callFactory()
                            .newCall(request.newBuilder()
                                            .url(request.url().newBuilder()
                                                    .setQueryParameter("scene", null2Empty(humanCheckParam.getScene()))
                                                    .setQueryParameter("sig", null2Empty(humanCheckParam.getSig()))
                                                    .setQueryParameter("nc_token", null2Empty(humanCheckParam.getNcToken()))
                                                    .setQueryParameter("csessionid", null2Empty(humanCheckParam.getSessionid()))
                                                    .build())
                                            //header存在长度限制,还是使用queryString
//                                    .addHeader("scene", null2Empty(humanCheckParam.getScene()))
//                                    .addHeader("sig", null2Empty(humanCheckParam.getSig()))
//                                    .addHeader("ncToken", null2Empty(humanCheckParam.getNcToken()))
//                                    .addHeader("sessionId", null2Empty(hu
//                                    manCheckParam.getSessionid()))
                                            .build()
                            );
                    ReComposeOkHttpCallBack.Companion.getMainHandler().post(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                onStart();
                            } catch (Exception ignore) {
                            }
                        }
                    });
                    if (ReComposeOkHttpCallBack.Companion.canHandle(oldCall)) {
                        call.enqueue(new ReComposeOkHttpCallBack<T>(oldCall, RetrofitSubscriber.this));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

                return null;
            }

            private String null2Empty(@Nullable String nullable) {
                if (nullable == null)
                    return "";
                return nullable;
            }

        });
        HumanChecker.INSTANCE.requestCheck(null, humanCheckRef);
    }
    
    
///////

class ReComposeOkHttpCallBack<T>(private val originalCall: Call<Result<T>>, private val subscriber: RetrofitSubscriber<T>) : okhttp3.Callback {
    companion object {
        val mainHandler: Handler = Handler(Looper.getMainLooper())
        fun canHandle(originalCall: Call<*>): Boolean {
            return originalCall is OkHttpCall
        }
    }

    override fun onFailure(call: okhttp3.Call, e: IOException) {
        mainHandler.post {
            subscriber.onFailure(RetrofitException(RetrofitException.NETWORK, RetrofitException.ERROR_NONE_NETWORK, e))
        }
    }

    override fun onResponse(call: okhttp3.Call, rawResponse: okhttp3.Response) {
        val response: Response<Result<T>>
        try {
            if (originalCall is OkHttpCall) {
                response = originalCall.parseResponse(rawResponse)

                mainHandler.post {
                    try {
                        subscriber.onNext(response.body())
                    } catch (e: Exception) {
                        subscriber.onFailure(RetrofitException(RetrofitException.UNKNOWN, "未知错误", e))
                    } finally {
                        try {
                            subscriber.onComplete()
                        } catch (e2: Exception) {

                        }
                    }
                }
            }
        } catch (e: Throwable) {
            mainHandler.post {
                subscriber.onFailure(RetrofitException(RetrofitException.UNKNOWN, "内部错误", e))
            }
        }
    }
}

总结

回归到导语立的FLAG,这一篇我们试图通过几个实战问题,和大家一同挖掘下Retrofit和OkHttp的设计意图,并记住一些细节部分。

那就先看看OkHttp吧,先刨去协议实现部分和OKio的部分,OkHttp使用了Request、Response和Call来描述请求中的主要对象和行为。

Call是对行为的抽象和封装,他的创建依赖于Request,通过执行可以得到Response,并且遵循工厂模式和原型模式设计。

Request是对请求的封装,包含请求动词、url、header、body以及与协议无关的tag,同样实现了工厂模式,Response内部和协议相关的内容比较多,这一次不展开。

Okhttp中的细节基本都是围绕着三者展开的:

  • 封装了HttpUrl、Header等对象,一同参与请求的抽象表达
  • 通过工厂利用Request生产Call,而请求的过程中,有很多细节,但是这些细节是有序的,于是采用AOP设计和责任链封装:
    • 封装了Chain来描述整个处理过程链
    • 封装了Interceptor来描述切面
    • 切面是按照责任链排序的,先后顺序:
      • interceptors += client.interceptors
      • interceptors += RetryAndFollowUpInterceptor(client)
      • interceptors += BridgeInterceptor(client.cookieJar)
      • interceptors += CacheInterceptor(client.cache)
      • interceptors += ConnectInterceptor
      • if (!forWebSocket) { interceptors += client.networkInterceptors }
      • interceptors += CallServerInterceptor(forWebSocket)
  • 而内部细节过于复杂,为了减轻使用者的使用成本,OkHttpClient使用Facade模式封装,如果对对应的模块有订制需求,实现相应模块后并在创建OkHttpClient时使用

我们再看看Retrofit,Retrofit原意是翻新、改进的含义,在Square看来,单独一个OkHttp并没有“革命性”的变化,无论其底层采用了怎样的优化,对于使用者来说,他并没有比前辈级的库优秀在哪里,还是需要用业务代码处理Request的构建,于是乎Retrofit应运而生。

他利用注解的形式来约定请求的的header、url(包括path参数和queryString参数)、body(最常用的是表单),通过反射获知这些信息,使用动态代理的方式,从而在底层封装Request的构建。

这还没完,使用预定的Callback来处理结果回调的制约太大,于是提供了CallAdapter机制,让使用者可以自定义处理方式。

到此为止,我们已经通过3个案例,回顾了Retrofit和OkHttp的关键源码,并且给出了解决方案,相信大家对于这两个常用库的认知又深了一些。