This is the first in a series of articles on Android performance optimization.
- Android performance optimization | animation to OOM? Optimized SurfaceView frame by frame resolution for frame animation
- A larger Android performance optimization | do animation to caton? Optimized SurfaceView sliding window frame reuse for frame animation
- Android performance optimization | to shorten the building layout is 20 times (on)
- Android performance optimization | to shorten the building layout is 20 times (below)
Android provides AnimationDrawable to animate frames. Before the animation starts, all the frames of the picture are parsed and take up memory. Once the animation is more complex and has more frames, it is easy to OOM on low configuration phones. Even if it doesn’t happen on the OOM, it will cause a lot of memory stress. The following code shows a frame animation with 4 frames:
Native frame animation
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();
Copy the code
Is there any way to make the data for the frame animation load frame by frame instead of loading it all into memory at once? SurfaceView provides that capability.
SurfaceView
The display mechanism is similar to frame animation, which is also a comic strip frame by frame, but the refresh rate is very high, and it feels like continuous. To display a frame, it needs to go through two processes: calculation and rendering. The CPU first calculates the image data of this frame and writes it to memory, and then calls the OpenGL command to render the image data in memory and store it in the GPU Buffer. The display device obtains the image from the Buffer at regular intervals and displays it.
For a View, the calculation in the above process is like traversing the View tree on the main thread to determine the size of the View (measure), where to draw (layout), and what to draw (draw). The calculation results are stored in memory. SurfaceFlinger will invoke the OpenGL command to render the in-memory data into an image and store it in the GPU Buffer. Every 16.6ms, the monitor takes frames from the Buffer and displays them. So a custom View can define the frame content by overloading onMeasure(), onLayout(), and onDraw(), but not the frame refresh rate.
SurfaceView can overcome this limitation. And it can calculate the frame data in a separate thread. Here is the template code for a custom SurfaceView:
public abstract class BaseSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
public static final int DEFAULT_FRAME_DURATION_MILLISECOND = 50;
// The thread used to compute frame data
private HandlerThread handlerThread;
private Handler handler;
// Frame refresh frequency
private int frameDuration = DEFAULT_FRAME_DURATION_MILLISECOND;
// The canvas used to draw the frame
private Canvas canvas;
private boolean isAlive;
public BaseSurfaceView(Context context) {
super(context);
init();
}
protected void init(a) {
getHolder().addCallback(this);
// Set the transparent background, otherwise the SurfaceView background will be black
setBackgroundTransparent();
}
private void setBackgroundTransparent(a) {
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;
}
// Stop the frame drawing thread
private void stopDrawThread(a) {
handlerThread.quit();
handler = null;
}
// Start the frame drawing thread
private void startDrawThread(a) {
handlerThread = new HandlerThread("SurfaceViewThread");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
handler.post(new DrawRunnable());
}
private class DrawRunnable implements Runnable {
@Override
public void run(a) {
if(! isAlive) {return;
}
try {
//1. Get the canvas
canvas = getHolder().lockCanvas();
//2. Draw a frame
onFrameDraw(canvas);
} catch (Exception e) {
e.printStackTrace();
} finally {
//3. Submit the frame data
getHolder().unlockCanvasAndPost(canvas);
//4. The drawing of a frame is finished
onFrameDrawFinish();
}
// Constantly push yourself to the drawing thread's message queue to achieve frame refresh
handler.postDelayed(this, frameDuration); }}protected abstract void onFrameDrawFinish(a);
protected abstract void onFrameDraw(Canvas canvas);
}
Copy the code
- with
HandlerThread
The benefit of being a separate frame drawing thread is that it can be bound to itHandler
Conveniently implements “refresh every once in a while” and inSurface
Can be conveniently called when destroyedHandlerThread.quit()
To terminate the logic executed by the thread. DrawRunnable.run()
The template method pattern is used to define the drawing algorithm framework, where the concrete implementation of the frame drawing logic is defined as two abstract methods, deferred to the subclass implementation, because the drawing things are diverse, for this article, drawing is a picture, so newBaseSurfaceView
A subclass ofFrameSurfaceView
:
Frame by frame parsing & timely recycling
public class FrameSurfaceView extends BaseSurfaceView {
public static final int INVALID_BITMAP_INDEX = Integer.MAX_VALUE;
private List<Integer> bitmaps = new ArrayList<>();
/ / frame images
private Bitmap frameBitmap;
/ / frame index
private int bitmapIndex = INVALID_BITMAP_INDEX;
private Paint paint = new Paint();
private BitmapFactory.Options options = new BitmapFactory.Options();
// The original size of the frame
private Rect srcRect;
// The target size of the frame
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;
// By default, the original size of the first frame is calculated
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(a) {
// Recycle a frame directly after it has been drawn
recycleOneFrame();
}
/ / recycling frame
private void recycleOneFrame(a) {
if(frameBitmap ! =null) {
frameBitmap.recycle();
frameBitmap = null; }}@Override
protected void onFrameDraw(Canvas canvas) {
// Clean the canvas before drawing a frame, otherwise all frames will be displayed on top of one another
clearCanvas(canvas);
if(! isStart()) {return;
}
if(! isFinish()) { drawOneFrame(canvas); }else{ onFrameAnimationEnd(); }}// Draw a frame, which is a Bitmap
private void drawOneFrame(Canvas canvas) {
frameBitmap = BitmapUtil.decodeOriginBitmap(getResources(), bitmaps.get(bitmapIndex), options);
canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);
bitmapIndex++;
}
private void onFrameAnimationEnd(a) {
reset();
}
private void reset(a) {
bitmapIndex = INVALID_BITMAP_INDEX;
}
// Whether the frame animation is finished
private boolean isFinish(a) {
return bitmapIndex >= bitmaps.size();
}
// Whether to start the frame animation
private boolean isStart(a) {
returnbitmapIndex ! = INVALID_BITMAP_INDEX; }// Start the frame animation
public void start(a) {
bitmapIndex = 0;
}
private void clearCanvas(Canvas canvas) {
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawPaint(paint);
paint.setXfermode(newPorterDuffXfermode(PorterDuff.Mode.SRC)); }}Copy the code
FrameSurfaceView
Inherited fromBaseSurfaceView
, so it reuses the base class’s drawing frame algorithm, and determines its own drawing content for each frame: oneBitmap
.Bitmap
Resource ID PassedsetBitmaps()
Pass it in,Draw a frame to parse oneIs called after each frame is drawnBitmap.recycle()
Free the image native memory and remove the reference to the image pixel data in the Java heap. So that when GC occurs, the image pixel data can be recycled in time.
Everything was so self-fulfilling that I couldn’t wait to run the code and open the Profiler TAB in AndroidStudio and switch to MEMORY to verify performance with real MEMORY data. But the hard truth hit me in the face… After playing the frame animation multiple times, the memory footprint is even larger than the native AnimationDrawable, and each time, there are N more Bitmap objects in memory (N is the total number of frames of the frame animation). The only good news is that animated images can be recycled after GC is manually triggered. Image data in AnimationDrawable will not be GC
The reason for this is that it is smart to recycle in time. After each frame is drawn, the frame data is recycled, and the next frame can only apply for a new piece of memory when the Bitmap is parsed. Each frame animation is of the same size, is it possible to reuse the memory space of the previous Bitmap? Hence the following version of The FrameSurfaceView:
Frame by frame parsing & frame reuse
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(a) {
super.init();
// Define the parse Bitmap parameters as variable types, so that the Bitmap can be reused
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(a) {
return defaultWidth;
}
@Override
protected int getDefaultHeight(a) {
return defaultHeight;
}
@Override
protected void onFrameDrawFinish(a) {
// After each frame is drawn, it is no longer recycled
// recycle();
}
public void recycle(a) {
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(), bitmaps.get(bitmapIndex), options);
// Overuse the memory of the previous Bitmap
options.inBitmap = frameBitmap;
canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);
bitmapIndex++;
}
private void onFrameAnimationEnd(a) {
reset();
}
private void reset(a) {
bitmapIndex = INVALID_BITMAP_INDEX;
}
private boolean isFinish(a) {
return bitmapIndex >= bitmaps.size();
}
private boolean isStart(a) {
returnbitmapIndex ! = INVALID_BITMAP_INDEX; }public void start(a) {
bitmapIndex = 0;
}
private void clearCanvas(Canvas canvas) {
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawPaint(paint);
paint.setXfermode(newPorterDuffXfermode(PorterDuff.Mode.SRC)); }}Copy the code
- will
Bitmap
Analytic parameters ofinBitmap
Set to successfully resolvedBitmap
Object for reuse.
This time, no matter how many times the frame animation is replayed, the number of bitmaps in memory only increases by 1, because memory is allocated only when the first image is parsed. This memory can be recycled manually by calling the Recycle () at the end of the FrameSurfaceView lifecycle.
talk is cheap, show me the code
For clarity, the above code snippet omits some custom View details that are not related to the topic. The full code can be found here.