幀動(dòng)畫(huà)內(nèi)存OOM绅作?不存在的!—— SurfaceView逐幀解析

Android 提供了AnimationDrawable用于實(shí)現(xiàn)幀動(dòng)畫(huà)蛾派。在動(dòng)畫(huà)開(kāi)始之前俄认,所有幀的圖片都被解析并占用內(nèi)存个少,一旦動(dòng)畫(huà)較復(fù)雜幀數(shù)較多,在低配置手機(jī)上容易發(fā)生 OOM眯杏。即使不發(fā)生 OOM夜焦,也會(huì)對(duì)內(nèi)存造成不小的壓力。下面代碼展示了一個(gè)幀數(shù)為4的幀動(dòng)畫(huà):

原生幀動(dòng)畫(huà)

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();

有沒(méi)有什么辦法讓幀動(dòng)畫(huà)的數(shù)據(jù)逐幀加載役拴,而不是一次性全部加載到內(nèi)存糊探?SurfaceView就提供了這種能力钾埂。

SurfaceView

屏幕的顯示機(jī)制和幀動(dòng)畫(huà)類(lèi)似河闰,也是一幀一幀的連環(huán)畫(huà),只不過(guò)刷新頻率很高褥紫,感覺(jué)像連續(xù)的姜性。為了顯示一幀,需要經(jīng)歷計(jì)算和渲染兩個(gè)過(guò)程髓考,CPU 先計(jì)算出這一幀的圖像數(shù)據(jù)并寫(xiě)入內(nèi)存部念,然后調(diào)用 OpenGL 命令將內(nèi)存中數(shù)據(jù)渲染成圖像存放在 GPU Buffer 中,顯示設(shè)備每隔一定時(shí)間從 Buffer 中獲取圖像并顯示氨菇。

上述過(guò)程中的計(jì)算儡炼,對(duì)于View來(lái)說(shuō),就好比在主線程遍歷 View樹(shù) 以決定視圖畫(huà)多大(measure)查蓉,畫(huà)在哪(layout)乌询,畫(huà)些啥(draw),計(jì)算結(jié)果存放在內(nèi)存中豌研,SurfaceFlinger 會(huì)調(diào)用 OpenGL 命令將內(nèi)存中的數(shù)據(jù)渲染成圖像存放在 GPU Buffer 中妹田。每隔16.6ms,顯示器從 Buffer 中取出幀并顯示鹃共。所以自定義 View 可以通過(guò)重載onMeasure()鬼佣、onLayout()onDraw()來(lái)定義幀內(nèi)容霜浴,但不能定義幀刷新頻率晶衷。

SurfaceView可以突破這個(gè)限制。而且它可以將計(jì)算幀數(shù)據(jù)放到獨(dú)立的線程中進(jìn)行阴孟。下面是自定義SurfaceView的模版代碼:

public abstract class BaseSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    public static final int DEFAULT_FRAME_DURATION_MILLISECOND = 50;
    //用于計(jì)算幀數(shù)據(jù)的線程
    private HandlerThread handlerThread;
    private Handler handler;
    //幀刷新頻率
    private int frameDuration = DEFAULT_FRAME_DURATION_MILLISECOND;
    //用于繪制幀的畫(huà)布
    private Canvas canvas;
    private boolean isAlive;

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

    protected void init() {
        getHolder().addCallback(this);
        //設(shè)置透明背景房铭,否則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;
    }

    //啟動(dòng)幀繪制線程
    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.獲取畫(huà)布
                canvas = getHolder().lockCanvas();
                //2.繪制一幀
                onFrameDraw(canvas);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //3.將幀數(shù)據(jù)提交
                getHolder().unlockCanvasAndPost(canvas);
                //4.一幀繪制結(jié)束
                onFrameDrawFinish();
            }
            //不停的將自己推送到繪制線程的消息隊(duì)列以實(shí)現(xiàn)幀刷新
            handler.postDelayed(this, frameDuration);
        }
    }
    
    protected abstract void onFrameDrawFinish();

    protected abstract void onFrameDraw(Canvas canvas);
}
  • HandlerThread作為獨(dú)立幀繪制線程,好處是可以通過(guò)與其綁定的Handler方便地實(shí)現(xiàn)“每隔一段時(shí)間刷新”温眉,而且在Surface被銷(xiāo)毀的時(shí)候可以方便的調(diào)用HandlerThread.quit()來(lái)結(jié)束線程執(zhí)行的邏輯缸匪。
  • DrawRunnable.run()運(yùn)用模版方法模式定義了繪制算法框架,其中幀繪制邏輯的具體實(shí)現(xiàn)被定義成兩個(gè)抽象方法类溢,推遲到子類(lèi)中實(shí)現(xiàn)凌蔬。本文的主角FrameSurfaceView應(yīng)該繼承自BaseSurfaceView

逐幀解析 & 及時(shí)回收

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;
    //幀圖片目標(biāo)大小
    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;
        //默認(rèn)情況下露懒,計(jì)算一幀圖片的原始大小
        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) {
        //繪制一幀前需要先清畫(huà)布砂心,否則所有幀都疊在一起同時(shí)顯示
        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;
    }

    //幀動(dòng)畫(huà)是否結(jié)束
    private boolean isFinish() {
        return bitmapIndex >= bitmaps.size();
    }

    //幀動(dòng)畫(huà)是否開(kāi)始
    private boolean isStart() {
        return bitmapIndex != INVALID_BITMAP_INDEX;
    }
    
    //開(kāi)始播放幀動(dòng)畫(huà)
    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,所以它復(fù)用了基類(lèi)的繪制框架算法辩诞,并且定了自己每一幀的繪制內(nèi)容:一張Bitmap坎弯。
  • Bitmap資源 id 通過(guò)setBitmap()傳遞進(jìn)來(lái), 繪制一幀解析一張 译暂,在每一幀繪制完畢后抠忘,調(diào)用Bitmap.recycle()釋放圖片 native 內(nèi)存并去除 java 堆中圖片像素?cái)?shù)據(jù)的引用。這樣當(dāng) GC 發(fā)生時(shí)外永,圖片像素?cái)?shù)據(jù)可以及時(shí)被回收崎脉。

一切都是這么地能夠自圓其說(shuō),我迫不及待地運(yùn)行代碼并打開(kāi)AndroidStudioProfiler標(biāo)簽頁(yè)伯顶,切換到MEMORY囚灼,想用真實(shí)內(nèi)存數(shù)據(jù)驗(yàn)證下性能。但殘酷的事實(shí)狠狠地打了下臉祭衩。灶体。。多次播放幀動(dòng)畫(huà)后掐暮,內(nèi)存占用居然比原生AnimationDrawable還大蝎抽,而且每播放一次,內(nèi)存中都會(huì)多出 N 個(gè)Bitmap對(duì)象(N為播放一邊的幀數(shù))劫乱。唯一令人欣慰的是织中,手動(dòng)觸發(fā) GC 后幀動(dòng)畫(huà)圖片能夠被回收。(AnimationDrawable中的圖片數(shù)據(jù)不會(huì)被 GC)

原因就在于自作聰明地及時(shí)回收衷戈,每一幀繪制完后幀數(shù)據(jù)被回收狭吼,那下一幀解析Bitmap時(shí)只能新申請(qǐng)一塊內(nèi)存。幀動(dòng)畫(huà)每張圖片大小是一致的殖妇,是不是能復(fù)用上一幀Bitmap的內(nèi)存空間刁笙?于是乎有了下面這個(gè)版本的FrameSurfaceView

逐幀解析 & 幀復(fù)用

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參數(shù)為可變類(lèi)型,這樣才能復(fù)用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(),
        //復(fù)用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的解析參數(shù)inBitmap設(shè)置為已經(jīng)成功解析的Bitmap對(duì)象以實(shí)現(xiàn)復(fù)用谦趣。

這一次不管重新播放多少次幀動(dòng)畫(huà)疲吸,內(nèi)存中Bitmap數(shù)量只會(huì)增加1,因?yàn)橹辉诮馕龅谝粡垐D片是分配了內(nèi)存前鹅。而這塊內(nèi)存可以在FrameSurfaceView生命周期結(jié)束時(shí)手動(dòng)調(diào)用recycle()回收摘悴。

talk is cheap, show me the code

為了更清晰的展示祟同,上述代碼段省略了一些和主題無(wú)關(guān)的自定義 View 細(xì)節(jié)瞧剖,完整的代碼可以點(diǎn)擊這里痹筛。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末开泽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子口四,更是在濱河造成了極大的恐慌孵运,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔓彩,死亡現(xiàn)場(chǎng)離奇詭異治笨,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)赤嚼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)旷赖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人探膊,你說(shuō)我怎么就攤上這事杠愧〈疲” “怎么了逞壁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)锐锣。 經(jīng)常有香客問(wèn)我腌闯,道長(zhǎng),這世上最難降的妖魔是什么雕憔? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任姿骏,我火速辦了婚禮,結(jié)果婚禮上斤彼,老公的妹妹穿的比我還像新娘分瘦。我一直安慰自己,他們只是感情好琉苇,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布嘲玫。 她就那樣靜靜地躺著,像睡著了一般并扇。 火紅的嫁衣襯著肌膚如雪去团。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天穷蛹,我揣著相機(jī)與錄音土陪,去河邊找鬼。 笑死肴熏,一個(gè)胖子當(dāng)著我的面吹牛鬼雀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蛙吏,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼源哩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蹋肮!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起璧疗,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤坯辩,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后崩侠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體漆魔,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年却音,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了改抡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡系瓢,死狀恐怖阿纤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情夷陋,我是刑警寧澤欠拾,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站骗绕,受9級(jí)特大地震影響藐窄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜酬土,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一荆忍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧撤缴,春花似錦刹枉、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至凉袱,卻和暖如春芥吟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背专甩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工钟鸵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涤躲。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓棺耍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親种樱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蒙袍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容