Android性能優(yōu)化14 --- 幀動(dòng)畫OOM砂轻?優(yōu)化幀動(dòng)畫之SurfaceView逐幀解析

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

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

有沒有什么辦法讓幀動(dòng)畫的數(shù)據(jù)逐幀加載罕邀,而不是一次性全部加載到內(nèi)存畅形?SurfaceView就提供了這種能力。

SurfaceView

屏幕的顯示機(jī)制和幀動(dòng)畫類似诉探,也是一幀一幀的連環(huán)畫日熬,只不過刷新頻率很高,感覺像連續(xù)的肾胯。為了顯示一幀竖席,需要經(jīng)歷計(jì)算和渲染兩個(gè)過程,CPU 先計(jì)算出這一幀的圖像數(shù)據(jù)并寫入內(nèi)存敬肚,然后調(diào)用 OpenGL 命令將內(nèi)存中數(shù)據(jù)渲染成圖像存放在 GPU Buffer 中毕荐,顯示設(shè)備每隔一定時(shí)間從 Buffer 中獲取圖像并顯示。

上述過程中的計(jì)算艳馒,對于View來說东跪,就好比在主線程遍歷 View樹 以決定視圖畫多大(measure),畫在哪(layout)鹰溜,畫些啥(draw),計(jì)算結(jié)果存放在內(nèi)存中丁恭,SurfaceFlinger 會(huì)調(diào)用 OpenGL 命令將內(nèi)存中的數(shù)據(jù)渲染成圖像存放在 GPU Buffer 中曹动。每隔16.6ms,顯示器從 Buffer 中取出幀并顯示牲览。所以自定義 View 可以通過重載onMeasure()墓陈、onLayout()、onDraw()來定義幀內(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;
//用于繪制幀的畫布
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.獲取畫布
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ú)立幀繪制線程衫樊,好處是可以通過與其綁定的Handler方便地實(shí)現(xiàn)“每隔一段時(shí)間刷新”,而且在Surface被銷毀的時(shí)候可以方便的調(diào)用HandlerThread.quit()來結(jié)束線程執(zhí)行的邏輯利花。

  • DrawRunnable.run()運(yùn)用模版方法模式定義了繪制算法框架科侈,其中幀繪制邏輯的具體實(shí)現(xiàn)被定義成兩個(gè)抽象方法,推遲到子類中實(shí)現(xiàn)炒事,因?yàn)槔L制的東西是多樣的臀栈,對于本文來說,繪制的就是一張張圖片挠乳,所以新建BaseSurfaceView的子類FrameSurfaceView权薯。

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

public class FrameSurfaceView extends BaseSurfaceView {
    public static final int INVALID_BITMAP_INDEX = Integer.MAX_VALUE;
    private List 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 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) {
        //繪制一幀前需要先清畫布盟蚣,否則所有幀都疊在一起同時(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)畫是否結(jié)束
private boolean isFinish() {
return bitmapInde
x >= bitmaps.size();
}

//幀動(dòng)畫是否開始
private boolean isStart() {
return bitmapIndex != INVALID_BITMAP_INDEX;
_BITMAP_INDEX;
}

//幀動(dòng)畫是否結(jié)束
private boolean isFinish() {
return bitmapInde[外鏈圖片轉(zhuǎn)存中…(img-Eug6Luq9-1642137464297)]
x >= bitmaps.size();
}

//幀動(dòng)畫是否開始
private boolean isStart() {
return bitmapIndex != INVALID_BITMAP_INDEX;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末威蕉,一起剝皮案震驚了整個(gè)濱河市刁俭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌韧涨,老刑警劉巖牍戚,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異虑粥,居然都是意外死亡如孝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門娩贷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來第晰,“玉大人,你說我怎么就攤上這事彬祖∽率荩” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵储笑,是天一觀的道長甜熔。 經(jīng)常有香客問我,道長突倍,這世上最難降的妖魔是什么腔稀? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任盆昙,我火速辦了婚禮,結(jié)果婚禮上焊虏,老公的妹妹穿的比我還像新娘淡喜。我一直安慰自己,他們只是感情好诵闭,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布炼团。 她就那樣靜靜地躺著,像睡著了一般涂圆。 火紅的嫁衣襯著肌膚如雪们镜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天润歉,我揣著相機(jī)與錄音模狭,去河邊找鬼。 笑死踩衩,一個(gè)胖子當(dāng)著我的面吹牛嚼鹉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播驱富,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼锚赤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了褐鸥?” 一聲冷哼從身側(cè)響起线脚,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎叫榕,沒想到半個(gè)月后浑侥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晰绎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年寓落,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荞下。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡伶选,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出尖昏,到底是詐尸還是另有隱情仰税,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布抽诉,位于F島的核電站肖卧,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏掸鹅。R本人自食惡果不足惜塞帐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巍沙。 院中可真熱鬧葵姥,春花似錦、人聲如沸句携。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矮嫉。三九已至削咆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蠢笋,已是汗流浹背拨齐。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昨寞,地道東北人瞻惋。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像援岩,于是被迫代替她去往敵國和親歼狼。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

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