OkHttp网路框架原理分析

1,304 阅读11分钟

什么是OkHttp

  • OkHTTP 应用程序网络请求框架,能有效地执行 HTTP请求,可以使内容加载速度更快并节省带宽。能让开发者快速高效地进行网络请求开发,避免使用HttpURLConnection原生请求方式时的重复编码过程。

OkHttp快速使用

同步请求

  1. 创建OkHttpClient与Request对象
  2. 同过OkHttpClient对象将Request封装成Call对象
  3. 调用call.execute()发送同步请求
val client = OkHttpClient.Builder()
            .readTimeout(5,TimeUnit.SECONDS)
            .build()
val request = Request.Builder()
             .url("http://www.baidu.com")
             .build()
val call = client.newCall(request)
try {
    val response = call.execute()
    Log.d("OkHttp", response.body.toString())
}catch (e:IOException){
    e.printStackTrace()
}

异步请求

  1. 创建OkHttpClient与Request对象
  2. 同过OkHttpClient对象将Request封装成Call对象
  3. 调用call.enqueue()并传入Callback,发送异步请求
val client = OkHttpClient.Builder().readTimeout(5,TimeUnit.SECONDS).build()
val request = Request.Builder().url("http://www.baidu.com").build()
val call = client.newCall(request)
call.enqueue(object :Callback{
    override fun onFailure(call: Call, e: IOException) {
        Log.d("OkHttp", "onFailure: ")
    }
    override fun onResponse(call: Call, response: Response) {
        Log.d("OkHttp", response.body.toString())
    }
})

OkHttp原理分析

设计模式

  • Builder
  • 责任链
  • 模板方法

总体架构

  • okHttpClientl类在整个框架中处于核心位置,其中的分发器与线程池是核心内容,通过Bulider时候创建Dispatcher分发器以及线程池,不管是同步还是异步请求,都是通过分发器dispatcher进行调度,在网络请求过程中配合getResponseWithInterceptorChain方法连接器链进行工作。调用Call方法实际是调用RealCall方法。

okhttp.png

同步请求源码分析

  • 在我们调用call.execute()方法时,在源码中其实我们可以发现真正的实现类是RealCall类中的execute()方法

企业微信截图_20210723120231.png

  首先判断同一个同步请求是否执行过,然后进行事件监听回调,这时我们发现有两个重要的步骤,一个是dispatcher分发器所调用的execute()方法,另外一个是getResponseWithInterceptorChain()拦截器, 我们先点进client.dispatcher.executed(this)进行分析,拦截器后面再说。

企业微信截图_20210723120439.png

  当我们点进去一看,发现原来这个分发器在同步请求的时候搞了半天也只是把call添加到runningSyncCalls运行队列当中

异步请求源码分析

  • 还是直接看call.enqueue()方法,这里需要传入一个Callback接口实现类,与同步请求一样call的实现类是RealCall,调用call.enqueue()实际上是调用RealCall中的enqueue()方法

企业微信截图_20210723143641.png

    首先判断当前call是否执行过,若执行过就会抛出异常,前面定义的call只执行一次,这与连接池也有关系。captureCallStackTrace()是捕获http请求的异常堆栈信息。接下来就开始一些监听事件。其中
client.dispatcher().enqueue(new AsyncCall(responseCallback));
是最重要一步,我们发现我们传进来的callback被封装成了一个AsyncCall对象,这是一个Runnable对象,然后传进dispatcher(dispatcher在OkHttpClient的Builder()中已经创建)中的enqueue()方法。

    在okhttp3.9.x版本中,enqueue在同步代码块中,首先判断运行队列异步请求数是否大于maxRequests(64) 以及正在运行的每个主机的请求是否小于maxRequestsPerHost(5),满足则加入运行队列runningAsyncCalls中,并交给线程池执行,否则添加到等待队列readyAsyncCalls

企业微信截图_20210723152912.png

    在okhttp4.9.0版本中,AsyncCall则直接被添加到等待队列当中,并改AsyncCall变实现了共享现有运行调用的AtomicInteger到同一主机,而调度则交给了promoteAndExecute()进行处理

