关于android:面试官听说你熟悉OkHttp原理

257次阅读

共计 11923 个字符,预计需要花费 30 分钟才能阅读完成。

最近打算做网络相干的优化工作,未免须要从新相熟一下网络框架,在 Android 畛域网络框架的龙头老大非 OkHttp 莫属,借此机会对 OkHttp 的一些外部实现进行深刻的分析,同时这些问题也是面试时的常客,置信肯定对你有帮忙。

先来一发灵魂拷问四连击:

  • addInterceptor 与 addNetworkInterceptor 有什么区别?
  • 网络缓存如何实现的?
  • 网络连接怎么实现复用?
  • OkHttp 如何做网络监控?

是不是既相熟又生疏,实际上就是因为网络框架曾经为咱们实现了这些基本功能,所以很容易被咱们疏忽。为了残缺的剖析下面的问题,咱们须要先温习一下 OkHttp 的根底原理:

OkHttp 根本实现原理

OkHttp 的外部实现通过一个责任链模式实现,将网络申请的各个阶段封装到各个链条中,实现了各层的解耦。

文内源码基于 OkHttp 最新版本 4.2.2,从 4.0.0 版本开始,OkHttp 应用全 Kotlin 语言开发,没上车的小伙伴要放松了,要不源码都快看不懂了 [捂脸],学习 Kotlin 可参考旧文 Kotlin 学习系列文章 Overview。

咱们从发动一次申请的调用开始,相熟一下 OkHttp 执行的流程。

// 创立 OkHttpClient
val client = OkHttpClient.Builder().build();

// 创立申请
val request = Request.Builder()
           .url("https://wanandroid.com/wxarticle/list/408/1/json")
           .build()

// 同步工作开启新线程执行
Thread {
    // 发动网络申请
    val response = client.newCall(request).execute()
    if (!response.isSuccessful) throw IOException("Unexpected code $response")
    Log.d("okhttp_test", "response:  ${response.body?.string()}")
}.start()

所以外围的代码逻辑是通过 OkHttpClient 的 newCall 办法创立了一个 Call 对象,并调用其 execute 办法;Call 代表一个网络申请的接口,实现类只有一个 RealCall。execute 示意同步发动网络申请,与之对应还有一个 enqueue 办法,示意发动一个异步申请,因而同时须要传入 callback。

咱们来看 RealCall 的 execute 办法:

# RealCall
override fun execute(): Response {
    ...
    // 开始计时超时、发申请开始回调
    transmitter.timeoutEnter()
    transmitter.callStart()
    try {client.dispatcher.executed(this)// 第 1 步
      return getResponseWithInterceptorChain()// 第 2 步} finally {client.dispatcher.finished(this)// 第 3 步
    }
}

把大象装冰箱,统共也只须要三步。

第一步

调用 Dispatcher 的 execute 办法,那 Dispatcher 是什么呢?从名字来看它是一个调度器,调度什么呢?就是所有网络申请,也就是 RealCall 对象。网络申请反对同步执行和异步执行,异步执行就须要线程池、并发阈值这些货色,如果超过阈值须要将超过的局部存储起来,这样一剖析 Dispatcher 的性能就能够总结如下:

  • 记录同步工作、异步工作及期待执行的异步工作。
  • 线程池治理异步工作。
  • 发动 / 勾销网络申请 API:execute、enqueue、cancel。

OkHttp 设置了默认的最大并发申请量 maxRequests = 64 和单个 host 反对的最大并发量 maxRequestsPerHost = 5。

同时用三个双端队列存储这些申请:

# Dispatcher
// 异步工作期待队列
private val readyAsyncCalls = ArrayDeque<AsyncCall>()
// 异步工作队列
private val runningAsyncCalls = ArrayDeque<AsyncCall>()
// 同步工作队列
private val runningSyncCalls = ArrayDeque<RealCall>()

为什么要应用双端队列?很简略因为网络申请执行程序跟排队一样,考究先来后到,新来的申请放队尾,执行申请从对头部取。

说到这 LinkedList 示意不服,咱们晓得 LinkedList 同样也实现了 Deque 接口,外部是用链表实现的双端队列,那为什么不必 LinkedList 呢?

实际上这与 readyAsyncCalls 向 runningAsyncCalls 转换无关,当执行完一个申请或调用 enqueue 办法入队新的申请时,会对 readyAsyncCalls 进行一次遍历,将那些符合条件的期待申请转移到 runningAsyncCalls 队列中并交给线程池执行。只管二者都能实现这项工作,然而因为链表的数据结构以致元素离散的散布在内存的各个地位,CPU 缓存无奈带来太多的便当,另外在垃圾回收时,应用数组构造的效率要优于链表。

