也许你见过下面这样一段代码。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 的探索就先到这里了,其自己的实现仍是较为复杂的。尤为是对于非阻塞的实现,功力实在尚浅暂时没有分析的很清楚。
最后,感谢你能读到此文章。若是个人分享对你有帮忙,还请帮忙点个赞。谢谢。