自定义APngDrawable

背景

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

我的公众号已经开通,公众号会同步发布。
欢迎关注我的公众号

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mjlong123123

你的鼓励时我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值