java 如何以最快的速度读写文件?如何提高读写速度?
微wx笑
2021-07-31【编程语言】
3
0关键字:
java 文件 nio FileChannel
最近在研究 解码解密微信电脑版image文件夹下缓存的用户图片 dat文件解码解密查看方法 时,开始读取文件是一个字节一个字节的读取写入,速度非常之慢,让人无法忍受,于是开始研究尝试如何以最快的速度读写文件?
最近在研究 解码解密微信电脑版image文件夹下缓存的用户图片 dat文件解码解密查看方法 时,开始读取文件是一个字节一个字节的读取写入,速度非常之慢,让人无法忍受,于是开始研究尝试如何以最快的速度读写文件?
版本1不使用缓存
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.util.Date;
class decode
{
public static void main(String[] args) {
System.out.println("hello");
File f = new File("D:/Documents/WeChat Files/wxid_/Image/2021-07");
if (!f.exists() || !f.isDirectory()){
System.out.println("目录不存在");
}
File[] fl = f.listFiles(new FilenameFilter(){
@Override
public boolean accept(File dir, String name) {
if (name.endsWith(".dat")){
return true;
}
return false;
}
});
System.out.println("共找到 " + fl.length + " 个 dat 文件");
if (fl.length == 0){
System.out.println("没有需要解码的文件");
}
long begin = new Date().getTime();
System.out.println("====开始解码====time:" + begin);
for (File file : fl){
System.out.println(file.getAbsolutePath());
try {
FileInputStream fis = new FileInputStream(file);
File of = new File("D:/Documents/WeChat Files/wxid_/FileStorage/Image/decodeimg/" + file.getName() + ".jpeg");
FileOutputStream fos = new FileOutputStream(of);
int b;
while((b = fis.read()) != -1){
fos.write(b ^ 0x73);
}
fis.close();
fos.close();
} catch (Exception e) {
}
}
long end = new Date().getTime();
System.out.println("decode====" + fl.length + " 个dat文件解码完成====用时:" + (end - begin));
}
}一个字节一个字节的读取写入,真是让人崩溃!
为什么会这样呢?
调用I\O操作的时候,实际上还是一个一个的读或者写,关键就在,CPU只有一个,不论是几个核心。CPU在系统调用时,会不会还要参与主要操作?参与多次就会花更多的时间。
系统调用时,若不用缓冲,CPU会酌情考虑使用 中断。此时CPU是主动地,每个周期中都要花去一部分去询问I\O设备是否读完数据,这段时间CPU不能做任何其他的事情(至少负责执行这段模块的核不能)。所以,调用一次读了一个字,通报一次,CPU腾出时间处理一次。
而设置缓冲,CPU通常会使用 DMA 方式去执行 I\O 操作。CPU 将这个工作交给DMA控制器来做,自己腾出时间做其他的事,当DMA完成工作时,DMA会主动告诉CPU“操作完成”。这时,CPU接管后续工作。在此,CPU 是被动的。DMA是专门 做 I\O 与 内存 数据交换的,不仅自身效率高,也节约了CPU时间,CPU在DMA开始和结束时做了一些设置罢了。
所以,调用一次,不必通报CPU,等缓冲区满了,DMA 会对C PU 说 “嘿,伙计!快过来看看,把他们都搬走吧”。
综上,设置缓冲,就建立了数据块,使得DMA执行更方便,CPU也有空闲,而不是呆呆地候着I\O数据读来。从微观角度来说,设置缓冲效率要高很多。尽管,不能从这个程序上看出来。 几万字的读写\就能看到差距
版本2按块读取
参考 解码解密微信电脑版image文件夹下缓存的用户图片 dat文件解码解密查看方法 文章中的代码
设置了一个1M大小的Buffer,byte[] bs = new byte[1024 * 1024];
速度一下子就提上来了,如果不添加获取解密字节码及文件扩展名的代码,对1300左右个文件进行解码,两三秒就完成了,版本1可是需要十几分钟的。
版本3使用BufferedStream
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.util.Date;
class decode3
{
public static void main(String[] args) {
System.out.println("hello");
File f = new File("D:/Documents/WeChat Files/wxid_/FileStorage/Image/2021-07");
if (!f.exists() || !f.isDirectory()){
System.out.println("目录不存在");
}
File[] fl = f.listFiles(new FilenameFilter(){
@Override
public boolean accept(File dir, String name) {
if (name.endsWith(".dat")){
return true;
}
return false;
}
});
System.out.println("共找到 " + fl.length + " 个 dat 文件");
if (fl.length == 0){
System.out.println("没有需要解码的文件");
}
long begin = new Date().getTime();
System.out.println("====开始解码====time:" + begin);
for (File file : fl){
System.out.println(file.getAbsolutePath());
try {
FileInputStream fis = new FileInputStream(file);
File of = new File("D:/Documents/WeChat Files/wxid_/FileStorage/Image/decodeimg3/" + file.getName() + ".jpeg");
FileOutputStream fos = new FileOutputStream(of);
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
byte[] buff = new byte[1024 * 1024];
int rl = 0;
while((rl = bis.read(buff)) > 0){
for (int i = 0; i < rl; i++){
buff[i] = (byte)(buff[i] ^ 0x73);
}
bos.write(buff, 0, rl);
}
bis.close();
bos.close();
fis.close();
fos.close();
} catch (Exception e) {
}
}
long end = new Date().getTime();
System.out.println("decode3====" + fl.length + " 个dat文件解码完成====用时:" + (end - begin));
}
}这个版本中间又加了一层
BufferedInputStream bis = new BufferedInputStream(fis); BufferedOutputStream bos = new BufferedOutputStream(fos);
然而在性能速度上没有明显的提升,经过多次的测试,貌似性能更稳定一点点。
部分测试结果
decode1====解码完成====用时:1388225
decode1====解码完成====用时:1367731
decode1====解码完成====用时:1362141
decode2====解码完成====用时:3290
decode2====解码完成====用时:4474
decode2====1263 个dat文件解码完成====用时:3201
decode2====1263 个dat文件解码完成====用时:2622
decode2====1263 个dat文件解码完成====用时:2553
decode2====1263 个dat文件解码完成====用时:2607
decode2====1263 个dat文件解码完成====用时:3138
decode3====1263 个dat文件解码完成====用时:3242
decode3====1263 个dat文件解码完成====用时:2864
decode3====1263 个dat文件解码完成====用时:2449
decode3====1263 个dat文件解码完成====用时:2425
decode3====1263 个dat文件解码完成====用时:2528
版本4Nio.FileChannel
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.util.Date;
import java.nio.channels;
import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;
/**
* 微信电脑版image文件夹下缓存的用户图片dat文件解码解密类
* @author lipw
* @email admin@ivu4e.com
* @site https://ivu4e.com/
*/
class decode4
{
public static void main(String[] args) {
System.out.println("hello");
// 使用时修改这里的路径就可以了
BatchDecodeFileContent("D:/Documents/WeChat Files/wxid_/FileStorage/Image/2021-07", "D:/Documents/WeChat Files/wxid_/FileStorage/Image/decodeimg5");
}
/**
* 批量对文件进行解密处理
* @param inputFileDir 要解密的文件夹
* @param outputFileDir 解密后保存在哪个文件夹
*/
public static void BatchDecodeFileContent(String inputFileDir, String outputFileDir){
File f = new File(inputFileDir);
if (!f.exists() || !f.isDirectory()){
System.out.println("要解密的文件夹不存在!");
}
if (!outputFileDir.endsWith("/")){
outputFileDir = outputFileDir.concat("/");
}
File o = new File(outputFileDir);
if (!o.exists()){
try {
System.out.println("解密后保存的文件夹不存在,尝试创建...");
o.mkdirs();
} catch (Exception e) {
System.out.println("创建解密后保存的文件夹失败,程序终止执行");
return;
}
}
File[] fl = f.listFiles(new FilenameFilter(){
@Override
public boolean accept(File dir, String name) {
if (name.endsWith(".dat")){
return true;
}
return false;
}
});
System.out.println("共找到 " + fl.length + " 个 dat 文件");
if (fl.length == 0){
System.out.println("没有需要解码的文件");
}
long begin = new Date().getTime();
System.out.println("====开始解码====time:" + begin);
Bom bom;
FileInputStream fis;
File of;
FileOutputStream fos;
FileChannel channelIn;
FileChannel channelOut;
ByteBuffer bs = ByteBuffer.allocate(1024 * 10);
int rl;
for (File file : fl){
System.out.println(file.getAbsolutePath());
try {
fis = new FileInputStream(file);
channelIn = fis.getChannel();
bs.clear();
rl = channelIn.read(bs);
bs.position(0);
bom = getFileBom(bs.array());
if (bom.getXorVal() == 0x00 || bom.getExtn() == null){
System.out.println("获取加密的字节码失败");
continue;
}
of = new File(outputFileDir + file.getName() + bom.getExtn());
fos = new FileOutputStream(of);
channelOut = fos.getChannel();
while(rl > 0){
for (int i = 0; i < rl; i++){
bs.put(i, (byte)(bs.get(i) ^ bom.getXorVal()));
}
channelOut.write(bs);
bs.clear();
rl = channelIn.read(bs);
bs.position(0);
}
channelIn.close();
channelOut.close();
fis.close();
fos.close();
} catch (Exception e) {
}
}
long end = new Date().getTime();
System.out.println("decode4====" + fl.length + " 个dat文件解码完成====用时:" + (end - begin));
}
/**
* 获取加密的字节码
* @param buff 读取的文件的第一块,包含文件头的部分
* @return
*/
public static Bom getFileBom(byte[] buff){
Bom bom = new Bom();
if (buff.length < 2){
return bom;
}
// jpeg
if ((byte)(buff[0] ^ 0xFF) == (byte)(buff[1] ^ 0xD8)){
bom.setXorVal((byte)(buff[0] ^ 0xFF));
bom.setExtn(".jpeg");
return bom;
}
// png
if ((byte)(buff[0] ^ 0x89) == (byte)(buff[1] ^ 0x50)){ // Xor计算之后需要加上强制类型转换(byte),否则比较的时候会出现不相等的情况
bom.setXorVal((byte)(buff[0] ^ 0x89));
bom.setExtn(".png");
return bom;
}
// bmp
if ((byte)(buff[0] ^ 0x42) == (byte)(buff[1] ^ 0x4D)){
bom.setXorVal((byte)(buff[0] ^ 0x42));
bom.setExtn(".bmp");
return bom;
}
// gif
if ((byte)(buff[0] ^ 0x47) == (byte)(buff[1] ^ 0x49)){
bom.setXorVal((byte)(buff[0] ^ 0x47));
bom.setExtn(".gif");
return bom;
}
// tif
if ((byte)(buff[0] ^ 0x49) == (byte)(buff[1] ^ 0x49)){
bom.setXorVal((byte)(buff[0] ^ 0x49));
bom.setExtn(".tif");
return bom;
}
System.out.printf("%x%x==%x%x", buff[0], buff[1],(buff[0] ^ 0x89),(buff[1] ^ 0x50));
return bom;
}
// 文件头
static class Bom {
// 对文件加密解密使用的字节码
byte xorVal = 0x00;
// 文件扩展名
String extn = null;
public Bom() {
}
public byte getXorVal() {
return xorVal;
}
public void setXorVal(byte xorVal) {
this.xorVal = xorVal;
}
public String getExtn() {
return extn;
}
public void setExtn(String extn) {
this.extn = extn;
}
}
}这个版本使用了 java.nio.channels.FileChannel,开始的时候设置Buffer大小为1M(1024*1024),但是性能不理想,经常尝试,1024 * 10 的性能比较好,应该是跟文件的大小有关系,大多数文件都在1M以内。
测试结果
decode2====1490 个dat文件解码完成====用时:2894
decode2====1490 个dat文件解码完成====用时:2711
decode4====1492 个dat文件解码完成====用时:3770
decode4====1492 个dat文件解码完成====用时:4282
可以看出,使用 java.nio 的性能也没有显著的提高,反而变慢了,或者还是测试文件太少了;也可能与文件大小有关系,文件较大的话,估计差别会更明显一些。
版本5内存映射文件
/**
* 使用直接缓冲区完成文件的复制(内存映射文件)
* 耗费时间:142
*/
private static void nioCopyTest2() throws IOException {
long startTime = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("E:\\ 1.avi"), StandardOpenOption.READ);
FileChannel outChennel = FileChannel.open(Paths.get("E:\\ 12.avi"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
//内存映射文件(什么模式 从哪开始 到哪结束)
MappedByteBuffer inMappeBuf = inChannel.map(FileChannel.MapMode.READ_ONLY,0,inChannel.size());
MappedByteBuffer outMappeBuf = outChennel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size());
//直接都缓冲区进行数据的读写操作
byte[] dst = new byte[inMappeBuf.limit()];
inMappeBuf.get(dst);
outMappeBuf.put(dst);
inChannel.close();
outChennel.close();
long end = System.currentTimeMillis();
System.out.println("nioCopyTest2耗费时间:"+(end-startTime));
}总结
经过测试发现,昨天的测试结果是缓冲区开到1M的时候性能比较稳定,4K、8K、100K、512K时性能都不稳定,时快时慢,超过1M的话就越大越慢。今天得到的结果是1024 * 10 的性能比较好,应该是跟文件的大小有关系,大多数文件都在1M以内。另外 BufferedInputStream, BufferedOutputStream,java.nio.channels.FileChannel,这些高大上的类并没有性能上的明显提升。
另外参考:java四种文件读写方式及性能比较 文章中的测试结果:
| 文件大小 | 读写方式 | 耗时 |
|---|---|---|
| 30M | 普通文件流 | 50-60 ms |
| 缓存流 | 32-35 ms | |
| 随机文件方式 | 40-50 ms | |
| 内存映射文件 | 50-60 ms | |
| 461M | 普通文件流 | 1300-2300 ms |
| 缓存流 | 1700-2000 ms | |
| 随机文件方式 | 1300-3000 ms | |
| 内存映射文件 | 890-1000 ms | |
| 1.47G | 普通文件流 | 11s |
| 缓存流 | 9s | |
| 随机文件方式 | 10s | |
| 内存映射文件 | 3s(首次较慢) |
从他的结果来看,也是当文件较大的时候,才有性能上的明显提升。所以性能的提升需要根据文件的大小来确认具体使用哪一种方案。
本文由 微wx笑 创作,采用 署名-非商业性使用-相同方式共享 4.0 许可协议,转载请附上原文出处链接及本声明。
原文链接:https://www.ivu4e.cn/blog/lang/2021-07-31/710.html



