IO流详解

概述

流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各类类,方便更直观的进行数据操做。IO其实有两类,一类是BIO(BlockingIO),一类是NIO(Non-BlockingIO),不过咱们一般说的是IO默认指的是BIO;html

正文

基础知识

字符

字节是计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。java

####字符编码linux

字符是指人们使用的记号,抽象意义上的一个符号,好比一、二、三、·#¥%。数据库

####字节编程

字符编码(Character encoding)是一套法则,使用该法则可以对天然语言的字符的一个集合(如字母表或音节表),与其余东西的一个集合进行配对。各个国家和地区所制定的不一样 ANSI 编码标准中,都只规定了各自语言所需的字符。windows

常见的编码方式

ASCII编码:美国制定了一套字符编码,对英语字符与二进制位之间的关系,作了统一规定。这被称为 ASCII 码,一直沿用至今。设计模式

非ASCII 编码:英语用128个符号编码就够了,可是用来表示其余语言,128个符号是不够的,因此在别的国家编码符号会比128要多,因此问题就出现了,不一样的国家有不一样的字母,所以,哪怕它们都使用256个符号的编码方式,表明的字母却不同。数组

UTF-8编码:UTF-8最大的一个特色,就是它是一种变长的编码方式。它可使用1~4个字节表示一个符号,根据不一样的符号而变化字节长度。。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其余实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一。缓存

联系与区别

不少时候咱们常常说起到字符跟字节之间的关系,这个问题的前提是基于某一种编程语言好比说Java或者C来讲的,由于字符跟字节之间的关系跟字符编码是有着紧密联系的,因此单独讨论字符跟字节之间的关系没有意义,下面简单来看一下他们在不一样编码上的的对应关系:
语言 | 中文字符| 英文字符
---|---|---
GBK | 2个字节| 1个字节
UTF-8| 2个字节| 2个字节
java语言默认是采用Utf-8来进行编码的,下面用Java来测试一下:bash

测试GBK编码

