OkHttp 源码解析:enqueue 非同步流程与 Dispatcher 调度

OkHttp 的由来

对于 HTTP,早期的安卓提供了两种 API:一种是 Java 原生的 URLConnection,另一种是 Apache 的 HttpClient。

这两种方案,Square 公司觉得都不好用,就对 API 进行了封装,形成了最初版本的 OkHttp 框架。之后 Square 先是移除了 HttpClient 内部实现,接着连 URLConnection 内部实现也移除了。两者都不用,选择自己实现底层支持,比如 TCP 连接的建立等工作。

接着,安卓官方也将其 URLConnection 的底层实现改为了 OkHttp 的底层实现,比如:如何通过 DNS 获取 IP 地址、通过 Socket 发送和接收 HTTP 数据等过程,都是使用的 OkHttp 的代码。

我们先来看看如何简单使用 OkHttp。

简单用法

来到 OkHttp 的主页,通过 OkHttp 来发送一个 GET 请求,并打印响应的状态码。

添加依赖

kotlin 复制代码
// Module-level: build.gradle.kts
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.11.0") // 如果没有看到 OkHttp 的源码,可以换个版本
}

然后创建一个 Empty Views Activity 项目,其 MainActivity 中的代码如下所示:

kotlin 复制代码
import java.io.IOException
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val url = "https://www.baidu.com"
        val client = OkHttpClient()
        val request = Request.Builder()
            .url(url)
            .build()

        // 安卓中一般是使用 enqueue() 方法
        // 因为 execute() 方法是同步的,enqueue() 方法是异步的
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {

            }

            override fun onResponse(call: Call, response: Response) {
                println("Status Code: ${response.code}")
            }
        })

    }
}

运行结果:

less 复制代码
I/System.out    Status Code: 200

源码

了解了其基本用法后,我们来跟踪源码,看看它究竟在背后干了什么。

先点开最直接的 enqueue() 方法:

kotlin 复制代码
// Call.kt
interface Call : Cloneable {
  fun enqueue(responseCallback: Callback)
}

但发现它是 Call 接口中的一个抽象方法,所以我们需要回头看看 newCall() 方法创建的对象是什么。

kotlin 复制代码
// OkHttpClient.kt
override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)

发现它创建了一个 RealCall 对象。

kotlin 复制代码
// RealCall.kt
class RealCall(
  val client: OkHttpClient,
  val originalRequest: Request,
  val forWebSocket: Boolean
) : Call {...}

创建该对象需要传入三个参数:

  • client: 就是我们先前创建的 OkHttpClient 对象,所有通用配置都通过它来完成,比如网络的超时时长。

  • originalRequest: 是我们前面创建的 Request 请求对象,存放用来发起 HTTP 请求的基本信息,比如请求方法、请求路径、请求头等。

    另外参数名的意思是"原始请求",这是因为 OkHttp 会把该请求对象进行多次封装改变,得到的更完整的请求对象,才拿去用于网络请求。

  • forWebSocket: 这个参数一般用不到,默认为 false 就行。因为 WebSocket 协议的作用是让服务器能够主动发送消息给客户端,但这种需求我们一般用不到,它常用于需要频繁刷新数据的场景,比如股票交易、虚拟货币交易。

知道了创建的是 RealCall 对象后,我们来到 RealCall 中查找其 enqueue() 方法的实现:

kotlin 复制代码
// RealCall.kt
override fun enqueue(responseCallback: Callback) {
  check(executed.compareAndSet(false, true)) { "Already Executed" }

  callStart()
  client.dispatcher.enqueue(AsyncCall(responseCallback))
}

这段代码的主要逻辑在于后两行,我们先看 callStart() 方法。

kotlin 复制代码
// RealCall.kt
private fun callStart() {
  this.callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()")
  eventListener.callStart(this)
}

第一行在跟踪程序错误,用于错误分析;第二行是调用了一个回调方法,eventListener 是一个监听器,用于监听与 HTTP 交互的过程,比如 TCP 连接的建立。

总的来说,callStart 方法就是一个辅助方法,可以不用管,对程序运行无实际影响。我们再回去看看 client.dispatcher.enqueue(AsyncCall(responseCallback)) 中的 dispatcherenqueue()AsyncCall()

先来看 dispatcher,发现它是一个 Dispatcher 对象。

kotlin 复制代码
// OkHttpClient.kt
@get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher

而 Dispatcher 是 OkHttp 用于线程调度的,当需要多个请求同时运行,就需要有多个线程,Dispatcher 就是管理多个线程的。OkHttp 使用 Dispatcher,是想要通过统一的线程池和请求队列来复用线程、管理并发,避免每次请求都需要创建新线程。

接着看 enqueue() 方法:

kotlin 复制代码
// Dispatcher.kt
internal fun enqueue(call: AsyncCall) {
  synchronized(this) {
    readyAsyncCalls.add(call)
    
    // 统计某个主机的请求数
    if (!call.call.forWebSocket) {
      val existingCall = findExistingCallWithHost(call.host)
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
    }
  }
  promoteAndExecute()
}

