JNI探秘-----你不知道的FileInputStream的秘密


                 作者:zuoxiaolong8810(左瀟龍),轉載請注明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特復制到此留存,如需轉載請注明新博客地址即可。

                 設計模式系列結束,迎來了LZ第一篇關於JAVA虛擬機的文章,這一系列文章不再像之前的設計模式一樣,有着嚴格的約束力,本系列文章相對會比較隨性,本次LZ就跟各位分享一個關於FileInputStream的小秘密。

                 在探究這個秘密之前,各位如果沒有openjdk的源碼,可以去LZ的資源先下載下來,鏈接是:JVM源碼 和 JDK源碼

                 由於資源有最大60MB的限制,所以LZ分成了兩部分,一個是JVM的源碼,一個是JDK中的源碼,而本地方法的源碼都在JDK的那個壓縮包當中,全部源碼下載在openjdk的官網上也有,各位也可以去那里找一下,如果嫌麻煩的話,就去LZ的資源里下載即可。

                 現在源碼我們已經有了,可以來看下我們研究的小秘密了。大家都知道我們在讀取文件時離不開FileInputStream這個類,那么不知道各位有沒有好奇過,我們的FileInputStream是如何建立的呢?

                 我們一起先來看看FileInputStream的源碼,我們平時都是通過new FileInputStream(name or File)的方式得到的文件輸入流,所以我們來看FileInputStream的構造方法。

public
class FileInputStream extends InputStream
{
    /* File Descriptor - handle to the open file */
    private FileDescriptor fd;

    private FileChannel channel = null;

    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }
    //這個方法是我們創建文件輸入流時的方式
    public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
        if (name == null) {
            throw new NullPointerException();
        }
    fd = new FileDescriptor();
    open(name);
    }

            我們忽略安全管理器的檢查,可以看到,在創建一個文件輸入流時,主要做了兩件事,一個是new一個FileDescriptor(文件描述符),一個便是調用了open方法

            不過在此之前,其實還調用了一個方法,在FileInputStream源碼的下方,有這樣一個靜態塊。

static {
       initIDs();
    }  

            它將在第一次加載FileInputStream類的時候,調用一個靜態的initIDs的本地方法,這里我們不跟蹤這個方法的源碼,它並不是我們的重點,它的作用是設置類中(也就是FileInputStream)的屬性的地址偏移量,便於在必要時操作內存給它賦值,而FileInputStream的initIDs方法只設置了fd這一個屬性的地址偏移量。

            接下來,我們首先看下FileDescriptor這個類是什么樣子的,它的源碼如下。

package java.io;

public final class FileDescriptor {
 
    private int fd;

    private long handle;

    /**
     * Constructs an (invalid) FileDescriptor
     * object.
     */
    public /**/ FileDescriptor() {
    fd = -1;
        handle = -1;
    }

    private /* */ FileDescriptor(int fd) {
    this.fd = fd;
        handle = -1;
    }

    static {
        initIDs();
    }

    /**
     * A handle to the standard input stream. Usually, this file
     * descriptor is not used directly, but rather via the input stream
     * known as <code>System.in</code>.
     *
     * @see     java.lang.System#in
     */
    public static final FileDescriptor in = standardStream(0);

    /**
     * A handle to the standard output stream. Usually, this file
     * descriptor is not used directly, but rather via the output stream
     * known as <code>System.out</code>.
     * @see     java.lang.System#out
     */
    public static final FileDescriptor out = standardStream(1);

    /**
     * A handle to the standard error stream. Usually, this file
     * descriptor is not used directly, but rather via the output stream
     * known as <code>System.err</code>.
     *
     * @see     java.lang.System#err
     */
    public static final FileDescriptor err = standardStream(2);

    /**
     * Tests if this file descriptor object is valid.
     *
     * @return  <code>true</code> if the file descriptor object represents a
     *          valid, open file, socket, or other active I/O connection;
     *          <code>false</code> otherwise.
     */
    public boolean valid() {
    return ((handle != -1) || (fd != -1));
    }

    public native void sync() throws SyncFailedException;

    /* This routine initializes JNI field offsets for the class */
    private static native void initIDs();

    private static native long set(int d);

