用烂OkIO,隔壁产品看不懂了
/ 今日科技快讯 /
近日,随着巴西国内下一代5G无线网络频谱拍卖时间的临近,华为已经聘请巴西前总统米歇尔·特梅尔担任顾问,以确保该公司业务能够在巴西当地市场保持持久性地运营。巴西国内下一代5G无线网络频谱的拍卖时间,最初曾定于2020年3月。但是,后来巴西新冠疫情严重,拍卖时间现在将设定在2021年初。
/ 作者简介 /
本篇文章来自彭也的投稿,分享了他对OkIO的精彩剖析,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!
彭也的博客地址:
https://juejin.cn/user/2330620379283064
/ 它是什么? /
它是一套在Java IO基础上再次进行封装的IO框架,在形式上比原来的Java IO更简洁易用。引入OkHttp依赖即可引用之,源码地址:
https://github.com/square/okio
/ 如何使用? /
利用kotlin扩展函数,代码简洁,两行代码搞定
当然,前提是记得加入文件读写权限。
@Test
fun useExtension() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
var file:File = File(appContext.filesDir.path.toString()+"/test.txt")
//写文件
file.sink().buffer().writeString("write sth", Charset.forName("utf-8")).close()
//读文件
val fileContent = file.source().buffer().readString(Charset.forName("utf-8”))
println()
}
运行结果如下:
老版本的java调用方式
代码如下:
@Test
public void testIO() {
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
File file = new File(appContext.getFilesDir().getPath().toString());
try {
// 写文件
BufferedSink bufferedSink = Okio.buffer(Okio.sink(file));
bufferedSink.writeString("write sth", Charset.forName("utf-8"));
bufferedSink.close();
// 读文件
BufferedSource bufferedSource = Okio.buffer(Okio.source(file));
bufferedSource.readString(Charset.forName("utf-8"));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("===end");
}
运行结果也一样,不再列出。
/ 看看原理 /
它使用了谁?
InputStream、OutputStream
跟踪sink()、source()方法很明显,扩展了InputStream、OutputStream的相关函数,并将其子类封装成Source、Sink的实例。
SegmentPool和Segment
跟踪Okio的buffer方法,可以看到RealBufferedSink、RealBufferedSource类,他们分别实现了Sink、Source。而RealBufferedSink、RealBufferedSource都持有Buffer类型的属性buffer。这个Buffer类很关键,内部使用到了SegmentPool,SegmentPool是管理Segment的工具,Segment是缓冲区中的某一片段,正是有它的存在,可以实现分段存储。
ByteString
还值得一提的是ByteString,这是一个方便Byte和String类互转的工具。
谁使用了它?
OkHttp
okhttp中使用okio支撑相关流操作。
支付宝等众多大型应用中
原理解析
类图
Sink负责输出相关的操作,而Source负责输入相关的操作。
可以看到,无论读写,都是通过Buffer统一操作的。底层还是使用了OutputSream、InputStream,本质上还是对Java IO相关的API的封装。
分段存储的Segment
Segment本质上是缓存片段,通过SegmentPool维护,数据结构表现为双向链表。如下图:
通过SegmentPool取出某一个Segment片段,用于缓存待写入的数据。
采用双向链表可以在数据的复制、转移等操作场景显得十分高效。
以文件写入为例,看看Segment是何时发挥作用的。
var file:File = File(appContext.getFilesDir().getPath().toString());
file.sink().buffer().writeString("write sth", Charset.forName("utf-8")).close()
File类没有sink方法,这里的sink方法为扩展函数,返回Sink的实例。
/** Returns a sink that writes to `file`. */
@JvmOverloads
@Throws(FileNotFoundException::class)
fun File.sink(append: Boolean = false): Sink = FileOutputStream(this, append).sink()
FileOutputStream类的sink方法同样是扩展函数,继续跟进可以看到,此为OutputStream的扩展函数,FileOutputStream继承自OutputStream。
/** Returns a sink that writes to `out`. */
fun OutputStream.sink(): Sink = OutputStreamSink(this, Timeout())
OutputStreamSink中的this为FileOutputStream,是File.sink方法调用时初始化的实例,再看看OutputStreamSink类的具体实现,内部复写了write方法,本质上最终还是调用Java IO中的OutputStream进行数据写入。
private class OutputStreamSink(
private val out: OutputStream,
private val timeout: Timeout
) : Sink {
override fun write(source: Buffer, byteCount: Long) {
/*省略*/
out.write(head.data, head.pos, toCopy)
/*省略*/ }
}
虽然看到了最终的通过OutputStream实现,但此时还未进行真正的写入方法的调用,继续跟进buffer()方法,此方法返回RealBufferedSink实例。注意,构造方法中的this就是OutputStreamSink。
fun Sink.buffer(): BufferedSink = RealBufferedSink(this)
继续跟进至RealBufferedSink的writeString方法,此方法逻辑步骤比较简单,第一步通过Buffer写入数据,第二步提交已完成的Segment片段。
override fun writeString(string: String, charset: Charset): BufferedSink {
check(!closed) { "closed" }
buffer.writeString(string, charset)
return emitCompleteSegments()
}
跟进至Buffer类,最终会调用commonWrite方法,而commonWrite内部调用commonWritableSegment方法,此时Segment登场了,通过SegmentPool取出Segment片段,最终将数据拷贝至Segment片段中。
internal inline fun Buffer.commonWritableSegment(minimumCapacity: Int): Segment {
require(minimumCapacity >= 1 && minimumCapacity <= Segment.SIZE) { "unexpected capacity" }
if (head == null) {
val result = SegmentPool.take() // Acquire a first segment.
head = result
result.prev = result
result.next = result
return result
}
var tail = head!!.prev
if (tail!!.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
tail = tail.push(SegmentPool.take()) // Append a new empty segment to fill up.
}
return tail
}
至此依然没发生真正的读写调用,跟进emitCompleteSegments方法,最终调用commonEmitCompleteSegment方法。
internal inline fun RealBufferedSink.commonEmitCompleteSegments(): BufferedSink {
check(!closed) { "closed" }
val byteCount = buffer.completeSegmentByteCount()
if (byteCount > 0L) sink.write(buffer, byteCount)
return this
}
sink.write调用中的sink是谁?是OutputStreamSink!最终调用了OutputStream的write方法完成了文件的写入,至此时间线回收。最后别忘了调用close方法关闭IO流!
override fun close() = out.close()
ByteString
此类的设计目的就是为了方便String与byte进行互转。原理也很简单,同时持有原始字符串和与之对应的byte数组,这样在取数据的时候减少它们互转的操作。
/ 适用场景 /
网络请求
如TCP请求框架中的IO流可以使用它。
缓存
对于一些特殊数据,可以使之以流的形式保存下来,此时使用OkIO将更简洁。对于需要频繁复制、转移的数据,通过OkIO中的各种类也可以很好地进行缓存。
比如,在设计网络请求框架时,有些禁止频繁调用的接口(如:来自同一客户端的首页列表数据拉取),可以通过OkIO缓存最近请求过的数据,当业务方频繁请求时,框架无需发生真正的网络连接请求,直接返回缓存中的数据即可。
/ 注意事项 /
OkIO的本质是Java IO的封装,代码编写时需要注意IO流的关闭,流操作完成后,最后调用close方法。
国际惯例,文章末尾列出工程地址,代码在androitTest的com.pengyeah.kkp中。
https://gitee.com/null_077_5468/using-rotten
推荐阅读:
kotlin-android-extensions插件也被废弃了?扶我起来
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注