回到主题,上述的外围逻辑在 promoteAndExecute 办法中:

#Dispatcher
private fun promoteAndExecute(): Boolean {val executableCalls = mutableListOf<AsyncCall>()
    val isRunning: Boolean
    synchronized(this) {val i = readyAsyncCalls.iterator()
      // 遍历 readyAsyncCalls
      while (i.hasNext()) {val asyncCall = i.next()
        // 阈值校验
        if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
        if (asyncCall.callsPerHost().get() >= this.maxRequestsPerHost) continue // Host max capacity.
        // 符合条件 从 readyAsyncCalls 列表中删除
        i.remove()
        //per host 计数加 1
        asyncCall.callsPerHost().incrementAndGet()
        executableCalls.add(asyncCall)
        // 移入 runningAsyncCalls 列表
        runningAsyncCalls.add(asyncCall)
      }
      isRunning = runningCallsCount() > 0}
    
    for (i in 0 until executableCalls.size) {val asyncCall = executableCalls[i]
      // 提交工作到线程池
      asyncCall.executeOn(executorService)
    }
    
    return isRunning
}

这个办法在 enqueue 和 finish 办法中都会调用,即当有新的申请入队和以后申请实现后,须要从新提交一遍工作到线程池。

讲了半天线程池,那 OkHttp 外部到底用的什么线程池呢?

#Dispatcher 
@get:JvmName("executorService") val executorService: ExecutorService
get() {if (executorServiceOrNull == null) {
    executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
        SynchronousQueue(), threadFactory("OkHttp Dispatcher", false))
  }
  return executorServiceOrNull!!
}

这不是一个 newCachedThreadPool 吗?没错,除了最初一个 threadFactory 参数之外与 newCachedThreadPool 一毛一样,只不过是设置了线程名字而已,用于排查问题。

阻塞队列用的 SynchronousQueue,它的特点是不存储数据,当增加一个元素时,必须期待一个生产线程取出它,否则始终阻塞,如果以后有闲暇线程则间接在这个闲暇线程执行,如果没有则新启动一个线程执行工作。通常用于须要疾速响应工作的场景,在网络申请要求低提早的大背景下比拟适合,详见旧文 Java 线程池工作原理浅析。

持续回到主线,第二步比较复杂咱们先跳过,来看第三步。

第三步

调用 Dispatcher 的 finished 办法

// 异步工作执行完结
internal fun finished(call: AsyncCall) {call.callsPerHost().decrementAndGet()
    finished(runningAsyncCalls, call)
}

// 同步工作执行完结
internal fun finished(call: RealCall) {finished(runningSyncCalls, call)
}

// 同步异步工作 对立汇总到这里
private fun <T> finished(calls: Deque<T>, call: T) {
    val idleCallback: Runnable?
    synchronized(this) {
      // 将实现的工作从队列中删除
      if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
      idleCallback = this.idleCallback
    }
    // 这个办法在第一步中曾经剖析,用于将期待队列中的申请移入异步队列,并交由线程池执行。val isRunning = promoteAndExecute()
    
    // 如果没有申请须要执行,回调闲置 callback
    if (!isRunning && idleCallback != null) {idleCallback.run()
    }
}

第二步

当初咱们回过头来看最简单的第二步,调用 getResponseWithInterceptorChain 办法,这也是整个 OkHttp 实现责任链模式的外围。

#RealCall
fun getResponseWithInterceptorChain(): Response {
    // 创立拦截器数组
    val interceptors = mutableListOf<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)
    
    // 创立责任链
    val chain = RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this,
        client.connectTimeoutMillis, client.readTimeoutMillis, client.writeTimeoutMillis)
    ...
    try {
      // 启动责任链
      val response = chain.proceed(originalRequest)
      ...
      return response
    } catch (e: IOException) {...}
  }

