​OkHttp源码深度解析

本文来自OPPO互联网基础技术团队,转载请注名作者。同时欢迎关注我们的公众号:OPPO_tech,与你分享OPPO前沿互联网技术及活动。

OkHttp应该是目前Android平台上使用最为广泛的开源网络库了,Android 在6.0之后也将内部的HttpUrlConnection的默认实现替换成了OkHttp。

大部分开发的同学可能都有接触过OkHttp的源码,但很少有比较全面的阅读和了解的,目前网络上的大部分源码解析文章也都是点到为止,并且大段的贴源码,这种解析方式是我无法认可的,因此才有了想要重新写一篇解析OkHttp源码的想法。

这篇文章的目的,一个是要比较全面的介绍OkHttp源码,另一个是要尽量避免大段的贴出源码,涉及到源码的部分,会尽量通过调用关系图来展示。

本篇选用的OkHttp源码是目前最新的4.4.0版本,面向的读者也是有一定使用基础的Android开发同学。

OkHttp的源码可以从github上下载到(github.com/square/OkHtt)。

直奔主题,文章将从一下几个方面开始来拆解OkHttp的源码:

  1. 整体结构
  2. 拦截器
  3. 任务队列
  4. 连接复用和连接池

1. 从一个例子出发

首先来看一个最简单的Http请求是如何发送的。

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("google.com").build();
Response response = client.newCall(request).execute();
return response.body().string();

这一段代码就是日常使用OkHttp最常见的用法,跟进源码后,可以得到一张更为详细的流程图,通过这张图来看下内部的逻辑是如何流动的。

涉及到了几个核心类,我们一个个来看下。

  1. OkHttpClient
  2. Request 和 Response
  3. RealCall

OkHttpClient:这个是整个OkHttp的核心管理类,所有的内部逻辑和对象归OkHttpClient统一来管理,它通过Builder构造器生成,构造参数和类成员很多,这里先不做具体的分析。

Request 和Response:Request是我们发送请求封装类,内部有url, header , method,body等常见的参数,Response是请求的结果,包含code, message, header,body ;这两个类的定义是完全符合Http协议所定义的请求内容和响应内容。

RealCall :负责请求的调度(同步的话走当前线程发送请求,异步的话则使用OkHttp内部的线程池进行);同时负责构造内部逻辑责任链,并执行责任链相关的逻辑,直到获取结果。虽然OkHttpClient是整个OkHttp的核心管理类,但是真正发出请求并且组织逻辑的是RealCall类,它同时肩负了调度和责任链组织的两大重任,接下来我们来着重分析下RealCall类的逻辑。

RealCal类的源码地址:https://github.com/square/OkH...

RealCall类并不复杂,有两个最重要的方法,execute() 和 enqueue(),一个是处理同步请求,一个是处理异步请求。跟进enqueue的源码后发现,它只是通过异步线程和callback做了一个异步调用的封装,最终逻辑还是会调用到execute()这个方法,然后调用了getResponseWithInterceptorChain()获得请求结果。

