java 如何以最快的速度读写文件?如何提高读写速度?
微wx笑
2021-07-31【编程语言】
172
3
0关键字:
java 文件 nio FileChannel
最近在研究 解码解密微信电脑版image文件夹下缓存的用户图片 dat文件解码解密查看方法 时,开始读取文件是一个字节一个字节的读取写入,速度非常之慢,让人无法忍受,于是开始研究尝试如何以最快的速度读写文件?
目录
最近在研究 解码解密微信电脑版image文件夹下缓存的用户图片 dat文件解码解密查看方法 时,开始读取文件是一个字节一个字节的读取写入,速度非常之慢,让人无法忍受,于是开始研究尝试如何以最快的速度读写文件?
版本1不使用缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | 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)); } } |
这个版本中间又加了一层
1 2 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | 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内存映射文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * 使用直接缓冲区完成文件的复制(内存映射文件) * 耗费时间: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