咱们先不关怀每个拦截器具体做了什么,主流程最终走到chain.proceed(originalRequest)。咱们看一下这个 procceed 办法:

  # RealInterceptorChain
  override fun proceed(request: Request): Response {return proceed(request, transmitter, exchange)
  }

  @Throws(IOException::class)
  fun proceed(request: Request, transmitter: Transmitter, exchange: Exchange?): Response {if (index >= interceptors.size) throw AssertionError()
    // 统计以后拦截器调用 proceed 办法的次数
    calls++

    // exchage 是对申请流的封装,在执行 ConnectInterceptor 前为空,连贯和流曾经建设但此时此连贯不再反对以后 url
    // 阐明之前的网络拦截器对 url 或端口进行了批改,这是不容许的!!
    check(this.exchange == null || this.exchange.connection()!!.supportsUrl(request.url)) {"network interceptor ${interceptors[index - 1]} must retain the same host and port"
    }

    // 这里是对拦截器调用 proceed 办法的限度,在 ConnectInterceptor 及其之后的拦截器最多只能调用一次 proceed!!
    check(this.exchange == null || calls <= 1) {"network interceptor ${interceptors[index - 1]} must call proceed() exactly once"}

    // 创立下一层责任链 留神 index + 1
    val next = RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout)

    // 取出下标为 index 的拦截器,并调用其 intercept 办法,将新建的链传入。val interceptor = interceptors[index]
    val response = interceptor.intercept(next) 

    // 保障在 ConnectInterceptor 及其之后的拦截器至多调用一次 proceed!!
    check(exchange == null || index + 1 >= interceptors.size || next.calls == 1) {"network interceptor $interceptor must call proceed() exactly once"
    }

    return response
  }

代码中的正文曾经写得比较清楚了,总结起来就是创立下一级责任链,而后取出以后拦截器,调用其 intercept 办法并传入创立的责任链。++为保障责任链能顺次进行上来,必须保障除最初一个拦截器(CallServerInterceptor)外,其余所有拦截器 intercept 办法外部必须调用一次 chain.proceed()办法++,如此一来整个责任链就运行起来了。

比方 ConnectInterceptor 源码中:

# ConnectInterceptor 这里应用单例
object ConnectInterceptor : Interceptor {@Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    val request = realChain.request()
    val transmitter = realChain.transmitter()

    val doExtensiveHealthChecks = request.method != "GET"
    // 创立连贯和流
    val exchange = transmitter.newExchange(chain, doExtensiveHealthChecks)
    // 执行下一级责任链
    return realChain.proceed(request, transmitter, exchange)
  }
}

除此之外在责任链不同节点对于 proceed 的调用次数有不同的限度,ConnectInterceptor 拦截器及其之后的拦截器能且只能调用一次,因为网络握手、连贯、发送申请的工作产生在这些拦截器内,示意正式收回了一次网络申请;而在这之前的拦截器能够执行屡次 proceed,比方谬误重试。

通过责任链一级一级的递推上来,最终会执行到 CallServerInterceptor 的 intercept 办法,此办法会将网络响应的后果封装成一个 Response 对象并 return。之后沿着责任链一级一级的回溯,最终就回到 getResponseWithInterceptorChain 办法的返回。

拦截器分类

当初咱们须要先大抵总结一下责任链的各个节点拦截器的作用:

拦截器 作用
利用拦截器 拿到的是原始申请,能够增加一些自定义 header、通用参数、参数加密、网关接入等等。
RetryAndFollowUpInterceptor 处理错误重试和重定向
BridgeInterceptor 应用层和网络层的桥接拦截器,次要工作是为申请增加 cookie、增加固定的 header,比方 Host、Content-Length、Content-Type、User-Agent 等等,而后保留响应后果的 cookie,如果响应应用 gzip 压缩过,则还须要进行解压。
CacheInterceptor 缓存拦截器,如果命中缓存则不会发动网络申请。
ConnectInterceptor 连贯拦截器,外部会保护一个连接池,负责连贯复用、创立连贯(三次握手等等)、开释连贯以及创立连贯上的 socket 流。
networkInterceptors(网络拦截器) 用户自定义拦截器,通常用于监控网络层的数据传输。
CallServerInterceptor 申请拦截器,在前置筹备工作实现后,真正发动了网络申请。

至此,OkHttp 的外围执行流程就完结了,是不是有种恍然大悟的感觉?当初咱们终于能够答复开篇的问题:

addInterceptor 与 addNetworkInterceptor 的区别