企业微信截图_20210723154107.png

    在promoteAndExecute()方法中,对等待队列readyAsyncCalls进行了迭代,若当前异步请求大于maxRequests(64)则直接退出循环,请求主机数大于maxRequestsPerHost(5)则跳出本次循环,都不满足则取出一个队列元素,并把他放入到运行队列与执行列表中,最后方法返回ture表示正在运行请求。

企业微信截图_20210723154404.png

    在更新相应队列列表信息后,运行asyncCall.executeOn(executorService),从某种程度上实现了解耦,把线程服务executorService传入executeOn()方法,该方法最主要(是跟okhttp3.9.x版本一样)executorService.execute(this)方法让线程池执行,最后 执行 client.dispatcher.finished(this)主要的是递归调用了promoteAndExecute()直到异步请求等待队列为空

企业微信截图_20210723163017.png

  • 我们来回过投来看asyncCall.executeOn(executorService)     其中executorService就是一个单例线程池,第一个参数传入0代表没有核心线程空闲时可以销毁,第二个参数设为最大线程数,但也受maxRequests影响,第三、四个参数为60s后关闭空闲线程

企业微信截图_20210723171705.png

    线程池创建好后,我们的asyncCall.executeOn(executorService)方法中主要执行的是executorService.execute(this)this指的是当前AsycCall,由于是个线程池,所以会调用每一个子线程的run方法,也就是说调用AsyncCall中的run方法

企业微信截图_20210723172816.png

    在AsnycCall中,在4.9.0版本中AsyncCall相对以前的java版(如3.12.1)少了NamedRunnable这一层,直接实现了runable接口因此主要流程直接执行在run中 企业微信截图_20210723180832.png 这里有一个okhttp核心的部分getResponseWithInterceptorChain()拦截链(后面详细讲)。主要的网络成功请求在responseCallback.onResponse(this@RealCall, response),这里也验证了回调的onResponseonFailure都是在子线程当中。catch块主要做一些请求失败的操作和打印一些日志等。最后执行了重要的client.dispatcher.finished(this)方法,里面重要的是调用了另一个finished()方法 企业微信截图_20210723182008.png 在finish()中主要做了三件事

  • 在同步代码块中把当前请求移除出队列
  • 然后调整个异步请求队列
  • 重新计算正在执行的队列线程数量并把它复制 第二三点均在promoteAndExecute()里完成

OkHttp任务调度dispatcher

  • dispatcher分发器是在OkHttpClient.Builder()过程中所创建的
  • diapatcher维护同/异步请求状态
  • diapatcher维护线程池ExcutorService,并用于执行的readyAsynCallsrunningAsycCalls队列中的call

微信截图_20210724103415.png

export.png

为什么同步请求只用一个队列而异步请求需要连个队列

  • Dispatcher想当与生产者(默认在主线程),而ExcutorService相对于消费者池,Deque<readyAsycCall>作为缓存,Deque<runningAsycCall>为正运行队列

readyAsycCall队列中的线程在什么时候才被执行

  • 在okhttp3.9.x这些早点的版本中,是在最终被调用的分发器的finsh方法中,先把当前call移出,再调用promotecalls()来调整任务队列,然后重新计算线程数,而在4.9.0这些新的版本中不但在在最终被调用的分发器的finsh方法中移除call后,调用promoteAndExecute()进行调整队列以及重新计算线程数,而且在enqueue()方法执行时就调用promoteAndExecute()进行调整队列以及重新计算线程数。特别要注意的是在新版okhttp中线程池的执行也在promoteAndExecute()方法中。

  • okhttp3.9.x版本finish()方法 微信截图_20210724104246.png

  • okhttp4.9.0版本finish()方法

微信截图_20210724104541.png


OkHttp拦截器

  • 拦截器是OkHttp一个强大机制,可以实现网络监听、请求以及相应重写、请求失败重写等功能。拦截器在工作中是不区分同步还是异步的。

