Android FileInputStream 和FileOutputStream 分析

简介

Java中针对文件的读写操作设置了一系列的流,其中主要有FileInputStream,FileOutputStream,FileReader,FileWriter四种最为常用的流。其余流操作类的都是对这四种流的一种包装了。其实严格意义来说的话FileWrite和FileReader其实也是对FileOutputStream和FileInputStream的一种包装的,都是通过对字节流的处理、截图、转化的。所以我们也在严格的意义上来说Java针对文件读写只有FileInputStream(文件读),FileOutputStream(文件写)。

类结构图

image_1du72dui8grbvsntes1rt1p0712.png-11.5kB

代码演示

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对象的时候一些方法调用。

  1. 首先创建File对象,接着调用IoBridge的open 方法
  2. 接着调用Libcore.os.open 方法,其本质是调用 BlockGuardOs的 open方法
  3. 接着调用Posix类的 open 本地native方法。
  4. 最后调用 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的流程也是一样的。只不过一个是读取数据一个写数据的。其大致流程:

  1. 通过文件全路径找到该文件,判断文件是否具有写权限,然后调用C语言的open()方法获取文件描述符,并且创建FileDescriptor对象,然后修改descriptor的值。
  2. 写内容到文件时,首先将内容装载到内存中(一般是字节数组中),最后通过调用Linux C中的write()将字节数组中的内容写入到文件中
  3. 调用底层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++的代码。从而达到我们的需求。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值