Android进阶之从IO到NIO的模型机制演进

目录
  • 引言
  • 1 Basic IO模型
    • 1.1 RandomAccessFile的缓冲区和BufferedInputStream缓冲区的区别
    • 1.2 Basic IO模型底层原理
  • 2 NIO模型
  • 3 OKIO

引言

其实IO操作相较于服务端,客户端做的并不多,基本的场景就是读写文件的时候会使用到InputStream或者OutputStream,然而客户端能做的就是发起一个读写的指令,真正的操作是内核层通过ioctl指令执行读写操作,因为每次的IO操作都涉及到了线程的操作,因此会有性能上的损耗,那么从本篇文章开始,我们将进入IO的世界,了解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方法,此时指针就是移动到了文件第五个字符的位置,然后又写入了一些文字。

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

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

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

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就不会有复制的操作,那么我们实际看到的是数据还是写进去了。

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

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

1.2 Basic IO模型底层原理

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

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

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

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

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

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

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

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

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

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

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

2 NIO模型

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

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

那么对此,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

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

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

以上就是Android进阶之从IO到NIO的模型机制演进的详细内容,更多关于Android模型从IO到NIO机制的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android进阶NestedScroll嵌套滑动机制实现吸顶效果详解

    目录 引言 1 自定义滑动布局,实现吸顶效果 1.1 滑动容器实现 1.2 嵌套滑动机制完成交互优化 1.2.1 NestedScrollingParent接口和NestedScrollingChild接口 1.2.2 预滚动阶段实现 1.2.3 滚动阶段实现 1.2.4 滚动结束 引言 在上一篇文章Android进阶宝典 -- 事件冲突怎么解决?先从Android事件分发机制开始说起中,我们详细地介绍了Android事件分发机制,其实只要页面结构复杂,联动众多就会产生事件冲突,处理不得当就是b

  • Android进阶从字节码插桩技术了解美团热修复实例详解

    目录 引言 1 插件发布 2 Javassist 2.1 准备工作 2.2 Transform 2.3 transform函数注入代码 2.3.1 Jar包处理 2.3.2 字节码处理 2.4 Javassist织入代码 2.4.1 ClassPool 2.4.2 CtClass 引言 热修复技术如今已经不是一个新颖的技术,很多公司都在用,而且像阿里.腾讯等互联网巨头都有自己的热修复框架,像阿里的AndFix采用的是hook native底层修改代码指令集的方式:腾讯的Tinker采用类加载的方

  • Android隐私协议提示弹窗的实现流程详解

    android studio版本:2021.2.1 例程名称:pravicydialog 功能: 1.启动app后弹窗隐私协议 2.屏蔽返回键 3.再次启动不再显示隐私协议. 本例程的绝大部分代码来自下面链接,因为本人改了一些,增加了一些功能,所以不有脸的算原创了. 下面这个例子是“正宗”app隐私协议实现方法,而且协议内容使用的是txt格式文件,据说如果使用html格式文件,各大平台在审核的时候大概率无法通过,但协议内容的还应该有更详细协议及说明的链接,我没做,暂时还没学会,会了再修改一下.

  • Android进阶Handler应用线上卡顿监控详解

    目录 引言 1 Handler消息机制 1.1 方案确认 1.2 Looper源码 1.3 Blockcanary原理分析 1.4 Handler监控的缺陷 2 字节码插桩实现方法耗时监控 2.1 字节码插桩流程 2.2 引入ASM实现字节码插桩 2.3 Blockcanary的优化策略 引言 在上一篇文章中# Android进阶宝典 -- KOOM线上APM监控最全剖析,我详细介绍了对于线上App内存监控的方案策略,其实除了内存指标之外,经常有用户反馈卡顿问题,其实这种问题是最难定位的,因为不

  • Android Jetpack组件ViewModel基本用法详解

    目录 引言 一.概述与作用 二.基本用法 小结 引言 天道好轮回,终于星期五,但是还是忙碌了一天.在项目中,我遇到了一个问题,起因则是无法实时去获取信息来更新UI界面,因为我需要知道我是否获取到了实时信息,我想到的办法有三,利用Handler收发消息在子线程与主线程切换从而更新信息,其二则是利用在页面重绘的时候(一般是页面变动如跳转下个页面和将应用切至后台),其三就是利用Jetpack中最重要的组件之一ViewModel,最后我还是选择了ViewModel,因为感觉更方便. 其实想到的前面两个方

  • Android进阶CoordinatorLayout协调者布局实现吸顶效果

    目录 引言 1 CoordinatorLayout功能介绍 1.1 CoordinatorLayout的依赖交互原理 1.2 CoordinatorLayout的嵌套滑动原理 2 CoordinatorLayout源码分析 2.1 CoordinatorLayout的依赖交互实现 2.2 CoordinatorLayout交互依赖的源码分析 2.3 CoordinatorLayout子控件拦截事件源码分析 2.4 CoordinatorLayout嵌套滑动原理分析 引言 在上一节Android进

  • Android进阶KOOM线上APM监控全面剖析

    目录 正文 1 Leakcanary为什么不能用于线上 1.1 Leakcanary原理简单剖析 1.2 小结 2 KOOM原理分析 2.1 KOOM引入 2.2 KOOM源码分析 2.2.1 trackOOM方法分析 2.2.2 HeapOOMTracker 2.2.3 ThreadOOMTracker 2.2.4 FastHugeMemoryOOMTracker 2.3 dump为何不能放在子线程 2.3.1 ForkJvmHeapDumper分析 2.3.2 C++层分析dumpHprof

  • Android进阶之从IO到NIO的模型机制演进

    目录 引言 1 Basic IO模型 1.1 RandomAccessFile的缓冲区和BufferedInputStream缓冲区的区别 1.2 Basic IO模型底层原理 2 NIO模型 3 OKIO 引言 其实IO操作相较于服务端,客户端做的并不多,基本的场景就是读写文件的时候会使用到InputStream或者OutputStream,然而客户端能做的就是发起一个读写的指令,真正的操作是内核层通过ioctl指令执行读写操作,因为每次的IO操作都涉及到了线程的操作,因此会有性能上的损耗,那

  • 浅谈Java中IO和NIO的本质和区别

    IO的本质 IO的作用就是从外部系统读取数据到java程序中,或者把java程序中输出的数据写回到外部系统.这里的外部系统可能是磁盘,网络流等等. 因为对所有的外部数据的处理都是由操作系统内核来实现的,对于java应用程序来说,只是调用操作系统中相应的接口方法,从而和外部数据进行交互. 所有IO的本质就是对Buffer的处理,我们把数据放入Buffer供系统写入外部数据,或者从系统Buffer中读取从外部系统中读取的数据.如下图所示: 用户空间也就是我们自己的java程序有一个Buffer,系统

  • Android 进阶实现性能优化之OOM与Leakcanary详解原理

    目录 Android内存泄漏常见场景以及解决方案 资源性对象未关闭 注册对象未注销 类的静态变量持有大数据 单例造成的内存泄漏 非静态内部类的静态实例 Handler临时性内存泄漏 容器中的对象没清理造成的内存泄漏 WebView 使用ListView时造成的内存泄漏 Leakcanary leakcanary 导入 leakcanary 是如何安装的 leakcanary 如何监听Activity.Fragment销毁 RefWatcher 核心原理 流程图 本文主要探讨以下几个问题: And

  • Android 深入探究自定义view之事件的分发机制与处理详解

    目录 题引 Activity对事件的分发过程 父布局拦截的分发处理过程 ACTION_DOWN 事件 ACTION_MOVE 事件 父布局不拦截时的分发处理过程 ACTION_DOWN ACTION_MOVE 解决冲突方案 外部拦截 内部拦截 本文主要探讨下面几个问题: 学习事件分发机制是为了解决什么问题 Activity对事件的分发过程 父布局拦截的分发处理过程 父布局不拦截时的分发处理过程 冲突解决方案 题引 事件只有一个,多个人想要处理,处理的对象不是我们想给的对象就是事件冲突. 如上图,

  • Redis缓存IO模型的演进教程示例精讲

    目录 前言 事件模型 通信 copy数据的开销 数据怎么知道发给哪个socket socket的数据怎么通知程序来取 Reactor IO多路复用器 select epoll epoll是怎么做到的? 单线程到多线程的演进 单线程 异步线程 多线程 多线程的作用点? 多线程的原理 前言 redis作为应用最广泛的nosql数据库之一,大大小小也经历过很多次升级.在4.0版本之前,单线程+IO多路复用使得redis的性能已经达到一个非常高的高度了.作者也说过,之所以设计成单线程是因为redis的瓶

  • Android同步异步任务与多线程及Handler消息处理机制基础详细讲解

    目录 一.同步与异步 Android中的多线程 Android中的多线程与主线程与子线程 Handler异步通信系统 使用新线程计算质数 一.同步与异步 同步的执行任务:在执行程序时,如果没有收到执行结果,就一直等,不继续往下执行,直到收到执行结果,才接着往下执行. 异步的执行任务:在执行程序时,如果遇到需要等待的任务,就另外开辟一个子线程去执行它,自己继续往下执行其他程序.子线程有结果时,会将结果发送给主线程 Android中的多线程 线程:通俗点讲就是一个执行过程.多线程自然就是多个执行过程

随机推荐