编程语言您现在的位置是:首页 > 博客日志 > 编程语言

java 如何以最快的速度读写文件?如何提高读写速度?

<a href='mailto:'>微wx笑</a>的头像微wx笑 2021-07-31编程语言 1 0关键字: java  文件  nio  FileChannel  

最近在研究 解码解密微信电脑版image文件夹下缓存的用户图片 dat文件解码解密查看方法 时,开始读取文件是一个字节一个字节的读取写入,速度非常之慢,让人无法忍受,于是开始研究尝试如何以最快的速度读写文件?

最近在研究 解码解密微信电脑版image文件夹下缓存的用户图片 dat文件解码解密查看方法 时,开始读取文件是一个字节一个字节的读取写入,速度非常之慢,让人无法忍受,于是开始研究尝试如何以最快的速度读写文件?F7V无知人生


F7V无知人生

版本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));
    }


}

一个字节一个字节的读取写入,真是让人崩溃!F7V无知人生

为什么会这样呢?

调用I\O操作的时候,实际上还是一个一个的读或者写,关键就在,CPU只有一个,不论是几个核心。CPU在系统调用时,会不会还要参与主要操作?参与多次就会花更多的时间。F7V无知人生

系统调用时,若不用缓冲,CPU会酌情考虑使用 中断。此时CPU是主动地,每个周期中都要花去一部分去询问I\O设备是否读完数据,这段时间CPU不能做任何其他的事情(至少负责执行这段模块的核不能)。所以,调用一次读了一个字,通报一次,CPU腾出时间处理一次。F7V无知人生

而设置缓冲,CPU通常会使用 DMA 方式去执行 I\O 操作。CPU 将这个工作交给DMA控制器来做,自己腾出时间做其他的事,当DMA完成工作时,DMA会主动告诉CPU“操作完成”。这时,CPU接管后续工作。在此,CPU 是被动的。DMA是专门 做 I\O 与 内存 数据交换的,不仅自身效率高,也节约了CPU时间,CPU在DMA开始和结束时做了一些设置罢了。F7V无知人生

所以,调用一次,不必通报CPU,等缓冲区满了,DMA 会对C PU 说 “嘿,伙计!快过来看看,把他们都搬走吧”。F7V无知人生

综上,设置缓冲,就建立了数据块,使得DMA执行更方便,CPU也有空闲,而不是呆呆地候着I\O数据读来。从微观角度来说,设置缓冲效率要高很多。尽管,不能从这个程序上看出来。 几万字的读写\就能看到差距F7V无知人生

版本2按块读取

参考  解码解密微信电脑版image文件夹下缓存的用户图片 dat文件解码解密查看方法  文章中的代码F7V无知人生

设置了一个1M大小的Buffer,byte[] bs = new byte[1024 * 1024];F7V无知人生

速度一下子就提上来了,如果不添加获取解密字节码及文件扩展名的代码,对1300左右个文件进行解码,两三秒就完成了,版本1可是需要十几分钟的。
F7V无知人生

版本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));
    }
}

这个版本中间又加了一层F7V无知人生

BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);

然而在性能速度上没有明显的提升,经过多次的测试,貌似性能更稳定一点点。F7V无知人生

部分测试结果

decode1====解码完成====用时:1388225F7V无知人生

decode1====解码完成====用时:1367731F7V无知人生

decode1====解码完成====用时:1362141F7V无知人生

decode2====解码完成====用时:3290F7V无知人生

decode2====解码完成====用时:4474F7V无知人生

decode2====1263 个dat文件解码完成====用时:3201F7V无知人生

decode2====1263 个dat文件解码完成====用时:2622F7V无知人生

decode2====1263 个dat文件解码完成====用时:2553F7V无知人生

decode2====1263 个dat文件解码完成====用时:2607F7V无知人生

decode2====1263 个dat文件解码完成====用时:3138F7V无知人生

decode3====1263 个dat文件解码完成====用时:3242F7V无知人生

decode3====1263 个dat文件解码完成====用时:2864F7V无知人生

decode3====1263 个dat文件解码完成====用时:2449F7V无知人生

decode3====1263 个dat文件解码完成====用时:2425F7V无知人生

decode3====1263 个dat文件解码完成====用时:2528F7V无知人生

版本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以内。F7V无知人生

测试结果

decode2====1490 个dat文件解码完成====用时:2894F7V无知人生

decode2====1490 个dat文件解码完成====用时:2711F7V无知人生


F7V无知人生

decode4====1492 个dat文件解码完成====用时:3770F7V无知人生

decode4====1492 个dat文件解码完成====用时:4282F7V无知人生

可以看出,使用 java.nio 的性能也没有显著的提高,反而变慢了,或者还是测试文件太少了;也可能与文件大小有关系,文件较大的话,估计差别会更明显一些。F7V无知人生

版本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,这些高大上的类并没有性能上的明显提升。F7V无知人生

另外参考:java四种文件读写方式及性能比较 文章中的测试结果:F7V无知人生

文件大小读写方式耗时
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(首次较慢)


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生


F7V无知人生

从他的结果来看,也是当文件较大的时候,才有性能上的明显提升。所以性能的提升需要根据文件的大小来确认具体使用哪一种方案。F7V无知人生

本文由 微wx笑 创作,采用 CC BY-NC 4.0 许可协议。 非商业性使用可自由转载、引用、甚至修改,但需署名作者且注明出处。

很赞哦! () 有话说 ()