搜索

影视聚合站

压缩 20M 文件从 30 秒到 1 秒的优化过程

发布时间:2020-11-08 22:21:36来源:Android编程精选

Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

扫码关注添加客服

进Java社群▲

jianshu.com/p/25b328753017

第一次优化过程-从30秒到2秒

第二次优化过程-从2秒到1秒

使用频道

使用内存映射文件

使用管道

总结

参考文章

有一个需求需要将前端传过来的10张照片,然后进行进行处理以后压缩成一个压缩包通过网络流传输出去。之前没有接触过用Java压缩文件的,所以就直接上网找了一个例子改了一下用了,改完以后也能使用,但是连续前端所传图片的大小越来越大的时候,耗费的时间同时急剧增加,最后测了一下压缩20M的文件竟然需要30秒的时间。压缩文件的代码如下。

publicstaticvoidzipFileNoBuffer(){FilezipFile=newFile(ZIP_FILE);try(ZipOutputStreamzipOut=newZipOutputStream(newFileOutputStream(zipFile))){//开始时间longbeginTime=System.currentTimeMillis();for(inti=0;i<10;i++){try(InputStreaminput=newFileInputStream(JPG_FILE)){zipOut.putNextEntry(newZipEntry(FILE_NAME+i));inttemp=0;while((temp=input.read())!=-1){zipOut.write(temp);}}}printInfo(beginTime);}catch(Exceptione){e.printStackTrace();}}这里找了一张2M大小的图片,并且循环十次进行测试。打印的结果如下,时间大概是30秒。

fileSize:20Mconsumtime:29599第一次优化过程-从30秒到2秒优化进行想到首先的的英文利用缓冲区BufferInputStream。在FileInputStream中read()方法每次只读取一个字节。源码中也有说明。

/***Readsabyteofdatafromthisinputstream.Thismethodblocks*ifnoinputisyetavailable.**@returnthenextbyteofdata,or-1iftheendofthe*fileisreached.*@exceptionIOExceptionifanI/Oerroroccurs.*/publicnativeintread()throwsIOException;这是一个调用本地方法与原生操作系统进行交互,从磁盘中读取数据。每读取一个字节的数据就调用一次本地方法与操作系统交互,是非常耗时的。例如我们现在有30000个字节的数据,如果使用FileInputStream那么就需要调用30000次的本地方法来获取这些数据,而如果使用标题的话(这里假设初始的尺寸大小足够放下30000字节的数据)那么只需要调用一次就行。原因在于在第一次调用read()方法的时候会直接从磁盘中将数据直接读取到内存中。随后再一个字节一个字节的慢慢返回。

BufferedInputStream内部封装了一个字节的用于存放数据,最小大小是8192

优化过后的代码如下

publicstaticvoidzipFileBuffer(){FilezipFile=newFile(ZIP_FILE);try(ZipOutputStreamzipOut=newZipOutputStream(newFileOutputStream(zipFile));BufferedOutputStreambufferedOutputStream=newBufferedOutputStream(zipOut)){//开始时间longbeginTime=System.currentTimeMillis();for(inti=0;i<10;i++){try(BufferedInputStreambufferedInputStream=newBufferedInputStream(newFileInputStream(JPG_FILE))){zipOut.putNextEntry(newZipEntry(FILE_NAME+i));inttemp=0;while((temp=bufferedInputStream.read())!=-1){bufferedOutputStream.write(temp);}}}printInfo(beginTime);}catch(Exceptione){e.printStackTrace();}}输出

------BufferfileSize:20Mconsumtime:1808可以看到相比较于第一次使用FileInputStream效率已经提升了很多了

使用buffer正规的话已经是满足了我的需求了,但是秉着学以致用的想法,就想着用NIO中知识进行优化一下。

要用为什么Channel呢?因为在NIO新中出了Channel状语从句:ByteBuffer。正是因为它们的结构更加符合操作系统执行I/O的方式,所以其速度相比较于传统IO而言速度有了显着的提高。Channel就像一个包含着煤矿的矿藏,而ByteBuffer则是派遣煤矿藏的卡车。如此我们与数据的相互作用都是与ByteBuffer的相互作用。

在NIO能够中产生FileChannel的有三个类,分别是FileInputStream,,FileOutputStream以及既能读又能写的RandomAccessFile。

源码如下

publicstaticvoidzipFileChannel(){//开始时间longbeginTime=System.currentTimeMillis();FilezipFile=newFile(ZIP_FILE);try(ZipOutputStreamzipOut=newZipOutputStream(newFileOutputStream(zipFile));WritableByteChannelwritableByteChannel=Channels.newChannel(zipOut)){for(inti=0;i<10;i++){try(FileChannelfileChannel=newFileInputStream(JPG_FILE).getChannel()){zipOut.putNextEntry(newZipEntry(i+SUFFIX_FILE));fileChannel.transferTo(0,FILE_SIZE,writableByteChannel);}}printInfo(beginTime);}catch(Exceptione){e.printStackTrace();}}我们可以看到这里并没有使用ByteBuffer进行数据传输,甚至使用了transferTo的方法。这个方法是将两个通道进行直连。

Thismethodispotentiallymuchmoreefficientthanasimpleloop*thatreadsfromthischannelandwritestothetargetchannel.Many*operatingsystemscantransferbytesdirectlyfromthefilesystemcache*tothetargetchannelwithoutactuallycopyingthem.这是二进制上的描述文字,大概意思就是使用transferTo的效率比循环一个Channel读取出来然后重启写入另一个Channel好。操作系统能够直接传输字节从文件系统缓存到目标的Channel中,而不需要实际的copy阶段。

复制阶段就是从内核空间转到用户空间的一个过程

可以看到速度相比较使用范围已经有了一些的提高。

------ChannelfileSize:20Mconsumtime:1416内核空间和用户空间那么为什么从内核空间转向用户空间阶段过程会慢呢??首先我们需要了解的是什么是内核空间和用户空间。在常用的操作系统中为了保护系统中的核心资源,于是将系统设计为四个区域,越往里权限限制,所以Ring0被称为内核空间,用来访问一些关键性的资源。Ring3被称为用户空间。

图片

用户态,内核态:线程位于内核空间称为内核态,线程处于用户空间属于用户态

那么我们如果此时应用程序(应用程序是都属于用户态的)需要访问核心资源怎么办呢?那就需要调用内核中所暴露的接口调用,称为系统调用。例如此时我们应用程序需要访问磁盘上的文件。此时应用程序就会调用系统调用的接口open方法,然后内核去访问磁盘中的文件,将文件内容返回给应用程序。

图片

既然我们要读取一个磁盘的文件,要废这么大的周折。有没有什么简单的方法能够使我们的应用直接操作磁盘文件,不需要内核进行中转呢?有,那就是建立直接归档了。

非直接竞争:非直接歧视就是我们上面所讲的内核态作为中间人,每次都需要内核在中间作为中转。

图片

直接分区:直接偏置不需要内核空间作为中转副本数据,或者直接在物理内存申请一块空间,这块空间映射到内核地址空间和用户地址空间,应用程序与磁盘之间数据的访问通过这块直接申请的物理内存进行交互。

图片

既然直接缩短那么快,我们为什么不都用直接盯呢呢?实际上直接切断有以下的缺点。直接截断的缺点:

不安全

消耗更多,因为它不是在JVM中直接开辟空间。这部分内存的回收只能依赖于垃圾回收机制,垃圾什么时候回收不受我们控制。

数据写入物理内存缓冲区中,程序就丢失了对这些数据的管理,即什么时候这些数据被最终写入从磁盘只能由操作系统来决定,应用程序无法重新插入。

综上所述,所以我们使用transferTo方法就是直接开辟了一段直接短路。所以性能相对而言提高了很多

NIO中新出的另一个特性就是内存映射文件,内存映射文件为什么速度快呢?其实原因和上面所讲的一样,也是在内存中开辟了一段直接分开。与数据直接作交互。

//Version4使用Map映射文件publicstaticvoidzipFileMap(){//开始时间longbeginTime=System.currentTimeMillis();FilezipFile=newFile(ZIP_FILE);try(ZipOutputStreamzipOut=newZipOutputStream(newFileOutputStream(zipFile));WritableByteChannelwritableByteChannel=Channels.newChannel(zipOut)){for(inti=0;i<10;i++){zipOut.putNextEntry(newZipEntry(i+SUFFIX_FILE));//内存中的映射文件MappedByteBuffermappedByteBuffer=newRandomAccessFile(JPG_FILE_PATH,"r").getChannel().map(FileChannel.MapMode.READ_ONLY,0,FILE_SIZE);writableByteChannel.write(mappedByteBuffer);}printInfo(beginTime);}catch(Exceptione){e.printStackTrace();}}打印如下

---------MapfileSize:20Mconsumtime:1305可以看到速度和使用Channel的速度差不多的。

JavaNIO管道是2个线程之间的单向数据连接。Pipe有一个源通道和一个接收器通道。其中的源通道用于读取数据,接收器用于写入数据。如果没有数据偏移,读线程也会重叠至写入线程写入数据。直到通道关闭。

Whetherornotathreadwritingbytestoapipewillblockuntilanotherthreadreadsthosebytes图片

我想要的效果是这样的。

//Version5使用PippublicstaticvoidzipFilePip(){longbeginTime=System.currentTimeMillis();try(WritableByteChannelout=Channels.newChannel(newFileOutputStream(ZIP_FILE))){Pipepipe=Pipe.open();//异步任务CompletableFuture.runAsync(()->runTask(pipe));//获取读通道ReadableByteChannelreadableByteChannel=pipe.source();ByteBufferbuffer=ByteBuffer.allocate(((int)FILE_SIZE)*10);while(readableByteChannel.read(buffer)>=0){buffer.flip();out.write(buffer);buffer.clear();}}catch(Exceptione){e.printStackTrace();}printInfo(beginTime);}//异步任务publicstaticvoidrunTask(Pipepipe){try(ZipOutputStreamzos=newZipOutputStream(Channels.newOutputStream(pipe.sink()));WritableByteChannelout=Channels.newChannel(zos)){System.out.println("Begin");for(inti=0;i<10;i++){zos.putNextEntry(newZipEntry(i+SUFFIX_FILE));FileChanneljpgChannel=newFileInputStream(newFile(JPG_FILE_PATH)).getChannel();jpgChannel.transferTo(0,FILE_SIZE,out);jpgChannel.close();}}catch(Exceptione){e.printStackTrace();}}总结生活处处都需要学习,有时候只是一个简单的优化,可以让你深入学习到各种不同的知识。所以在学习中要不求甚解,完全要知道这个知识也要了解为什么要这么做。

知行合一:学习完一个知识要努力应用一遍。这样才能记得牢靠。

https://www.jianshu.com/p/f90866dcbffc

https://juejin.im/post/5af942c6f265da0b7026050c

趣谈Linux操作系统

JAVANIO直接透视和非直接重叠

程序员专栏

扫码关注填加客服

长按识别下方二维码进群

近期精彩内容推荐:

在看点这里好文分享给更多人↓↓