帧动画内存OOM?不存在的!—— SurfaceView逐帧解析

栏目: IOS · Android · 发布时间: 6年前

内容简介:Android 提供了有没有什么办法让帧动画的数据逐帧加载,而不是一次性全部加载到内存?屏幕的显示机制和帧动画类似,也是一帧一帧的连环画,只不过刷新频率很高,感觉像连续的。为了显示一帧,需要经历计算和渲染两个过程,CPU 先计算出这一帧的图像数据并写入内存,然后调用 OpenGL 命令将内存中数据渲染成图像存放在 GPU Buffer 中,显示设备每隔一定时间从 Buffer 中获取图像并显示。

Android 提供了 AnimationDrawable 用于实现帧动画。在动画开始之前,所有帧的图片都被解析并占用内存,一旦动画较复杂帧数较多,在低配置手机上容易发生 OOM。即使不发生 OOM,也会对内存造成不小的压力。下面代码展示了一个帧数为4的帧动画:

原生帧动画

AnimationDrawable drawable = new AnimationDrawable();
drawable.addFrame(getDrawable(R.drawable.frame1), frameDuration);
drawable.addFrame(getDrawable(R.drawable.frame2), frameDuration);
drawable.addFrame(getDrawable(R.drawable.frame3), frameDuration);
drawable.addFrame(getDrawable(R.drawable.frame4), frameDuration);
drawable.setOneShot(true);

ImageView ivFrameAnim = ((ImageView) findViewById(R.id.frame_anim));
ivFrameAnim.setImageDrawable(drawable);
drawable.start();
复制代码

有没有什么办法让帧动画的数据逐帧加载,而不是一次性全部加载到内存? SurfaceView 就提供了这种能力。

SurfaceView

屏幕的显示机制和帧动画类似,也是一帧一帧的连环画,只不过刷新频率很高,感觉像连续的。为了显示一帧,需要经历计算和渲染两个过程,CPU 先计算出这一帧的图像数据并写入内存,然后调用 OpenGL 命令将内存中数据渲染成图像存放在 GPU Buffer 中,显示设备每隔一定时间从 Buffer 中获取图像并显示。

上述过程中的计算,对于 View 来说,就好比在主线程遍历 View树 以决定视图画多大(measure),画在哪(layout),画些啥(draw),计算结果存放在内存中,SurfaceFlinger 会调用 OpenGL 命令将内存中的数据渲染成图像存放在 GPU Buffer 中。每隔16.6ms,显示器从 Buffer 中取出帧并显示。所以自定义 View 可以通过重载 onMeasure()onLayout()onDraw() 来定义帧内容,但不能定义帧刷新频率。

SurfaceView 可以突破这个限制。而且它可以将计算帧数据放到独立的线程中进行。下面是自定义 SurfaceView 的模版代码:

public abstract class BaseSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    public static final int DEFAULT_FRAME_DURATION_MILLISECOND = 50;
    //用于计算帧数据的线程
    private HandlerThread handlerThread;
    private Handler handler;
    //帧刷新频率
    private int frameDuration = DEFAULT_FRAME_DURATION_MILLISECOND;
    //用于绘制帧的画布
    private Canvas canvas;
    private boolean isAlive;

    public BaseSurfaceView(Context context) {
        super(context);
        init();
    }

    protected void init() {
        getHolder().addCallback(this);
        //设置透明背景,否则SurfaceView背景是黑的
        setBackgroundTransparent();
    }

    private void setBackgroundTransparent() {
        getHolder().setFormat(PixelFormat.TRANSLUCENT);
        setZOrderOnTop(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        isAlive = true;
        startDrawThread();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        stopDrawThread();
        isAlive = false;
    }

    //停止帧绘制线程
    private void stopDrawThread() {
        handlerThread.quit();
        handler = null;
    }

    //启动帧绘制线程
    private void startDrawThread() {
        handlerThread = new HandlerThread("SurfaceViewThread");
        handlerThread.start();
        handler = new Handler(handlerThread.getLooper());
        handler.post(new DrawRunnable());
    }

    private class DrawRunnable implements Runnable {

        @Override
        public void run() {
            if (!isAlive) {
                return;
            }
            try {
                //1.获取画布
                canvas = getHolder().lockCanvas();
                //2.绘制一帧
                onFrameDraw(canvas);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //3.将帧数据提交
                getHolder().unlockCanvasAndPost(canvas);
                //4.一帧绘制结束
                onFrameDrawFinish();
            }
            //不停的将自己推送到绘制线程的消息队列以实现帧刷新
            handler.postDelayed(this, frameDuration);
        }
    }
    
    protected abstract void onFrameDrawFinish();

    protected abstract void onFrameDraw(Canvas canvas);
}
复制代码
  • HandlerThread 作为独立帧绘制线程,好处是可以通过与其绑定的 Handler 方便地实现“每隔一段时间刷新”,而且在 Surface 被销毁的时候可以方便的调用 HandlerThread.quit() 来结束线程执行的逻辑。
  • DrawRunnable.run() 运用模版方法模式定义了绘制算法框架,其中帧绘制逻辑的具体实现被定义成两个抽象方法,推迟到子类中实现。本文的主角 FrameSurfaceView 应该继承自 BaseSurfaceView

