也許你見過下面這樣一段代碼。html
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
好吧,Java NIO 是 Java New IO。是 JDK 1.4 開始提供的一套新的可用來代替原 Java IO 的接口。然而這麼多年過去了,結果並木有。android
這裏看到了 Java NIO 中的核心概念:Channel,Buffer 以及 selector。關於 Java NIO 的更詳細的說明,可參考c#
不論是 NIO 仍是 IO,都須要 new 一個 File***Stream 或者 RandomAccessFile 從而獲取它的 FileChannel。而在這以前,咱們須要弄明白一些事情。當咱們 new 一個流對象時究竟發生了什麼?與之密切相關的 FileDescriptor 又是什麼?它與 Channel 之間有着怎麼樣的聯繫?bash
這裏先看一個簡單的類圖,在內心有一個簡單的地圖。 app
這裏爲了簡單起見,以 new 一個 FileInputStream 爲例。dom
public FileInputStream(File file) throws FileNotFoundException {
......
154 fd = new FileDescriptor();
155
......
165 open(name);
166
......
169 }
複製代碼
去掉校驗和 BlockGuard 相關的代碼,FileInputStream 的構造方法簡化下來還有 2 個步驟,new 一個 FileDescriptor 對象 和 open() 文件。先來看看 FileDescriptor。函數
public /**/ FileDescriptor() {
62 descriptor = -1;
63 }
複製代碼
默認爲 -1,這個是虛晃一槍。確定得有地方給它真正的值。我想,應該是 open() 裏面。不過 open() 是調用的 native 方法 open0()。因此須要進一步看 open0() 的實現。這裏須要看到 FileInputStream 的 native 代碼 FileInputStream.c 中對於 open0 的實現。源碼分析
66 FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
67 fileOpen(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) {
61 jclass clazz = (*env)->FindClass(env, "java/io/FileInputStream");
62 fis_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{
91 WITH_PLATFORM_STRING(env, path, ps) {
92 FD fd;
93
......
100 fd = handleOpen(ps, flags, 0666);
101 if (fd != -1) {
102 SET_FD(this, fd, fid);
103 } else {
104 throwFileNotFoundException(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) {
67 FD fd;
68 RESTARTABLE(open64(path, oflag, mode), fd);
......
84 return fd;
85}
複製代碼
open64() 是一個宏定義,指向 open() 函數。RESTARTABLE 也是一個宏定義,其就是將前面的參數結果賦值給後面的參數。那麼,這裏就是將 open() 函數的返回結果文件描述符 FD 賦值給 fd。
經過上述 handleOpen() 就打開了文件,而且返回了文件的描述符,而若是文件描述符爲 -1 的話那就會拋出著名的 exception —— FileNotFoundException。而後再來看看
49#define SET_FD(this, fd, fid) \
50 if ((*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 之上的。
在 new 完 FileInputStream 後,能夠經過其 getChannel() 方法得到一個 FileChannel 對象。從上面的類圖中可知,FileChannel 是一個抽象類,真正的實現類在 FileChannelImpl。FileChannelImpl 中有 2 個核心屬性分別是 fd 和 nd。fd 好理解,就是 FileDescriptor。而 nd 是 FileDispatcherImpl,字面意思 「文件分發」?仍是一塊兒來看看吧。再回到 FileInputStream.getChannel() 看看是如何得到 FileChannel 的。
456 public FileChannel getChannel() {
457 synchronized (this) {
458 if (channel == null) {
459 channel = FileChannelImpl.open(fd, path, true, false, this);
460 }
461 return channel;
462 }
463 }
複製代碼
FileChannelImpl 的構造函數是私有的,只能經過其靜態方法 open() 來構造,而這裏傳入的參數依次是文件描述符 fd,路徑,可讀,可寫(inputstream 不可寫),FileInputStream。在 open() 方法中,就是直接 new 一個 FileChannelImple 對象。那來看看它的構造方法。
98 private FileChannelImpl(FileDescriptor fd, String path, boolean readable,
99 boolean writable, boolean append, Object parent)
100 {
101 this.fd = fd;
102 this.readable = readable;
103 this.writable = writable;
104 this.append = append;
105 this.parent = parent;
106 this.path = path;
107 this.nd = new FileDispatcherImpl(append);
112 }
複製代碼
前面幾個屬性都是基本的賦值操做,主要須要進一步分析 FileDispatcherImpl。
43 FileDispatcherImpl(boolean append) {
44 /* append is ignored */
45 }
複製代碼
呃,什麼都沒有,......
看到這裏,就有點懵了,仍是沒明白 FileChannel 是個什麼東西。不過仍是能夠總結下就是,其有兩個核心的屬性 fd 和 nd,看起來 FileChannel 對 Buffer 的讀寫操做應該是經過 nd 來實現的,nd 操做的也必將是 fd 。
前面有說過 Channel 是 NIO 的核心之一,那除了 FileChannel,還有......看看類圖吧。
先來看一看 Buffer 的類圖結構。
Buffer 確實就是緩衝區,上圖中,頂級父類 Buffer 下能夠當作左邊 ByteBuffer 和右邊其餘類型的 Buffer。其實只存在 ByteBuffer,其餘類型 Buffer 都是爲了方便操做而言的。而 ByteBuffer 從內存的角度來看又分爲 HeapByteBuffer 和 DirectedByteBuffer,詳細以下圖。
這裏可能須要注意一下的是,在 Android 中和在 Java 中,它們的實現是有差別的。另外,若是以前有熟悉的 okio 的同窗,看到這裏應該更加不會陌生。固然,你如今也能夠去看一看,okio 也是充分運用了緩衝來讀寫數據,以提升IO性能的。Okio深刻分析—源碼分析部分
278 public static ByteBuffer allocate(int capacity) {
279 if (capacity < 0)
280 throw new IllegalArgumentException();
281 return new HeapByteBuffer(capacity, capacity);
282 }
53 private HeapByteBuffer(int cap, int lim, boolean isReadOnly) {
54 super(-1, 0, lim, cap, new byte[cap], 0);
55 this.isReadOnly = isReadOnly;
56 }
複製代碼
初始化完成後,狀態以下。
fileChannel.read(byteBuffer);
複製代碼
將數據寫入到了 Buffer 後,Buffer 的狀態以下。
181 public int read(ByteBuffer dst) throws IOException {
182 ensureOpen();
183 if (!readable)
184 throw new NonReadableChannelException();
185 synchronized (positionLock) {
186 int n = 0;
187 int ti = -1;
188 try {
189 begin();
190 ti = threads.add();
191 if (!isOpen())
192 return 0;
193 do {
194 n = IOUtil.read(fd, dst, -1, nd);
195 } while ((n == IOStatus.INTERRUPTED) && isOpen());
196 return IOStatus.normalize(n);
197 } finally {
198 threads.remove(ti);
199 end(n > 0);
200 assert IOStatus.check(n);
201 }
202 }
203 }
複製代碼
下圖是這段代碼主要作的事情。
關於 Java NIO 的探索就先到這裏了,其自己的實現仍是較爲複雜的。尤爲是對於非阻塞的實現,功力實在尚淺暫時沒有分析的很清楚。
最後,感謝你能讀到此文章。若是個人分享對你有幫忙,還請幫忙點個贊。謝謝。