二者通常的叫法为利用拦截器和网络拦截器,从整个责任链路来看,利用拦截器是最先执行的拦截器,也就是用户本人设置 request 属性后的原始申请,而网络拦截器位于 ConnectInterceptor 和 CallServerInterceptor 之间,此时网络链路曾经筹备好,只期待发送申请数据。

  1. 首先,利用拦截器在 RetryAndFollowUpInterceptor 和 CacheInterceptor 之前,所以一旦产生谬误重试或者网络重定向,网络拦截器可能执行屡次,因为相当于进行了二次申请,然而利用拦截器永远只会触发一次。另外如果在 CacheInterceptor 中命中了缓存就不须要走网络申请了,因而会存在短路网络拦截器的状况。
  2. 其次,如上文提到除了 CallServerInterceptor,每个拦截器都应该至多调用一次 realChain.proceed 办法。实际上在利用拦截器这层能够屡次调用 proceed 办法(本地异样重试)或者不调用 proceed 办法(中断),然而网络拦截器这层连贯曾经筹备好,可且仅可调用一次 proceed 办法。
  3. 最初,从应用场景看,利用拦截器因为只会调用一次,通常用于统计客户端的网络申请发动状况;而网络拦截器一次调用代表了肯定会发动一次网络通信,因而通常可用于统计网络链路上传输的数据。

网络缓存机制 CacheInterceptor

这里的缓存是指基于 Http 网络协议的数据缓存策略,侧重点在客户端缓存,所以咱们要先来温习一下 Http 协定如何依据申请和响应头来标识缓存的可用性。

提到缓存,就必须要聊聊缓存的有效性、有效期。

HTTP 缓存原理

在 HTTP 1.0 时代,响应应用 Expires 头标识缓存的有效期,其值是一个相对工夫,比方 Expires:Thu,31 Dec 2020 23:59:59 GMT。当客户端再次收回网络申请时可比拟以后工夫 和上次响应的 expires 工夫进行比拟,来决定是应用缓存还是发动新的申请。

应用 Expires 头最大的问题是它依赖客户端的本地工夫,如果用户本人批改了本地工夫,就会导致无奈精确的判断缓存是否过期。

因而,从 HTTP 1.1 开始应用 Cache-Control 头示意缓存状态,它的优先级高于 Expires,常见的取值为上面的一个或多个。

  • private,默认值,标识那些公有的业务逻辑数据,比方依据用户行为下发的举荐数据。该模式下网络链路中的代理服务器等节点不应该缓存这部分数据,因为没有实际意义。
  • public 与 private 相同,public 用于标识那些通用的业务数据,比方获取新闻列表,所有人看到的都是同一份数据,因而客户端、代理服务器都能够缓存。
  • no-cache 可进行缓存,但在客户端应用缓存前必须要去服务端进行缓存资源有效性的验证,即下文的比照缓存局部,咱们稍后介绍。
  • max-age 示意缓存时长单位为秒,指一个时间段,比方一年,通常用于不常常变动的动态资源。
  • no-store 任何节点禁止应用缓存。

强制缓存

在上述缓存头规约根底之上,强制缓存是指网络申请响应 header 标识了 Expires 或 Cache-Control 带了 max-age 信息,而此时客户端计算缓存并未过期,则能够间接应用本地缓存内容,而不必真正的发动一次网络申请。

协商缓存

强制缓存最大的问题是,一旦服务端资源有更新,直到缓存工夫截止前,客户端无奈获取到最新的资源(除非申请时手动增加 no-store 头),另外大部分状况下服务器的资源无奈间接确定缓存生效工夫,所以应用比照缓存更灵便一些。

应用 Last-Modify / If-Modify-Since 头实现协商缓存,具体方法是服务端响应头增加 Last-Modify 头标识资源的最初批改工夫,单位为秒,当客户端再次发动申请时增加 If-Modify-Since 头并赋值为上次申请拿到的 Last-Modify 头的值。

服务端收到申请后自行判断缓存资源是否依然无效,如果无效则返回状态码 304 同时 body 体为空,否则下发最新的资源数据。客户端如果发现状态码是 304,则取出本地的缓存数据作为响应。

应用这套计划有一个问题,那就是资源文件应用最初批改工夫有肯定的局限性:

  1. Last-Modify 单位为秒,如果某些文件在一秒内被批改则并不能精确的标识批改工夫。
  2. 资源批改工夫并不能作为资源是否批改的惟一根据,比方资源文件是 Daily Build 的,每天都会生成新的,然而其理论内容可能并未扭转。

因而,HTTP 还提供了另外一组头信息来解决缓存,ETag/If-None-Match。流程与 Last-Modify 一样,只是把服务端响应的头变成 Last-Modify,客户端收回的头变成 If-None-Match。ETag 是资源的惟一标识符,服务端资源变动肯定会导致 ETag 变动。具体的生成形式有服务端管制,场景的影响因素包含,文件最终批改工夫、文件大小、文件编号等等。

OKHttp 的缓存实现

