⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
当一个开发者或团队的水平积累到一定程度,会有自内向外输出价值的需求。在这个专栏里,小彭将为你分享 Android 方向主流开源组件的实现原理,包括网络、存储、UI、监控等。
本文是 Android 开源库系列的第 4 篇文章,完整文章目录请移步到文章末尾~
前言
大家好,我是小彭。
在上一篇文章里,我们聊到了 Square 开源的 I/O 框架 Okio 的三个优势:精简且全面的 API、基于共享的缓冲区设计以及超时机制。前两个优势已经分析过了,今天我们来分析 Okio 的超时检测机制。
本文源码基于 Okio v3.2.0。
学习路线图:
- 认识 Okio 的超时机制
超时机制是一项通用的系统设计,能够避免系统长时间阻塞在某些任务上。例如网络请求在超时时间内没有响应,客户端就会提前中断请求,并提示用户某些功能不可用。
1.1 说一下 Okio 超时机制的优势
先思考一个问题,相比于传统 IO 的超时有什么优势呢?我认为主要体现在 2 个方面:
- 优势 1 - Okio 弥补了部分 IO 操作不支持超时检测的缺陷:
Java 原生 IO 操作是否支持超时,完全取决于底层的系统调用是否支持。例如,网络 Socket 支持通过 setSoTimeout
API 设置单次 IO 操作的超时时间,而文件 IO 操作就不支持,使用原生文件 IO 就无法实现超时。
而 Okio 是统一在应用层实现超时检测,不管系统调用是否支持超时,都能提供统一的超时检测机制。
- 优势 2 - Okio 不仅支持单次 IO 操作的超时检测,还支持包含多次 IO 操作的复合任务超时检测:
Java 原生 IO 操作只能实现对单次 IO 操作的超时检测,无法实现对包含多次 IO 操作的复合任务超时检测。例如,OkHttp 支持配置单次 connect、read 或 write 操作的超时检测,还支持对一次完整 Call 请求的超时检测,有时候单个操作没有超时,但串联起来的完整 call 却超时了。
而 Okio 超时机制和 IO 操作没有强耦合,不仅支持对 IO 操作的超时检测,还支持非 IO 操作的超时检测,所以这种复合任务的超时检测也是可以实现的。
1.2 Timeout 类的作用
Timeout 类是 Okio 超时机制的核心类,Okio 对 Source 输入流和 Sink 输出流都提供了超时机制,我们在构造 InputStreamSource 和 OutputStreamSink 这些流的实现类时,都需要携带 Timeout 对象:
Source.kt
1 | kotlin复制代码interface Source : Closeable { |
Sink.kt
1 | kotlin复制代码actual interface Sink : Closeable, Flushable { |
Timeout 类提供了两种配置超时时间的方式(如果两种方式同时存在的话,Timeout 会优先采用更早的截止时间):
- 1、timeoutNanos 任务处理时间: 设置处理单次任务的超时时间,
最终触发超时的截止时间是任务的 startTime + timeoutNanos
;
- 2、deadlineNanoTime 截止时间: 直接设置未来的某个时间点,多个任务整体的超时时间点。
Timeout.kt
1 | kotlin复制代码// hasDeadline 这个属性显得没必要 |
创建 Source 和 Sink 对象时,都需要携带 Timeout 对象:
JvmOkio.kt
1 | kotlin复制代码// ---------------------------------------------------------------------------- |
在 Timeout 类的基础上,Okio 提供了 2 种超时机制:
- Timeout 是同步超时
- AsyncTimeout 是异步超时
Okio 框架
- Timeout 同步超时
Timeout 同步超时依赖于 Timeout#throwIfReached() 方法。
同步超时在每次执行任务之前,都需要先调用 Timeout#throwIfReached()
检查当前时间是否到达超时截止时间。如果超时则会直接抛出超时异常,不会再执行任务。
JvmOkio.kt
1 | kotlin复制代码private class InputStreamSource( |
看一眼 Timeout#throwIfReached 的源码。 可以看到,同步超时只考虑 “deadlineNanoTime 截止时间”,如果只设置 “timeoutNanos 任务处理时间” 是无效的,我觉得这个设计容易让开发者出错。
Timeout.kt
1 | kotlin复制代码@Throws(IOException::class) |
有必要解释所谓 “同步” 的意思:
同步超时就是指任务的 “执行” 和 “超时检查” 是同步的。当任务超时时,Okio 同步超时不会直接中断任务执行,而是需要检主动查超时时间(Timeout#throwIfReached)来判断是否发生超时,再决定是否中断任务执行。
这其实与 Java 的中断机制是非常相似的:
当 Java 线程的中断标记位置位时,并不是真的会直接中断线程执行,而是主动需要检查中断标记位(Thread.interrupted)来判断是否发生中断,再决定是否中断线程任务。所以说 Java 的线程中断机制是一种 “同步中断”。
可以看出,同步超时存在 “滞后性”:
因为同步超时需要主动检查,所以即使在任务执行过程中发生超时,也必须等到检查时才会发现超时,无法及时触发超时异常。因此,就需要异步超时机制。
同步超时示意图
- AsyncTimeout 异步超时
- 异步超时监控进入: 异步超时在每次执行任务之前,都需要先调用
AsyncTimeout#enter()
方法将 AsyncTimeout 挂载到超时队列中,并根据超时截止时间的先后顺序排序,队列头部的节点就是会最先超时的任务; - 异步超时监控退出: 在每次任务执行结束之后,都需要再调用
AsyncTimeout#exit()
方法将 AsyncTimeout 从超时队列中移除。
注意: enter() 方法和 eixt() 方法必须成对存在。
AsyncTimeout.kt
1 | kotlin复制代码open class AsyncTimeout : Timeout() { |
同时,在首次添加异步超时监控时,AsyncTimeout 内部会开启一个 WatchDog
守护线程,按照 “检测 - 等待” 模型观察超时队列的头节点:
- 如果发生超时,则将头节点移除,并回调
AsyncTimeout#timeOut()
方法。这是一个空方法,需要由子类实现来主动取消任务; - 如果未发生超时,则 WatchDog 线程会计算距离超时发生的时间间隔,调用
Object#wait(时间间隔)
进入限时等待。
需要注意的是: AsyncTimeout#timeOut() 回调中不能执行耗时操作,否则会影响后续检测的及时性。
有意思的是:我们会发现 Okio 的超时检测机制和 Android ANR 的超时检测机制非常类似,所以我们可以说 ANR 也是一种异步超时机制。
AsyncTimeout.kt
1 | kotlin复制代码private class Watchdog internal constructor() : Thread("Okio Watchdog") { |
异步超时示意图
直接看代码不好理解,我们来举个例子:
- 举例:OkHttp Call 的异步超时监控
在 OkHttp 中,支持配置一次完整的 Call 请求上的操作时间 callTimeout。一次 Call 请求包含多个 IO 操作的复合任务,使用传统 IO 是不可能监控超时的,所以需要使用 AsyncTimeout 异步超时。
在 OkHttp 的 RealCall 请求类中,就使用了 AsyncTimeout 异步超时:
- 1、开始任务: 在 execute() 方法中,调用
AsyncTimeout#enter()
进入异步超时监控,再执行请求; - 2、结束任务: 在 callDone() 方法中,调用
AsyncTimeout#exit()
退出异步超时监控。分析源码发现:callDone() 不仅在请求正常时会调用,在取消请求时也会回调,保证了 enter() 和 exit() 成对存在; - 3、超时回调: 在
AsyncTimeout#timeOut
超时回调中,调用了 Call#cancel() 提前取消请求。Call#cancel() 会调用到 Socket#close(),让阻塞中的 IO 操作抛出 SocketException 异常,以达到提前中断的目的,最终也会走到 callDone() 执行 exit() 退出异步监控。
Call 超时监控示意图
RealCall
1 | kotlin复制代码class RealCall( |
调用 Socket#close() 会让阻塞中的 IO 操作抛出 SocketException 异常:
Socket.java
1 | java复制代码// Any thread currently blocked in an I/O operation upon this socket will throw a {@link SocketException}. |
Exchange 中会捕获 Socket#close() 抛出的 SocketException 异常:
Exchange.kt
1 | kotlin复制代码private inner class RequestBodySink( |
- OkHttp 超时检测总结
先说一下 Okhttp 定义的 2 种颗粒度的超时:
- 第 1 种是在单次 connect、read 或 write 操作上的超时;
- 第 2 种是在一次完整的 call 请求上的超时,有时候单个操作没有超时,但连接起来的完整 call 却超时。
其实 Socket 支持通过 setSoTimeout
API 设置单次操作的超时时间,但这个 API 无法满足需求,比如说 Call 超时是包含多个 IO 操作的复合任务,而且不管是 HTTP/1 并行请求还是 HTTP/2 多路复用,都会存在一个 Socket 连接上同时承载多个请求的情况,无法区分是哪个请求超时。
因此,OkHttp 采用了两种超时监测:
- 对于 connect 操作,OkHttp 继续使用 Socket 级别的超时,没有问题;
- 对于 call、read 和 write 的超时,OkHttp 使用一个 Okio 的异步超时机制来监测超时。
版权声明
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
推荐阅读
Android 开源库系列完整目录如下(2023/07/12 更新):
- #1 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(上)
- #2 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)
- #3 IO 框架 Okio 的实现原理,到底哪里 OK?
- #4 IO 框架 Okio 的实现原理,如何检测超时?
- #5 序列化框架 Gson 原理分析,可以优化吗?
- #6 适可而止!看 Glide 如何把生命周期安排得明明白白
- #7 为什么各大厂自研的内存泄漏检测框架都要参考 LeakCanary?因为它是真强啊!
- #8 内存缓存框架 LruCache 的实现原理,手写试试?
- #9 这是一份详细的 EventBus 使用教程
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~
本文转载自: 掘金