看来是 getResponseWithInterceptorChain() 方法承载了整个请求的核心逻辑,那么只需要把这个方法分析清楚了,真个OkHttp的请求流程就大体搞明白了。既然这么重要的方法,还是不能免俗的贴下完整的源码。

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)

    var calledNoMoreExchanges = false
    try {
      val response = chain.proceed(originalRequest)
     ............

从源码可以看到,即使是 getResponseWithInterceptorChain() 方法的逻辑其实也很简单,它生成了一个Interceptors拦截器的List列表,按顺序依次将:

  • client.Interceptors
  • RetryAndFollowUpInterceptor,
  • BridgeInterceptor
  • CacheInterceptor
  • ConnectInterceptor
  • client.networkInterceptors
  • CallServerInterceptor

这些成员添加到这个List中,然后创建了一个叫RealInterceptorChain的类,最后的Response就是通过chain.proceed获取到的。

通过进一步分析RealInterceptorChain和Interceptors,我们得到了一个结论,OkHttp将整个请求的复杂逻辑切成了一个一个的独立模块并命名为拦截器(Interceptor),通过责任链的设计模式串联到了一起,最终完成请求获取响应结果。

具体这些拦截器是如何串联,每个拦截器都有什么功能,下面的内容会作更详细的分析。

2. OkHttp的核心:拦截器

我们已经知道了OkHttp的核心逻辑就是一堆拦截器,那么它们是如何构造并关联到一起的呢?这里就要来分析RealInterceptorChain这个类了。

通过前面的分析可知,RealCall将Interceptors一个一个添加到List之后 ,就构造生成了一个RealInterceptorChain对象,并调用chain.proceed获得响应结果。那么就来分析下chain.proceed这个方法到底干了啥。为了不让篇幅太长,这里就不贴出源码内容,仅给出分析后的结论,大家对照源码可以很快看懂。

RealInterceptorChain的源码:https://github.com/square/OkH...

根据对RealInterceptorChain的源码解析,可得到如下示意图(省略了部分拦截器):

结合源码和该示意图,可以得到如下结论:

  1. 拦截器按照添加顺序依次执行
  2. 拦截器的执行从RealInterceptorChain.proceed()开始,进入到第一个拦截器的执行逻辑
  3. 每个拦截器在执行之前,会将剩余尚未执行的拦截器组成新的RealInterceptorChain
  4. 拦截器的逻辑被新的责任链调用next.proceed()切分为start、next.proceed、end这三个部分依次执行
  5. next.proceed() 所代表的其实就是剩余所有拦截器的执行逻辑
  6. 所有拦截器最终形成一个层层内嵌的嵌套结构

了解了上面拦截器的构造过程,我们再来一个个的分析每个拦截器的功能和作用。

从这张局部图来看,总共添加了五个拦截器(不包含自定义的拦截器如client.interceptors和client.networkInterceptors,这两个后面再解释)。

先来大概的了解下每一个拦截器的作用

  • retryAndFollowUpInterceptor——失败和重定向拦截器
  • BridgeInterceptor——封装request和response拦截器
  • CacheInterceptor——缓存相关的过滤器,负责读取缓存直接返回、更新缓存
  • ConnectInterceptor——连接服务,负责和服务器建立连接 这里才是真正的请求网络
  • CallServerInterceptor——执行流操作(写出请求体、获得响应数据) 负责向服务器发送请求数据、从服务器读取响应数据 进行http请求报文的封装与请求报文的解析

下面就来一个个的拦截器进行分析。

2.1 RetryAndFollowUpInterceptor

源码地址:https://github.com/square/OkH...

根据源码的逻辑走向,直接画出对应的流程图(这段逻辑在RetryAndFollowUpInterceptor的intercept()方法内部):

从上图中可以看出,RetryAndFollowUpInterceptor开启了一个while(true)的循环,并在循环内部完成两个重要的判定,如图中的蓝色方框:

  1. 当请求内部抛出异常时,判定是否需要重试
  2. 当响应结果是3xx重定向时,构建新的请求并发送请求

重试的逻辑相对复杂,有如下的判定逻辑(具体代码在RetryAndFollowUpInterceptor类的recover方法):

  • 规则1: client的retryOnConnectionFailure参数设置为false,不进行重试
  • 规则2: 请求的body已经发出,不进行重试
  • 规则3: 特殊的异常类型不进行重试(如ProtocolException,SSLHandshakeException等)
  • 规则4: 没有更多的route(包含proxy和inetaddress),不进行重试

前面这四条规则都不符合的条件下,则会重试当前请求。重定向的逻辑则相对简单,这里就不深入了。

2.2 Interceptors和NetworkInterceptors的区别

前面提到,在OkHttpClient.Builder的构造方法有两个参数,使用者可以通过addInterceptor 和 addNetworkdInterceptor 添加自定义的拦截器,分析完 RetryAndFollowUpInterceptor 我们就可以知道这两种自动拦截器的区别了。

从前面添加拦截器的顺序可以知道 Interceptors 和 networkInterceptors 刚好一个在 RetryAndFollowUpInterceptor 的前面,一个在后面。

结合前面的责任链调用图可以分析出来,假如一个请求在 RetryAndFollowUpInterceptor 这个拦截器内部重试或者重定向了 N 次,那么其内部嵌套的所有拦截器也会被调用N次,同样 networkInterceptors 自定义的拦截器也会被调用 N 次。而相对的 Interceptors 则一个请求只会调用一次,所以在OkHttp的内部也将其称之为 Application Interceptor。

2.3 BridgeInterceptor 和 CacheInterceptor

BridageInterceptor 拦截器的功能如下:

  1. 负责把用户构造的请求转换为发送到服务器的请求 、把服务器返回的响应转换为用户友好的响应,是从应用程序代码到网络代码的桥梁
  2. 设置内容长度,内容编码
  3. 设置gzip压缩,并在接收到内容后进行解压。省去了应用层处理数据解压的麻烦
  4. 添加cookie
  5. 设置其他报头,如User-Agent,Host,Keep-alive等。其中Keep-Alive是实现连接复用的必要步骤

CacheInterceptor 拦截器的逻辑流程如下:

  1. 通过Request尝试到Cache中拿缓存,当然前提是OkHttpClient中配置了缓存,默认是不支持的。
  2. 根据response,time,request创建一个缓存策略,用于判断怎样使用缓存。
  3. 如果缓存策略中设置禁止使用网络,并且缓存又为空,则构建一个Response直接返回,注意返回码=504
  4. 缓存策略中设置不使用网络,但是又缓存,直接返回缓存
  5. 接着走后续过滤器的流程,chain.proceed(networkRequest)
  6. 当缓存存在的时候,如果网络返回的Resposne为304,则使用缓存的Resposne。
  7. 构建网络请求的Resposne
  8. 当在OkHttpClient中配置了缓存,则将这个Resposne缓存起来。
  9. 缓存起来的步骤也是先缓存header,再缓存body。
  10. 返回Resposne

接下来的两个应该是所有内部拦截器里最重要的两个了,一个负责处理Dns和Socket连接,另一个则负责Http请求体的发送。

2.4 ConnectInterceptor

上面已经提到了,connectInterceptor应该是最重要的拦截器之一了,它同时负责了Dns解析和Socket连接(包括tls连接)。

源码地址:https://github.com/square/OkH...

这个类本身很简单,从源码来看,关键的代码只有一句。

val exchange = transmitter.newExchange(chain, doExtensiveHealthChecks)

从Transmitter获得了一个新的ExChange的对象,这句简单的代码仔细跟进去以后,会发现其实埋藏了非常多的逻辑,涉及整个网络连接建立的过程,其中包括dns过程和socket连接的过程,这里我们通过两个图来了解下整个网络连接的过程。

先来看方法调用的时序图,梳理出关键步骤:

  1. ConnectInterceptor调用transmitter.newExchange
  2. Transmitter先调用ExchangeFinder的find()获得ExchangeCodec
  3. ExchangeFinder调用自身的findHealthConnectio获得RealConnection
  4. ExchangeFinder通过刚才获取的RealConnection的codec()方法获得ExchangeCodec
  5. Transmitter获取到了ExchangeCodec,然后new了一个ExChange,将刚才的ExchangeCodec包含在内

通过刚才的5步,最终Connectinterceptor通过Transmitter获取到了一个Exchange的类,这个类有两个实现,一个是Http1ExchangeCodec,一个是Http2Exchangecodec,分别对应的是Http1协议和Http2协议。

那么获取到Exchange类有什么用呢?再来看这几个类的关系图,如下:

从上面可以看到,前面获得的Exchange类里面包含了ExchangeCodec对象,而这个对象里面又包含了一个RealConnection对象,RealConnection的属性成员有socket、handlShake、protocol等,可见它应该是一个Socket连接的包装类,而ExchangeCode对象是对RealConnection操作(writeRequestHeader、readResposneHeader)的封装。

通过这两个图可以很清晰的知道,最终获得的是一个已经建立连接的Socket对象,也就是说,在ConnectInterceptor内部已经完成了socket连接,那么具体是哪一步完成的呢?

看上面的时序图,可以知道,获得RealConnection的ExchangeFinder调用的findHealthConnection()方法,因此,socket连接的获取和建立都是在这里完成的。

同样,在socket进行连接之前,其实还有一个dns的过程,也是隐含在findHealthConnection 里的内部逻辑,详细的过程在后面DNS的过程再进行分析,这里ConnectionInterceptor的任务已经完成了。

另外还需要注意的一点是,在执行完ConnectInterceptor之后,其实添加了自定义的网络拦截器networkInterceptors,按照顺序执行的规定,所有的networkInterceptor执行执行,socket连接其实已经建立了,可以通过realChain拿到socket做一些事情了,这也就是为什么称之为network Interceptor的原因。

2.5 CallServerInterceptor

CalllServerInterceptor是最后一个拦截器了,前面的拦截器已经完成了socket连接和tls连接,那么这一步就是传输http的头部和body数据了。

CallServerInterceptor源码:https://github.com/square/OkH...

CallServerInterceptor由以下步骤组成:

  1. 向服务器发送 request header
  2. 如果有 request body,就向服务器发送
  3. 读取 response header,先构造一个 Response 对象
  4. 如果有 response body,就在 3 的基础上加上 body 构造一个新的 Response 对象

这里我们可以看到,核心工作都由 HttpCodec 对象完成,而 HttpCodec 实际上利用的是 Okio,而 Okio 实际上还是用的 Socket,只不过一层套一层,层数有点多。

3. 整体架构

至此为止,所有的拦截器都讲完了,我们已经知道了一个完整的请求流程是如何发生的。那么这个时候再来看OkHttp的架构图就比较清晰了

整个OkHttp的架构纵向来看就是五个内部拦截器,横向来看被切分成了几个部分,而纵向的拦截器就是通过对横向分层的调用来完成整个请求过程,从这两个方面来把握和理解OkHttp就比较全面了。

针对横向的部分将在接下来的部分进行详细分析。

3.1 连接复用,DNS和Socket的连接

通过前面的分析知道,Socket连接和Dns过程都是在ConnecInterceptor中通过Transmitter和ExchangeFinder来完成的,而在前面的时序图中可以看到,最终建立Socket连接的方法是通过ExchangeFinder的findConnection来完成的,可以说一切秘密都是findConnection方法中。

因此接下来详细解析下findConnection(),这里贴出源码和注释。

synchronized(connectionPool) {
      //前面有一大段判定当前的conencection是否需要释放,先删除
      .....

      if (result == null) {
        // 1, 第一次尝试从缓冲池里面获取RealConnection(Socket的包装类)
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
          foundPooledConnection = true
          result = transmitter.connection
        } else if (nextRouteToTry != null) {
          //2, 如果缓冲池中没有,则看看有没有下一个Route可以尝试,这里只有重试的情况会走进来
          selectedRoute = nextRouteToTry
          nextRouteToTry = null
        } else if (retryCurrentRoute()) {
          //3,如果已经设置了使用当前Route重试,那么会继续使用当前的Route
          selectedRoute = transmitter.connection!!.route()
        }
      }
    }
    if (result != null) {
      // 4,如果前面发现ConnectionPool或者transmiter中有可以复用的Connection,这里就直接返回了
      return result!!
    }

    // 5, 如果前面没有获取到Connection,这里就需要通过routeSelector来获取到新的Route来进行Connection的建立
    var newRouteSelection = false
    if (selectedRoute == null && (routeSelection == null || !routeSelection!!.hasNext())) {
      newRouteSelection = true
      //6,获取route的过程其实就是DNS获取到域名IP的过程,这是一个阻塞的过程,会等待DNS结果返回
      routeSelection = routeSelector.next()
    }

    var routes: List<Route>? = null
    synchronized(connectionPool) {
      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        routes = routeSelection!!.routes
        //7,前面如果通过routeSelector拿到新的Route,其实就是相当于拿到一批新的IP,这里会再次尝试从ConnectionPool
        // 中检查是否有可以复用的Connection
        if (connectionPool.transmitterAcquirePooledConnection( address, transmitter, routes, false)) {
          foundPooledConnection = true
          result = transmitter.connection
        }
      }
 if (!foundPooledConnection) {
        if (selectedRoute == null) {
           //8,前面我们拿到的是一批IP,这里通过routeSelection获取到其中一个IP,Route是proxy和InetAddress的包装类
          selectedRoute = routeSelection!!.next()
        }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        //9,用新的route创建RealConnection,注意这里还没有尝试连接
        result = RealConnection(connectionPool, selectedRoute!!)
        connectingConnection = result
      }
  }

    // If we found a pooled connection on the 2nd time around, we're done.
    // 10,注释说得很清楚,如果第二次从connectionPool获取到Connection可以直接返回了
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result!!)
      return result!!
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    //11,原文注释的很清楚,这里是进行TCP + TLS连接的地方
    result!!.connect(
        connectTimeout,   
 readTimeout,
        writeTimeout,
        pingIntervalMillis,
        connectionRetryEnabled,
        call,
        eventListener
    )
 //后面一段是将连接成功的RealConnection放到ConnectionPool里面,这里就不贴出来了
    }
    return result!!

