查看原文
其他

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

彭也 郭霖 2022-12-14



/   今日科技快讯   /


近日,随着巴西国内下一代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插件也被废弃了?扶我起来

我的新书,《第一行代码 第3版》已出版!

像使用Activity一样使用Fragment


欢迎关注我的公众号

学习技术或投稿



长按上图,识别图中二维码即可关注


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存