okhttp 可以说是 Android 开发中最常见的网络请求框架,OkHttp 使用方便,扩展性强,功能强大,OKHttp 源码与原理也是面试中的常客。
创新互联公司是一家集网站建设,乌恰企业网站建设,乌恰品牌网站建设,网站定制,乌恰网站建设报价,网络营销,网络优化,乌恰网站推广为一体的创新建站企业,帮助传统企业提升企业形象加强企业竞争力。可充分满足这一群体相比中小企业更为丰富、高端、多元的互联网需求。同时我们时刻保持专业、时尚、前沿,时刻以成就客户成长自我,坚持不断学习、思考、沉淀、净化自己,让我们为更多的企业打造出实用型网站。
但是 OKHttp 的源码内容比较多,想要学习它的源码往往千头万绪,一时抓不住重点. 本文从几个问题出发梳理 OKHttp 相关知识点,以便快速构建 OKHttp 知识体,本文主要包括以下内容
首先来看一个最简单的 Http 请求是如何发送的。
- val okHttpClient = OkHttpClient()
- val request: RequestRequest = Request.Builder()
- .url("https://www.google.com/")
- .build()
- okHttpClient.newCall(request).enqueue(object :Callback{
- override fun onFailure(call: Call, e: IOException) {
- }
- override fun onResponse(call: Call, response: Response) {
- }
- })
这段代码看起来比较简单,OkHttp 请求过程中最少只需要接触 OkHttpClient、Request、Call、 Response,但是框架内部会进行大量的逻辑处理。
所有网络请求的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。关于分发器与拦截器,我们在这里先简单介绍下,后续会有更加详细的讲解
整个网络请求过程大致如上所示
分发器的主要作用是维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为正在请求中的列表和正在等待的列表, 等请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求
而这里同步请求各异步请求又略有不同
同步请求
- synchronized void executed(RealCall call) {
- runningSyncCalls.add(call);
- }
因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可
异步请求
- synchronized void enqueue(AsyncCall call) {
- //请求数最大不超过64,同一Host请求不能超过5个
- if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
- runningAsyncCalls.add(call);
- executorService().execute(call);
- } else {
- readyAsyncCalls.add(call);
- }
- }
当正在执行的任务未超过最大限制64,同时同一 Host 的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。每个任务完成后,都会调用分发器的 finished 方法,这里面会取出等待队列中的任务继续执行
经过上面分发器的任务分发,下面就要利用拦截器开始一系列配置了
- # RealCall
- override fun execute(): Response {
- try {
- client.dispatcher.executed(this)
- return getResponseWithInterceptorChain()
- } finally {
- client.dispatcher.finished(this)
- }
- }
我们再来看下 RealCall的execute方法,可以看出,最后返回了 getResponseWithInterceptorChain ,责任链的构建与处理其实就是在这个方法里面
- internal fun getResponseWithInterceptorChain(): Response {
- // Build a full stack of interceptors.
- val interceptors = mutableListOf
() - 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(
- call = this,interceptorsinterceptors = interceptors,index = 0
- )
- val response = chain.proceed(originalRequest)
- }
如上所示,构建了一个 OkHttp 拦截器的责任链
责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。
如上所示责任链添加的顺序及作用,如下表所示:
拦截器 | 作用 |
---|---|
应用拦截器 | 拿到的是原始请求,可以添加一些自定义 header、通用参数、参数加密、网关接入等等。 |
RetryAndFollowUpInterceptor | 处理错误重试和重定向 |
BridgeInterceptor | 应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。 |
CacheInterceptor | 缓存拦截器,如果命中缓存则不会发起网络请求。 |
ConnectInterceptor | 连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。 |
networkInterceptors(网络拦截器) | 用户自定义拦截器,通常用于监控网络层的数据传输。 |
CallServerInterceptor | 请求拦截器,在前置准备工作完成后,真正发起了网络请求。 |
我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到 CallServerInterceptor的intercept 方法,此方法会将网络响应的结果封装成一个 Response 对象并 return。之后沿着责任链一级一级的回溯,最终就回到 getResponseWithInterceptorChain 方法的返回,如下图所示:
从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置 request 属性后的原始请求,而网络拦截器位于 ConnectInterceptor 和 CallServerInterceptor 之间,此时网络链路已经准备好,只等待发送请求数据。它们主要有以下区别
1. 首先,应用拦截器在 RetryAndFollowUpInterceptor 和 CacheInterceptor 之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在 CacheInterceptor 中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。
2. 其次,除了 CallServerInterceptor 之外,每个拦截器都应该至少调用一次 realChain.proceed 方法。实际上在应用拦截器这层可以多次调用 proceed 方法(本地异常重试)或者不调用 proceed 方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次 proceed 方法。
3. 最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。
ConnectInterceptor 的主要工作就是负责建立 TCP 连接,建立 TCP 连接需要经历三次握手四次挥手等操作,如果每个 HTTP 请求都要新建一个 TCP 消耗资源比较多 而 Http1.1 已经支持 keep-alive ,即多个 Http 请求复用一个 TCP 连接,OKHttp 也做了相应的优化,下面我们来看下 OKHttp 是怎么复用 TCP 连接的
ConnectInterceptor 中查找连接的代码会最终会调用到 ExchangeFinder.findConnection 方法,具体如下:
- # ExchangeFinder
- //为承载新的数据流 寻找 连接。寻找顺序是 已分配的连接、连接池、新建连接
- private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
- int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
- synchronized (connectionPool) {
- // 1.尝试使用 已给数据流分配的连接.(例如重定向请求时,可以复用上次请求的连接)
- releasedConnection = transmitter.connection;
- result = transmitter.connection;
- if (result == null) {
- // 2. 没有已分配的可用连接,就尝试从连接池获取。(连接池稍后详细讲解)
- if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
- result = transmitter.connection;
- }
- }
- }
- synchronized (connectionPool) {
- if (newRouteSelection) {
- //3. 现在有了IP地址,再次尝试从连接池获取。可能会因为连接合并而匹配。(这里传入了routes,上面的传的null)
- routes = routeSelection.getAll();
- if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
- foundPooledConnection = true;
- result = transmitter.connection;
- }
- }
- // 4.第二次没成功,就把新建的连接,进行TCP + TLS 握手,与服务端建立连接. 是阻塞操作
- result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
- connectionRetryEnabled, call, eventListener);
- synchronized (connectionPool) {
- // 5. 最后一次尝试从连接池获取,注意最后一个参数为true,即要求 多路复用(http2.0)
- //意思是,如果本次是http2.0,那么为了保证 多路复用性,(因为上面的握手操作不是线程安全)会再次确认连接池中此时是否已有同样连接
- if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
- // 如果获取到,就关闭我们创建里的连接,返回获取的连接
- result = transmitter.connection;
- } else {
- //最后一次尝试也没有的话,就把刚刚新建的连接存入连接池
- connectionPool.put(result);
- }
- }
- return result;
- }
上面精简了部分代码,可以看出,连接拦截器使用了5种方法查找连接:
以上就是连接拦截器尝试复用连接的操作,流程图如下:
上面说到我们会建立一个 TCP 连接池,但如果没有任务了,空闲的连接也应该及时清除,OKHttp 是如何做到的呢?
- # RealConnectionPool
- private val cleanupQueue: TaskQueue = taskRunner.newQueue()
- private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
- override fun runOnce(): Long = cleanup(System.nanoTime())
- }
- long cleanup(long now) {
- int inUseConnectionCount = 0;//正在使用的连接数
- int idleConnectionCount = 0;//空闲连接数
- RealConnection longestIdleConnection = null;//空闲时间最长的连接
- long longestIdleDurationNs = Long.MIN_VALUE;//最长的空闲时间
- //遍历连接:找到待清理的连接, 找到下一次要清理的时间(还未到最大空闲时间)
- synchronized (this) {
- for (Iterator
i = connections.iterator(); i.hasNext(); ) { - RealConnection connection = i.next();
- //若连接正在使用,continue,正在使用连接数+1
- if (pruneAndGetAllocationCount(connection, now) > 0) {
- inUseConnectionCount++;
- continue;
- }
- //空闲连接数+1
- idleConnectionCount++;
- // 赋值最长的空闲时间和对应连接
- long idleDurationNs = now - connection.idleAtNanos;
- if (idleDurationNs > longestIdleDurationNs) {
- longestIdleDurationNs = idleDurationNs;
- longestIdleConnection = connection;
- }
- }
- //若最长的空闲时间大于5分钟 或 空闲数 大于5,就移除并关闭这个连接
- if (longestIdleDurationNs >= this.keepAliveDurationNs
- || idleConnectionCount > this.maxIdleConnections) {
- connections.remove(longestIdleConnection);
- } else if (idleConnectionCount > 0) {
- // else,就返回 还剩多久到达5分钟,然后wait这个时间再来清理
- return keepAliveDurationNs - longestIdleDurationNs;
- } else if (inUseConnectionCount > 0) {
- //连接没有空闲的,就5分钟后再尝试清理.
- return keepAliveDurationNs;
- } else {
- // 没有连接,不清理
- cleanupRunning = false;
- return -1;
- }
- }
- //关闭移除的连接
- closeQuietly(longestIdleConnection.socket());
- //关闭移除后 立刻 进行下一次的 尝试清理
- return 0;
- }
思路还是很清晰的:
1. 在将连接加入连接池时就会启动定时任务
2. 有空闲连接的话,如果最长的空闲时间大于5分钟 或 空闲数 大于5,就移除关闭这个最长空闲连接;如果 空闲数 不大于5 且 最长的空闲时间不大于5分钟,就返回到5分钟的剩余时间,然后等待这个时间再来清理。
3. 没有空闲连接就等5分钟后再尝试清理。
4. 没有连接不清理。
流程如下图所示:
当前文章:面试突击:OkHttp原理八连问
当前链接:http://www.shufengxianlan.com/qtweb/news28/271478.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联