方法内部会将参数的 call 添加到双向队列中,存放已准备好要执行的请求。那么进入队列的请求会在什么时候被执行?其实就在 promoteAndExecute() 方法中。

我们进入 promoteAndExecute() 方法:

kotlin 复制代码
// Dispatcher.kt
// 将合适的 Call 取出,拿去执行
private fun promoteAndExecute(): Boolean {
  this.assertThreadDoesntHoldLock()

  val executableCalls = mutableListOf<AsyncCall>()
  val isRunning: Boolean
  synchronized(this) {
    // 遍历 readyAsyncCalls
    val i = readyAsyncCalls.iterator()
    while (i.hasNext()) {
      val asyncCall = i.next()

      if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
      if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.
      
      // 选择执行后,不会导致超出 maxRequests、maxRequestsPerHost 容量的 asyncCall
      i.remove()
      asyncCall.callsPerHost.incrementAndGet()
      // 放进 executableCalls 可执行的 Call 列表中
      executableCalls.add(asyncCall)
      // 放进 runningAsyncCalls 正在执行的 Call 队列中,用于记录
      runningAsyncCalls.add(asyncCall)
    }
    isRunning = runningCallsCount() > 0
  }
  // 执行上述的 executableCalls 列表
  for (i in 0 until executableCalls.size) {
    val asyncCall = executableCalls[i]
    asyncCall.executeOn(executorService) // 关键执行代码
  }

  return isRunning
}

所以,为了知道 enqueue() 方法的具体逻辑,我们来看看 executeOn() 方法的内部:

kotlin 复制代码
// RealCall.kt
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!
    }
  }
}

execute() 方法是 Executor 接口中的抽象方法,参数是 Runnable 类型,它会将参数放到后台去执行。

java 复制代码
// Executor.java
public interface Executor {
    void execute(Runnable command);
}

那我们来看看它实际执行的内容,来到 AsyncCallrun 方法。(注意:AsyncCallRealCall 的内部类,它实现了 Runnable 接口)。

kotlin 复制代码
// RealCall.kt
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)
      }
    }
  }
}

可以看到其中调用了 getResponseWithInterceptorChain() 方法获得了一个服务器返回的响应,那么我们很清楚,这就是这个方法的关键。之后它会将这个响应传给 onResponse 回调方法,而 responseCallback 就是我们在调用 enqueue 方法时传入的 Callback 实例,所以我们可以在 onResponse 方法中打印响应返回的状态码。

出错的话,会调用 responseCallbackonFailure 回调。

流程总结

我们来对 enqueue 的异步流程做个总结。其本质是经典的 "生产者-消费者"模型:

  1. 首先,生产者是 RealCall.enqueue,它将请求任务包装成 AsyncCall,然后放入 Dispatcher 的待处理队列 readyAsyncCalls 中。
  2. 调度中心是 Dispatcher.promoteAndExecute,它根据当前的并发限制,从待处理队列中取出合适的任务,然后放进运行中队列 runningAsyncCalls 进行状态跟踪,并最终通过 ExecutorService 线程池来处理。
  3. 消费者是 AsyncCall.run,执行网络请求,并通过 Callback 将结果返回。

理解了这个模型,整个异步调度的框架就很清晰了。

至此,OkHttp 执行一个异步请求的调度流程 框架,我们就已经看完了。接下来我们会进入请求的关键,也就是 getResponseWithInterceptorChain 方法的内部实现。不过在讲它之前,会先看看 OkHttpClient 中各个可配置的参数有哪些,让我们先对 OkHttp 有个大致了解。

相关推荐
2501_916013745 分钟前
App 上架全流程指南,iOS App 上架步骤、App Store 应用发布流程、uni-app 打包上传与审核要点详解
android·ios·小程序·https·uni-app·iphone·webview
牛蛙点点申请出战13 分钟前
仿微信语音 WaveView 实现
android·前端·ios
用户0918 分钟前
Android View 事件分发机制详解及应用
android·kotlin
ForteScarlet26 分钟前
Kotlin 2.2.20 现已发布!下个版本的特性抢先看!
android·开发语言·kotlin·jetbrains
诺诺Okami35 分钟前
Android Framework-Input-8 ANR相关
android
法欧特斯卡雷特38 分钟前
Kotlin 2.2.20 现已发布!下个版本的特性抢先看!
android·前端·后端
人生游戏牛马NPC1号38 分钟前
学习 Android (二十一) 学习 OpenCV (六)
android·opencv·学习
用户20187928316739 分钟前
Native 层 Handler 机制与 Java 层共用 MessageQueue 的设计逻辑
android
lichong9511 小时前
【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之android 把assert里的dist.zip 包解压到sd卡里
android·vue.js·iphone
·云扬·2 小时前
MySQL 日志全解析:Binlog/Redo/Undo 等 5 类关键日志的配置、作用与最佳实践
android·mysql·adb