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

問題來源:

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

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

 

日常思考模式:

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

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

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

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

 

實際狀況:

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

    2.JRE1.8this

    3.接口工做流程:(1) 生成BufferedImage (2) BufferedImage經過ImageIO.write(BufferedImage,"png",ByteArrayOutputStream out) (3)將ByteArrayOutputStream轉化爲base64 (4) 接口返回spa

    4.一個普通的連接,生成二維碼並返回base64,接口耗時1.7Scode

    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內

相關文章
相關標籤/搜索