    private static FileDescriptor standardStream(int fd) {
        FileDescriptor desc = new FileDescriptor();
        desc.handle = set(fd);
        return desc;
    }

}

           可以看到,這里面也有initIDs的靜態塊,它與FileInputStream中的靜態塊的作用類似,只不過這里設置了兩個屬性(fd和handle)的地址偏移量。

           如果拋開這兩個靜態塊不說,其實到現在只是做了很簡單的一件事,就是new了一個FileDescriptor對象,而最關鍵的地方其實都在FileInputStream的構造方法中一個名叫open(name)的這個本地方法當中,這個我們接下來再去看。

           我們先看下FileDescriptor這個類,這個類有幾個屬性,一個是int類型的fd,目前沒發現它有什么作用,唯一與它相關的構造方法還是私有的,而且在類中也沒有調用,不過它與本次的分析並無關系,可先忽略。一個是long類型的handle(句柄),而handle這個屬性就是最重要的屬性了,它是一個文件的句柄,我們讀取文件全靠它了,剩下的就是三個靜態的標准流的FileDescriptor對象。

           接下來我們就來看看open(name)這個方法到底做了什么,猜一下其實也大致知道,它一定是打開了一個文件,然后把得到的文件句柄賦給了handle屬性,而賦值的時候,就要依賴於剛才initIDs所初始化的地址偏移量

           下面我們就要看下open這個方法的源碼了,不過本地方法以及JVM使用的編程語言是C/C++,所以研究JVM源碼時,會給只懂JAVA的猿友們造成一定阻礙。不過不懂C++的猿友也不要失望,LZ會詳細標注上每一句話的含義,只要是熟悉JAVA的猿友,基本上是能看懂的。

           以下便是open這個方法的源碼,FileInputStream.c中的一段代碼。

JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open(JNIEnv *env, jobject this, jstring path) {
    fileOpen(env, this, path, fis_fd, O_RDONLY);
}

           這個方法沒什么難度,它只是單純的調用了一個叫fileOpen的方法,而這個方法是與具體的操作系統相關的,這也是為什么這里沒有直接寫實現的原因,我們隨便找一個操作系統的實現來做例子,我們看一下windows當中的fileOpen的方法實現,以下是io_util_md.c文件的一段代碼。

/*
    env是一個指向JAVA本地方法環境的指針,它的作用大部分用來獲取環境參數,比如當前線程。
    this相信大家都不陌生,這就是指的當前FileInputStream的實例,只不過在C/C++環境中,它是jobject類型
    path就是文件路徑了,也是我們傳進來的name參數
    fid是FileInputStream類中fd屬性的地址偏移量
    flags是打開文件的方式,一般就是只讀方式。
*/
void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    jlong h = winFileHandleOpen(env, path, flags);//這一句話就得到了一個文件的句柄
    if (h >= 0) {
        SET_FD(this, h, fid);//這一句話就是將這個句柄賦給了FileDescriptor類的handle屬性
    }
}

           LZ已經加了詳細的注釋,那么關鍵點還有兩個,一個是winFileHandleOpen方法里做了什么,一個是SET_FD這個宏定義做了什么。雖然LZ已經解釋了它們各自都做了什么,但是不看源碼是不是始終不爽呢?

           接下來我們先來看下winFileHandleOpen方法,這個方法就在fileOpen的上面。

