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

相關文章
相關標籤/搜索