一起分析OkHttp的源码原理
背景
- 那么我们为什么要聚焦于OkHttp呢,这是因为OkHttp是Android最主流的HTTP客户端。像咱们使用的Retrofit底层就是OkHttp
- 了解OkHttp有助于我们在遇到网络请求问题的时候,通过阅读源码解决问题,更加重温的了解OkHttp
OkHttp流程图
OkHttpClient
-
它是OkHttp的门面类
-
它是统一的高层接口
-
通俗解释的话
- 你作为客人,到了一家酒店入住,你并不需要找到厨师点菜,找到清洁工去打扫房间。这样对于客人,太麻烦了
- 你只需要找到前台,对前台说“我要吃饭,中午帮我打扫一下房间”。前台回去安排员工去满足你的要求
- 这个前台就是门面类
-
所以我们第一步通常是通过builder去构造门面类,然后执行相关操作
Request
-
本质上是一个不可变的数据载体,他把HTTP协议中定义的各个部分封装在一起
-
HttpUrl(URL)
- 指定要发给谁
-
Method(方法)
- GET、POST、PUT等等
-
Headers(请求头)
- 传递数据,比如
Content-Type(告诉服务器你发送的是图片还是文字)、User-Agent(我是谁)
- 传递数据,比如
-
RequestBody(请求体)
- 当你使用POST或者PUT的时候,实际上船的数据
-
TAG(标签)
- 这不是HTTP协议的一部分,OKHttp可以给请求贴上标签,然后统一处理
RealCall
-
RealCall是Call接口的唯一实现类 -
它负责连接“应用层”和“网络层”,是一个桥梁
-
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只会被调用一次 - 这是因为,如果不加标记可能会出现先回调了成功,然后又回调了失败的情况
- 这是一个Boolean,确保
-
二保险:
双重异常捕获catch (e: IOException):捕获正常的网络异常(如断网、超时),回调onFailurecatch (t: Throwable):捕获非 IO 异常(如代码 Bug、内存溢出)。它会先调用cancel()取消请求,再通过onFailure告知上层,最后重新抛出异常
-
三保险:
finally的“交接棒”机制client.dispatcher.finished(this):这是 OkHttp 永动机的秘密- 每单一个任务在子线程跑完了,它都会告诉调度器:“我腾出空位了”。调度器此时会立刻检查等待队列,把排队的请求拉进来运行
-
-
其余优秀的点
threadName("OkHttp ${redactedUrl()}"):这是一个非常好的习惯。OkHttp在执行时临时更改一下线程名字,这样你在排查线上 Bug 或查看 Logcat 堆栈时,一眼就能看出来哪个线程在跑哪个请求
-
涉及到的其他知识点
- CAS与原子性
- 责任链的触发
-
一个小问题:“如果 OkHttp 的异步请求失败了,它是怎么通知下一条请求开始执行的”
- 在
AsyncCall的run方法中,使用了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? = nullidleCallback:空闲通知机制- 这个是一个可选的回调。当
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() } }- 遍历所有队列(等待、异步运行、同步运行的),调用每一个
Call的cancel() - 注意:
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() } }- 这个是一个自循环机制:请求入队 -> 触发执行 -> 请求结束 -> 触发下一个请求执行 -> 没有的话就触发空闲
-
总结
- 到这里,我们已经看到了
OkHttpClient、Request、RealCall、Dispatcher,接下来还有一个非常著名的拦截器链。再下一篇中,我们讲分析这个责任链模式 - 本文首发于CSDN blog.csdn.net/2302_794601…