从上面的流程可以看到,findConnection这个方法做了以下几件事:

  1. 检查当前exchangeFinder所保存的Connection是否满足此次请求
  2. 检查当前连接池ConnectionPool中是否满足此次请求的Connection
  3. 检查当前RouteSelector列表中,是否还有可用Route(Route是proxy,IP地址的包装类),如果没有就发起DNS请求
  4. 通过DNS获取到新的Route之后,第二次从ConnectionPool查找有无可复用的Connection,否则就创建新的RealConnection
  5. 用RealConnection进行TCP和TLS连接,连接成功后保存到ConnectionPool

Connection的连接复用

可以看到,第二步和第四步对ConnectionPool做了两次复用检查,第五步创建了新的RealConnection之后就会写会到ConnectionPool中。

因此这里就是OkHttp的连接复用其实是通过ConnectionPool来实现的,前面的类图中也反映出来,ConnectionPool内部有一个connections的ArrayDeque对象就是用来保存缓存的连接池。

DNS过程

从前面解析的步骤可知,Dns的过程隐藏在了第三步RouteSelector检查中,整个过程在findConnection方法中写的比较散,可能不是特别好理解,但是只要搞明白了RouteSelector, RouteSelection,Route这三个类的关系,其实就比较容易理解了,如下图中展示了三个类之间的关系。

