Android入門教程 | mmap 檔案對映介紹

Android_anzi發表於2021-12-08

Android 開發中,我們可能需要記錄一些檔案。例如記錄 log 檔案。如果使用流來寫檔案,頻繁操作檔案 io 可能會引起效能問題。

為了降低寫檔案的頻率,我們可能會採用快取一定數量的 log,再一次性把它們寫到檔案中。如果 app 異常退出,我們有可能會丟失記憶體中的 log 資訊。

那麼有什麼比較穩妥的寫檔案方式,既能降低 io,又能儘可能地保證資料被寫入檔案呢?

mmap 概念

mmap 是一種記憶體對映檔案的方法(memory-mapped),即將一個檔案或者其它物件對映到程式的地址空間,實現檔案磁碟地址和程式虛擬地址空間中一段虛擬地址的一一對映關係。

特點:實現這樣的對映關係後,程式就可以採用指標的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁碟上,即完成了對檔案的操作而不必再呼叫 read,write 等系統呼叫函式。相反,核心空間對這段區域的修改也直接反映使用者空間,從而可以實現不同程式間的檔案共享。如下圖所示:

mmap 記憶體對映原理

mmap 記憶體對映的實現過程,總的來說可以分為三個階段:

應用程式啟動對映,在程式的虛擬地址空間中,尋找一段空閒的滿足要求的連續的虛擬地址作為對映區域; 呼叫系統函式 mmap,實現檔案實體地址和程式虛擬地址的一一對映; 應用程式對對映區域訪問,引發缺頁異常,實現檔案內容到實體記憶體(主存)的複製。

mmap 優缺點

  • 只有一次資料複製:當發生缺頁異常時,直接將資料從磁碟複製到程式的使用者空間,跳過了頁快取。
  • 實現了使用者空間和核心空間的高效互動方式:兩空間的各自修改操作可以直接反映在對映的區域內,從而被對方空間及時捕捉。 提供程式間共享記憶體及相互通訊的方式。

不管是父子程式還是無親緣關係的程式,都可以將自身使用者空間對映到同一個檔案或匿名對映到同一片區域。從而透過各自對對映區域的改動,達到程式間通訊和程式間共享的目的。

同時,如果程式A和程式B都對映了區域C,當A第一次讀取C時透過缺頁從磁碟複製檔案頁到記憶體中;但當B再讀C的相同頁面時,雖然也會產生缺頁異常,但是不再需要從磁碟中複製檔案過來,而可直接使用已經儲存在記憶體中的檔案資料。

mmap 注意點

對於大檔案而言,記憶體對映比普通IO流要快,小檔案則未必; 不要經常呼叫MappedByteBuffer.force() 方法,這個方法強制作業系統將記憶體中的內容寫入硬碟,所以如果你在每次寫記憶體對映檔案後都呼叫 force() 方法,你就不能真正從記憶體對映檔案中獲益,而是跟 disk IO 差不多。

讀寫記憶體對映檔案是作業系統來負責的,因此,即使你的 Java 程式在寫入記憶體後就掛掉了,只要作業系統工作正常,資料就會寫入磁碟。 如果電源故障或者主機癱瘓,有可能記憶體對映檔案還沒有寫入磁碟,意味著可能會丟失一些關鍵資料。

Android中的Binder也利用的mmap。Binder傳遞資料時,只需要複製一次,就能把資料傳遞到另一個程式中。

Android 中使用 mmap

Android中使用mmap,可以透過RandomAccessFile與MappedByteBuffer來配合。透過  randomAccessFile.getChannel().map 獲取到  MappedByteBuffer。然後呼叫 ByteBuffer 的 put 方法新增資料。

使用 RandomAccessFile來獲取MappedByteBuffer

 private static void writeDemo() {
        System.out.println("[writeDemo] start");        String inputStr = "This write demo. 維多利亞女王紀念碑是位於英國倫敦的一座大型紀念碑和雕塑組合,建於1911年。";        byte[] inputBytes = inputStr.getBytes();        final int length = inputBytes.length;        try {            RandomAccessFile randomAccessFile = new RandomAccessFile("out/writeDemo.txt", "rw");            MappedByteBuffer mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, length);
            mappedByteBuffer.put(inputBytes);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
        System.out.println("[writeDemo] end");
    }
  private static void writeAppendDemo() {        System.out.println("[writeAppendDemo] start");
        String appendText = "\nThis is append text. 紀念碑的基座由2300噸漢白玉構成。";
        byte[] bytes = appendText.getBytes();
        final int length = bytes.length;        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile("out/writeDemo.txt", "rw");
            MappedByteBuffer mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, randomAccessFile.length(), length);
            mappedByteBuffer.put(bytes);
        } catch (Exception e) {            System.out.println(e.getMessage());
            e.printStackTrace();
        }        System.out.println("[writeAppendDemo] end");
    }

Android 中使用 MappedByteBuffer 寫入檔案。記住當前寫檔案的位置 gCurrentLogPos,處理檔案增長的問題。

File dir = new File(logFileDir);if (!dir.exists()) {    boolean mk = dir.mkdirs();
    Log.d(defTag, "make dir " + mk);
}File eFile = new File(logFileDir + File.separator + fileName);byte[] strBytes = logContent.getBytes();try {
    RandomAccessFile randomAccessFile = new RandomAccessFile(eFile, "rw");
    MappedByteBuffer mappedByteBuffer;    final int inputLen = strBytes.length;    if (!eFile.exists()) {        boolean nf = eFile.createNewFile();
        Log.d(defTag, "new log file " + nf);
        mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, LOG_FILE_GROW_SIZE);
    } else {
        mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, inputLen);
    }    if (mappedByteBuffer.remaining() < inputLen) {
        mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, LOG_FILE_GROW_SIZE + inputLen);
    }
    mappedByteBuffer.put(strBytes);
    gCurrentLogPos += inputLen;
} catch (Exception e) {
    Log.e(defTag, "WriteRunnable run: ", e);    if (!eFile.exists()) {        boolean nf = eFile.createNewFile();
        Log.d(defTag, "new log file " + nf);
    }
    FileOutputStream os = new FileOutputStream(eFile, true);
    os.write(logContent.getBytes());
    os.flush();
    os.close();
}


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70008155/viewspace-2846588/,如需轉載,請註明出處,否則將追究法律責任。

相關文章