public static void main(String[] args) {
    String str = "Hello_安卓";
    int byte_len = 0;
    try {
        byte_len = str.getBytes("gbk").length;
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    System.out.println("字节长度:" + byte_len);

}复制代码
字节长度:10复制代码

测试UTF-8编码

public static void main(String[] args) {
    String str = "Hello_安卓";
    int byte_len =str.getBytes().length;
    System.out.println("字节长度:" + byte_len);

}复制代码
字节长度:12复制代码

输出的结果,跟以前的规则是一致的,到这里,字节,编码方式,字符,以及他们之间的联系基本上介绍完了,理解了他们之间的关系,下面的File类跟IO之间的关系也就比较好理解了。

File类

File类翻译过来是一个文件,实际上它并非一个文件,定义为Path更为合适,这个Path能够是文件的路径也能够是文件夹的路径,由于当咱们new File的时候,只是建立了一个路径,这个路径若是建立成功,没有后缀名就是文件夹,有后缀名则建立了一个空文件。下面看一下File类的继承关系:

File的继承关系
File的继承关系

构造函数

列举几个常见的构造函数

File(String pathname)
File(String parent,String child)
File(File parent,String child)复制代码

由于Java命名比较规范,因此很好理解,有一点须要注意的是,这个方法不能保证文件必定会建立成功,可是即便失败也不会报异常,因此通常咱们在文件建立以后须要判断一下当前文件是否建立成功,调用一下 exists()方法来判断文件是否建立成功,不成功则调用createNewFile(),此方法失败会抛异常,概括起来就是:

File file=new File("E:\\demo","a.txt");
    if (file.exists()){
        //继续文件的操做
    }else {
        try {
            boolean result = file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }复制代码

路径

  • 相对路径:./表示当前路径../表示上一级路径
  • 绝对路径:绝对路径名是完整的路径名,不须要任何其余信息就能够定位自身表示的文件
路径分隔符:
  • windows: "/" "\" 均可以
  • linux/unix: "/"

    注意:若是windows选择用"\"作分割符的话,那么请记得替换成"\",由于Java中"\"表明转义字符因此推荐都使用"/",也能够直接使用代码File.separator,表示跨平台分隔符。

建立与删除
boolean createNewFile();//建立具体的文件
boolean mkdir();//建立单个目录
boolean mkdirs();//建立多个目录
boolean delete();//删除File复制代码
判断方法
boolean canRead();//判断文件是否可读
boolean canWrite();//判断文件是否可写
boolean exists();//判断文件是否存在
boolean isDirectory();//判断是不是目录
boolean isFile();//判断是不是文件
boolean isAbsolute();//判断是不是绝对路径复制代码
获取方法
String getName();//返回文件或者是目录的名称
String getPath();//返回路径
String getAbsolutePath();//返回绝对路径
String getParent();//返回父目录,若是没有父目录则返回null
long lastModified();//返回最后一次修改的时间
long length();//返回文件的长度
File[] listRoots();// 列出全部的根目录(Window中就是全部系统的盘符)
String[] list() ;//返回一个字符串数组,给定路径下的文件或目录名称字符串
String[] list(FilenameFilter filter);//返回知足过滤器要求的一个字符串数组
File[]  listFiles();//返回一个文件对象数组,给定路径下文件或目录复制代码
文件过滤

File[] listFiles(FilenameFilter filter);//返回知足过滤器要求的一个文件对象数组
其中包含了一个重要的接口FileNameFilter,该接口是个文件过滤器,包含了一个accept(File dir,String name)方法,该方法依次对指定File的全部子目录或者文件进行迭代,按照指定条件,进行过滤,过滤出知足条件的全部文件。

// 文件过滤
 File[] files = file.listFiles(new FilenameFilter() {
    @Override
  public boolean accept(File file, String filename) {
           return filename.endsWith(".apk");
            }
        });复制代码

file目录下的全部子文件若是知足后缀是.apk的条件的文件都会被过滤出来。

分类

按数据类型分

IO流分类
IO流分类

-

  • 字节流:字节流主要是操做byte类型数据
  • 字符流: 由于数据编码的不一样,而有了对字符进行高效操做的流对象。本质其实就是基于字节流读取时,去查了指定的码表。

区别

  • 读写单位不一样:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读16位字节。
  • 处理对象不一样:字节流能处理全部类型的数据(如image、avi等),而字符流只能处理字符类型的数据。

设备上的数据不管是图片或者视频,文字,它们都以二进制存储的。二进制的最终都是以一个8位为数据单元进行体现,因此计算机中的最小数据单元就是字节。意味着,字节流能够处理设备上的全部数据,因此字节流同样能够处理字符数据。

结论:只要是处理纯文本数据,就优先考虑使用字符流。换句话说,能使用字符流的必定也可使用字节流。

按照数据流向分

IO流按照流向分
IO流按照流向分

  • 输入流:InputStream或者Reader:从文件中读到程序中;
  • 输出流:OutputStream或者Writer:从程序中输出到文件中;

    这里的输入和输出都是以程序为参照物

按照流的角色分

IO流按照角色分
IO流按照角色分

  • 节点流:直接与数据源相连,读入或读出,能够从/向一个特定的IO设备(如磁盘、网络)读/写数据的流,称为节点流,节点流也被成为低级流。
  • 处理流:处理流是对一个已存在的流进行链接或封装,经过封装后的流来实现数据读/写功能,处理流也被称为高级流。

当使用处理流进行输入/输出时,程序并不会直接链接到实际的数据源,没有和实际的输入/输出节点链接。使用处理流的一个明显好处是,只要使用相同的处理流,程序就能够采用彻底相同的输入/输出代码来访问不一样的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地发生变化。

经常使用的节点流

父类 :InputStream 、OutputStream、 Reader、 Writer

文件 :FileInputStream 、 FileOutputStrean 、FileReader 、FileWriter 文件进行处理的节点流

数组:ByteArrayInputStream、 ByteArrayOutputStream、 CharArrayReader 、CharArrayWriter 对数组进行处理的节点流(对应的再也不是文件,而是内存中的一个数组)

字符串 :StringReader、 StringWriter 对字符串进行处理的节点流

管 道 :PipedInputStream 、PipedOutputStream 、PipedReader 、PipedWriter 对管道进行处理的节点流ByteArrayOutputStream、FileOutputStream 是两种基本的介质流,它们分别向Byte 数组、和本地文件中写入数据。
PipedOutputStream 是向与其它线程共用的管道中写入数据。
ObjectOutputStream和全部FilterOutputStream 的子类都是装饰流。

经常使用的处理流
缓冲流:BufferedInputStrean 、BufferedOutputStream、 BufferedReader、 BufferedWriter 增长缓冲功能,避免频繁读写硬盘。
转换流:InputStreamReader 、OutputStreamReader实现字节流和字符流之间的转换。
数据流: DataInputStream 、DataOutputStream 等-提供将基础数据类型写入到文件中,或者读取出来。

转换流:InputStreamReader 、OutputStreamWriter 要InputStream或OutputStream做为参数,实现从字节流到字符流的转换。

父类介绍

InputStream

InputStream继承关系
InputStream继承关系

InputStream 是全部的输入字节流的父类,它是一个抽象类,主要包含三个方法:

//读取一个字节并以整数的形式返回(0~255),若是返回-1已到输入流的末尾。 
int read() ; 
//读取一系列字节并存储到一个数组buffer,返回实际读取的字节数,若是读取前已到输入流的末尾返回-1。 
int read(byte[] buffer) ; 
//读取length个字节并存储到一个字节数组buffer,从off位置开始存,最多len, 返回实际读取的字节数,若是读取前以到输入流的末尾返回-1。 
int read(byte[] buffer, int off, int len) ;复制代码

ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流,它们分别从Byte 数组、StringBuffer、和本地文件中读取数据。
PipedInputStream 是从与其它线程共用的管道中读取数据,与Piped 相关的知识后续单独介绍。
ObjectInputStream 和全部FilterInputStream 的子类都是装饰流(装饰器模式的主角)

Reader

Reader继承关系
Reader继承关系

Reader 是全部的输入字符流的父类,它是一个抽象类,主要包含三个方法:

//读取一个字符并以整数的形式返回(0~255),若是返回-1已到输入流的末尾。 
int read() ; 
//读取一系列字符并存储到一个数组buffer,返回实际读取的字符数,若是读取前已到输入流的末尾返回-1。 
int read(char[] cbuf) ; 
//读取length个字符,并存储到一个数组buffer,从off位置开始存,最多读取len,返回实际读取的字符数,若是读取前以到输入流的末尾返回-1。 
int read(char[] cbuf, int off, int len)复制代码

对比InputStream和Reader所提供的方法,就不难发现两个基类的功能基本同样的,只不过读取的数据单元不一样。

在执行完流操做后,要调用close()方法来关系输入流,由于程序里打开的IO资源不属于内存资源,垃圾回收机制没法回收该资源,因此应该显式关闭文件IO资源。

除此以外,InputStream和Reader还支持以下方法来移动流中的指针位置:

//在此输入流中标记当前的位置
//readlimit - 在标记位置失效前能够读取字节的最大限制。
void mark(int readlimit)
// 测试此输入流是否支持 mark 方法
boolean markSupported()
// 跳过和丢弃此输入流中数据的 n 个字节/字符
long skip(long n)
//将此流从新定位到最后一次对此输入流调用 mark 方法时的位置
void reset()复制代码

OutputStream

OutputStream继承关系
OutputStream继承关系

OutputStream 是全部的输出字节流的父类,它是一个抽象类,主要包含以下四个方法:

//向输出流中写入一个字节数据,该字节数据为参数b的低8位。 
void write(int b) ; 
//将一个字节类型的数组中的数据写入输出流。 
void write(byte[] b); 
//将一个字节类型的数组中的从指定位置(off)开始的,len个字节写入到输出流。 
void write(byte[] b, int off, int len); 
//将输出流中缓冲的数据所有写出到目的地。 
void flush();复制代码

Writer

Writer继承关系
Writer继承关系

Writer 是全部的输出字符流的父类,它是一个抽象类,主要包含以下六个方法:

//向输出流中写入一个字符数据,该字节数据为参数b的低16位。 
void write(int c); 
//将一个字符类型的数组中的数据写入输出流, 
void write(char[] cbuf) 
//将一个字符类型的数组中的从指定位置(offset)开始的,length个字符写入到输出流。 
void write(char[] cbuf, int offset, int length); 
//将一个字符串中的字符写入到输出流。 
void write(String string); 
//将一个字符串从offset开始的length个字符写入到输出流。 
void write(String string, int offset, int length); 
//将输出流中缓冲的数据所有写出到目的地。 
void flush()复制代码

能够看出,Writer比OutputStream多出两个方法,主要是支持写入字符和字符串类型的数据。

使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了能够保证流的物理资源被回收以外,还能将输出流缓冲区的数据flush到物理节点里(由于在执行close()方法以前,自动执行输出流的flush()方法)

IO中的一股清流——RandomAccessFIle

RandomAccessFIle继承关系
RandomAccessFIle继承关系

咱们发现RandomAccessFIle跟File类并无联系,只是实现了DataOutput跟DataInput两个接口,彻底本身从新定义了一遍File的读取操做

RandomAccessFile是Java中输入,输出流体系中功能最丰富的文件内容访问类,它提供不少方法来操做文件,包括读写支持,与普通的IO流相比,它最大的特别之处就是支持任意访问的方式,程序能够直接跳到任意地方来读写数据。

若是咱们只但愿访问文件的部份内容,而不是把文件从头读到尾,使用RandomAccessFile将会带来更简洁的代码以及更好的性能。

方法
方法名 做用
getFilePointer() 返回文件记录指针的当前位置
seek(long pos) 将文件记录指针定位到pos的位置
功能
  • 1.读取任意位置的数据
  • 2.追加数据
  • 3.任意位置插入数据

NIO

Java NIO是java 1.4以后新出的一套IO接口,这里的的新是相对于原有标准的Java IO和Java Networking接口。NIO提供了一种彻底不一样的操做方式。标准的IO编程接口是面向字节流字符流的。而NIO是面向Channel(通道)和Buffer(缓冲区)的,数据老是从Channel中读到Buffer内,或者从Buffer写入到Channel中,Channel是须要注册到Selector(选择器)中去。咱们知道无论是NIO仍是BIO在读写数据的过程当中都有两个操做:等待就绪和操做。举例来讲,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡能够写和真正的写。NIO跟BIO的操做都是同样的,区别在于等待就绪的这一过程,看看下面这张图:

阻塞式IO
阻塞式IO

非阻塞式IO
非阻塞式IO

NIO跟BIO的区别在于无论如今有没有数据,都会给调用者一个返回值,在没有准备就绪以前,当前线程能够进行其余的操做,而不会阻塞。

缓冲区

Buffer的实现关系
Buffer的实现关系

缓冲区实质上就是一个数组,但它不只仅是一个数组,缓冲区还提供了对数据的结构化访问,并且还能够跟踪系统的读/写进程。

通道

Channel的实现关系
Channel的实现关系

通道用于在缓冲区和位于通道另外一侧的实体(文件、套接字)之间有效的传输数据

选择器

Selector的实现关系
Selector的实现关系

选择器类Selector并无和通道有直接的关系,而是经过叫选择键的对象SelectionKey来联系的,并且Selector能够注册过个key,也就是说能够同时管理多个通道。选择键表明了通道与选择 器之间的一种注册关系,channel()和selector()方法分别返回注册的通道与选择器。

工做原理

NIO调用不会被阻塞,在IO开始的时候须要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数:如可读数据到达,新的套接字链接等等,在发生特定事件时,系统再通知咱们。NIO中实现非阻塞I/O的核心对象就是Selector,Selector就是注册各类I/O事件地 方,并且当那些事件发生时,就是这个对象告诉咱们所发生的事件,以下图所示:

NIO工做原理
NIO工做原理

从图中能够看出,当有读或写等任何注册的事件发生时,能够从Selector中得到相应的SelectionKey,同时从 SelectionKey中能够找到发生的事件和该事件所发生的具体的SelectableChannel,以得到客户端发送过来的数据。

如何选择

NIO的优点在于单线程管理多个链接,能够在线程数较少的状况下实现快速地读取,当链接数<1000,并发程度不高并无显著的性能优点。NIO可以让您只使用一个(或几个)单线程管理多个通道(网络链接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。若是须要管理同时打开的成千上万个链接,这些链接每次只是发送少许的数据,例如聊天服务器,实现NIO的服务器多是一个优点。若是你有少许的链接使用很是高的带宽,一次发送大量的数据,也许典型的BIO服务器实现可能很是契合。

IO与装饰者模式

其实装饰者模式在IO中的运用很是普遍,先看一下什么是装饰者。
装饰者(Decorator)模式:动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。
设计原则:开闭原则(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭)。
下面用图简单描述一下:

装饰者模式图解
装饰者模式图解

下面解释一下这几个变量:

  • Component:抽象组件
  • ConcreteComponent:抽象组件的具体实现类
  • Decorator: 装饰者的抽象类,继承自Component
  • DecoratorA/B:具体的装饰实现类

经过装饰者对已有的对象进行包装,能够扩展已有类的方法跟属性,下面经过代码来讲明:

Component

public abstract class Component {
    protected String description;

    protected abstract String getDescription();

    public abstract int getAge();

}复制代码

ConcreteComponent

public class ConcreteComponent extends Component {
    public ConcreteComponent() {
        description = "ConcreteComponent";
    }

    @Override
    protected String getDescription() {
        return description;
    }

    @Override
    public int getAge() {
        return 10;
    }
}复制代码

Decorator

public abstract class Decorator extends Component {
//空实现,具体的实现放在子类
}复制代码

DecoratorA/B

public class DecoratorA extends Decorator {
    private Component mComponent;

    public DecoratorA(Component component) {
        this.mComponent = component;
    }

    @Override
    protected String getDescription() {
        return mComponent.getDescription();
    }

    @Override
    public int getAge() {
        return mComponent.getAge() + 1;
    }

    public String getTime() {
        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }

}复制代码

测试代码:

Component component = new ConcreteComponent();
        System.out.println("装饰前的参数:" + "描述:" + component.getDescription() + " 年龄:" + component.getAge());
        DecoratorA decoratorA = new DecoratorA(component);
        System.out.println("装饰后的参数:" + "描述:" + decoratorA.getDescription() + " 年龄复制代码

使用装饰者模式以后,不修改Description,将age+1,同时增长一个getTime方法来获取当前的时间

输出结果

装饰前的参数:描述:ConcreteComponent   年龄:10
装饰后的参数:描述:ConcreteComponent   年龄:11 时间:2017-11-04 15:11:03复制代码

跟咱们预期的同样,再也不赘述。下面看看IO中的设计模式,以OutpuStream为例:

OutputStream中的装饰者模式
OutputStream中的装饰者模式

对照着上面的装饰者模式图应该很容易看出来,FileterOutputStream就是咱们装饰者的抽象类,看一下他的构造方法,确实装饰了OutputStream :

public FilterOutputStream(OutputStream out) {
        this.out = out;
    }复制代码

Android中的"path"

在开发Android的过程当中,也会涉及到不少的IO操做,好比说网络请求,下载图片等,因为不少框架平时已经帮咱们封装好了,因此平时容易忽略,下面简单分析一下Android下的存储目录:

Android平台的存储目录
Android平台的存储目录

内部存储

data文件夹就是咱们常说的内部存储,对于没有root的手机来讲,咱们是没有权限打开这个文件夹的可是能够访问到,

外部存储

外部存储才是咱们平时操做最多的,外部存储通常就是咱们上面看到的storage文件夹,固然也有多是mnt文件夹,这个名称不影响咱们操做数据。

路径获取

两种存储方式都是经过Context类来进行获取的

内部存储

getFilesDir();//获取内部存储的File路径
      getCacheDir();//获取内部存储的Cache路径
      getDatabasePath("demo.db");//获取database路径
      getSharedPreferences("demo",MODE_PRIVATE);//获取SP复制代码

外部存储

getExternalCacheDir();//获取外部存储私有目录
   getExternalFilesDir(Environment.DIRECTORY_DCIM);//获取外部存储公有目录复制代码

清除缓存/清除数据

在手机的应用程序管理里面打开某个程序能够看到两个按钮,一个是清除缓存,一个是清除数据,区别以下:
清除缓存:缓存是程序运行时的临时存储空间,它能够存放从网络下载的临时图片,从用户的角度出发清除缓存对用户并无太大的影响,可是清除缓存后用户再次使用该APP时,因为本地缓存已经被清理,全部的数据须要从新从网络上获取,注意:为了在清除缓存的时候可以正常清除与应用相关的缓存,请将缓存文件存放在getCacheDir()或者 getExternalCacheDir()路径下。

清除数据:清除用户配置,好比SharedPreferences、数据库等等,这些数据都是在程序运行过程当中保存的用户配置信息,清除数据后,下次进入程序就和第一次进入程序时同样

关于权限

Android6.0之后,谷歌增强了对用户权限的控制,可是这个权限只是针对于外部存储的公有目录,对于私有目录的,仍然能够正常访问。因此当遇到有些手机权限很难适配的时候能够把文件存储在外部存储的私有目录。

参考资料

tech.meituan.com/nio.html

相关文章
相关标签/搜索