从图中可以得到如下结论:

  • RouteSelector在调用next遍历在不同proxy情况下获得下一个Selection封装类,Selection持有一个Route的列表,也就是每个proxy都对应有Route列表
  • Selection其实就是针对List<Route>封装的一个迭代器,通过next()方法获得下一个Route,Route持有proxy、address和inetAddress,可以理解为Route就是针对IP和Proxy配对的一个封装
  • RouteSelector的next()方法内部调用了nextProxy(), nextProxy()又会调用resetNextInetSocketAddres()方法
  • resetNextInetSocketAddres通过address.dns.lookup获取InetSocketAddress,也就是IP地址

通过上面一系列流程知道,IP地址最终是通过address的dns获取到的,而这个dns又是怎么构建的呢?

反向追踪代码,定位到address的dns是transmitter在构建address的时候,将内置的client.dns传递进来,而client.dns是在OkHttpclient的构建过程中传递进来Dns.System,里面的lookup是通过InetAddress.getAllByName 方法获取到对应域名的IP,也就是默认的Dns实现。

至此,整个DNS的过程就真相大白了。OkHttp在这一块设计的时候,为了强调接耦和开放性,将DNS的整个过程隐藏的比较深,如果不仔细debug跟代码的话,可能还不是很容易发现。

3.2 Socket连接的建立

