個人ImageIO.write ByteArrayOutputStream爲何這麼慢?

File.createTempFile(prefix, suffix),建立一個臨時文件,再使用完以後清理便可。
可是遇到以下兩個坑:centos

String prefix = "temp"; String suffix = ".txt"; File tempFile = File.createTempFile(prefix, suffix);

以上代碼中,須要注意的兩個地方:
一、prefix必須大於3個字符
二、suffix須要帶上 . , 好比:.png、.zip緩存



問題來源:

      1.系統生成二維碼,須要不一樣的圖片格式來適應客戶端要求app

      2.圖片經過接口模式給客戶端,最終使用base64來傳遞dom

 

日常思考模式:

     1.BufferedImage首先經過工具把數據生成出來。工具

     2.我絕對不會把這個BufferedImage寫磁盤,直接放內存ByteArrayOutputstream後轉base64豈不是更快?測試

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

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

 

實際狀況:

    1.Linux環境centos6.8 虛擬化環境spa

    2.JRE1.8.net

    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內

https://my.oschina.net/u/2461727/blog/3024892?from=timeline&isappinstalled=0

相關文章
相關標籤/搜索