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)AndroidStudio
的Profiler
標(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)擊這里痹筛。