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

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

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

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

OkHttp根本实现原理

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

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

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

//创立OkHttpClientval 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办法:

# RealCalloverride 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办法中:

#Dispatcherprivate 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: ExecutorServiceget() {  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实现责任链模式的外围。

#RealCallfun 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.ktclass 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