简介
Java中针对文件的读写操作设置了一系列的流,其中主要有FileInputStream,FileOutputStream,FileReader,FileWriter四种最为常用的流。其余流操作类的都是对这四种流的一种包装了。其实严格意义来说的话FileWrite和FileReader其实也是对FileOutputStream和FileInputStream的一种包装的,都是通过对字节流的处理、截图、转化的。所以我们也在严格的意义上来说Java针对文件读写只有FileInputStream(文件读),FileOutputStream(文件写)。
类结构图
代码演示
public void readFile() {
FileInputStream fis = null;
try {
//首先我们就创建一个文件输入流,构造函数需要传入文件的名称
fis = new FileInputStream("/sdcard/test.txt");
//定义一个字节数组,然后
byte buffer[] = new byte[2048];
//然后将内容读取放到字节数组中,并且返回读取的内容长度
int length = fis.read(buffer);
String data = new String(buffer, 0, length, "utf-8");
} catch (IOException e) {
e.printStackTrace();
} finally {
//这里需要处理异常
fis.close();
}
}
- 文件描述符的创建
上面是我们写了几十遍的一个文件读取内容的代码。没有什么需要介绍的,非常简单,下面我就非常的好奇Java中的文件读取究竟是怎么样的。
//文件描述符
private FileDescriptor fd;
//如果我们传入文件的绝对路径的话,最后还是会生成File对象,如果文件不存在则会抛出异常
public FileInputStream(String path) throws FileNotFoundException {
this(new File(path));
}
public FileInputStream(File file) throws FileNotFoundException {
//首先这里我们进行空指针判断。
if (file == null) {
throw new NullPointerException("file == null");
}
//这里有一个非常重要的概念就是[文件描述符][2],具体就不在这里细说了。
this.fd = IoBridge.open(file.getPath(), O_RDONLY);
}
接着调用 IoBridge 类的 open 方法
//flag 为 O_RDONLY 表示只对文件进行读取操作
public static FileDescriptor open(String path, int flags) throws FileNotFoundException {
FileDescriptor fd = null;
try {
//通过 & 运算来获取 mode。
int mode = ((flags & O_ACCMODE) == O_RDONLY) ? 0 : 0600;
//接下来调用Libcore中的方法.
fd = Libcore.os.open(path, flags, mode);
// Java disallows reading directories too.
//判断 java 是否允许用户读取文件内容
if (S_ISDIR(Libcore.os.fstat(fd).st_mode)) {
throw new ErrnoException("open", EISDIR);
}
return fd;
} catch (ErrnoException exception) {
......
FileNotFoundException ex = new FileNotFoundException(path + ": " + exception.getMessage());
ex.initCause(errnoException);
throw ex;
}
}
通过代码我们发现最后调用的还是 BlockGuardOs的代码。
public final class Libcore {
private Libcore() { }
public static Os os = new BlockGuardOs(new Posix());
}
}
在创建BlockGuardOs 对象的时候我们又创建了一个Posix对象,最后我们发现调用的是 Posix 类下的 open方法。
public FileDescriptor open(String path, int flags, int mode) throws ErrnoException {
BlockGuard.getThreadPolicy().onReadFromDisk();
if ((mode & O_ACCMODE) != O_RDONLY) {
BlockGuard.getThreadPolicy().onWriteToDisk();
}
return os.open(path, flags, mode);
}
Posix 类下面的open方法是一个native方法,那我们就需要去找一下对应的C或者C++文件的(libcore_io.Posix.cpp)。
//open这个方法最后结果就是需要创建一个文件描述符然后返回给java调用者。
public native FileDescriptor open(String path, int flags, int mode) throws ErrnoException;
下面就是Jni的代码,如果对Jni方面又不熟悉可以继续在看看以前的代码。
static jobject Posix_open(JNIEnv* env, jobject, jstring javaPath, jint flags, jint mode) {
//这里首先会创建一个类型为ScopedUtfChars的变量 path。主要目的是为了将java中的string转换为C中可用的字符数组
ScopedUtfChars path(env, javaPath);
//判断转换后的字符是否为 null
if (path.c_str() == NULL) {
return NULL;
}
//通过这里我们就知道原来调用的 还是 Linux C中的 open方法来打开文件,如果打开文件成功的话,则会返回一个 != -1 的整数,在 C语言中表示文件操作符,但是这个文件描述并不是 java中的。
//
int fd = throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode)));
return fd != -1 ? jniCreateFileDescriptor(env, fd) : NULL;
}
- 首先我们需要将java中的String(字符串)进行转换为C中字符数组,因为C中没
class ScopedUtfChars {
private:
const char* utf_chars_;
public:
ScopedUtfChars(JNIEnv* env, jstring s) : env_(env), string_(s) {
if (s == NULL) {
utf_chars_ = NULL;
jniThrowNullPointerException(env, NULL);
} else {
utf_chars_ = env->GetStringUTFChars(s, NULL);
}
}
const char* c_str() const {
return utf_chars_;
}
}
- 通过调用Linux C中的 open方法来打开文件,并且得到文件描述符(整数型的)
//通过之前的文章我们可以分析得出 flags就是 O_RDONLY ,mode 是 0。如果 open方法返回 -1,也就是打开文件失败的话,那么会直接抛出异常了。
int fd = throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode)));
template <typename rc_t>
static rc_t throwIfMinusOne(JNIEnv* env, const char* name, rc_t rc) {
if (rc == rc_t(-1)) {
throwErrnoException(env, name);
}
return rc;
}
- 最后我们通过 C 的 fd,然后创建一个 java的 FileDescriptor对象,并且赋值给FileInputStream对象中的成员变量 fd。
//该方法定义在 JNIHelp.h中,其实现是在 JNIHelp.cpp中
jobject jniCreateFileDescriptor(C_JNIEnv* env, int fd) {
JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
//首先获取 FileDescriptor构造方法的 methodId。
static jmethodID ctor = e->GetMethodID(JniConstants::fileDescriptorClass, "<init>", "()V");
//然后通过 methodId,创建一个 FileDescriptor对象。
jobject fileDescriptor = (*env)->NewObject(e, JniConstants::fileDescriptorClass, ctor);
// NOTE: NewObject ensures that an OutOfMemoryError will be seen by the Java
// caller if the alloc fails, so we just return NULL when that happens.
if (fileDescriptor != NULL) {
//接着这里我们将给 FileDescriptor对象的 descriptor 属性赋值(也就是我们刚刚通过C open一个文件成功后返回的文件描述符)
jniSetFileDescriptorOfFD(env, fileDescriptor, fd);
}
//最后将新创建的文件描述符对象返回。
return fileDescriptor;
}
void jniSetFileDescriptorOfFD(C_JNIEnv* env, jobject fileDescriptor, int value) {
JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
//首先获取FileDescriptor类中属性为 descriptor字段编号
static jfieldID fid = e->GetFieldID(JniConstants::fileDescriptorClass, "descriptor", "I");
//然后通过 filedId 给该对象的属性的进行赋值
(*env)->SetIntField(e, fileDescriptor, fid, value);
}
通过上面的代码我们可以得出在FileInputStream对象的时候一些方法调用。
- 首先创建File对象,接着调用IoBridge的open 方法
- 接着调用Libcore.os.open 方法,其本质是调用 BlockGuardOs的 open方法
- 接着调用Posix类的 open 本地native方法。
- 最后调用 Linux C中的 open()用于打开文件。
- 文件读取
通过上面的代码我们清楚的知道了FileInputStream在创建的时候的时候,同时会去调用底层操作系统打开文件并且返回文件操作符。接下来我们分析读取数据的操作。
public void readFile() {
byte buffer[] = new byte[2048];
//调用的是InputStream的 read(byte[])方法,
int length = fis.read(buffer);
}
public class InputStream {
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
//这里需要注意了,InputStream读取文件内容的时候是调用了 FileInputStream read()读取单个字符到字节数组中,这种效率其实是非常低下的。然而FileInputStream重写了该方法。
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
Arrays.checkOffsetAndCount(buffer.length, byteOffset, byteCount);
for (int i = 0; i < byteCount; ++i) {
int c;
try {
if ((c = read()) == -1) {
return i == 0 ? -1 : i;
}
} catch (IOException e) {
if (i != 0) {
return i;
}
throw e;
}
buffer[byteOffset + i] = (byte) c;
}
return byteCount;
}
}
FileInputStream读取内容到字节数组中
public class FileInputStream {
//所以 FileInputStream read(byte[])最后还是调用了它自己本身的该方法来实现的。
@Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
return IoBridge.read(fd, buffer, byteOffset, byteCount);
}
}
public static int read(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) {
//首先检查参数是否越界
Arrays.checkOffsetAndCount(bytes.length, byteOffset, byteCount);
if (byteCount == 0) {
return 0;
}
try {
//通过上面的代码我们知道调用了libcore_io_Posix.cpp里面的代码。
int readCount = Libcore.os.read(fd, bytes, byteOffset, byteCount);
if (readCount == 0) {
return -1;
}
return readCount;
} catch (ErrnoException errnoException) {
.......
}
}
通过上面的代码直接跟踪 libcore_io.Posix.cpp代码。
static jint Posix_readBytes(JNIEnv* env, jobject, jobject javaFd, jobject javaBytes,
jint byteOffset, jint byteCount) {
ScopedBytesRW bytes(env, javaBytes);
if (bytes.get() == NULL) {
return -1;
}
//这段代码的本质就是调用了 Linux C 中的 read()方法读取文件中的内容到字节数组中。具体请参考Linux C对文件读写操作就明白了。
//通过read函数将字符串读取到内存中(字节数组byte[]中),同时返回读取内容的长度。如果是-1表示已经到尽头了。
return IO_FAILURE_RETRY(env, ssize_t, read, javaFd, bytes.get() + byteOffset, byteCount);
}
通过上面的代码我们可以得出Java层面的代码 文件读取内容最后其实调用的还是 C语言的方法去实现的。只是Java平台帮我们屏蔽了各种操作系统底层的一些差异化,只是暴漏了一个接口给我们调用就行了,这个才是Java最关键核心的地方,至于那些“脏乱”的活都交给写虚拟机的那些大神们完成了。
总结
FileInputStream文件读写流程:
1. 首先创建一个FileInputStream,然后调用C语言的 open()方法获取一个文件描述符(C语言中为int行),
2. 创建一个java层的 FileDescriptor对象,并且修改对象中的descriptor值为open的返回值
3. 将 FileDescriptor和字节数组传入到 libcore_io_Posix.cpp中的 Posix_readBytes方法中
4. 获取descriptor 内容值,同时调用 read() 方法将内容读取到内存中(字节数组),然后返回读取内容的长度,如果返回-1表示读取完成。
5. 最后关闭文件流,其实也是调用 C语言中的 close 方法的。这就不在进行分析了。
###FileOutputStream 分析
其实FileOutputStream的流程跟FileInputStream的流程也是一样的。只不过一个是读取数据一个写数据的。其大致流程:
- 通过文件全路径找到该文件,判断文件是否具有写权限,然后调用C语言的open()方法获取文件描述符,并且创建FileDescriptor对象,然后修改descriptor的值。
- 写内容到文件时,首先将内容装载到内存中(一般是字节数组中),最后通过调用Linux C中的write()将字节数组中的内容写入到文件中
- 调用底层C语言的关闭流(close方法)关闭文件
代码分析
创建FileOutputStream
public FileOutputStream(File file, boolean append) throws FileNotFoundException {
if (file == null) {
throw new NullPointerException("file == null");
}
//append true 表示可以追加内容,false 表示不能追加内容
this.mode = O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC);
//然后调用 libcore_io_Posix.cpp中的 open(native方法)方法获取文件描述,这里就不详细描述了
//如果打开文件则抛出异常
this.fd = IoBridge.open(file.getPath(), mode);
this.shouldClose = true;
this.guard.open("close");
}
通过调用 IoBridge.java 类 写文件
public static void write(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) {
//首先检查 byteOffset和byteCount是否在数组中越界。
Arrays.checkOffsetAndCount(bytes.length, byteOffset, byteCount);
if (byteCount == 0) {
return;
}
try {
//这里有一个循环不断的写入数据的操作
while (byteCount > 0) {
//实际写入数据的长度,比如说我们定义的byte[]大于系统的缓冲写入区的时候,这个时候需要分多次写入的。
int bytesWritten = Libcore.os.write(fd, bytes, byteOffset, byteCount);
//同时之前剩下的总数 - 实际写入的总数
byteCount -= bytesWritten;
//同时位置偏移 + 实际写入的大小
byteOffset += bytesWritten;
}
} catch (ErrnoException errnoException) {
throw errnoException.rethrowAsIOException();
}
}
通过层层的代码追踪,最后调用的是libcore_io_Posix.cpp中的 Posix_writeBytes 方法
static jint Posix_writeBytes(JNIEnv* env, jobject, jobject javaFd, jbyteArray javaBytes, jint byteOffset, jint byteCount) {
//首先将 javaBytes封装到 ScopedBytesRO中,主要作用是为了将java中的字符串转为C中字符数组。
ScopedBytesRO bytes(env, javaBytes);
if (bytes.get() == NULL) {
return -1;
}
//这里最终调用Linux C中的 write() 方法将 bytes 数据写入到文件中,并且返回实际写入数据的长度
return IO_FAILURE_RETRY(env, ssize_t, write, javaFd, bytes.get() + byteOffset, byteCount);
}
当我们执行完写文件操作以后,我们还需要调用关闭文件的操作。
public void close() throws IOException {
guard.close();
synchronized (this) {
if (channel != null) {
channel.close();
}
//当我们打开一个文件并且获取文件描述符以后,需要关闭文件描述符
if (shouldClose) {
IoBridge.closeAndSignalBlockedThreads(fd);
} else {
// An owned fd has been invalidated by IoUtils.close, but
// we need to explicitly stop using an unowned fd (http://b/4361076).
fd = new FileDescriptor();
}
}
}
调用 IoBridge 类方法进行关闭
public static void closeAndSignalBlockedThreads(FileDescriptor fd) throws IOException {
if (fd == null || !fd.valid()) {
return;
}
//首先将我们传入进入的 FileDescriptor的 descriptor属性置为 -1,避免其他的函数或者是线程继续调用写入方法
int intFd = fd.getInt$();
fd.setInt$(-1);
//然后重新创建一个 FileDescriptor对象,并且设置descriptor的内容为以前内容。
FileDescriptor oldFd = new FileDescriptor();
oldFd.setInt$(intFd);
AsynchronousCloseMonitor.signalBlockedThreads(oldFd);
try {
//最后调用libcore_io_Posix.cpp文件的 Posix_close 方法
Libcore.os.close(oldFd);
} catch (ErrnoException errnoException) {
// TODO: are there any cases in which we should throw?
}
}
libcore_io_Posix.cpp
static void Posix_close(JNIEnv* env, jobject, jobject javaFd) {
//首先获取 FileDescriptor对象的 descriptor属性的值。
int fd = jniGetFDFromFileDescriptor(env, javaFd);
//通知将 FileDescriptor对象的 descriptor属性的值置为 -1
jniSetFileDescriptorOfFD(env, javaFd, -1);
//最后调用 Linux C语言中的 close 方法关闭文件操作符。
throwIfMinusOne(env, "close", close(fd));
}
总结
上面就是FileInputStream和FileOutputStream文件读写的相关代码分析,从代码中我们可以总结出不管Java如何的封装最后都是调用 Linux C语言的的代码去实现,比如Java的话就是通过Jni去调用C语言实现的。如果是python活着PHP对文件读写的话,其实最后还是调用 C或者是C++去实现的,而 C则是去调用操作系统的API去实现文件读写操作。在往下的事情就是操作系统调用磁盘的操作,具体我们也不再深入的研究和分析,目前只在这个方面打住了。这个时候我们才发现又回到了大学中所学的 C 语言来了,以前我们厌恶的C 语言又回到我们的视野中来了?Java 其实是帮我们屏蔽了各个操作系统或者是硬件中差异化,然后封装了更加适合于我们使用的语言,那些又脏又累的活都是那些Java语言大神帮我做了,这里也可以总结的出来 Java的运行速度肯定要比 C 语言中的速度慢,有时候项目中有一些代码对运行速度有要求的,我们可以通过 JNI 来调用 C或者C++的代码。从而达到我们的需求。