OkHttp源码详细解析

0 阅读8分钟

一起分析OkHttp的源码原理

背景

  • 那么我们为什么要聚焦于OkHttp呢,这是因为OkHttp是Android最主流的HTTP客户端。像咱们使用的Retrofit底层就是OkHttp
  • 了解OkHttp有助于我们在遇到网络请求问题的时候,通过阅读源码解决问题,更加重温的了解OkHttp

OkHttp流程图

OkHttp.png

OkHttpClient

  • 它是OkHttp的门面类

  • 它是统一的高层接口

  • 通俗解释的话

    • 你作为客人,到了一家酒店入住,你并不需要找到厨师点菜,找到清洁工去打扫房间。这样对于客人,太麻烦了
    • 你只需要找到前台,对前台说“我要吃饭,中午帮我打扫一下房间”。前台回去安排员工去满足你的要求
    • 这个前台就是门面类
  • 所以我们第一步通常是通过builder去构造门面类,然后执行相关操作

Request

  • 本质上是一个不可变的数据载体,他把HTTP协议中定义的各个部分封装在一起

  • HttpUrl(URL)

    • 指定要发给谁
  • Method(方法)

    • GET、POST、PUT等等
  • Headers(请求头)

    • 传递数据,比如Content-Type(告诉服务器你发送的是图片还是文字)、User-Agent(我是谁)
  • RequestBody(请求体)

    • 当你使用POST或者PUT的时候,实际上船的数据
  • TAG(标签)

    • 这不是HTTP协议的一部分,OKHttp可以给请求贴上标签,然后统一处理

