Android开发中,我们可能需要记录一些文件。例如记录log文件。如果使用流来写文件,频繁操作文件io可能会引起性能问题。
为了降低写文件的频率,我们可能会采用缓存一定数量的log,再一次性把它们写到文件中。如果app异常退出,我们有可能会丢失内存中的log信息。
那么有什么比较稳妥的写文件方式,既能降低io,又能尽可能地保证数据被写入文件呢?
mmap 简介
mmap 概念
mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
特点:实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图所示:
mmap 内存映射原理
mmap 内存映射的实现过程,总的来说可以分为三个阶段:
应用进程启动映射,在进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址作为映射区域;
调用系统函数 mmap,实现文件物理地址和进程虚拟地址的一一映射;
应用进程对映射区域访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。
mmap优缺点
只有一次数据拷贝:当发生缺页异常时,直接将数据从磁盘拷贝到进程的用户空间,跳过了页缓存。
实现了用户空间和内核空间的高效交互方式:两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
提供进程间共享内存及相互通信的方式。
不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。
mmap注意点
对于大文件而言,内存映射比普通IO流要快,小文件则未必;
不要经常调用MappedByteBuffer.force()方法,这个方法强制操作系统将内存中的内容写入硬盘,所以如果你在每次写内存映射文件后都调用force()方法,你就不能真正从内存映射文件中获益,而是跟disk IO差不多。
读写内存映射文件是操作系统来负责的,因此,即使你的 Java 程序在写入内存后就挂掉了,只要操作系统工作正常,数据就会写入磁盘。
如果电源故障或者主机瘫痪,有可能内存映射文件还没有写入磁盘,意味着可能会丢失一些关键数据。
参考
- https://stackoverflow.com/questions/258091/when-should-i-use-mmap-for-file-access
- https://www.jianshu.com/p/187eada7b900
- https://juejin.im/post/5c3ec9ebf265da61223a93de#heading-0
- https://stackoverflow.com/questions/30180268/android-ndk-mmap-call-broken-on-32-bit-devices-after-upgrading-to-lollipop
- https://stackoverflow.com/questions/33897711/android-mmap-fails-with-out-of-memory
Android 中的 Binder 也利用的 mmap 。Binder 传递数据时,只需要复制一次,就能把数据传递到另一个进程中。参考 Binder 机制介绍。
Android 中使用 mmap
Android中使用mmap,可以通过RandomAccessFile与MappedByteBuffer来配合。参考drone开发记录 - log记录工具
通过randomAccessFile.getChannel().map获取到MappedByteBuffer。然后调用ByteBuffer的put方法添加数据。