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();
}

******************