初識 Java NIO
一、前言
也許你見過下面這樣一段程式碼。
File file = new File("file-map-sample.txt"); file.delete(); file.createNewFile(); RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw"); FileChannel fileChannel = randomAccessFile.getChannel(); MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,Integer.MAX_VALUE); System.out.println("MappedByteBuffer capacity " + mappedByteBuffer.capacity()); long currentTime = System.currentTimeMillis(); int size = Integer.MAX_VALUE / 4; for (int i = 0; i < size; i++) { mappedByteBuffer.putInt(i); } mappedByteBuffer.force(); fileChannel.close(); randomAccessFile.close(); System.out.println("MappedByteBuffer Write " + (System.currentTimeMillis() - currentTime) + " ms");
通過 Java NIO 中的檔案對映進行寫檔案。關於 NIO 大部分同學應該知道有這麼個東西,但好像又不怎麼熟悉,因為平時要用到的地方可能真的不太多吧。
二、關於 Java NIO
好吧,Java NIO 是 Java New IO。是 JDK 1.4 開始提供的一套新的可用來代替原 Java IO 的介面。然而這麼多年過去了,結果並木有。

Java NIO.jpg
這裡看到了 Java NIO 中的核心概念:Channel,Buffer 以及 selector。關於 Java NIO 的更詳細的說明,可參考
三、原理探索
不管是 NIO 還是IO,都需要 new 一個 File***Stream 或者 RandomAccessFile 從而獲取它的 FileChannel。而在這之前,我們需要弄明白一些事情。當我們 new 一個流物件時究竟發生了什麼?與之密切相關的 FileDescriptor 又是什麼?它與 Channel 之間有著怎麼樣的聯絡?
3.1 探究 FileDescriptor
這裡先看一個簡單的類圖,在心裡有一個簡單的地圖。

FileChannel.jpg
這裡為了簡單起見,以 new 一個 FileInputStream 為例。
public FileInputStream(File file) throws FileNotFoundException { ...... 154fd = new FileDescriptor(); 155 ...... 165open(name); 166 ...... 169}
去掉校驗和 BlockGuard 相關的程式碼,FileInputStream 的構造方法簡化下來還有 2 個步驟,new 一個 FileDescriptor 物件 和 open() 檔案。先來看看 FileDescriptor。
public /**/ FileDescriptor() { 62descriptor = -1; 63}
預設為 -1,這個是虛晃一槍。肯定得有地方給它真正的值。我想,應該是 open() 裡面。不過 open() 是呼叫的 native 方法 open0()。所以需要進一步看 open0() 的實現。這裡需要看到 FileInputStream 的 native 程式碼FileInputStream.c 中對於 open0 的實現。
66 FileInputStream_open0(JNIEnv *env, jobject this, jstring path) { 67fileOpen(env, this, path, fis_fd, O_RDONLY); 68}
open0() 進一步呼叫了函式 fileOpen()。注意這裡的第 4 個引數 fis_fd。它是 Java 層 fd 在 native 層的 fieldId。可以看看它的定義和初始化,就會一目瞭然了。
jfieldID fis_fd; /* id for jobject 'fd' in java.io.FileInputStream */ 60static void FileInputStream_initIDs(JNIEnv *env) { 61jclass clazz = (*env)->FindClass(env, "java/io/FileInputStream"); 62fis_fd = (*env)->GetFieldID(env, clazz, "fd", "Ljava/io/FileDescriptor;"); 63}
接著繼續看 fileOpen() 函式,它在io_util_md.c 中定義。
88void 89fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags) 90{ 91WITH_PLATFORM_STRING(env, path, ps) { 92FD fd; 93 ...... 100fd = handleOpen(ps, flags, 0666); 101if (fd != -1) { 102SET_FD(this, fd, fid); 103} else { 104throwFileNotFoundException(env, path); 105} 106} END_PLATFORM_STRING(env, ps); 107}
這裡看到了 FD 的定義,不過它只不過是一個巨集定義而已,原型就是 jint。那這個函式所做的事情就是開啟檔案獲得 fd,然後通過巨集定義 SET_FD 賦值給 Java 層的 fd 物件中的 descriptor。對,這是個結論,我們來看看具體的實現過程。先看 handleOpen()。
65 FD 66 handleOpen(const char *path, int oflag, int mode) { 67FD fd; 68RESTARTABLE(open64(path, oflag, mode), fd); ...... 84return fd; 85}
open64() 是一個巨集定義,指向 open() 函式。RESTARTABLE 也是一個巨集定義,其就是將前面的引數結果賦值給後面的引數。那麼,這裡就是將 open() 函式的返回結果檔案描述符 FD 賦值給 fd。
通過上述 handleOpen() 就打開了檔案,並且返回了檔案的描述符,而如果檔案描述符為 -1 的話那就會丟擲著名的 exception —— FileNotFoundException。然後再來看看
49#define SET_FD(this, fd, fid) \ 50if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \ 51(*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))
這裡的 (*env)->GetObjectField(env, (this), (fid)) 就是獲取 FileInputStream 的 fd 屬性,而 IO_fd_fdID 就是其屬性的屬性 descriptor,程式碼如下。
IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
至此,就分析完了檔案的開啟與檔案描述符 FD 了。當我們 new 一個 FileInputStream 的時候,其實底層是呼叫了函式 open(),並且返回了一個檔案描述符 fd,而後對檔案的所有操作其實都是作用在這個 fd 之上的。
3.2 探究 FileChannel
在 new 完 FileInputStream 後,可以通過其 getChannel() 方法獲得一個 FileChannel 物件。從上面的類圖中可知,FileChannel 是一個抽象類,真正的實現類在 FileChannelImpl。FileChannelImpl 中有 2 個核心屬性分別是 fd 和 nd。fd 好理解,就是 FileDescriptor。而 nd 是 FileDispatcherImpl,字面意思 “檔案分發”?還是一起來看看吧。再回到 FileInputStream.getChannel() 看看是如何獲得 FileChannel 的。
456public FileChannel getChannel() { 457synchronized (this) { 458if (channel == null) { 459channel = FileChannelImpl.open(fd, path, true, false, this); 460} 461return channel; 462} 463}
FileChannelImpl 的建構函式是私有的,只能通過其靜態方法 open() 來構造,而這裡傳入的引數依次是檔案描述符 fd,路徑,可讀,可寫(inputstream 不可寫),FileInputStream。在 open() 方法中,就是直接 new 一個 FileChannelImple 物件。那來看看它的構造方法。
98private FileChannelImpl(FileDescriptor fd, String path, boolean readable, 99boolean writable, boolean append, Object parent) 100{ 101this.fd = fd; 102this.readable = readable; 103this.writable = writable; 104this.append = append; 105this.parent = parent; 106this.path = path; 107this.nd = new FileDispatcherImpl(append); 112}
前面幾個屬性都是基本的賦值操作,主要需要進一步分析 FileDispatcherImpl。
43FileDispatcherImpl(boolean append) { 44/* append is ignored */ 45}
呃,什麼都沒有,......
看到這裡,就有點懵了,還是沒明白 FileChannel 是個什麼東西。不過還是可以總結下就是,其有兩個核心的屬性 fd 和 nd,看起來 FileChannel 對 Buffer 的讀寫操作應該是通過 nd 來實現的,nd 操作的也必將是fd 。
前面有說過 Channel 是 NIO 的核心之一,那除了 FileChannel,還有......看看類圖吧。