下面讲了这么多,实际上 OKHttp 就是将上述流程用代码实现了一下,即:

  1. 第一次拿到响应后依据头信息决定是否缓存。
  2. 下次申请时判断是否存在本地缓存,是否须要应用比照缓存、封装申请头信息等等。
  3. 如果缓存生效或者须要比照缓存则收回网络申请,否则应用本地缓存。

OKHttp 外部应用 Okio 来实现缓存文件的读写。

缓存文件分为 CleanFiles 和 DirtyFiles,CleanFiles 用于读,DirtyFiles 用于写,他们都是数组,长度为 2,示意两个文件,即缓存的申请头和申请体;同时记录了缓存的操作日志,记录在 journalFile 中。

开启缓存须要在 OkHttpClient 创立时设置一个 Cache 对象,并指定缓存目录和缓存大小,缓存零碎外部应用 LRU 作为缓存的淘汰算法。

## Cache.kt
class Cache internal constructor(
  directory: File,
  maxSize: Long,
  fileSystem: FileSystem
): Closeable, Flushable

OkHttp 晚期的版本有个一个 InternalCache 接口,反对自定义实现缓存,但到了 4.x 的版本后删减了 InternalCache,Cache 类又为 final 的,相当于敞开了扩大性能。

具体源码实现都在 CacheInterceptor 类中,大家能够自行查阅。

通过 OkHttpClient 设置缓存是全局状态的,如果咱们想对某个特定的 request 应用或禁用缓存,能够通过 CacheControl 相干的 API 实现:

// 禁用缓存
Request request = new Request.Builder()
    .cacheControl(new CacheControl.Builder().noCache().build())
    .url("http://publicobject.com/helloworld.txt")
    .build();

OKHttp 不反对的缓存状况

最初须要留神的一点是,OKHttp 默认只反对 get 申请的缓存。

# okhttp3.Cache.java
@Nullable CacheRequest put(Response response) {String requestMethod = response.request().method();
    ...
    // 缓存仅反对 GET 申请
    if (!requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    }
    
    // 对于 vary 头的值为 * 的状况,对立不缓存
    if (HttpHeaders.hasVaryAll(response)) {return null;}
    ...
}

这是当网络申请响应后,筹备进行缓存时的逻辑代码,当返回 null 时示意不缓存。从代码正文中不难看出,咱们从技术上能够缓存 method 为 HEAD 和局部 POST 申请,但实现起来的复杂性很高而收益甚微。这实质上是由各个 method 的应用场景决定的。

咱们先来看看常见的 method 类型及其用处。

  • GET 申请资源,参数都在 URL 中。
  • HEAD 与 GET 基本一致,只不过其不返回音讯体,通常用于速度或带宽优先的场景,比方查看资源有效性,可拜访性等等。
  • POST 提交表单,批改数据,参数在 body 中。
  • PUT 与 POST 基本一致,最大不同为 PUT 是幂等的。
  • DELETE 删除指定资源。

能够看到对于规范的 RESTful 申请,GET 就是用来获取数据,最适宜应用缓存,而对于数据的其余操作缓存意义不大或者基本不须要缓存。

也是基于此在仅反对 GET 申请的条件下,OKHTTP 应用 request URL 作为缓存的 key(当然还会通过一系列摘要算法)。

最初下面代码中贴到,如果申请头中蕴含 vary:* 这样的头信息也不会被缓存。vary 头用于进步多端申请时的缓存命中率,比方两个客户端,一个反对 gzip 压缩而另一个不反对,二者的申请 URL 都是统一的,但 Accept-Encoding 不同,这很容易导致缓存错乱,咱们能够申明 vary:Accept-Encoding 避免这种状况产生。

而蕴含 vary:* 头信息,标识着此申请是惟一的,不应被缓存,除非无意为之,个别不会这样做来就义缓存性能。

学习资源分享

为了帮忙大家更好的学习框架原理,在这里给大家分享一份谷歌开源的《百大框架源码解析》这份完整版的《Android 开发相干源码精编解析》PDF 版电子书,点这里能够看到全部内容 。或者点击【 这里】查看获取形式。

相干视频举荐:

【2021 最新版】Android studio 装置教程 +Android(安卓)零基础教程视频(适宜 Android 0 根底,Android 初学入门)含音视频_哔哩哔哩_bilibili

【Android 进阶教程】——OkHttp 原理_哔哩哔哩_bilibili

Android OkHttp 原理解读——带你深刻把握 OkHttp 散发器与拦截器开发_哔哩哔哩_bilibili

【Android 进阶教程】——基于 Okhttp 的可用网络框架原理解析_哔哩哔哩_bilibili

正文完
 0