用烂OkIO,隔壁产品看不懂了

1,100 阅读4分钟

1、它是什么?

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

2、如何使用?

2.1 利用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()
}

运行结果如下

2.2 老版本的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");
}

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

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方法。

国际惯例,文章末尾列出工程地址,代码在androitTest的com.pengyeah.kkp中。

点我点我点我