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