前言
本篇是 IO系列 的第4篇,前三篇文章中,我们已经对JAVA经典IO设计、JAVA-NIO内容、操作系统IO架构基础概念、Zero-Copy做了较为系统的回顾。
而绝大部分Android应用中都会涉及到网络模块,Retrofit
、Okhttp
几乎是必用的框架, Okio
作为 Okhttp
中的重要模块,原先用于处理网络模块中的IO问题,随着其项目发展,Okio也开始面向其他问题。
这一篇,我们一同对OKIO做一次系统的梳理,搞明白OKIO为什么OK,做到在面试中自如的吹牛批、在日常工作中灵活使用。
编者按:面试吹牛批需要把握尺度,避免远超岗位预期,导致浪费时间
因文章篇幅较长,可结合内容导图阅读:
okio的主旨与架构
在OKIO项目的 wiki 中,对其主旨有如下介绍:
Okio is a library that complements java.io and java.nio to make it much easier to access, store, and process your data. It started as a component of OkHttp, the capable HTTP client included in Android. It's well-exercised and ready to solve new problems.
简单直译为中文如下:
Okio是一个类库,对
java.io
和java.nio
进行了补充,使得访问、存储和处理数据变得更加容易。它最初是OkHttp的一个组件,OkHttp是安卓中的一个功能强大的HTTP客户端。它非常健壮,可以解决新问题。
简言之:为了更简单的访问、存储、处理数据,基于 java.io
和 java.nio
进行了功能补充
wiki中,简单介绍了设计中的几个重点角色:
- ByteStrings and Buffers
- Sources and Sinks
分层架构中相对扁平、简单:在应用和Java IO 之间增加了一层,即OKIO本身,包含 数据封装
、 输入输出
、 超时机制
体现在类图上还是比较复杂的:
在库内部,ByteStrings
的使用不多,对 Buffer
数据包装后为上层应用服务,单独拎出。
信息噪声比较多,去掉功能装饰的实现类后较为精简:
与Java的输入输出的对比
Java经典IO中的输入输出定义为Stream,在 系列文章 中进行了介绍。字符流类似,图略
在JDK的IO框架中,使用装饰模式建立了规模庞大、功能丰富的输入输出流。从OKIO的主旨出发,不难理解其设计者希望类库尽可能简单、易扩展、内建部分功能足够完善。因此,OKIO会适当的另起炉灶,不会全面的使用JDK中的Stream。
OKIO中使用了自定义的输入、输出,即 Source
和 Sink
,注意淡黄色、淡粉色部分:
Sink 在计算机领域有特定含义:指程序或者线程,可以接收数据,并且可以处理或者发送这些数据
差异点
在wiki中提到如下内容:
An elegant part of the java.io design is how streams can be layered for transformations like encryption and compression. Okio includes its own stream types called Source and Sink that work like InputStream and OutputStream, but with some key differences:
- Timeouts.
- Easy to implement.
- Easy to use.
- No artificial distinction between byte streams and char streams.
- Easy to test.
简单翻译下, Java IO的设计中有一处非常优雅:可以调整流的分层包装以实现加密、压缩等转换。OKIO包含自有的流类型 Source
、Sink
,与Java的 InputStream
、 OutputStream
功能类似,但是有几点关键的不同:
- 超时机制
- 更容易实现
- 更容易使用
- 字节流、字符流之间没有人为的差异
- 更容易测试
从输入方面看:
在JDK中,InputStream
使用多种层(以及复合层)处理种类繁多的各类数据
DataInputStream
用于处理基础数据类型BufferedInputStream
处理缓冲InputStreamReader
处理文本字符串
而OKIO在这些层之上建立了 BufferedSource
,Source避免了一些无法实现 available()
方法的困境, 转而由调用者指定它们需要的byte个数
在实现一个Source时,不必操心 read()
方法,它难以有效实现且需从257种值中返回一个 ,注:null & [0x00,0xFF]
从输出方面看:
类似的,在JDK中 OutputStream
使用多种层(以及复合层)处理种类繁多的各类数据,而Sink也非常容易采用分层设计
相同点
Source
、Sink
的功能与InputStream
、OutputStream
、Reader
、Writer
相同- 使用时可以通过装饰追加功能
对于功能相同,wiki中提到如下内容:
Sources and sinks interoperate with InputStream and OutputStream. You can view any Source as an InputStream, and you can view any InputStream as a Source. Similarly for Sink and OutputStream.
下文的Source、Sink详解中,解析他们与IOStream 为何 "等价"、如何"互操作"
Source、Sink 详解
Source 体系
抛开功能类(压缩、哈希、加密、装饰等),主要关注:
- Source
- BufferedSource
- Buffer
- RealBufferedSource
Source的定义中规中矩:
kotlin
interface Source : Closeable {
@Throws(IOException::class)
fun read(sink: Buffer, byteCount: Long): Long
fun timeout(): Timeout
@Throws(IOException::class)
override fun close()
}
其中的 timeout
将在下文超时机制章节中展开。
BufferedSource接口约定使用 Buffer
承接实际数据,并且定义了一系列方便使用的接口,如:
- 读取Int
- 读取Short
- 读取字符串
- 内容选择 等,不做罗列
RealBufferedSource
实现了 BufferedSource
接口,从本质上可以认为是 Buffer
类的代理,增加了边界校验
Buffer
实现了 BufferedSource
接口,包含 读 的具体实现
Sink 体系
抛开功能类(压缩、哈希、加密、装饰等),主要关注:
- Sink
- BufferedSink
- Buffer
- RealBufferedSink
kotlin
interface Sink : Closeable {
@Throws(IOException::class)
fun write(source: Buffer, byteCount: Long)
@Throws(IOException::class)
fun flush()
fun timeout(): Timeout
@Throws(IOException::class)
override fun close()
}
同样,Sink接口的定义也中规中矩。
类似的,BufferedSink
接口继承了 Sink
接口,约定了使用 Buffer
承接实际数据, RealBufferedSink
是具体实现,从本质上是 Buffer
作为 Sink
时的代理,进行了边界校验。
Buffer
实现了 BufferedSink
接口,包含 写 的具体实现
Source、Sink与I/O-Stream的互操作
作者按:请仔细思考一下 互操作
,其本质是:使用一种实例对象的API去操作另一种对象实例的API, 请留意直接操作和间接操作,国内程序员更习惯使用 "转"、"转换" ,着眼点是从一种实例对象获得另一种实例对象。 从转换角度思考时,容易陷入误区,枚举出没必要地转换情况、忽略掉必要的间接转换。
首先明确一点:在基于I/O-Stream进行读写时,InputStreamSource、 OutputStreamSink 是 I/O-Stream的读写代理; I/O-Stream 是 InputStreamSource、 OutputStreamSink 的读写委托。
依靠转换API:
kotlin
fun InputStream.source(): Source = InputStreamSource(this, Timeout())
fun OutputStream.sink(): Sink = OutputStreamSink(this, Timeout())
排除此 "基本情况1" ,还有一种 基本情况2 ,即我们希望使用I/O-Stream的API去实质操作OKIO。不难想象,此时必然存在用OKIO实现的定制业务,即数据的实质处理,OKIO选择了 BufferedSink
和 BufferedSource
而非 Sink
、 Source
,此时 BufferedSink
和 BufferedSource
是 I/O-Stream 的业务委托。
API如下:
kotlin
interface BufferedSink : Sink, WritableByteChannel {
//... ignore
/** Returns an output stream that writes to this sink. */
fun outputStream(): OutputStream
}
kotlin
interface BufferedSource : Source, ReadableByteChannel {
//... ignore
/** Returns an input stream that reads from this source. */
fun inputStream(): InputStream
}
更复杂的情况 -- 基于基本情况加各类业务的组合
排除掉这些基本情况,还需要需要互操作时,意味着存在两套模块,一套使用Okio的Source、Sink,一套使用Java的InputStream、OutputStream。假定使用Sink、Source的模块为A,使用I/O-Stream的模块为B。
以写为例,进行分析,有两种可能:
- 操作模块A,数据从A流向B,进而写入 (一般来说,B存在业务定制,否则是基本情况1)
- 操作模块B,数据从B流向A,进而写入(A最终使用了基本情况1,此情况属于基本情况2的复杂版)
1. 数据从A流向B,进而写入
关注A模块的数据出口,A模块可能的设计
- A1:接受一个Sink实例,或者接受一些参数构建Sink实例
- A2:对外提供一个Source实例,由外界自行消费 -- 这种设计思路比较奇葩
- 接受回调函数,提供实际数据 -- 不属于实际讨论范畴,忽略
关注B模块的数据入口,B模块可能的设计
- B1:接受一个InputStream实例,或者接受一些参数构建InputStream实例,消费其数据 -- 这种设计思路比较奇葩
- B2:对外提供一个OutputStream实例,由外界控制实际写
- 暴露使用ByteArray等写入API -- 不属于实际讨论范畴,忽略
A1+B1 情况的伪代码
A1:接受一个Sink实例,或者接受一些参数构建Sink实例
B1:接受一个InputStream实例,或者接受一些参数构建InputStream实例,消费其数据
kotlin
class Client {
fun demo() {
//使用Sink、Source的模块A
val moduleA = A()
//使用I/O-Stream的模块B
val moduleB = B()
val buffer = Buffer()
//设置模块A的数据出口
moduleA.setSink(buffer)
//设置模块B的数据入口
moduleB.setInputStream(buffer.inputStream())
//假定最终写入file:
moduleB.setOutputFile(File("/target.txt"))
//调用模块A开始写入
moduleA.write("some thing")
}
}
值得注意的是,需要在Buffer区的数据被消费后,进行清理,以避免内存占用越来越多,而因为B模块的奇葩设计,往往带入多线程问题,编程难度较大
A1+B2 情况的伪代码
A1:接受一个Sink实例,或者接受一些参数构建Sink实例
B2:对外提供一个OutputStream实例,由外界控制实际写
kotlin
class Client {
fun demo() {
//使用Sink、Source的模块A
val moduleA = A()
//使用I/O-Stream的模块B
val moduleB = B()
//假定其写入方式为:moduleB.writer().write("xxx"), moduleB.writer()获得OutputStream实例
//假定最终写入file:
moduleB.setOutputFile(File("/target.txt"))
//使用B的入口,套接到模块A的数据出口
moduleA.setSink(moduleB.writer().sink())
//调用模块A开始写入
moduleA.write("some thing")
}
}
A2+B1 情况的伪代码
A2:对外提供一个Source实例,由外界自行消费
B1:接受一个InputStream实例,或者接受一些参数构建InputStream实例,消费其数据
kotlin
class Client {
fun demo() {
//使用Sink、Source的模块A
val moduleA = A()
//使用I/O-Stream的模块B
val moduleB = B()
//假定最终写入file:
moduleB.setOutputFile(File("/target.txt"))
//此情况需要moduleA.getSource() 提供 BufferedSource实例,
//如果实现了Sink而并未实现BufferedSource, 需要模块提供者自己考虑接口系统的转换
moduleB.setInputStream(moduleA.getSource().inputStream())
//调用模块A开始写入
moduleA.write("some thing")
}
}
A2+B2 情况的伪代码
A2:对外提供一个Source实例,由外界自行消费
B2:对外提供一个OutputStream实例,由外界控制实际写
kotlin
class Client {
fun demo() {
//使用Sink、Source的模块A
val moduleA = A()
//使用I/O-Stream的模块B
val moduleB = B()
//假定最终写入file:
moduleB.setOutputFile(File("/target.txt"))
val resultFromA = moduleA.getSource()
val buffer = Buffer()
while (resultFromA.read(buffer, count) != -1) {
buffer.writeTo(moduleB.writer())
buffer.clear()
}
//调用模块A开始写入
moduleA.write("some thing")
}
}
2. 数据从B流向A,进而写入
关注A模块的数据入口,A模块可能的设计
- A1:接受一个Source实例,或者接受一些参数构建Source实例 -- 这种设计思路比较奇葩,在主动拥抱复杂
- A2:对外提供一个Sink实例,由外界自行控制写入
- 暴露使用ByteArray等写入API -- 不属于实际讨论范畴,忽略
关注B模块的数据出口,B模块可能的设计
- B1:接受一个OutputStream实例,或者接受一些参数构建OutputStream实例
- B2:对外提供一个InputStream实例,由外界自行消费数据 -- 这种设计思路比较奇葩
- 接受回调函数,提供实际数据 -- 不属于实际讨论范畴,忽略
简单归纳伪代码如下
scss
//A1 + B1 同样会有内存释放、多线程编程难度问题
val buffer = Buffer()
moduleA.setSource(buffer)
moduleB.setWriter(buffer.outputStream())
//A1 + B2
// moduleB.getResult() 返回InputStream实例
moduleA.setSource(moduleB.getResult().source())
//A2 + B1
moduleB.setWriter(moduleA.getSink().outputStream())
//A2 + B2
val resultFromB:InputStream = moduleB.getResult()
val buffer = ByteArray(1024)
while ((len = in.read(buffer))!=-1) {
moduleA.sink().write(buffer,0,len)
}
很显然,OKIO提供的转化方式,能够满足正常的设计,而剩余的奇葩设计,自然需要设计者自行处理内存、多线程问题。
而读的例子也是类似的,考虑到篇幅已经很长,读者诸君可以自行梳理。
在读和写都能够完成两套系统的互操作时,即可随心随意地构建出更加复杂的层叠layer,亦不再展开
Buffer 详解
顾名思义,OKIO 中的 Buffer 是特意设计的缓冲区。它在 数据处理
和 数据读写
之间进行缓冲
class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel
它的设计意图可以概括为三个方面,这三方面并不孤立互斥:
- 内存中的读写缓冲区
- 更方便的API
- 和 Java IO 互操作
更方便的API:系列文章中提到过,数据的实质内容可以编码成ByteArray,Buffer提供了更方便的编解码
和 Java IO 互操作:除了上文中已经提及的内容,还包含readFrom(input: InputStream)
,fun writeTo(out: OutputStream, byteCount: Long = size)
等API,这些也可以算为更方便的APIBuffer实现了ByteChannel接口,可以适应NIO的设计体系,当然此时它又是内存中的读写缓冲区
API方面,读者诸君可自行研读代码,内容比较简单。让我们将精力放在 缓冲区
上,看一看它的实现原理。
核心实现
移除掉干扰代码后, 可以发现它的重点为 head: Segment
,代码简单扫一眼有个印象即可:
kotlin
class Buffer : /*BufferedSource, BufferedSink,*/ Cloneable, ByteChannel {
internal var head: Segment? = null
var size: Long = 0L
internal set
@Throws(EOFException::class)
override fun readByte(): Byte = commonReadByte()
//和下面的read类似,仅作为Source的API示例
@Throws(IOException::class)
override fun read(sink: ByteBuffer): Int {
val s = head ?: return -1
val toCopy = minOf(sink.remaining(), s.limit - s.pos)
sink.put(s.data, s.pos, toCopy)
s.pos += toCopy
size -= toCopy.toLong()
if (s.pos == s.limit) {
head = s.pop()
SegmentPool.recycle(s)
}
return toCopy
}
override fun write(source: ByteArray): Buffer = commonWrite(source)
//也用到了writableSegment(1),可类比下面的write代码,仅留作Sink的API示例
@Throws(IOException::class)
override fun write(source: ByteBuffer): Int {
val byteCount = source.remaining()
var remaining = byteCount
while (remaining > 0) {
val tail = writableSegment(1)
val toCopy = minOf(remaining, Segment.SIZE - tail.limit)
source.get(tail.data, tail.limit, toCopy)
remaining -= toCopy
tail.limit += toCopy
}
size += byteCount.toLong()
return byteCount
}
}
而Segment是什么呢?是一个链表数据结构:
less
internal class Segment {
@JvmField val data: ByteArray
@JvmField var pos: Int = 0
@JvmField var limit: Int = 0
@JvmField var shared: Boolean = false
@JvmField var owner: Boolean = false
@JvmField var next: Segment? = null
@JvmField var prev: Segment? = null
//ignore
}
包含了三种内容:
- 实际数据 data
- 标记 pos+limit和读写有关,shared、owner和数据保护有关
- 上下游节点,链表的本质
而 SegmentPool
是很典型的池化设计,毕竟 ByteArray
需要分配内存空间,使用池化可以很好地减少无效内存管理(频繁分配回收)
不难得出总结:
Segment
是实现了方便使用的API的ByteArray链表Buffer
是使用Segment
包装而成的数据缓冲区,实现了方便使用的API,实现了和JAVA IO间的互操作Buffer
既可以作为上游Source
角色,也可以作为下游Sink
角色- 在设计
Buffer
和Segment
时,结合了日常使用场景进行了特定优化,例如通过转移、分享数据而非拷贝实质数据
ByteString 概述
在Okio中,ByteString也是一个重要的、方便使用的设计,但比Buffer简单的多。
它的命名也非常有趣,计算机领域中,String一词对应字符串,它本身拥有一个更宽泛的含义就是"一串"。 在不足够严谨的讨论场景下,我们可以认为 String
就是指定了编码的CharArray或ByteArray。而CharSequence又太过于抽象,设计者似乎仅希望将一些特定的具体情况进行封装,因此制造了ByteString。
kotlin
open class ByteString
internal actual constructor(
internal actual val data: ByteArray,
) : Serializable, Comparable<ByteString> {}
_ 作者按:我在开发一些蓝牙应用时,数据传输层和应用协议层有一些特定的数据操作,例如:信息摘要计算、CRC校验、AES加解密、Hex-String转换用作日志输出。在很多年前才接触Android时,使用HttpUrlConnection,对body也有类似的处理。属实枯燥繁琐_
它是一个内容不可变byte序列,这一点可以通过观测API发现,它并不提供修改内容的相关API。但它封装了一系列好用的API,例如:
- utf8(): String
- string(charset: Charset)
- fun base64()
- fun md5() = digest("MD5")
- fun sha1() = digest("SHA-1")
- digest(algorithm: String): ByteString 等等
相比于定义与使用Utility类,代码可读性更强。
值得注意 :虽然它在设计意图上是内容不可变的,但注意它的构造函数,它只保留了引用,并没有对内容进行拷贝,这意味实质内容可以在外部篡改
它实现了 Comparable
接口,值得一提的是,它按照无符号数大小进行 "字典序" 比对。
- "字典序" 比对,即按照从头到尾的顺序,依次比对,脑补一下英文词典。
- Byte使用8bit表示,0xFF(补码)如果视作符号数为255,排在0x00后面,如果视作有符号数,则为-1,排在0x00前面
超时机制
简单思考一下,你的BOSS是如何按照Deadline来检查你的工作的。
如果你没有提前告知已完成,
- 最理想的BOSS会在到期时查你
- 宽松一点的BOSS会在Deadline当天或提前一天过问一下,到点再查一下
- 焦虑一点的BOSS会频繁一点
- 有毛病的BOSS会一天到晚盯着你
显然,需要先约定一个超时的信息:
kotlin
class Timeout {
private var hasDeadline = false
private var deadlineNanoTime = 0L
private var timeoutNanos = 0L
}
假定有一项具体工作,当你和BOSS约定好时间,他会记录这一信息,得到一个timeout
,当然,并非所有事情都会有Deadline
此时,你去执行这一事项:
arduino
timeout.withTimeout {
//具体的事项
}
而你的BOSS,则会根据是否有真实Deadline,决定是否记录到他的检查单上。
很显然,你的BOSS需要跟踪的事项进度比较多,他按照到期时间先后顺序对检查单内容进行整理,这样他就省事了,他只需要盯着第一个到期时间进行追踪即可。
当发现超时时,他会将这一项移除,调整他的检查单,并通报此项已经超时...
不难想象,如果他的检查单上没有追踪项,他不会给自己来一个遥遥无期的休假,否则有后续事项没被跟踪,他就惨了,但一直盯着有没有新事项产生会很累,所以他每两小时就会看一下,是否有事项需要写入检查单。即便写入检查单时此事已经延期,但一个2小时内就会到Deadline的事情,稍微拖延了一会去追查,也没啥毛病。
将BOSS的这部分工作写成代码如下:
kotlin
internal fun awaitTimeout(): AsyncTimeout? {
// Get the next eligible node.
val node = head!!.next
// The queue is empty. Wait until either something is enqueued or the idle timeout elapses.
if (node == null) {
val startNanos = System.nanoTime()
condition.await(IDLE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
return if (head!!.next == null && System.nanoTime() - startNanos >= IDLE_TIMEOUT_NANOS) {
head // The idle timeout elapsed.
} else {
null // The situation has changed.
}
}
var waitNanos = node.remainingNanos(System.nanoTime())
// The head of the queue hasn't timed out yet. Await that.
if (waitNanos > 0) {
condition.await(waitNanos, TimeUnit.NANOSECONDS)
return null
}
// The head of the queue has timed out. Remove it.
head!!.next = node.next
node.next = null
return node
}
private class Watchdog internal constructor() : Thread("Okio Watchdog") {
init {
isDaemon = true
}
override fun run() {
while (true) {
try {
var timedOut: AsyncTimeout? = null
AsyncTimeout.lock.withLock {
timedOut = awaitTimeout()
// The queue is completely empty. Let this thread exit and let another watchdog thread
// get created on the next call to scheduleTimeout().
if (timedOut === head) {
head = null
return
}
}
// Close the timed out node, if one was found.
timedOut?.timedOut()
} catch (ignored: InterruptedException) {
}
}
}
}
再回到你这边,如果你的工作完成了,你去会找BOSS汇报工作,让他处理检查单:
kotlin
inline fun <T> withTimeout(block: () -> T): T {
var throwOnTimeout = false
enter()
try {
val result = block()
throwOnTimeout = true
return result
} catch (e: IOException) {
throw if (!exit()) e else `access$newTimeoutException`(e)
} finally {
// 找BOSS汇报工作,让他处理检查单
val timedOut = exit()
if (timedOut && throwOnTimeout) throw `access$newTimeoutException`(null)
}
}
此时轮到BOSS来处理,如果这个事项并没有真实Deadline,他并不会额外做什么。否则他会重新维护检查单内容,但如果没有在检查单中发现这一项,则说明该项在之前通报过已经超时。
kotlin
fun exit(): Boolean {
return cancelScheduledTimeout(this)
}
private fun cancelScheduledTimeout(node: AsyncTimeout): Boolean {
AsyncTimeout.lock.withLock {
if (!node.inQueue) return false
node.inQueue = false
// Remove the node from the linked list.
var prev = head
while (prev != null) {
if (prev.next === node) {
prev.next = node.next
node.next = null
return false
}
prev = prev.next
}
// The node wasn't found in the linked list: it must have timed out!
return true
}
}
我们通过一个故事演示了一种异步的超时检测机制,在Okio中,对应了 AsyncTimeout
。当然,实际场景中还有一些更复杂的,例如两个事项合并。
甚至,我们可以直接借用此机制:
演示代码:
kotlin
class Job<T>(
val block: () -> T,
private val onTimeout: ((afterResult: Boolean, result: T?) -> Boolean)? = null
) {
@Volatile
private var timeout = false
@Volatile
private var timeoutHandled = false
@Volatile
private var execFinished = false
fun exec(): T = block().also { result ->
execFinished = true
if (timeout && !timeoutHandled) {
onTimeout?.let {
timeoutHandled = it(true, result)
}
}
}
fun timeout() {
if (execFinished) return
timeout = true
onTimeout?.let {
timeoutHandled = it(false, null)
}
}
}
class JobsAsyncTimeout<T>(private val job: Job<T>) : AsyncTimeout() {
override fun timedOut() {
job.timeout()
}
override fun timeout(timeout: Long, unit: TimeUnit): JobsAsyncTimeout<T> {
super.timeout(timeout, unit)
return this
}
fun delegate(): () -> T {
return {
withTimeout {
job.exec()
}
}
}
}
fun <T> (() -> T).timeoutJob(
timeout: Long,
timeUnit: TimeUnit,
onTimeout: ((afterResult: Boolean, result: T?) -> Boolean)? = null
): () -> T {
return JobsAsyncTimeout(Job(block = this, onTimeout = onTimeout))
.timeout(timeout = timeout, unit = timeUnit)
.delegate()
}
以一个效率低下的递归计算斐波那契数列进行演示(如果你的机器性能异常的好,可以适当调大入参):
Demo 代码 :
kotlin
class Demo {
@Test
fun testTimeOut() {
val fib30 = {
fibonacci(30)
}.timeoutJob(1, TimeUnit.NANOSECONDS) { afterResult, result ->
if (!afterResult) {
// 如果是可以打断的操作,执行打断;除非你仍然想要结果,这样使用超时机制是很牵强的
println("on timeout, callback before result, you should interrupt the job")
// 返回true则意味着已经消费
false
} else {
//如果 afterResult 为false时,已经返回true,则不会有此轮回调
//除非你真的需要结果
println("on timeout, callback after result, $result")
//返回true则意味着已经消费
false
}
}
//超时是会抛出InterruptedIOException
assertThrows(InterruptedIOException::class.java) {
println("fib100-> ${fib30()}")
}
}
@Throws(Exception::class)
fun fibonacci(n: Int): Long {
when {
n < 0 -> throw Exception("n为非法值!")
else -> return when (n) {
0 -> 0
1 -> 1
else -> fibonacci(n - 1) + fibonacci(n - 2)
}
}
}
}
结语
至此,IO系列告一段落。
按照惯例,再絮叨几句。在去年放缓节奏后,读了一些书、想了一些事、观了一些人。近期于《孟子》中得一句:
或劳心,或劳力;劳心者治人,劳力者治于人;治于人者食人,治人者食于人:天下之通义
下个系列还在构思斟酌中,下个月再见。