RealCall

  • RealCallCall接口的唯一实现类

  • 它负责连接“应用层”和“网络层”,是一个桥梁

  • RealCall的两种请求

    • 同步请求:execute()

      • 他会阻塞当前线程,知道服务器返回结构或者发生超时报错

      •     // 这是相关源码
            override fun execute(): Response {
            
          // 原子性检查,防止重复执行
          check(executed.compareAndSet(false, true)) { "Already Executed" }
            // 超时监控
          timeout.enter()
          // 事件监听 开始执行的回调
          callStart()
          try {
            // 记录这个同步请求
            client.dispatcher.executed(this)
            // 开启拦截器链
            return getResponseWithInterceptorChain()
          } finally {
            // 调度器结束
            client.dispatcher.finished(this)
          }
        }
        
      • executed.compareAndSet(false, true)

        • 在旧版本中,使用的是synchronized同步锁
        • 这是一个原子操作,它检查executed是否为false,如果是就设为true,返回成功
        • 确保一个Call对象只能被执行一次,如果你调用了两次execute(),会抛出异常
      • timeout.enter()

        • RealCall内部持有一个AsyncTimeout对象
        • 虽然同步请求会阻塞线程,但是不能永远的等待它的返回把。这个就是开启了一个倒计时,如果超过设置的事件,就会强制关闭连接,抛出异常
      • callStart()

        • 这是监控功能,会通知注册观察者”请求开始了“,
      • client.dispatcher.executed(this)

        • 同步请求虽然不需要分配线程,但是调度器需要知道有一个同步的任务正在运行
        • 它会将这个RealCall放到一个runningSyncCalls的双端队列里
        • 主要用途是:当你想cancelAll()时候,调度器可以找到这个请求,并且将它杀掉
      • getResponseWithInterceptorChain()

        • 责任链
        • 整个架构的中心,也是RealCall的核心职责
        • 详细见《Okhttp的拦截器》
      • finally { client.dispatcher.finished(this) }

        • 不管是执行成功还是失败,都必须执行这一步
        • 在调度器的runningSyncCalls队列中移除自己。如果没有这一步,会导致内存泄露,因为调度器会认为你还在运行。
    • 异步请求:qneueue(callback) !!!

      • override fun enqueue(responseCallback: Callback) {
            // 原子检查
            check(executed.compareAndSet(false, true)) { "Already Executed" }
            // 事件监听 回调callStart
            callStart()
            // 将任务交给调度器
            // 这里面船舰了一个 AsyncCall对象,将你的responseCallback穿给了这个对象
            client.dispatcher.enqueue(AsyncCall(responseCallback))
          }
        
      • 这里面新建了一个AsyncCall对象,那么我们就聚焦这个AsyncCall吧

        • 这是RealCall的一个内部类,本质上是一个Runnable

        • internal inner class AsyncCall(
              private val responseCallback: Callback
            ) : Runnable {
              @Volatile var callsPerHost = AtomicInteger(0)
                private set
          ​
              fun reuseCallsPerHostFrom(other: AsyncCall) {
                this.callsPerHost = other.callsPerHost
              }
          ​
              val host: String
                get() = originalRequest.url.host
          ​
              val request: Request
                  get() = originalRequest
          ​
              val call: RealCall
                  get() = this@RealCall
          ​
              /**
               * Attempt to enqueue this async call on [executorService]. This will attempt to clean up
               * if the executor has been shut down by reporting the call as failed.
               */
              fun executeOn(executorService: ExecutorService) {
                client.dispatcher.assertThreadDoesntHoldLock()
          ​
                var success = false
                try {
                  executorService.execute(this)
                  success = true
                } catch (e: RejectedExecutionException) {
                  val ioException = InterruptedIOException("executor rejected")
                  ioException.initCause(e)
                  noMoreExchanges(ioException)
                  responseCallback.onFailure(this@RealCall, ioException)
                } finally {
                  if (!success) {
                    client.dispatcher.finished(this) // This call is no longer running!
                  }
                }
              }
          ​
              override fun run() {
                threadName("OkHttp ${redactedUrl()}") {
                  var signalledCallback = false
                  timeout.enter()
                  try {
                    val response = getResponseWithInterceptorChain()
                    signalledCallback = true
                    responseCallback.onResponse(this@RealCall, response)
                  } catch (e: IOException) {
                    if (signalledCallback) {
                      // Do not signal the callback twice!
                      Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)
                    } else {
                      responseCallback.onFailure(this@RealCall, e)
                    }
                  } catch (t: Throwable) {
                    cancel()
                    if (!signalledCallback) {
                      val canceledException = IOException("canceled due to $t")
                      canceledException.addSuppressed(t)
                      responseCallback.onFailure(this@RealCall, canceledException)
                    }
                    throw t
                  } finally {
                    client.dispatcher.finished(this)
                  }
                }
              }
            }
          
        • internal inner class AsyncCall

          • 这个inner关键字的作用是 它持有外部RealCall类的引用
          • 因为他得知道发给谁,然后执行拦截器链
        • callsPerHost与并发控制

          • @Volatile var callsPerHost = AtomicInteger(0)

          • 为什么用AtomicInteger呢?

            • OKHttp限制同一个域名的并发数 默认应该是五个
            • 这个变量是线程安全的,用来记录当前有多少个请求正在访问同一个Host
            • Dispatcher在调度时会检查这个值,如果超过限制就让它排队
            • 这是为了防止App瞬间的压力过大被服务器拉黑
        • executeOn(executorService) 任务上架
          • executorService.execute(this)
          • 这是RealCall进入线程池的时刻
          • 异常捕获:如果线程池拒绝接受任务(源码中的RejectedExecutionException),代码在catch块里通过onFailure回调了错误(responseCallback.onFailure(this@RealCall, ioException)
          • 兜底操作finally里的dispatcher.finished(this)十分的重要。因为任务没能成功进入到线程池的话,需要通知调度器,否则调度器的计数器会出现错误,导致后续任务无法运行
        • run():真正的执行

          • 这个代码中有三重保险

          • 一保险:signalledCallback(状态标志)

            • 这是一个Boolean,确保Callback只会被调用一次
            • 这是因为,如果不加标记可能会出现先回调了成功,然后又回调了失败的情况
          • 二保险:双重异常捕获

            • catch (e: IOException):捕获正常的网络异常(如断网、超时),回调onFailure
            • catch (t: Throwable):捕获非 IO 异常(如代码 Bug、内存溢出)。它会先调用cancel()取消请求,再通过onFailure告知上层,最后重新抛出异常
          • 三保险:finally的“交接棒”机制

            • client.dispatcher.finished(this):这是 OkHttp 永动机的秘密
            • 每单一个任务在子线程跑完了,它都会告诉调度器:“我腾出空位了”。调度器此时会立刻检查等待队列,把排队的请求拉进来运行
        • 其余优秀的点

          • threadName("OkHttp ${redactedUrl()}"):这是一个非常好的习惯。OkHttp在执行时临时更改一下线程名字,这样你在排查线上 Bug 或查看 Logcat 堆栈时,一眼就能看出来哪个线程在跑哪个请求
        • 涉及到的其他知识点

          • CAS与原子性
          • 责任链的触发
        • 一个小问题:“如果 OkHttp 的异步请求失败了,它是怎么通知下一条请求开始执行的”

          • AsyncCallrun方法中,使用了try-finally结构。无论请求成功还是失败,最终都会在finally块中调用dispatcher.finished(this)。调度器收到这个信息之后,会调用其内部的promoteCalls()方法,从等待队列中取出下一个符合条件的请求并丢入线程池。这就是它的自动流转机制

Dispatcher(任务分发机制)

  • RealCall只是一个任务的载体,而Dispatcher是真正的“大脑”,负责决定什么时候执行这个任务

  • @get:Synchronized var maxRequests = 64
        set(maxRequests) {
          require(maxRequests >= 1) { "max < 1: $maxRequests" }
          synchronized(this) {
            field = maxRequests
          }
          promoteAndExecute()
        }
    
    • @get:Synchronized

      • 保证了多线程安全。因为Dispatcher会被多个线程访问,必须保证读取到的值是最新的、完整的
    • set(maxRequests) { ... }(自定义 Setter)

      • 当你执行client.dispatcher.maxRequests = 100时,这段逻辑就会执行

      • 确保你设置的值必须 >= 1,如果你传入0或者负数,程序会直接抛出异常

      • 然后是线程安全地赋值

        • field代表背后的真是存储空间
      • 之后就是核心动作:promoteAndExecute()

        • 假设你之前的maxRequests是 5 ,现在有 10 个请求,那么有 5 个在运行,5 个在等待队列中排队
        • 此时你把maxRequests更改为64个
        • 就会立刻调用promoteAndExecute()。然后扫描一遍等待队列,把卡在这里的请求放到运行队列中,然后立刻发出请求
    • 总结的话:这段就是OkHttp支持动态修改并发上限

  • @get:Synchronized var maxRequestsPerHost = 5
        set(maxRequestsPerHost) {
          require(maxRequestsPerHost >= 1) { "max < 1: $maxRequestsPerHost" }
          synchronized(this) {
            field = maxRequestsPerHost
          }
          promoteAndExecute()
        }
    
    • 这个逻辑基本同上

      • 然后是,上面的maxRequests是控制总出口的流量
      • maxRequestsPerHost是控去同一个目的地的流量
  • @set:Synchronized
    @get:Synchronized
    var idleCallback: Runnable? = null
    
    • idleCallback:空闲通知机制
    • 这个是一个可选的回调。当Dispatcher变得“空闲”时,这个回调会被触发
    • 常用于性能监控
  • @get:Synchronized
      @get:JvmName("executorService") val executorService: ExecutorService
        get() {
          if (executorServiceOrNull == null) {
            executorServiceOrNull = ThreadPoolExecutor(
                0, // 核心线程数
                Int.MAX_VALUE, // 最大线程数
                60, TimeUnit.SECONDS, // 线程存活时间 和 单位
                SynchronousQueue(),  // 直接交付队列
                threadFactory("$okHttpName Dispatcher", false))
          }
          return executorServiceOrNull!!
        }
    
    • 当你看到这个无限大的线程时候,可能觉得这个很危险,但是由于Dispatcher在外部已经通过maxRequests(64)限制了任务数量,所以这里涉及无限是为了保证:只要Dispatcher允许运行,线程池就能立刻开辟线程,绝不再线程内部排队
    • 然后SynchronousQueue():这是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的移除操作。这配合“无限大的线程”使用的,达到了“及时交付、立刻执行
  • private val readyAsyncCalls = ArrayDeque<AsyncCall>()   // 等待异步队列
    private val runningAsyncCalls = ArrayDeque<AsyncCall>() // 运行异步队列
    private val runningSyncCalls = ArrayDeque<RealCall>()  // 运行同步队列
    
    • 这是三个双端队列

    • 那为什么不用Deque呢,

      • 这是因为双端队列支持再头尾高效插入删除,OkHttp主要讲作为FIFO(先进先出) 队列使用。也方便后续可能的优先级调整和任务取消
  • internal fun enqueue(call: AsyncCall) {
      synchronized(this) {
        readyAsyncCalls.add(call) // 1. 先加入等待队列
    
        // 2. 共享 Host 计数器逻辑
        if (!call.call.forWebSocket) {
          val existingCall = findExistingCallWithHost(call.host)
          if (existingCall != null) {
              // 如果发现已有相同 Host 的请求,就复用它的计数器
              call.reuseCallsPerHostFrom(existingCall)
          }
        }
      }
      promoteAndExecute() // 3. 尝试执行
    }
    
    • findExistingCallWithHost(call.host)

      • OkHttp需要显示同一个Host的并发数。如果不共享计数器,每个AsyncCall都要去遍历runningAsyncCalls队列来输一遍,效率太低了

      • AsyncCall内部维护了一个callsPerHost类型是AtomicInteger

      • 当新请求到来时,先去runningAsyncCalls或者readyAsyncCalls寻找,有没有域名一样的请求呀

      • 如果有的话,直接通过reuseCallsPerHostFrom引用那个请求里面的AtomicInteger

        • fun reuseCallsPerHostFrom(other: AsyncCall) {
                this.callsPerHost = other.callsPerHost
              }
          
    • private fun findExistingCallWithHost(host: String): AsyncCall? {
        // 先从运行中找,再从等待中找
        for (existingCall in runningAsyncCalls) {
          if (existingCall.host == host) return existingCall
        }
        for (existingCall in readyAsyncCalls) {
          if (existingCall.host == host) return existingCall
        }
        return null
      }
      
    • @Synchronized fun cancelAll() {
          for (call in readyAsyncCalls) {
            call.call.cancel()
          }
          for (call in runningAsyncCalls) {
            call.call.cancel()
          }
          for (call in runningSyncCalls) {
            call.cancel()
          }
        }
      
      • 遍历所有队列(等待、异步运行、同步运行的),调用每一个Callcancel()
      • 注意cancel():并不意味着线程立即停止,它会标志Call为已取消,并关闭底层 Socket,从而导致正在进行的 IO 操作抛出异常,进而结束请求
    • private fun promoteAndExecute(): Boolean {
        this.assertThreadDoesntHoldLock() // 确认当前线程没持有锁,防止死锁
      
        val executableCalls = mutableListOf<AsyncCall>()
        val isRunning: Boolean
        synchronized(this) {
          val i = readyAsyncCalls.iterator()
          while (i.hasNext()) {
            val asyncCall = i.next()
      
            // 1. 检查总并发上限 (64)
            if (runningAsyncCalls.size >= this.maxRequests) break 
            // 2. 检查单 Host 并发上限 (5)
            if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue 
      
            // 3. 满足条件,从等待队列转移到运行队列
            i.remove()
            asyncCall.callsPerHost.incrementAndGet() // 该 Host 计数加 1
            executableCalls.add(asyncCall)
            runningAsyncCalls.add(asyncCall)
          }
          isRunning = runningCallsCount() > 0
        }
      
        // 4. 真正开始执行(在同步块之外,避免阻塞调度器)
        for (i in 0 until executableCalls.size) {
          val asyncCall = executableCalls[i]
          asyncCall.executeOn(executorService) // 交给线程池
        }
      
        return isRunning
      }
      
      • while循环遍历。如果总数满了就直接结束循环,因为超过总数的发不出去

        • 如果只是某个Host满了,就跳过然后查看下一个,因为下一个请求可能是去另外一个目的地的
    • // 异步任务结束
      internal fun finished(call: AsyncCall) {
        call.callsPerHost.decrementAndGet() // 该 Host 计数减 1
        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) {
          // 1. 将任务从运行中队列移除
          if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
          idleCallback = this.idleCallback
        }
      
        // 2. 每结束一个请求,就尝试从等待队列里拉出新的请求
        val isRunning = promoteAndExecute()
      
        // 3. 如果当前没有任何任务在跑了,触发空闲回调
        if (!isRunning && idleCallback != null) {
          idleCallback.run()
        }
      }
      
      • 这个是一个自循环机制:请求入队 -> 触发执行 -> 请求结束 -> 触发下一个请求执行 -> 没有的话就触发空闲

总结

  • 到这里,我们已经看到了OkHttpClientRequestRealCallDispatcher,接下来还有一个非常著名的拦截器链。再下一篇中,我们讲分析这个责任链模式
  • 本文首发于CSDN blog.csdn.net/2302_794601…