OkHttp所提供拦截器

  • 主要包括重试和重定向拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求写入I/O网路流拦截器 微信截图_20210724112218.png

getResponseWithInterceptorChain()方法

  • 创建拦截器放入list中(除5个系统okhttp提供拦截器外还包括应用程序拦截器与网络拦截器)此方法返回Response响应,此方法主要是构成一个拦截器链,依次执行不同功能的拦截器来获取我们的响应返回。由于源码所知该方法把各拦截器通过集合放入RealInterceptorChain,然后调用chain.proceed()方法。

QQ截图20210725155002.png

  • chain.proceed()方法中最重要的就是创建了一个拦截器链,与刚才所创建的区别在于这里传入的参数是index+1,代表访问时只能从下一个拦截器进行访问。这就是设计的巧妙之处,把拦截器构成了一个链。val response = interceptor.intercept(next)为执行索引,并把我们刚创建好的传递进去,这样我们的连接器就构成的一个完整的链条。

QQ截图20210725155527.png

QQ截图20210725155549.png

  • 我们先看下RetryAndFollowUpInterceptor拦截器的intercept方法,在这里面依然执行了下一个拦截器的proceed方法.因此整个okhttp的网络请求相当于一个个拦截器的执行

QQ截图20210725163200.png

小结一下

QQ截图20210725164718.png

RetryAndFollowUpInterceptor拦截器

  • 主要负责失败重连。在okhttp3.9.x版本中会创建一个StreamAllocation,用来提供Http请求得一些组件以及分配Stream,这里创建将提供给ConnectInterceptor拦截器使用。而在4.9.x版本中则无创建此类。
  • 主要的重连代码都在RetryAndFollowUpInterceptorwhile循环中根据网络请求结果判断是否重连,并调用下一个拦截器,其中失败的网络请求最大重连被设置成为MAX_FOLLOW_UPS(20)

微信截图_20210726145925.png

BridgeInterceptor拦截器

  • 主要负责设置内容长度、编码方法、压缩以及添加头部等,还有连接复用Keep-Alive(一定时间内保持链接状态)

微信截图_20210726153459.png

  • 同样的也调用了proeccd方法,并返回Response。这里体现了当前拦截器的作用是调用下一拦截器,处理返回Response,返回上一拦截器.

微信截图_20210726154936.png

  • cookieJar.receiveHeaders(userRequest.url, networkResponse.headers) 的作用是把服务器返回的Response转为用户可以处理的Response

  • 接下来的主要是判断是否服务端支持gzip压缩,确保相应头Content-Encoding支持gzip压缩以http头部有body体。然后将responseBody的输入流转化为GzipSource,这样的话调者在使用body体的时候就以解压形式。

微信截图_20210726160506.png

BridgeInterceptor拦截器总结

  • 将用户构建的Request转化成能够网络访问的请求
  • 将这符合网络请求得Request进行网络请求
  • 将服务端返回得Response转化为用户可用得Response

如何使用okhttp缓存功能

  • 添加Cache类并设置缓存目录以及大小 微信截图_20210726161317.png

Cache缓存中的put方法

  • 在Cache类中有一个重要的类DiskLruCache.Editor,这其实代表着整个缓存核心都是这个LRU算法,同时okhttp内部维护者清理线程池,并由这个线程池来清理缓存。
  • 由于requestMethod"GET"时,它返回null 它不缓存Get方法
  • Entry是一个由url、varyHeaders、requestMethod、protocol、code、message等构成的包装类。

微信截图_20210726162508.png

  • 最后editor保存请求和响应头信息,那RealCacheRequest(editor)则保存请求主体,同时它实现的接口CacheRequest是用于暴露给缓存拦截器的,拦截器就可以根据接口来更新缓存信息。

微信截图_20210726163754.png

Cache缓存中的get方法

  • 主要用户从缓存中读取响应体response,从源码中可以看到,除了获取key外还有缓存快照与实体,通过entry.response()获取response,最后进行request与response匹配。

