天天看點

自定義APngDrawable背景Android 平台播放Apng開源庫資源狀況改進方案實作細節性能表現總結GIT 位址

自定義APngDrawable

  • 背景
  • Android 平台播放Apng開源庫資源狀況
  • 改進方案
  • 實作細節
  • 性能表現
  • 總結
  • GIT 位址

背景

Android手機裝置性能越來越好,是以在app界面設計的時候經常會使用到動圖,這樣畫面顯示會更加的豐富多彩。Android程式支援幀動畫的方式播放動圖,但是這種方式需要将所有的幀Bitmap加載到記憶體。Bitmap是app中消耗記憶體大戶,申請Bitmap對象的多少直接影響app的性能。是以在app 應用中多數情況下不會選擇使用幀動畫播放動圖效果。Gif也是一種動圖的格式,Android系統不支援gif播放。開發者可以通過引用開源庫進行gif播放。由于Gif不支援真彩色,它隻能使用256色,是以色階過度不自然,顯示的圖檔多顆粒感。同時Gif也不支援alpha通道,這直接導緻了圖像邊緣過度生硬,有鋸齒。

Apng格式是Png的擴充格式,它支援動圖顯示。Apng格式支援真彩色和alpha通道,在色彩方面表現更加飽滿,同時由于增加了alpha通道控制色彩透明度,圖像的邊緣處理更加柔和,沒有明顯的鋸齒。Apng除了應用到了幀壓縮技術,他還使用到了幀間壓縮技術。簡單了解就是把兩幀進行比較,儲存幀間差異。當顯示幀資料時,通過幀控制資訊、上一幀和差異幀資料合成目前幀資料。由于apng支援幀間資料壓縮技術,是以圖檔的壓縮比例更高。

因為apng在色彩表現和壓縮比例上有更好的表現,是以Android app通常會選擇支援apng動圖播放。

Android 平台播放Apng開源庫資源狀況

我們可以輕松地在網上找到Android Apng開源庫,開源庫的實作思路包括兩種:

1.解碼apng檔案中的所有幀到記憶體中,通過自定義Drawable描繪apng每一幀資料。這種實作的問題是占用記憶體較大,影響應用性能。

2.解碼apng檔案中的所有幀并把每一幀資料按照png格式(android系統支援的格式)儲存到外部存儲器,當播放的時候,app可以從外部存儲器加載每幀圖檔。這種實作可以有效減小記憶體的使用,但是儲存到外部存儲器上的幀圖檔的完整性不好保證。這些檔案都是大檔案,他們被清理程式清理掉的機率更大。

方案2的處理使應用的運作有更好的性能,隻是我們需要檢查本地幀檔案的完整性。

改進方案