通过Dns获得Connectoin之后,就是建立连接的过程了,在findConnection中只体现为一句代码,如下:

result!!.connect(
        connectTimeout,
        readTimeout,
        writeTimeout,
        pingIntervalMillis,
        connectionRetryEnabled,
        call,
        eventListener
)

这里的result是RealConnection类型的对象,就是调用了RealConnection.connect方法,终于离开findConnection 了,接下来看下connect 方法的源码。

//省略前面一大段 
....

    while (true) {
      try {
        if (route.requiresTunnel()) {
 //这里进入的条件是,通过http代理了https请求,有一个特殊的协议交换过程
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
        } else {
 //建立socket连接
          connectSocket(connectTimeout, readTimeout, call, eventListener)
        }
 //如果前面判定是https请求,这里就是https的tls建立过程
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
        break
      } catch (e: IOException) {
 //清理资源
        socket?.closeQuietly()
 //对异常做二次封装,然后抛出
        if (routeException == null) {
          routeException = RouteException(e)
        } else {
          routeException.addConnectException(e)
        }
        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException
        }
      }
    }

connect方法并不复杂,先会判定是否有代理的情况做一些特殊处理,然后调用系统方法建立socket连接。

如果是https请求,还有一个tls的连接要建立,这中间如果有抛出异常,会做一个二次封装再抛出去。

4. 总结

到此为止,基本上OkHttp源码设计的一个全貌都有了,有一些内容因为日常使用经常会遇到,比如OkHttpClient的Builde参数,比如Request和Response的用法,这里就不再多讲。另外针对http2和https的支持,因为涉及到更为复杂的机制和原理,以后有机会另开一篇文章来说。

weixin.qq.com/r/3By8pK7 (二维码自动识别)

发布于 03-25