微信截图_20210726232035.png

CacheInterceptor拦截器

  • 首先通过CacheStrategy的工厂类获取缓存策略到底是通过网络还是缓存还是两者都使用。
  • cache?.trackResponse(strategy)为更新统计指标缓存命中率
  • 接下来就是判断缓存是否符合指标否则关闭cacheCandidate.body?.closeQuietly()
  • 若不能使用网络同时也不没有缓存,通过构建者模式构建Response同时抛出504错误

微信截图_20210726233644.png

  • 若有缓存不能使用网络则返回缓存结果

微信截图_20210726235737.png

  • 通过调用proceed方法进行网络响应获取,同时把工作交给下一拦截器处理

微信截图_20210727000025.png

  • 若响应码为HTTP_NOT_MODIFIED(304)则直接从缓存中获取

微信截图_20210727000510.png

  • 最后判断Http头部有没响应体和是否可以被缓存,那么就通过cache.put(response)写入到缓存中,然后通过HttpMethod.invalidatesCache(networkRequest.method)判断是否为无效的缓存方法,是则把它从缓存池删除。

微信截图_20210727000658.png

ConnectInterceptor拦截器

主要功能为打开与服务器之间的链接与建立流对象,正式开启okhttp网络请求。

  • 在okhttp3.9.x版本中,这里获取了从重定向连接器中创建的StreamAllocation,它的作用是分配stream。
  • 通过StreamAllocation.newStream()来创建HttpCodec,HttpCodec的主要功能的编码request和解码response
  • 获取到的RealConnection是实际进行网络I/O传输的。
  • 最后调用proceed方法进行拦截器操作

微信截图_20210727001213.png

  • 在okhttp4.9x版本中,是没有StreamAllocation类的,但拦截器的功能主要还是相同
  • 通过realChain.call.initExchange(chain)找到一个新的或池化的连接来承载即将到来的请求和响应
  • exchange传入连接器链,并用proceed方法

微信截图_20210727002451.png

连接池ConnectionPool

  • 主要功能为在时间范围内对connectio复用管理
  • initExchange方法中每执行一次exchangeFinder.find(client, chain)->findHealthyConnection->findConnection-> connectionPool.put(newConnection)方法,把创建好的RealConnection(connectionPool, route)放入ConcurrentLinkedQueue<RealConnection>()队列中
  • ExchangeFinderfindHealthyConnection方法中通过调用connectionPool.callAcquirePooledConnection方法尝试为 [call] 获取到 [address] 的回收连接。如果获得了连接,则返回 true

微信截图_20210727014104.png

  • 在RealConnectionPool中的cleanup做了些GC回收算法,保证多个健康的keep-alie连接

CallServerInterceptor拦截器

  • 发起真正的网络请求,以及接受服务器返回给我们的响应
  • exchange.writeRequestHeaders(request)写入请求的头部信息

微信截图_20210727015208.png

  • requestBody.writeTo(bufferedRequestBody)写入我们请求的body信息 ![微信截图_20210727015527.png](p6-juejin.byteimg.com/tos-cn-i-k3… plv-k3u1fbpfcp-watermark.image)
  • 接下来结束写入网络请求信息

微信截图_20210727015846.png

  • 紧接着就读取响应的头部信息

微信截图_20210727020021.png

  • 然后在非特殊情况下读取响应体信息

微信截图_20210727020228.png

  • 最后调用exchange.noNewExchangesOnConnection()防止在此连接上创建进一步的交换,并在204或205情况下抛异常

微信截图_20210727020446.png

CallServerInterceptor拦截器小结

  • 写入request头部信息
  • 写入request的body信息
  • 结束整个网路请求写入工作完成
  • 读取response头部信息
  • 读取response的body信息

okhttp中一次网络请求大致过程

  • 通过okhttpClient把Request封装封装成Call对象进行同/异请求
  • dispatcher分发器的请求分发
  • getRsponseWithInterceptors()拦截器链 okhttp.png