逐帧解析 & 及时回收

public class FrameSurfaceView extends BaseSurfaceView {
    public static final int INVALID_BITMAP_INDEX = Integer.MAX_VALUE;
    private List<Integer> bitmaps = new ArrayList<>();
    //帧图片
    private Bitmap frameBitmap;
    //帧索引
    private int bitmapIndex = INVALID_BITMAP_INDEX;
    private Paint paint = new Paint();
    private BitmapFactory.Options options = new BitmapFactory.Options();
    //帧图片原始大小
    private Rect srcRect;
    //帧图片目标大小
    private Rect dstRect = new Rect();
    private int defaultWidth;
    private int defaultHeight;

    public void setDuration(int duration) {
        int frameDuration = duration / bitmaps.size();
        setFrameDuration(frameDuration);
    }

    public void setBitmaps(List<Integer> bitmaps) {
        if (bitmaps == null || bitmaps.size() == 0) {
            return;
        }
        this.bitmaps = bitmaps;
        //默认情况下,计算一帧图片的原始大小
        getBitmapDimension(bitmaps.get(0));
    }
    
    private void getBitmapDimension(Integer integer) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(this.getResources(), integer, options);
        defaultWidth = options.outWidth;
        defaultHeight = options.outHeight;
        srcRect = new Rect(0, 0, defaultWidth, defaultHeight);
        requestLayout();
    }

    public FrameSurfaceView(Context context) {
        super(context);
    }
    
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        dstRect.set(0, 0, getWidth(), getHeight());
    }

    @Override
    protected void onFrameDrawFinish() {
        //在一帧绘制完后,直接回收它
        recycleOneFrame();
    }

    //回收帧
    private void recycleOneFrame() {
        if (frameBitmap != null) {
            frameBitmap.recycle();
            frameBitmap = null;
        }
    }

    @Override
    protected void onFrameDraw(Canvas canvas) {
        //绘制一帧前需要先清画布,否则所有帧都叠在一起同时显示
        clearCanvas(canvas);
        if (!isStart()) {
            return;
        }
        if (!isFinish()) {
            drawOneFrame(canvas);
        } else {
            onFrameAnimationEnd();
        }
    }

    //绘制一帧,是张Bitmap
    private void drawOneFrame(Canvas canvas) {
        frameBitmap = BitmapUtil.decodeOriginBitmap(getResources(), bitmaps.get(bitmapIndex), options);
        canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);
        bitmapIndex++;
    }

    private void onFrameAnimationEnd() {
        reset();
    }

    private void reset() {
        bitmapIndex = INVALID_BITMAP_INDEX;
    }

    //帧动画是否结束
    private boolean isFinish() {
        return bitmapIndex >= bitmaps.size();
    }

    //帧动画是否开始
    private boolean isStart() {
        return bitmapIndex != INVALID_BITMAP_INDEX;
    }
    
    //开始播放帧动画
    public void start() {
        bitmapIndex = 0;
    }

    private void clearCanvas(Canvas canvas) {
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        canvas.drawPaint(paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
    }
}
复制代码
  • FrameSurfaceView 继承自 BaseSurfaceView ,所以它复用了基类的绘制框架算法,并且定了自己每一帧的绘制内容:一张 Bitmap
  • Bitmap 资源 id 通过 setBitmap() 传递进来, 绘制一帧解析一张 ,在每一帧绘制完毕后,调用 Bitmap.recycle() 释放图片 native 内存并去除 java 堆中图片像素数据的引用。这样当 GC 发生时,图片像素数据可以及时被回收。