/*
    path是文件路徑,flags代表的是只讀
*/
jlong
winFileHandleOpen(JNIEnv *env, jstring path, int flags)
{
    const DWORD access =
        (flags & O_WRONLY) ?  GENERIC_WRITE :
        (flags & O_RDWR)   ? (GENERIC_READ | GENERIC_WRITE) :
        GENERIC_READ;//訪問權限
    const DWORD sharing =
        FILE_SHARE_READ | FILE_SHARE_WRITE;//是否共享訪問
    const DWORD disposition =
        /* Note: O_TRUNC overrides O_CREAT */
        (flags & O_TRUNC) ? CREATE_ALWAYS :
        (flags & O_CREAT) ? OPEN_ALWAYS   :
        OPEN_EXISTING;
    const DWORD  maybeWriteThrough =
        (flags & (O_SYNC | O_DSYNC)) ?
        FILE_FLAG_WRITE_THROUGH :
        FILE_ATTRIBUTE_NORMAL;
    const DWORD maybeDeleteOnClose =
        (flags & O_TEMPORARY) ?
        FILE_FLAG_DELETE_ON_CLOSE :
        FILE_ATTRIBUTE_NORMAL;
    const DWORD flagsAndAttributes = maybeWriteThrough | maybeDeleteOnClose;//
    HANDLE h = NULL;//定義一個句柄

    if (onNT) {//如果是NT系統
        WCHAR *pathbuf = pathToNTPath(env, path, JNI_TRUE);//轉成NT系統下的路徑
        if (pathbuf == NULL) {//等於空返回-1,-1就是空句柄
            /* Exception already pending */
            return -1;
        }
        h = CreateFileW(
            pathbuf,            /* Wide char path name */
            access,             /* Read and/or write permission */
            sharing,            /* File sharing flags */
            NULL,               /* Security attributes */
            disposition,        /* creation disposition */
            flagsAndAttributes, /* flags and attributes */
            NULL);//CreateFileW是一個WIN API,可以打開一個文件
        free(pathbuf);//釋放內存
    } else {//不是NT,那么就是XP WIN7 等各位熟悉的系統,WITH_PLATFORM_STRING和END_PLATFORM_STRING都是宏定義
            //這個就沒必要帶各位再去分析宏定義了,主要作用是將jstring轉換成與平台相關的char *類型變量。
        WITH_PLATFORM_STRING(env, path, _ps) {
            h = CreateFile(_ps, access, sharing, NULL, disposition,
                           flagsAndAttributes, NULL);//最終這個方法也是得到一個文件句柄
        } END_PLATFORM_STRING(env, _ps);
    }
    if (h == INVALID_HANDLE_VALUE) {//如果句柄為無效句柄,則拋出FileNotFoundException異常,相信各位都不陌生
        int error = GetLastError();
        if (error == ERROR_TOO_MANY_OPEN_FILES) {
            JNU_ThrowByName(env, JNU_JAVAIOPKG "IOException",
                            "Too many open files");
            return -1;
        }
        throwFileNotFoundException(env, path);
        return -1;
    }
    return (jlong) h;//返回句柄
}

         LZ已經在上面方法加了注釋,相信熟悉JAVA的同學哪怕不懂C/C++,也不難看懂上面這個函數,而唯一LZ沒有解釋全的就是CreateFile方法那幾個參數,這個如果各位有興趣可以去搜索一下,百度上有很多這個方法的參數的具體解釋,但是不管怎么說,我們都是以只讀方式打開了一個文件。也就是在上面調用fileOpen方法時傳入的O_RDONLY這個參數代表的含義。

         搞清楚了文件句柄獲取的過程,下面我們來看一下,這個句柄是如何賦給了FileDescriptor類的handle屬性,我們看一下SET_FD這個宏定義都做了什么。

#define SET_FD(this, fd, fid) \
    if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \//這一句,是判斷FileInputStream這個對象的fd屬性是不是空
        //如果不是空的話,調用了一個SetLongField的方法,看它的參數,(*env)->GetObjectField(env, (this), (fid))這個傳入
        //的是FileInputStream這個對象的fd屬性,IO_handle_fdID是handle屬性的地址偏移量,fd則是文件句柄的值
        //我們不需要進去看,就能看出來這個函數就是把fd賦給了FileInputStream這個對象的fd屬性的handle屬性。
        (*env)->SetLongField(env, (*env)->GetObjectField(env, (this), (fid)), IO_handle_fdID, (fd))

          這下我們已經明白了,綜合上面的分析過程,我們可以總結出當我們new一個FileInputStream的時候,都做了哪些步驟。

          下面LZ將這些步驟寫出來:

          1、如果FileInputStream類尚未加載,則執行initIDs方法,否則這一步直接跳過。

          2、如果FileDescriptor類尚未加載,則執行initIDs方法,否則這一步也直接跳過。

          3、new一個FileDescriptor對象賦給FileInputStream的fd屬性。

          4、打開一個文件句柄。

          5、將文件句柄賦給FileDescriptor對象的handle屬性。

          到此我們已經將FileInputStream的創建過程全部搞清楚了,不過一直分析下來好像都一直在看C/C++代碼了,下面LZ給各位寫了一個小程序,是使用的FileDescriptor這個類的靜態變量,也就是那幾個標准流。

import java.io.FileDescriptor;
import java.io.FileWriter;
import java.io.IOException;


public class Client {

    public static void main(String[] args) throws IOException {
        FileDescriptor descriptor  = FileDescriptor.out;
        FileWriter fileWriter = new FileWriter(descriptor);
        fileWriter.write("hello world");
        fileWriter.flush();
        fileWriter.close();
    }
}

            輸出結果為hello world,相信不出大家意料,不過這是不是挺有意思的呢?好了,本次簡單的分析了一下FileInputStream對象的創建,下次我們再來看看在獲得了handle(文件句柄)之后,read方法又是如何去讀取文件的。

            本章就到此結束了,感謝各位的收看,下次再見。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM