我的ImageIO.write ByteArrayOutputStream为什么这么慢?


声明:本文转载自https://my.oschina.net/u/2461727/blog/3024892,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

问题来源:

      1.系统生成二维码,需要不同的图片格式来适应客户端要求

      2.图片通过接口模式给客户端,最终使用base64来传递

 

平常思考模式:

     1.BufferedImage首先通过工具把数据生成出来。

     2.我绝对不会把这个BufferedImage写磁盘,直接放内存ByteArrayOutputstream后转base64岂不是更快?

     3.ImageIO.write正好有个write(BufferedImage img,String format,OutputStream output)

     4.真的舒服,我就用它了!

 

实际情况:

    1.Linux环境centos6.8 虚拟化环境

    2.JRE1.8

    3.接口工作流程:(1) 生成BufferedImage (2) BufferedImage通过ImageIO.write(BufferedImage,"png",ByteArrayOutputStream out) (3)将ByteArrayOutputStream转化为base64 (4) 接口返回

    4.一个普通的链接,生成二维码并返回base64,接口耗时1.7S

    5.png图片大小16K

 

分析问题&尝试更换接口:

     1.一个图片生成16K,不大

     2.一次请求1.7s,又是手机端应用,太慢了!不能接受

     3.根据代码跟踪分析得出速度慢在 ImageIO.write这里

     4.网上搜索信息也有相关的反馈说ImageIO.write png的时候奇慢无比,但是没有找到实际解决方法

     5.尝试更换write的ByteArrayOutputStream为File,因为 ImageIO.write正好支持写文件ImageIO.write(BufferedImage,"png",File out)

     6.测试结果:write到file后,接口响应时间在400ms!!!

 

查看源代码:

     1.对比write到Byte和File的源代码发现,使用ByteArrayOutputStream的底层写数据的时候使用了FileCacheImageOutputStream,而使用File的底层写数据的时候使用了FileImageOutputStream。

     2.查看FileCacheImageOutputStream的初始化方式、和写数据相关代码

     

//初始化代码
public FileCacheImageOutputStream(OutputStream stream, File cacheDir)
        throws IOException {
        if (stream == null) {
            throw new IllegalArgumentException("stream == null!");
        }
        if ((cacheDir != null) && !(cacheDir.isDirectory())) {
            throw new IllegalArgumentException("Not a directory!");
        }
        this.stream = stream;
        //这里竟然创建了临时文件
        if (cacheDir == null)
            this.cacheFile = Files.createTempFile("imageio", ".tmp").toFile();
        else
            this.cacheFile = Files.createTempFile(cacheDir.toPath(), "imageio", ".tmp")
                                  .toFile();
        this.cache = new RandomAccessFile(cacheFile, "rw");

        this.closeAction = StreamCloser.createCloseAction(this);
        StreamCloser.addToQueue(closeAction);
    }


// 写数据,没什么特殊
 public void write(int b) throws IOException {
        flushBits(); // this will call checkClosed() for us
        cache.write(b);
        ++streamPos;
        maxStreamPos = Math.max(maxStreamPos, streamPos);
    }


//关闭
public void close() throws IOException {
        maxStreamPos = cache.length();

        seek(maxStreamPos);
        //注意这里!!!!!
        flushBefore(maxStreamPos);
        super.close();
        cache.close();
        cache = null;
        cacheFile.delete();
        cacheFile = null;
        stream.flush();
        stream = null;
        StreamCloser.removeFromQueue(closeAction);
    }

//把数据写入ByteArrayOutputStream
public void flushBefore(long pos) throws IOException {
        long oFlushedPos = flushedPos;
        super.flushBefore(pos); // this will call checkClosed() for us

        long flushBytes = flushedPos - oFlushedPos;
        if (flushBytes > 0) {
            // 这里使用了一个逻辑每次只读512个字节到stream里面!!然后循环
            int bufLen = 512;
            byte[] buf = new byte[bufLen];
            cache.seek(oFlushedPos);
            while (flushBytes > 0) {
                int len = (int)Math.min(flushBytes, bufLen);
                cache.readFully(buf, 0, len);
                stream.write(buf, 0, len);
                flushBytes -= len;
            }
            stream.flush();
        }
    }

      3.而FileImageOutputStream 的相关代码如下,都很中规中矩没有什么特殊

//初始化
public FileImageOutputStream(File f)
        throws FileNotFoundException, IOException {
        this(f == null ? null : new RandomAccessFile(f, "rw"));
    }


//写数据
public void write(int b) throws IOException {
        flushBits(); // this will call checkClosed() for us
        raf.write(b);
        ++streamPos;
    }

//关闭
 public void close() throws IOException {
        super.close();
        disposerRecord.dispose(); // this closes the RandomAccessFile
        raf = null;
    }

 

分析源代码:

      1.使用了cache的方式对数据读取和写入做了优化,为了防止内存溢出他已512字节读取然后写入输出流。但是当写到ByteArrayOutputStream的时候反而显得笨拙,一个16k的图片走cache的方式需要反复读取32次。

      2.使用了普通模式的读取写入数据中规中矩,而读取因为了解文件大小都在16k左右,我采用了一次性读取到内存,所以将File类型的读取到内存再转化base64的时候,只发生了1次磁盘IO

 

结论:

    1. 我们不能被代码外表所欺骗,乍一眼觉得写内存肯定比写File要快。

    2.FileCacheImageOutputStream的出发点是好的,分批次数据读取然后写输出流

    3.ImageIO.write 下面这出代码针对ByteArrayOutputStream的策略选择有失误:

 while (iter.hasNext()) {
            ImageOutputStreamSpi spi = (ImageOutputStreamSpi)iter.next();
            if (spi.getOutputClass().isInstance(output)) {
                try {
                  //针对ByteArrayOutputStream输出流选择 ImageOutputStream的实现不理想
                    return spi.createOutputStreamInstance(output,
                                                          usecache,
                                                          getCacheDirectory());
                } catch (IOException e) {
                    throw new IIOException("Can't create cache file!", e);
                }
            }
        }

     磁盘缓存对ByteArray输出流没有效果,该溢出的还是会溢出,还不如直接不使用cache

     4.最终我们采用了先写图片到磁盘文件,然后读取文件转base64再返回,接口稳定在了 400ms内

 

本文发表于2019年03月20日 13:00
(c)注:本文转载自https://my.oschina.net/u/2461727/blog/3024892,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 1502 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1