一切都是这么地能够自圆其说,我迫不及待地运行代码并打开 AndroidStudioProfiler 标签页,切换到 MEMORY ,想用真实内存数据验证下性能。但残酷的事实狠狠地打了下脸。。。多次播放帧动画后,内存占用居然比原生 AnimationDrawable 还大,而且每播放一次,内存中都会多出 N 个 Bitmap 对象(N为播放一边的帧数)。唯一令人欣慰的是,手动触发 GC 后帧动画图片能够被回收。( AnimationDrawable 中的图片数据不会被 GC)

原因就在于自作聪明地及时回收,每一帧绘制完后帧数据被回收,那下一帧解析 Bitmap 时只能新申请一块内存。帧动画每张图片大小是一致的,是不是能复用上一帧 Bitmap 的内存空间?于是乎有了下面这个版本的 FrameSurfaceView

逐帧解析 & 帧复用

public class FrameSurfaceView extends BaseSurfaceView {
    public static final int INVALID_BITMAP_INDEX = Integer.MAX_VALUE;
    private List<Integer> bitmaps = new ArrayList<>();
    private Bitmap frameBitmap;
    private int bitmapIndex = INVALID_BITMAP_INDEX;
    private Paint paint = new Paint();
    private BitmapFactory.Options options;
    private Rect srcRect;
    private Rect dstRect = new Rect();

    public void setDuration(int duration) {
        int frameDuration = duration / bitmaps.size();
        setFrameDuration(frameDuration);
    }

    public void setBitmaps(List<Integer> bitmaps) {
        if (bitmaps == null || bitmaps.size() == 0) {
            return;
        }
        this.bitmaps = bitmaps;
        getBitmapDimension(bitmaps.get(0));
    }

    private void getBitmapDimension(Integer integer) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(this.getResources(), integer, options);
        defaultWidth = options.outWidth;
        defaultHeight = options.outHeight;
        srcRect = new Rect(0, 0, defaultWidth, defaultHeight);;
    }

    public FrameSurfaceView(Context context) {
        super(context);
    }

    @Override
    protected void init() {
        super.init();
        //定义解析Bitmap参数为可变类型,这样才能复用Bitmap
        options = new BitmapFactory.Options();
        options.inMutable = true;
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        dstRect.set(0, 0, getWidth(), getHeight());
    }

    @Override
    protected int getDefaultWidth() {
        return defaultWidth;
    }

    @Override
    protected int getDefaultHeight() {
        return defaultHeight;
    }

    @Override
    protected void onFrameDrawFinish() {
        //每帧绘制完毕后不再回收
//        recycle();
    }

    public void recycle() {
        if (frameBitmap != null) {
            frameBitmap.recycle();
            frameBitmap = null;
        }
    }

    @Override
    protected void onFrameDraw(Canvas canvas) {
        clearCanvas(canvas);
        if (!isStart()) {
            return;
        }
        if (!isFinish()) {
            drawOneFrame(canvas);
        } else {
            onFrameAnimationEnd();
        }
    }

    private void drawOneFrame(Canvas canvas) {
        frameBitmap = BitmapUtil.decodeOriginBitmap(getResources(),
        //复用Bitmap
        bitmaps.get(bitmapIndex), options);
        options.inBitmap = frameBitmap;
        canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);
        bitmapIndex++;
    }

    private void onFrameAnimationEnd() {
        reset();
    }

    private void reset() {
        bitmapIndex = INVALID_BITMAP_INDEX;
    }

    private boolean isFinish() {
        return bitmapIndex >= bitmaps.size();
    }

    private boolean isStart() {
        return bitmapIndex != INVALID_BITMAP_INDEX;
    }

    public void start() {
        bitmapIndex = 0;
    }

    private void clearCanvas(Canvas canvas) {
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        canvas.drawPaint(paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
    }
}
复制代码
  • Bitmap 的解析参数 inBitmap 设置为已经成功解析的 Bitmap 对象以实现复用。

这一次不管重新播放多少次帧动画,内存中 Bitmap 数量只会增加1,因为只在解析第一张图片是分配了内存。而这块内存可以在 FrameSurfaceView 生命周期结束时手动调用 recycle() 回收。

talk is cheap, show me the code

为了更清晰的展示,上述代码段省略了一些和主题无关的自定义 View 细节,完整的代码可以点击 这里


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Security, Privacy and Commerce, 2nd Edition

Web Security, Privacy and Commerce, 2nd Edition

Simson Garfinkel / O'Reilly Media / 2002-01-15 / USD 44.95

Since the first edition of this classic reference was published, World Wide Web use has exploded and e-commerce has become a daily part of business and personal life. As Web use has grown, so have ......一起来看看 《Web Security, Privacy and Commerce, 2nd Edition》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具