參考視訊播放器的實作原理,考慮是否可以按照流的方式播放apng。基本的實作步驟包括下面幾個内容:

  1. 建立InputStream用于讀取apng檔案。
  2. 從InputStream中按照apng格式讀取幀資料。資料類型包括頭資料、動畫控制資料、幀控制資料、幀資料等。(采用pngj【https://github.com/leonbloy/pngj】開源庫讀取apng,幀資料不用解碼,在播放時通過系統方法解碼)
  3. 根據幀控制資料和幀資料合成png格式資料。
  4. 通過系統的BitmapFactory 解碼png格式資料并生成Bitmap用于播放。
  5. 通過自定義Drawable播放生成的Bitmap。

實作細節

  1. 首先我們需要擴充PngReader類的實作,重寫createChunkSeqReader方法。重寫它的目的是定制生成ChunkSeqReaderPng。
  2. 通過覆寫ChunkSeqReaderPng 中的createChunkReaderForNewChunk方法定制ChunkReader。
  3. 定制的ChunkReader可以靈活的控制apng資料的讀取。根據讀取的資料合成png格式的資料。
  4. 使用BitmapFactory解碼png資料并生成Bitmap用于顯示。

定制ChunkReader代碼

protected ChunkReader createChunkReaderForNewChunk(final String id, final int lenOut, long offset, final boolean skip) {
    //由于IDAT和FDAT資料占用記憶體多,是以進行記憶體共享的定制化處理。減少記憶體的頻繁申請和釋放,減輕GC負擔。
    if (id.equals(PngChunkIDAT.ID) || id.equals(PngChunkFDAT.ID)) {
        return new ChunkReader(lenOut, id, offset, PROCESS) {
            CRC32 crc32 = null;
            public byte[] crcval = new byte[4];

            protected void chunkDone() {
                //每一幀資料結束後需要将crc檢驗資料追加在末尾。
                PngHelperInternal.writeInt4tobytes((int) crc32.getValue(), crcval, 0);
                dataInfo.write(crcval, 0, 4);
                crc32 = null;
            }

            protected void processData(int offsetinChhunk, byte[] buf, int off, int len) {
                if (crc32 == null) {
                    crc32 = new CRC32();
                    crc32.update(ChunkHelper.b_IDAT);
                    //寫入幀資料長度,由于FDAT資料開頭多了一個sequence number,是以長度要減4。
                    PngHelperInternal.writeInt4(dataInfo, id.equals(PngChunkIDAT.ID) ? lenOut : lenOut - 4);
                    //寫入資料類型。
                    PngHelperInternal.writeBytes(dataInfo, ChunkHelper.b_IDAT);
                }
                //寫入buffer資料
                if (id.equals(PngChunkIDAT.ID)) {
                    crc32.update(buf, off, len);
                    dataInfo.write(buf, off, len);
                } else {
                    //FDAT資料寫入時需要跳過sequence number資料。
                    if (offsetinChhunk >= 4) {
                        crc32.update(buf, off, len);
                        dataInfo.write(buf, off, len);
                    } else {
                        int skip = (4 - offsetinChhunk);
                        if (len <= skip) {
                            //do nothing.
                        } else {
                            crc32.update(buf, off + skip, len - skip);
                            dataInfo.write(buf, off + skip, len - skip);
                        }
                    }
                }
            }
        };
    }
    //IDAT和FDAT以外類型的資料占用空間較小,是以沒有使用共享記憶體。
    return super.createChunkReaderForNewChunk(id, lenOut, offset, skip);
}
           

解析控制資訊的代碼

@Override
protected void postProcessChunk(ChunkReader chunkR) {
    super.postProcessChunk(chunkR);
    processChunk(chunkR);
}

private void processChunk(ChunkReader chunkReader) {
    final String id = chunkReader.getChunkRaw().id;
    if (DEBUG) {
        Log.d(TAG, "processChunk " + id);
    }
    switch (id) {
        case PngChunkIHDR.ID: {//讀取apng的頭資訊
            pngChunkIHDR = (PngChunkIHDR) chunksList.getChunks().get(chunksList.getChunks().size() - 1);
            break;
        }
        case PngChunkACTL.ID: {//讀取apng動畫控制資訊
            pngChunkACTL = (PngChunkACTL) chunksList.getChunks().get(chunksList.getChunks().size() - 1);
            break;
        }
        case PngChunkFCTL.ID: {//根據幀控制資訊控制寫入png的頭尾資料
            initFrameCommonInfo();
            //write previous frame onDecodeEnd.
            if (needWriteEndBeforeWriteHeader) {
                writeEnd();
            }
            //write current header.
            writeHeader();
            //write common.
            dataInfo.write(commonInfoArray, 0, commonInfoArray.length);
            needWriteEndBeforeWriteHeader = true;
            break;
        }
        case PngChunkIDAT.ID: {//apng幀控制資訊,由于定制了ChunkReader。是以這裡不會調用
            readerCallback.onDecodeStart(APngDecoder.this);
            //write data.
            chunkReader.getChunkRaw().writeChunk(dataInfo);
            break;
        }
        case PngChunkFDAT.ID: {//apng幀控制資訊,由于定制了ChunkReader。是以這裡不會調用
            //write data
            ChunkRaw chunkRaw = new ChunkRaw(chunkReader.getChunkRaw().len - 4, ChunkHelper.b_IDAT, true);
            System.arraycopy(chunkReader.getChunkRaw().data, 4, chunkRaw.data, 0, chunkRaw.data.length);
            chunkRaw.writeChunk(dataInfo);
            break;
        }
        case PngChunkIEND.ID://apng檔案結束辨別
            writeEnd();
            readerCallback.onDecodeEnd(APngDecoder.this);
            break;
        default:
            break;
    }
}
//寫入png頭
private void writeHeader() {
    frameIndex++;
    pngChunkFCTL = (PngChunkFCTL) chunksList.getChunks().get(chunksList.getChunks().size() - 1);
    ImageInfo frameInfo = pngChunkFCTL.getEquivImageInfo();
    dataInfo.reset();
    //write signature.
    byte[] pngIdSignature = PngHelperInternal.getPngIdSignature();
    dataInfo.write(pngIdSignature, 0, pngIdSignature.length);
    //write header.
    new PngChunkIHDR(frameInfo).createRawChunk().writeChunk(dataInfo);
}
//寫入png結束辨別
private void writeEnd() {
    fetchFrameDone = true;
    new PngChunkIEND(null).createRawChunk().writeChunk(dataInfo);
    //通過png資料生成Bitmap。
    FrameData frameData = generateFrameDate(dataInfo.buffer(), dataInfo.size());
    mainFrameData = null;
    if (frameData != null) {
        readerCallback.onDecodeFrame(APngDecoder.this, frameData);
    }
}
           

生成Bitmap的代碼

private FrameData generateFrameDate(byte[] data, int length) {
    //使用png data生成Bitmap
    frameBitmap = BitmapFactory.decodeByteArray(data, 0, length, generateOptions(frameBitmap));
    final Canvas canvas = new Canvas(mainFrameData.bitmap);
    //Clear the canvas and draw cached bitmap(Previous content in cached bitmap).
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
    //繪制上一幀的緩存資料。
    if (frameIndex != 0) {
        canvas.drawBitmap(cachedBitmap, 0, 0, null);
    } else {
        canvas.drawBitmap(frameBitmap, 0, 0, null);
    }
    //Clear color in current frame position.
    //根據幀控制資訊将上幀資料進行摳圖
    if (pngChunkFCTL.getBlendOp() == PngChunkFCTL.APNG_BLEND_OP_SOURCE) {
        canvas.save();
        canvas.clipRect(pngChunkFCTL.getxOff(), pngChunkFCTL.getyOff(), pngChunkFCTL.getxOff() + frameBitmap.getWidth(), pngChunkFCTL.getyOff() + frameBitmap.getHeight());
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        canvas.restore();
    }
    //Cached the background before draw current frame.
    //根據幀控制資訊緩存合成前的畫面
    if (pngChunkFCTL.getDisposeOp() == APNG_DISPOSE_OP_BACKGROUND) {
        Canvas tempCanvas = new Canvas(cachedBitmap);
        tempCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        tempCanvas.drawBitmap(mainFrameData.bitmap, 0, 0, null);
    }
    //Draw current frame.
    //合成目前幀。
    canvas.drawBitmap(frameBitmap, pngChunkFCTL.getxOff(), pngChunkFCTL.getyOff(), null);
    //Cached the whole content after draw current frame.
    //根據控制資訊緩存好合成後的資料。
    if (pngChunkFCTL.getDisposeOp() == APNG_DISPOSE_OP_NONE) {
        Canvas tempCanvas = new Canvas(cachedBitmap);
        tempCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        tempCanvas.drawBitmap(mainFrameData.bitmap, 0, 0, null);
    }

    mainFrameData.index = frameIndex;
    mainFrameData.delay = (pngChunkFCTL.getDelayNum() * 1000 / pngChunkFCTL.getDelayDen());
    mainFrameData.firstDrawTime = 0;
    mainFrameData.frameCount = pngChunkACTL.getNumFrames();
    return mainFrameData;
}
           

性能表現

  1. 由于解析幀資料的時候,采用了共享的記憶體,是以在解碼apng檔案的過程中沒有給GC增加過多的負擔。
  2. 播放過程中最多隻緩存兩幀資料,并且這兩幀資料也是反複回收利用,是以占用了較少的記憶體。
  3. 在播放相同的apng檔案時,apng解碼器也是可以被共享的,是以在同一界面下同時播放相同的apng檔案的view可以隻使用一個apng,并且他們的播放是同步的,記憶體的占用也隻有一份。

總結

APngDrawable可以按照流的方式播放apng檔案。如果播放的是同一個apng檔案,那麼多個APngDrawable之間可以共享apng decoder和frame data。這樣處理可以節省大量的記憶體資源。在同一時刻APngDrawable隻緩存兩幀資料,并且不需要把apng檔案拆分成多個png檔案儲存到本地用于播放,是以播放過程中不需要檢查資料的完整性,同時也沒有占用更多的記憶體空間。

GIT 位址

https://github.com/mjlong123123/PlayAPng/releases/tag/1.0.0

我的公衆号已經開通,公衆号會同步釋出。

歡迎關注我的公衆号

自定義APngDrawable背景Android 平台播放Apng開源庫資源狀況改進方案實作細節性能表現總結GIT 位址