AnimationDrawable
尽量不要使用 AnimationDrawable,它在初始化的时候就将所有图片加载到内存中,特别占内存,并且还不能释放,释放之后下次进入再次加载时会报错。
说明:
Android 的帧动画可以使用 AnimationDrawable 实现,但是如果你的帧动画中如果
包含过多帧图片,一次性加载所有帧图片所导致的内存消耗会使低端机发生 OOM 异常。帧动画所使用的图片要注意降低内存消耗,当图片比较大时,容易出现 OOM。
下面简单分析AnimationDrawable的原理:java
//demo
testView = findViewById(R.id.testView);
final AnimationDrawable animationDrawable = (AnimationDrawable).getResources().getDrawable(R.drawable.animation_test);
testView.setBackgroundDrawable(animationDrawable);
animationDrawable.start();
1.生成AnimationDrawable的过程。
//我把log去掉只展示调用逻辑 Resource#getDrawable方法
public Drawable getDrawable(@DrawableRes int id) throws NotFoundException {
final Drawable d = getDrawable(id, null);
return d;
}
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
throws NotFoundException {
return getDrawableForDensity(id, 0, theme);
}
public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
final TypedValue value = obtainTempTypedValue();
try {
//最终调用到ResourceImpl的 loadDrawable方法
final ResourcesImpl impl = mResourcesImpl;
impl.getValueForDensity(id, density, value, true);
return impl.loadDrawable(this, value, id, density, theme);
} finally {
releaseTempTypedValue(value);
}
}
ResourcesImpl # loadDrawable方法
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws NotFoundException {
//省略的检查cache check preloaded的逻辑,现在不是关注的点
//First, check whether we have a cached version of this drawable....
// Next, check preloaded drawables. Preloaded drawables may contain..
if (cs != null) {
dr = cs.newDrawable(wrapper);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
//最初cs为null,同时不是color drawable所以走到该逻辑
dr = loadDrawableForCookie(wrapper, value, id, density, null);
}
if (dr instanceof DrawableContainer) {
needsNewDrawableAfterCache = true;
}
}
//ResourcesImpl#loadDrawableForCookie方法逻辑
try {
if (file.endsWith(".xml")) {
//我们是从xml中加载所以走一下逻辑
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
//调用drawable的方法
dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme);
rp.close();
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
is.close();
}
} catch (Exception e) {
}
Drawable的createFromXmlForDensity又会调用
Drawable#createFromXmlInnerForDensity方法
static Drawable createFromXmlInnerForDensity(@NonNull Resources r,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
@Nullable Theme theme) throws XmlPullParserException, IOException {
return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attrs,
density, theme);
}
最终是DrawableInflater来生成Drawable实例
Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, int density, @Nullable Theme theme)
throws XmlPullParserException, IOException {
1.根据节点生成相应的Drawable。到这里AnimationDrawable的实例构建成功
/**
private Drawable inflateFromTag(@NonNull String name) {
switch (name) {
对应着我们xml animation-list
case "animation-list":
return new AnimationDrawable();
}
**/
Drawable drawable = inflateFromTag(name);
if (drawable == null) {
drawable = inflateFromClass(name);
}
drawable.setSrcDensityOverride(density);
2.调用inflate方法
drawable.inflate(mRes, parser, attrs, theme);
return drawable;
}
生成Drawable实例后接着调用其inflate方法
我们查看AnimationDrawable的inflate方法
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
throws XmlPullParserException, IOException {
//inflate 子节点
inflateChildElements(r, parser, attrs, theme);
setFrame(0, true, false);
}
inflate item节点
private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
Theme theme) throws XmlPullParserException, IOException {
int type;
final int innerDepth = parser.getDepth()+1;
int depth;
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
//1.从xml中解析item对应的drawable
dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
//2.drawable 和 对应的duration展示时间存放在数组中
mAnimationState.addFrame(dr, duration);
if (dr != null) {
//3.设置Drawable的CallBack。这里CallBack是DrawableContainer实现。DrawableContainer也继承了Drawable类。这里有点类似View和ViewGroup的感觉。AnimationDrawable继承了该类。
dr.setCallback(this);
}
}
}
接着我们看AnimationDrawable的start方法
public void start() {
mAnimating = true;
if (!isRunning()) {
// Start from 0th frame.
//从第一帧开始
setFrame(0, false, mAnimationState.getChildCount() > 1
|| !mAnimationState.mOneShot);
}
}
接着看setFrame函数
private void setFrame(int frame, boolean unschedule, boolean animate) {
mAnimating = animate;
mCurFrame = frame;
//根据index来确定当前Drawable
selectDrawable(frame);
if (unschedule || animate) {
unscheduleSelf(this);
}
if (animate) {
// Unscheduling may have clobbered these values; restore them
mCurFrame = frame;
//Running状态值改变
mRunning = true;
//调度每一个item drawable,传递的时间是当前时间加上duration
//这里的this代表一个runnable实例,最后传递到Choreographer中
scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
}
}
//这个scheduleSelf函数 是Drawable类实现的。主要是回到drawable
//CallBack接口的scheduleDrawable函数。接下来的任务是看该CallBack的具体
//实现,即setCallBack的调用处。这里用debug查看调用栈。在给view设置background drawable时
public void scheduleSelf(@NonNull Runnable what, long when) {
final Callback callback = getCallback();
if (callback != null) {
callback.scheduleDrawable(this, what, when);
}
}
public void setBackgroundDrawable(Drawable background) {
background.setCallback(this);//此处设置Callback。this是View实例。
//接下来查看view如何实现该接口
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
if (verifyDrawable(who) && what != null) {
final long delay = when - SystemClock.uptimeMillis();
if (mAttachInfo != null) {
//view attach到window上时该mAttachInfo不为空。
//这里的callback类型为animation,之后会分析到
mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
Choreographer.CALLBACK_ANIMATION, what, who,
Choreographer.subtractFrameDelay(delay));
} else {
// Postpone the runnable until we know
// on which thread it needs to run.
//没attach的时候,先暂存推迟执行
getRunQueue().postDelayed(what, delay);
}
}
}
接下来去看Choreographer类的postCallbackDelayed方法调用了私有postCallbackDelayedInternal方法
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//根据callback类型存储CallbackRecord.内部实现是按时间
//将CallbackRecord插入到链表中
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
//如果当前时间大于应该执行的时间,直接调用scheduleFrameLocked
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
//否则延迟发送消息,接收消息后也是调用上面的函数进行处理
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
接下来查看scheduleFrameLocked的逻辑
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//默认USE_VSYNC为true
if (isRunningOnLooperThreadLocked()) {/判断是否是跟Choreographer的Looper相同
//这里最终还是会调用doFrame()函数
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//为false的话,直接发消息调用doFrame()函数
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
接着再看Choreographer的doFrame
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
try {
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
//这里先后执行四种类型的callback
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
//这里执行我们关心的Animation的callback
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
//这里将执行时间小于当前时间的特定类型的callback都取出来
mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
try {
for (CallbackRecord c = callbacks; c != null; c = c.next) {
//这里调用CallbackRecord的run函数,执行构造callbackrecord是传递的Action。针对我们要分析的情况,该callback的action是AnimationDrawable schedule函数传递进来的,AnimationDrawable实现的Runnable接口。
/**
public void run() {
//其实现就是调用下一帧
nextFrame(false);
}
**/
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
总结:到这里我们可以总结一下,AnimationDrawable调用start后首先set第一帧,接着通过Choreographer机制,不断的切换下一个drawable的drawable。
思考:AnimationDrawable只是控制当前的drawable序列。那画面如何更改的。
我们查看View的onDraw方法
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
注释中提到,先绘制background。
我们查看其具体实现。是调用Drawable的draw方法。
我们知道我们使用AnimationDrawable作为background drawable。
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
//接着查看AnimationDrawable的draw方法。其中并没有draw方法,这里去查看其父
//类的draw方法,发现他继承的DrawableContainer中有draw方法的实现
@Override
public void draw(Canvas canvas) {
if (mCurrDrawable != null) {
mCurrDrawable.draw(canvas);
}
if (mLastDrawable != null) {//为什么要调用last的draw方法?
mLastDrawable.draw(canvas);
}
}
具体实现是调用currentdrawable 和lastdrawable的draw方法
思考:如何触发draw操作?
//回过头看AnimationDrawable中setFrame()方法切换每一个drawable的时候
//除了调用上面分析过的scheduleSelf方法,还调用了 selectDrawable()方法
private void setFrame(int frame, boolean unschedule, boolean animate) {
selectDrawable(frame);
scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
//看selectDrawable方法实现最后会调用
//invalidateSelf();来通知重绘
}
**所以,总体来看AnimationDrawable根据每个item drawable的duration时间
来不断调整当前的drawable。切换drawable的同时出发Choregorapher 的doFrame函数。doFrame函数依次处理 CALLBACK_INPUT ,CALLBACK_ANIMATION,CALLBACK_TRAVERSAL,CALLBACK_COMMIT类型的callback。处理CALLBACK_ANIMATION 类型的callback时触发
**
实验
现在使用16张图
每张图320*320 按4byte一位来算400k 16张差不多6m
有两个页面 第一个页面A只有一个按钮启动第二个页面B B页面播放该帧动画。
刚进入A页面内存稳定35M
实验 | 进入B内存情况 | 退出B内存情况 | 退出B后手动gc | 退出A |
---|---|---|---|---|
1.退出B后不处理 | 118到122间波动 | 稳定112M | 112M | 77M |
2.B的onStop中 stop animation | 同上 | 稳定112M | 76M | 20M |
3.B的onDestroy中view的background设为null | 同上 | 稳定112M | 76M | 20M |
4.同时stop animation onDestroy中view的background设为null | 同上 | 稳定112M | 76M | 20M |
疑问:为什么渲染这些图需要100多M内存
推荐阅读
-
AnimationDrawable
-
AnimationDrawable
-
Android AnimationDrawable动画实现icon闪烁
-
AnimationDrawable(一) 博客分类: Android_笔记 AimationDrawable
-
Android之AnimationDrawable简单模拟动态图
-
Android之AnimationDrawable简单模拟动态图
-
Android中AnimationDrawable使用的简单实例
-
Android中AnimationDrawable使用的简单实例
-
Android自定义控件:动画类---逐帧动画AnimationDrawable
-
AnimationDrawable自定义