Channel.jpg
3.3 探究 Buffer
先來看一看 Buffer 的類圖結構。

Buffer.jpg
Buffer 確實就是緩衝區,上圖中,頂級父類 Buffer 下可以看成左邊 ByteBuffer 和右邊其他型別的 Buffer。其實只存在 ByteBuffer,其他型別 Buffer 都是為了方便操作而言的。而 ByteBuffer 從記憶體的角度來看又分為 HeapByteBuffer 和 DirectedByteBuffer,詳細如下圖。

ByteBuffer.jpg
這裡可能需要注意一下的是,在 Android 中和在 Java 中,它們的實現是有差異的。另外,如果之前有熟悉的 okio 的同學,看到這裡應該更加不會陌生。當然,你現在也可以去看一看,okio 也是充分運用了緩衝來讀寫資料,以提高IO效能的。Okio深入分析—原始碼分析部分
3.4 從 Channel 讀資料到 Buffer
-
Buffer 的初始化
這裡假設我們是直接從 Java 堆記憶體分配 Buffer 的空間,也就是我們是通過 ByteBuffer.allocate(1024) 初始化的 Buffer。這裡還是看一看程式碼,有個印象。
278public static ByteBuffer allocate(int capacity) { 279if (capacity < 0) 280throw new IllegalArgumentException(); 281return new HeapByteBuffer(capacity, capacity); 282} 53private HeapByteBuffer(int cap, int lim, boolean isReadOnly) { 54super(-1, 0, lim, cap, new byte[cap], 0); 55this.isReadOnly = isReadOnly; 56}
初始化完成後,狀態如下。

Buffer 初始化.jpg
- 讀取 512 個位元組到 Buffer
fileChannel.read(byteBuffer);
將資料寫入到了 Buffer 後,Buffer 的狀態如下。

Buffer 寫入512.jpg
- read() 方法的實現
181public int read(ByteBuffer dst) throws IOException { 182ensureOpen(); 183if (!readable) 184throw new NonReadableChannelException(); 185synchronized (positionLock) { 186int n = 0; 187int ti = -1; 188try { 189begin(); 190ti = threads.add(); 191if (!isOpen()) 192return 0; 193do { 194n = IOUtil.read(fd, dst, -1, nd); 195} while ((n == IOStatus.INTERRUPTED) && isOpen()); 196return IOStatus.normalize(n); 197} finally { 198threads.remove(ti); 199end(n > 0); 200assert IOStatus.check(n); 201} 202} 203}
下圖是這段程式碼主要做的事情。

Read Buffer.jpg
四、總結
關於 Java NIO 的探索就先到這裡了,其本身的實現還是較為複雜的。尤其是對於非阻塞的實現,功力實在尚淺暫時沒有分析的很清楚。
最後,感謝你能讀到此文章。如果我的分享對你有幫忙,還請幫忙點個贊。謝謝。