0%

帧动画优化思路

Android 帧动画(Frame Animation)的使用十分简单

定义 Animation-List 资源 res/anim/rocket.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/rocket_thrust1" android:duration="200" />
<item android:drawable="@drawable/rocket_thrust2" android:duration="200" />
<item android:drawable="@drawable/rocket_thrust3" android:duration="200" />
</animation-list>

设置到 ImageView

1
2
3
4
5
ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image);
rocketImage.setBackgroundResource(R.drawable.rocket_thrust);

rocketAnimation = (AnimationDrawable) rocketImage.getBackground();
rocketAnimation.start();

但是,当你的帧动画比较长,例如有40张图片,而每张图片又比较大的时候就会很容易出现OOM。
显然 Animation-List 满足不了40张图片这种情况,40张图片就会占用40份内存,非常糟糕的内存占用。

解决方案: BitmapFactory.Options.inBitmap

From: Managing Bitmap Memory
(中文)

从Android 3.0 (API Level 11)开始,引进了BitmapFactory.Options.inBitmap字段。
如果使用了这个设置字段,decode方法会在加载Bitmap数据的时候去重用已经存在的Bitmap。
这意味着Bitmap的内存是被重新利用的,这样可以提升性能,并且减少了内存的分配与回收。
然而,使用inBitmap有一些限制,特别是在Android 4.4 (API level 19)之前,只有同等大小的位图才可以被重用。详情请查看inBitmap文档。

使用该技术,在播放动画的时候只需要一份内存就可以了,重复使用 Bitmap 内存,连内存抖动都不会发生。

所以我的做法是自己实现动画播放: load resource -> decode(inBitmap) -> set to ImageView -> delay -> load resource -> …

要让动画流畅,需要确保 load resource 与 decode 的工作在16毫秒内完成,(16ms 来源)

16ms 优化思路:

  • load resource : 如果所有资源的大小加起来只有几Mb,则可以将资源缓存起来,事先把所有资源加载到内存,那后面解码的话将会很快。
  • 根据实际业务真机测试下decode工作时间大概为多少,如果小于16ms,那就在主线程实现,如果decode耗时有点长,那就启用两个线程,使用两份内存(two inBitmap)。

主线程大致实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public abstract class FrameAnimationHelper {
private byte bitmapBytes[][];
private int[] frames;

private Bitmap bitmap;
private ImageView imageView;
private BitmapFactory.Options options;
private int order = 0;

public FrameAnimationHelper(ImageView imageView) {
this.imageView = imageView;
frames = getFrameResources();
if (frames == null || frames.length < 1) {
throw new RuntimeException("FrameAnimationHelper need frame resource");
}
bitmapBytes = new byte[frames.length][];

options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
options.inMutable = true;

//load resource to memory
if (bitmapBytes[0] == null) {
Resources resources = imageView.getResources();
int bytesRead;
InputStream is;
ByteArrayOutputStream bos;
byte[] b = new byte[4096];
try {
for (int i = 0; i < frames.length; i++) {
is = resources.openRawResource(frames[i]);
bos = new ByteArrayOutputStream();
while ((bytesRead = is.read(b)) != -1) {
bos.write(b, 0, bytesRead);
}
bitmapBytes[i] = bos.toByteArray();
}
} catch (IOException e) {
}
}
imageView.setImageBitmap(decode(order));
}


public void start() {
if (order >= frames.length) {
order = 0;
}
imageView.post(runnable);
}

public void stop() {
imageView.removeCallbacks(runnable);
}

private Bitmap decode(int order) {
if (order < frames.length) {
options.inBitmap = bitmap;
bitmap = BitmapFactory.decodeByteArray(bitmapBytes[order], 0, bitmapBytes[order].length, options);
}
return bitmap;
}

private Runnable runnable = new Runnable() {
@Override
public void run() {
if (order < frames.length) {
imageView.setImageBitmap(decode(order));
imageView.postDelayed(runnable, 16);
order++;
}
}
};

protected abstract int[] getFrameResources();
}