什么是OkHttp
- OkHTTP 应用程序网络请求框架,能有效地执行 HTTP请求,可以使内容加载速度更快并节省带宽。能让开发者快速高效地进行网络请求开发,避免使用HttpURLConnection原生请求方式时的重复编码过程。
OkHttp快速使用
同步请求
- 创建OkHttpClient与Request对象
- 同过OkHttpClient对象将Request封装成Call对象
- 调用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()
}
异步请求
- 创建OkHttpClient与Request对象
- 同过OkHttpClient对象将Request封装成Call对象
- 调用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方法。
同步请求源码分析
- 在我们调用
call.execute()方法时,在源码中其实我们可以发现真正的实现类是RealCall类中的execute()方法
首先判断同一个同步请求是否执行过,然后进行事件监听回调,这时我们发现有两个重要的步骤,一个是dispatcher分发器所调用的execute()方法,另外一个是getResponseWithInterceptorChain()拦截器,
我们先点进client.dispatcher.executed(this)进行分析,拦截器后面再说。
当我们点进去一看,发现原来这个分发器在同步请求的时候搞了半天也只是把call添加到runningSyncCalls运行队列当中
异步请求源码分析
- 还是直接看
call.enqueue()方法,这里需要传入一个Callback接口实现类,与同步请求一样call的实现类是RealCall,调用call.enqueue()实际上是调用RealCall中的enqueue()方法
首先判断当前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
在okhttp4.9.0版本中,AsyncCall则直接被添加到等待队列当中,并改AsyncCall变实现了共享现有运行调用的AtomicInteger到同一主机,而调度则交给了promoteAndExecute()进行处理
在promoteAndExecute()方法中,对等待队列readyAsyncCalls进行了迭代,若当前异步请求大于maxRequests(64)则直接退出循环,请求主机数大于maxRequestsPerHost(5)则跳出本次循环,都不满足则取出一个队列元素,并把他放入到运行队列与执行列表中,最后方法返回ture表示正在运行请求。
在更新相应队列列表信息后,运行asyncCall.executeOn(executorService),从某种程度上实现了解耦,把线程服务executorService传入executeOn()方法,该方法最主要(是跟okhttp3.9.x版本一样)executorService.execute(this)方法让线程池执行,最后 执行 client.dispatcher.finished(this)主要的是递归调用了promoteAndExecute()直到异步请求等待队列为空
- 我们来回过投来看
asyncCall.executeOn(executorService)其中executorService就是一个单例线程池,第一个参数传入0代表没有核心线程空闲时可以销毁,第二个参数设为最大线程数,但也受maxRequests影响,第三、四个参数为60s后关闭空闲线程
线程池创建好后,我们的asyncCall.executeOn(executorService)方法中主要执行的是executorService.execute(this)this指的是当前AsycCall,由于是个线程池,所以会调用每一个子线程的run方法,也就是说调用AsyncCall中的run方法
在AsnycCall中,在4.9.0版本中AsyncCall相对以前的java版(如3.12.1)少了NamedRunnable这一层,直接实现了runable接口因此主要流程直接执行在run中
这里有一个okhttp核心的部分
getResponseWithInterceptorChain()拦截链(后面详细讲)。主要的网络成功请求在responseCallback.onResponse(this@RealCall, response),这里也验证了回调的onResponse与onFailure都是在子线程当中。catch块主要做一些请求失败的操作和打印一些日志等。最后执行了重要的client.dispatcher.finished(this)方法,里面重要的是调用了另一个finished()方法
在finish()中主要做了三件事
- 在同步代码块中把当前请求移除出队列
- 然后调整个异步请求队列
- 重新计算正在执行的队列线程数量并把它复制
第二三点均在
promoteAndExecute()里完成
OkHttp任务调度dispatcher
- dispatcher分发器是在
OkHttpClient.Builder()过程中所创建的 - diapatcher维护同/异步请求状态
- diapatcher维护线程池
ExcutorService,并用于执行的readyAsynCalls与runningAsycCalls队列中的call
为什么同步请求只用一个队列而异步请求需要连个队列
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()方法 -
okhttp4.9.0版本
finish()方法
OkHttp拦截器
- 拦截器是OkHttp一个强大机制,可以实现网络监听、请求以及相应重写、请求失败重写等功能。拦截器在工作中是不区分同步还是异步的。
OkHttp所提供拦截器
- 主要包括重试和重定向拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求写入I/O网路流拦截器
getResponseWithInterceptorChain()方法
- 创建拦截器放入list中(除5个系统okhttp提供拦截器外还包括应用程序拦截器与网络拦截器)此方法返回Response响应,此方法主要是构成一个拦截器链,依次执行不同功能的拦截器来获取我们的响应返回。由于源码所知该方法把各拦截器通过集合放入
RealInterceptorChain,然后调用chain.proceed()方法。
- 在
chain.proceed()方法中最重要的就是创建了一个拦截器链,与刚才所创建的区别在于这里传入的参数是index+1,代表访问时只能从下一个拦截器进行访问。这就是设计的巧妙之处,把拦截器构成了一个链。val response = interceptor.intercept(next)为执行索引,并把我们刚创建好的传递进去,这样我们的连接器就构成的一个完整的链条。
- 我们先看下
RetryAndFollowUpInterceptor拦截器的intercept方法,在这里面依然执行了下一个拦截器的proceed方法.因此整个okhttp的网络请求相当于一个个拦截器的执行
小结一下
RetryAndFollowUpInterceptor拦截器
- 主要负责失败重连。在okhttp3.9.x版本中会创建一个
StreamAllocation,用来提供Http请求得一些组件以及分配Stream,这里创建将提供给ConnectInterceptor拦截器使用。而在4.9.x版本中则无创建此类。 - 主要的重连代码都在
RetryAndFollowUpInterceptor的while循环中根据网络请求结果判断是否重连,并调用下一个拦截器,其中失败的网络请求最大重连被设置成为MAX_FOLLOW_UPS(20)
BridgeInterceptor拦截器
- 主要负责设置内容长度、编码方法、压缩以及添加头部等,还有连接复用
Keep-Alive(一定时间内保持链接状态)
- 同样的也调用了
proeccd方法,并返回Response。这里体现了当前拦截器的作用是调用下一拦截器,处理返回Response,返回上一拦截器.
-
cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)的作用是把服务器返回的Response转为用户可以处理的Response -
接下来的主要是判断是否服务端支持
gzip压缩,确保相应头Content-Encoding支持gzip压缩以http头部有body体。然后将responseBody的输入流转化为GzipSource,这样的话调者在使用body体的时候就以解压形式。
BridgeInterceptor拦截器总结
- 将用户构建的Request转化成能够网络访问的请求
- 将这符合网络请求得Request进行网络请求
- 将服务端返回得Response转化为用户可用得Response
如何使用okhttp缓存功能
- 添加Cache类并设置缓存目录以及大小
Cache缓存中的put方法
- 在Cache类中有一个重要的类
DiskLruCache.Editor,这其实代表着整个缓存核心都是这个LRU算法,同时okhttp内部维护者清理线程池,并由这个线程池来清理缓存。 - 由于
requestMethod为"GET"时,它返回null它不缓存Get方法 - Entry是一个由url、varyHeaders、requestMethod、protocol、code、message等构成的包装类。
- 最后
editor保存请求和响应头信息,那RealCacheRequest(editor)则保存请求主体,同时它实现的接口CacheRequest是用于暴露给缓存拦截器的,拦截器就可以根据接口来更新缓存信息。
Cache缓存中的get方法
- 主要用户从缓存中读取响应体
response,从源码中可以看到,除了获取key外还有缓存快照与实体,通过entry.response()获取response,最后进行request与response匹配。
CacheInterceptor拦截器
- 首先通过
CacheStrategy的工厂类获取缓存策略到底是通过网络还是缓存还是两者都使用。 cache?.trackResponse(strategy)为更新统计指标缓存命中率- 接下来就是判断缓存是否符合指标否则关闭
cacheCandidate.body?.closeQuietly() - 若不能使用网络同时也不没有缓存,通过构建者模式构建
Response同时抛出504错误
- 若有缓存不能使用网络则返回缓存结果
- 通过调用proceed方法进行网络响应获取,同时把工作交给下一拦截器处理
- 若响应码为
HTTP_NOT_MODIFIED(304)则直接从缓存中获取
- 最后判断Http头部有没响应体和是否可以被缓存,那么就通过
cache.put(response)写入到缓存中,然后通过HttpMethod.invalidatesCache(networkRequest.method)判断是否为无效的缓存方法,是则把它从缓存池删除。
ConnectInterceptor拦截器
主要功能为打开与服务器之间的链接与建立流对象,正式开启okhttp网络请求。
- 在okhttp3.9.x版本中,这里获取了从重定向连接器中创建的
StreamAllocation,它的作用是分配stream。 - 通过
StreamAllocation.newStream()来创建HttpCodec,HttpCodec的主要功能的编码request和解码response。 - 获取到的
RealConnection是实际进行网络I/O传输的。 - 最后调用
proceed方法进行拦截器操作
- 在okhttp4.9x版本中,是没有
StreamAllocation类的,但拦截器的功能主要还是相同 - 通过
realChain.call.initExchange(chain)找到一个新的或池化的连接来承载即将到来的请求和响应 - 把
exchange传入连接器链,并用proceed方法
连接池ConnectionPool
- 主要功能为在时间范围内对connectio复用管理
- 在
initExchange方法中每执行一次exchangeFinder.find(client, chain)->findHealthyConnection->findConnection->connectionPool.put(newConnection)方法,把创建好的RealConnection(connectionPool, route)放入ConcurrentLinkedQueue<RealConnection>()队列中 - 在
ExchangeFinder的findHealthyConnection方法中通过调用connectionPool.callAcquirePooledConnection方法尝试为 [call] 获取到 [address] 的回收连接。如果获得了连接,则返回 true
- 在RealConnectionPool中的
cleanup做了些GC回收算法,保证多个健康的keep-alie连接
CallServerInterceptor拦截器
- 发起真正的网络请求,以及接受服务器返回给我们的响应
exchange.writeRequestHeaders(request)写入请求的头部信息
requestBody.writeTo(bufferedRequestBody)写入我们请求的body信息 - 接下来结束写入网络请求信息
- 紧接着就读取响应的头部信息
- 然后在非特殊情况下读取响应体信息
- 最后调用
exchange.noNewExchangesOnConnection()防止在此连接上创建进一步的交换,并在204或205情况下抛异常
CallServerInterceptor拦截器小结
- 写入request头部信息
- 写入request的body信息
- 结束整个网路请求写入工作完成
- 读取response头部信息
- 读取response的body信息
okhttp中一次网络请求大致过程
- 通过okhttpClient把Request封装封装成Call对象进行同/异请求
- dispatcher分发器的请求分发
- getRsponseWithInterceptors()拦截器链