首页 文章详情

手把手带你重新认识IO框架:OkIO

Carson带你学习Android | 989 2021-03-05 23:19 0 0 0
UniSMS (合一短信)

1、它是什么?

它是一套在Java IO基础上再次进行封装的IO框架,在形式上比原来的Java IO更简洁易用。引入OkHttp依赖即可引用之,源码地址:https://github.com/square/okio

2、如何使用?

2.1 利用kotlin扩展函数,代码简洁,两行代码搞定

当然,前提是记得加入文件读写权限。

@Testfun 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()}

运行结果如下

2.2 老版本的java调用方式

代码如下:

@Testpublic 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");}

运行结果也一样,不再列出。

3、看看原理

*注:版本为最新版的kotlin代码

3.1 它使用了谁?

3.1.1 InputStream、OutputStream

跟踪sink()、source()方法很明显,扩展了InputStream、OutputStream的相关函数,并将其子类封装成Source、Sink的实例。

3.1.2 SegmentPool和Segment

跟踪Okio的buffer方法,可以看到RealBufferedSink、RealBufferedSource类,他们分别实现了Sink、Source。而RealBufferedSink、RealBufferedSource都持有Buffer类型的属性buffer。这个Buffer类很关键,内部使用到了SegmentPool,SegmentPool是管理Segment的工具,Segment是缓冲区中的某一片段,正是有它的存在,可以实现分段存储。

3.1.3 ByteString

还值得一提的是ByteString,这是一个方便Byte和String类互转的工具

3.2 谁使用了它?

3.2.1 OkHttp

okhttp中使用okio支撑相关流操作。

3.2.2 支付宝等众多大型应用中

3.3 原理解析

3.3.1 类图

Sink负责输出相关的操作,而Source负责输入相关的操作。

可以看到,无论读写,都是通过Buffer统一操作的。底层还是使用了OutputSream、InputStream,本质上还是对Java IO相关的API的封装。

3.3.2 分段存储的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()

3.3.3 ByteString

此类的设计目的就是为了方便String与byte进行互转。原理也很简单,同时持有原始字符串和与之对应的byte数组,这样在取数据的时候减少它们互转的操作。

4、适用场景

4.1 网络请求

如TCP请求框架中的IO流可以使用它

4.2 缓存

对于一些特殊数据,可以使之以流的形式保存下来,此时使用OkIO将更简洁。对于需要频繁复制、转移的数据,通过OkIO中的各种类也可以很好地进行缓存。

比如,在设计网络请求框架时,有些禁止频繁调用的接口(如:来自同一客户端的首页列表数据拉取),可以通过OkIO缓存最近请求过的数据,当业务方频繁请求时,框架无需发生真正的网络连接请求,直接返回缓存中的数据即可。

5、注意事项

OkIO的本质是Java IO的封装,代码编写时需要注意IO流的关闭,流操作完成后,最后调用close方法。

文章末尾列出工程地址,代码在com.pengyeah.kkp中。点击“阅读原文”获取。


「Carson每天带你学习一个Android知识点」,长按扫描关注公众号,我们明天见哦!


最后福利:学习资料赠送

  • 福利:由本人亲自撰写 & 整理的「Android学习方法资料」
  • 数量:10名
  • 参与方式:「点击文章右下角”在看“ -> 回复截图到公众号 即可,我将从中随机抽取」

    点击“在看”就能升职 & 加薪水哦!
good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter