温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Android之从IO到NIO的模型机制实例分析

发布时间:2023-02-01 09:42:26 来源:亿速云 阅读:76 作者:iii 栏目:开发技术

这篇文章主要讲解了“Android之从IO到NIO的模型机制实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Android之从IO到NIO的模型机制实例分析”吧!

    1 Basic IO模型

    那么在Java(Kotlin)中,IO主要分为两种:Basic IO 和 Net IO;Basic IO是我们在开发当中常用的一些IO流,例如:

    FileInputStream://文件输入流
    FileOutputStream://文件输出流
    BufferedInputStream://缓存字节输入流
    BufferedOutputStream://缓存字节输入流,此类数据流为了提高读写效率,可以缓存数据到buffer,通过flush一起写入;内核分配内存为一页4K,但是Java缓冲区默认是8K
    ObjectInputStream
    ObjectOutputStream:// 将数据序列化处理
    RandomAccessFile://提供位移数据插入

    对于前面的几个数据流,我就不介绍用法了,对于最后一个RandomAccessFile,我想简单介绍一下,因为很多伙伴们可能不知道RandomAccessFile的存在,这里曾经有个面试题:

    假设有一个5G的文件,我想在文章的末尾追加一段话,我该怎么处理?或者我指定任意位置添加一部分文字内容,该怎么处理?

    很多伙伴看到这个问题之后,一拍脑门说:先通过FileInputStream把文件读写进来,然后再在末尾追加一部分内容组合成新的字节流,然后再通过FileOutputStream写入到新的文件中。

    完蛋,直接pass掉!因为前提这里已经是5G的文件了,如果通过FileInputStream读写,大概率就会直接OOM! 所以如果知道RandomAccessFile的存在,这些就不是问题了。

    fun testAccessFile() {
        //file文件
        val file = File("/storage/emulated/0/NewTextFile.txt")
        val accessFile = RandomAccessFile(file, "rw")
        //先写一段
        val text = "IO主要分为两种:Basic IO 和 Net IO;"
        accessFile.write(text.toByteArray())
        //再等5s
        Thread.sleep(5000)
        accessFile.seek(5)
        accessFile.write("seek to pos 5".toByteArray())
        accessFile.close()
    }

    首先我们常见一个RandomAccessFile,传入要读写的文件,首先写入一段话,然后等到5s后,调用RandomAccessFile的seek方法,此时指针就是移动到了文件第五个字符的位置,然后又写入了一些文字。

    Android之从IO到NIO的模型机制实例分析

    所以按照这种思想,回到前面的问题,即便是5G的文件,也不需要进行读写操作获取之前的全部数据就能够实现零内存追加;当然还有一个场景也会经常用到,就是断点续传。

    1.1 RandomAccessFile的缓冲区和BufferedInputStream缓冲区的区别

    首先我先简单介绍下BufferedInputStream的缓存区效果,系统内核缓存区默认为4K,当缓存区满4K之后会进行磁盘的写入;那么在Java中是对其做了优化处理,将缓存区变为8K,当缓存区超过8K之后,会将数据复制给到内核缓存。

    Android之从IO到NIO的模型机制实例分析

    fun testBuffer() {
            val file = File("/storage/emulated/0/NewTextFile.txt")
            val bis = BufferedOutputStream(FileOutputStream(file))
            val text = "8888888888888888".toByteArray()
            bis.write(text, 0, text.size)
    //        bis.flush()
        }

    例如上面的案例,此时App的内存缓存区没有满,那么如果不调用flush,那么数据不会写到磁盘文件中,只有当缓冲区满了之后,才会复制到内核空间缓存区。

    fun testAccessFile() {
        //file文件
        val file = File("/storage/emulated/0/NewTextFile.txt")
        val accessFile = RandomAccessFile(file, "rw")
        //先写一段
        val text = "IO主要分为两种:Basic IO 和 Net IO;"
        accessFile.write(text.toByteArray())
        //再等5s
        Thread.sleep(5000)
        accessFile.seek(5)
        val channel = accessFile.channel
        val mapper = channel.map(FileChannel.MapMode.READ_WRITE, channel.position(), channel.size())
        mapper.put("seek to pos 5".toByteArray())
    }

    如果按照BufferedOutputStream的思想,我们往缓冲区写数据,没有flush就不会有复制的操作,那么我们实际看到的是数据还是写进去了。

    Android之从IO到NIO的模型机制实例分析

    其实MappedByteBuffer,是提供了一个类似于mmap性质的能力,实现了App缓冲区与内核缓冲区的桥接或者映射。

    Android之从IO到NIO的模型机制实例分析

    当App写入缓存数据的时候,直接映射到了内核缓存区,完成了磁盘的读写操作。

    1.2 Basic IO模型底层原理

    其实对于基础的IO模型,也就是Basic IO的实现是阻塞的,其实我们也可以自己验证,在主线程中进行读写操作就是阻塞的。

    那么对于IO来说,主要分为两个阶段:

    (1)数据准备阶段;这里是由Java实现的,写入到JVM中;

    (2)复制阶段;内核空间复制用户空间缓存数据,这部分需要调用内核函数(ioctl、sync),完成复制的工作。

    剩下的磁盘写入操作就完全是由内核完成的,如果对于读写操作有疑问的,可以去看看下面这篇对于Binder底层原理的介绍。

    Android Framework原理 -- Binder驱动源码分析

    对于传统的Socket来说,这种属于Net IO,本质也是阻塞性质的,例如App进程想要获取一些数据,

    Android之从IO到NIO的模型机制实例分析

    上图展示了read操作的整个调度过程:

    (1)当App调用系统方法想要获取某些数据的时候,首先系统内核会等待数据从网络中到达,这个过程内核处于阻塞的状态

    (2)等到数据到达之后,就会将网络数据复制到用户空间的缓冲区中,并通知App进程复制数据成功,此时App中其他业务才能够继续执行。

    所以整个过程中,App处于阻塞状态,而在高并发的场景中(客户端很少,这里拿服务端来举例),例如10000QPS(每秒10000次查询操作),此时如果采用IO阻塞模型,带来的后果就是CPU极速拉满最终可能导致熔断,所以针对这种情况,出现了NIO模型。

    2 NIO模型

    相对于IO模型来说,NIO模型做的优化是通过轮询机制获取内核的数据等待状态,看下图:

    Android之从IO到NIO的模型机制实例分析

    当一次询问发出之后,如果当前内核还是数据等待状态,那么内核空间会被”挂起“,此时App进程可以做其他的事情,等到下一次轮询时间到了之后,再次发起询问,如果此时已经拿到了数据,那么就会进行复制操作,将数据放入用户进程缓冲区。

    Android之从IO到NIO的模型机制实例分析

    那么对此,java.nio包下提供了很多非阻塞IO的API,例如我们前面提到的MappedByteBuffer。其实还是前面我们探讨的一个问题,在Android的场景下,很难碰到高并发的场景,所以基本上也很难用到这个,但是对于NIO模型的原理我们需要掌握透彻,在面试中可能会涉及到这些问题。

    3 OKIO

    最后介绍一个IO模型---OKIO,如果使用到OkHttp的伙伴们应该已经见到过这个,但是没有实际地去研究,为啥要引入这个okio三方库。

    首先okio是OkHttp团队基于Basic IO研发的一套自己的IO体系,为啥要搞一个这个玩意出来呢?通过前面我们分析Basic IO存在的一些问题,首先 Basic IO是阻塞的,而且在客户端端如果频繁地进行网络请求,而且网络请求是双向的,从客户端发出请求,服务端返回响应,那么这个过程必定会使用到InputStream和OutputStream。

    因为OkHttp是有自己的缓存策略的,如果使用到缓存,那么对于InputStream就需要一个buffer,对于OutputStream也需要一个buffer,每次读写操作都需要两个buffer来做支撑,因此针对这种场景,okio在底层做了处理。

    具体的处理就是不再使用byte[]数组存储数据,而是采用Segment数据结构。有熟悉Segment的伙伴应该知道,它是一个数组的双向链表,其中data就是一个byte数组,其中有next和pre两个指针。

    internal class Segment {
      @JvmField val data: ByteArray
      /** The next byte of application data byte to read in this segment.  */
      @JvmField var pos: Int = 0
      /** The first byte of available data ready to be written to.  */
      @JvmField var limit: Int = 0
      /** True if other segments or byte strings use the same byte array.  */
      @JvmField var shared: Boolean = false
      /** True if this segment owns the byte array and can append to it, extending `limit`.  */
      @JvmField var owner: Boolean = false
      /** Next segment in a linked or circularly-linked list.  */
      @JvmField var next: Segment? = null
      /** Previous segment in a circularly-linked list.  */
      @JvmField var prev: Segment? = null

    Android之从IO到NIO的模型机制实例分析

    当进行读写操作的时候,都会往Segment中写入,就是将InputStream和OutputStream需要创建的缓冲区合并。

    这里需要说明一点,okio属于OkHttp内部核心IO框架,并不是单独拿出来任意业务方可以使用,所以对于okio的具体实现原理,后续会放在OkHttp框架原理中做详细的介绍。

    感谢各位的阅读,以上就是“Android之从IO到NIO的模型机制实例分析”的内容了,经过本文的学习后,相信大家对Android之从IO到NIO的模型机制实例分析这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI