在wps Android端的日子过得很快,我所在的组是IO组。因为WPS需要大量的和文档打交道,所以有专门的组负责IO。
最近想探索一下,是否可以把mmap接入到wps上。于是做了 “手写mmap进行读盘”的探索。
这里说一句,因为java 层的MappedByteBuffer 虽然也是采用了mmap。但是我发现有一个缺点是
没有提供unmap方法,很容易超过最大限制导致映射失败
我不知道为什么很多博客说MappedByteBuffer效率会高,如果你有demo可以证明这一点,可以在评论一起交流。
先说用JNI层的mmap后,相比缓冲流提升了多少效率。
这是读一个G文件的测试数据
这里可以看到,大概提升了12%的效率。
后面我换了一台手机小米的Redmi K 30 5G 内存6G。
缓冲流:
第一个1.5G的文件,第一次读花了2641毫秒,第二次之后就直接变成470 毫秒左右。
所以应该是现在设备好了之后,自带的系统文件管理器做了很多优化,第一次才会发生直接IO,第二次以及以后,都是用缓存。
mmap:
读一个1.5G的文件,第一次花了3100毫秒,第二次之后就稳定在700毫秒左右
以上的第一次是在开机后的第一次。
总的来说:在一些低端设备上,mmap比缓冲流效率大概高了12%。而在较为高端的设备上,其实不建议采用mmap去实现文件的读取的,就算是大文件,还是建议用缓冲流即可。
所以我就有了另外的疑问:MMKV为什么会比Android自带的SharedPreferences快那么多。
官方文档里,MMKV主要做了两个优化,第一个是用mmap实现了文件的存取盘,第二个采用了protobuf协议。
虽然我还没有做频繁读写盘的测试,不知道频繁的读写盘 “缓冲流”和 “mmap” 的表现差异,但是这里就先猜测,protobuf协议才是性能提升的关键。
这是代码:
public class FileTestActivity extends Activity implements View.OnClickListener {
String filePath = "/storage/emulated/0/test1";
Button btn_mmap_read;
Button btn_buffer_read;
Button btn_map_read;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_file_test);
btn_mmap_read = findViewById(R.id.btn_mmap_read);
btn_buffer_read = findViewById(R.id.btn_buffer_read);
btn_map_read = findViewById(R.id.btn_map_read);
btn_mmap_read.setOnClickListener(this);
btn_buffer_read.setOnClickListener(this);
btn_map_read.setOnClickListener(this);
}
@Override
public void onClick(View v) {
try {
if (v == btn_mmap_read) {
readMmap();
} else if (v == btn_buffer_read) {
readBuffer();
} else if (v == btn_map_read) {
readMap();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void readMap() throws Exception {
long l = System.currentTimeMillis();
FileInputStream fileInputStream = new FileInputStream(filePath);
byte[] mmapBytes = new byte[1024 * 1024 * 10];
FileChannel channel = fileInputStream.getChannel();
MappedByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
while (buf.hasRemaining()) {
buf.get(mmapBytes);
}
channel.close();
fileInputStream.close();
long l1 = System.currentTimeMillis();
System.out.println("readMap 执行时间:" + (l1 - l));
}
private void readBuffer() throws Exception {
long l = System.currentTimeMillis();
byte[] mmapBytes = new byte[65535];
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
while(bufferedInputStream.read(mmapBytes,0,mmapBytes.length) > 0) {
}
bufferedInputStream.close();
long l1 = System.currentTimeMillis();
System.out.println("readBuffer 执行时间:" + (l1 - l));
}
private void readMmap() throws IOException {
long l = System.currentTimeMillis();
byte[] mmapBytes = new byte[1024 * 1024 * 10];
MmapInputStream mmapInputStream = new MmapInputStream(filePath);
int fileSize = mmapInputStream.getFileSize(filePath);
int tempLen = 0;
int len = 0;
while (len < fileSize) {
tempLen = mmapBytes.length;
if (len + tempLen >= fileSize) {
tempLen = fileSize - len;
}
mmapInputStream.readByte(mmapBytes,len,tempLen);
len += tempLen;
}
mmapInputStream.close();
long l1 = System.currentTimeMillis();
System.out.println("readMmap 执行时间:" + (l1 - l));
}
}
public class MmapInputStream extends InputStream {
static {
System.loadLibrary("native-lib");
}
private String mFilePath;
private int fileSize;
public MmapInputStream(String filePath) {
this.mFilePath = filePath;
fileSize = getFileSize(filePath);
mmapOpen(filePath,fileSize);
}
private byte[] cacheByte;
private int position = -1;
private final int len = 65535 * 100;
private int readLen = 0;
private void readCacheByte() {
if (cacheByte == null || position >= cacheByte.length) {
cacheByte = null;
int tempLen = len;
if (fileSize - readLen < tempLen) {
tempLen = fileSize - readLen;
}
long l = System.currentTimeMillis();
cacheByte = read(readLen, tempLen);
long l1 = System.currentTimeMillis();
System.out.println("readCacheByte:" + (l1 - l));
position = 0;
}
}
@Override
public int available() throws IOException {
return fileSize - readLen;
}
@Override
public synchronized void mark(int readlimit) {
super.mark(readlimit);
}
@Override
public boolean markSupported() {
return super.markSupported();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (readLen >= fileSize) {
return -1;
}
readCacheByte();
int temp = len;
if (position + len >= cacheByte.length) {
temp = cacheByte.length - position;
Arrays.fill(b,temp,b.length, (byte) 0);
}
System.arraycopy(cacheByte,position,b,off,temp);
readLen += temp;
position += temp;
return temp;
}
@Override
public int read() throws IOException {
if (readLen >= fileSize) {
return -1;
}
readCacheByte();
if (cacheByte == null || cacheByte.length == 0 || position >= cacheByte.length) {
return -1;
}
readLen += 1;
return cacheByte[position++] & 0xff;
}
@Override
public long skip(long n) throws IOException {
return super.skip(n);
}
/**
* 开启共享映射
* @param absolutePath
*/
public native void mmapOpen(String absolutePath,int mapLen);
@Override
public void close() throws IOException {
super.close();
mmapClose(fileSize);
}
/**
* 关闭共享映射
*/
public native void mmapClose(int fileSize);
/**
* 得到文件的大小
*/
public native int getFileSize(String filePath);
/**
* 写入数据
* @param content
*/
public native void mmapWrite(String content);
public native byte[] read(int start,int len);
public native char[] readChar(int start,int len);
public native String readString(int start,int len);
public native int readByte(byte[] bytes,int start,int len);
}
#include <jni.h>
#include <string>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <filesystem>
extern "C" {
char *mmapPtr = NULL;
}
int fileSize(const char* filename)
{
FILE *fp=fopen(filename,"r");
if(!fp) return -1;
fseek(fp,0L,SEEK_END);
int size=ftell(fp);
fclose(fp);
return size;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_testjni_temp_MmapInputStream_mmapOpen(JNIEnv *env, jobject thiz, jstring path,
jint len) {
const char *file_path = env->GetStringUTFChars(path, 0);
// 文件不存在
if (access(file_path, F_OK) == -1) {
return;
}
int fd = open(file_path, O_RDWR | O_CREAT, 0644); //打开本地磁盘中的文件(如果没有就创建一个), 获取fd,0644是可读写的意思
if (fd == -1) {
perror("open error");
}
mmapPtr = (char *) mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mmapPtr == MAP_FAILED) {
perror("mmap error");
}
//关闭文件句柄
close(fd);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_testjni_temp_MmapInputStream_mmapClose(JNIEnv *env, jobject thiz,jint fileSize) {
if (mmapPtr != NULL) {
// 释放内存映射区
int ret = munmap(mmapPtr, fileSize);
if (ret == -1) {
perror("munmap error");
}
}
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_testjni_temp_MmapInputStream_mmapWrite(JNIEnv *env, jobject thiz, jstring content) {
if (mmapPtr != NULL) {
const char *c_content = env->GetStringUTFChars(content, 0);
// 修改映射区数据
strcpy(mmapPtr, c_content);
}
}
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_example_testjni_temp_MmapInputStream_read(JNIEnv *env, jobject thiz, jint start, jint len) {
auto *b = (jbyte *) mmapPtr; //转化
b += start;
jbyteArray myJByteArray = env->NewByteArray(len);
env->SetByteArrayRegion(myJByteArray, 0, len, b);
b -= start;
return myJByteArray;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_testjni_temp_MmapInputStream_getFileSize(JNIEnv *env, jobject thiz, jstring file_path) {
const char *filePath = env->GetStringUTFChars(file_path, 0);
return fileSize(filePath);
}
extern "C"
JNIEXPORT jcharArray JNICALL
Java_com_example_testjni_temp_MmapInputStream_readChar(JNIEnv *env, jobject thiz, jint start, jint len) {
auto *b = (jchar *) mmapPtr; //转化
b += start;
jcharArray myJByteArray = env->NewCharArray(len);
env->SetCharArrayRegion(myJByteArray, 0, len, b);
b -= start;
return myJByteArray;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_testjni_temp_MmapInputStream_readString(JNIEnv *env, jobject thiz, jint start,
jint len) {
int cLen = len;
auto *b = mmapPtr;
// char * p = new char[cLen + 1];
char *p = (char *) malloc(len);
memset(p, ' ', len);
memcpy(p, b + start, len);
auto *p1 = (const char *) p;
jstring result = env->NewStringUTF(p1);
delete p;
return result;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_testjni_temp_MmapInputStream_readByte(JNIEnv *env, jobject thiz,
jbyteArray bytes,jint start,int len) {
auto *b = (jbyte *) mmapPtr; //转化
b += start;
env->SetByteArrayRegion(bytes, 0, len, b);
return 1;
}
因为wps在处理文件的时候,还涉及到转码的过程。转码的过程是用reader处理的,我把MmapInputStream传入reader后,发现效果其实和传入FileInputStream差不多多少,一个1G的文件大概可以快1秒钟。
wps处理的文档大都是50M以内,超过了本身进程的内存也不够了,所以这一次的优化探索就只是当作学习mmap的使用了。
在贴上我关于读盘的结论: