自定義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。基本的實作步驟包括下面幾個内容:
- 建立InputStream用于讀取apng檔案。
- 從InputStream中按照apng格式讀取幀資料。資料類型包括頭資料、動畫控制資料、幀控制資料、幀資料等。(采用pngj【https://github.com/leonbloy/pngj】開源庫讀取apng,幀資料不用解碼,在播放時通過系統方法解碼)
- 根據幀控制資料和幀資料合成png格式資料。
- 通過系統的BitmapFactory 解碼png格式資料并生成Bitmap用于播放。
- 通過自定義Drawable播放生成的Bitmap。
實作細節
- 首先我們需要擴充PngReader類的實作,重寫createChunkSeqReader方法。重寫它的目的是定制生成ChunkSeqReaderPng。
- 通過覆寫ChunkSeqReaderPng 中的createChunkReaderForNewChunk方法定制ChunkReader。
- 定制的ChunkReader可以靈活的控制apng資料的讀取。根據讀取的資料合成png格式的資料。
- 使用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;
}
性能表現
- 由于解析幀資料的時候,采用了共享的記憶體,是以在解碼apng檔案的過程中沒有給GC增加過多的負擔。
- 播放過程中最多隻緩存兩幀資料,并且這兩幀資料也是反複回收利用,是以占用了較少的記憶體。
- 在播放相同的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
我的公衆号已經開通,公衆号會同步釋出。
歡迎關注我的公衆号