简介
本篇主要是介绍FrameSequenceDrawable的相关实现原理的文章,FrameSequenceDrawable是Google实现的可以播放Webp动画的Drawable,这个并没有在SDK里面,但是我们可以在googlesource中看到相关的代码,FrameSequenceDrawable相关代码地址
播放效果
在介绍之前,我们可以先看一下播放效果:
我想直接用
如果你说我不想看原理,我就想知道咋播放webp,那么我就帮助你完成一个简单小库,虽然是我封装的,但是代码可都是人家google开发哥哥写的,我帮你搬运过来,哈哈
这里是链接
如何引入到工程
- Add the JitPack repository to your build file
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
- Add the dependency
dependencies {
compile 'com.github.humorousz:FrameSequenceDrawable:1.0.1-SNAPSHOT'
}
如何使用
- xml
<com.humrousz.sequence.view.AnimatedImageView
android:id="@+id/google_sequence_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/group"
app:loopCount="1"
app:loopBehavior="loop_default|loop_finite|loop_inf"
android:scaleType="centerCrop"
android:src="@drawable/webpRes"/>
- java
public void setImage(){
AnimatedImageView mGoogleImage;
mGoogleImage = findViewById(R.id.google_sequence_image);
//设置重复次数
mGoogleImage.setLoopCount(1);
//重复行为默认 根据webp图片循环次数决定
mGoogleImage.setLoopDefault();
//重复行为无限
mGoogleImage.setLoopInf();
//重复行为为指定 跟setLoopCount有关
mGoogleImage.setLoopFinite();
//设置Assets下的图片
mGoogleImage.setImageResourceFromAssets("newyear.webp");
//设置图片通过drawable
mGoogleImage.setImageResource(R.drawable.newyear);
Uri uri = Uri.parse("file:"+Environment.getExternalStorageDirectory().toString()+"/animation");
//通过添加"file:"协议,可以展示指定路径的图片,如例子中的本地资源
mGoogleImage.setImageURI(uri);
}
当然你也可以不使用我这里的AnimatedImageView,AnimatedImageView是我参考其它的代码后修改封装的类,直接使用FrameSequenceDrawable+ImageView也是可以的,使用方法如下
ImageView mImage;
InputStream in = null;
in = getResources().getAssets().open("anim.webp");
final FrameSequenceDrawable drawable = new FrameSequenceDrawable(in);
drawable.setLoopCount(1);
drawable.setLoopBehavior(FrameSequenceDrawable.LOOP_FINITE);
drawable.setOnFinishedListener(new FrameSequenceDrawable.OnFinishedListener() {
@Override
public void onFinished(FrameSequenceDrawable frameSequenceDrawable) {
}
});
mImage.setImageDrawable(drawable);
原理介绍
原理简介
- 利用了两个Bitmap对象,其中一个用于绘制到屏幕上,另外一个用于解析下一张要展示的图片,利用了HandlerThread在子线程解析,每次解析的时候获取上一张图片的展示时间,然后使用Drawable自身的scheduleSelf方法在指定时间替换图片,在达到替换时间时,会调用draw方法,在draw之前先去子线程解析下一张要展示的图片,然后重复这个步骤,直到播放结束或者一直播放
涉及到的类
- FrameSequenceDrawable
这个我们直接使用播放webp动画的类,它继承了Drawable并且实现了Animatable, Runnable两个接口,所以我们可以像使用Drawable一样的去使用它 - FrameSequence
从名字上来看这个类的意思很明确,那就是帧序列,它主要负责对传入的webp流进行解析,解析的地方是在native层,所以如果自己想编译FrameSequenceDrawable源码的话,需要编译JNI文件夹下的相关文件生成so库
流程分析
在分析源码之前,先把整个代码的流程分步骤简单介绍一下,后面根据这里介绍的流程去逐个分析源码
- 在FrameSequenceDrawable构造函数中创建解析线程,使用HandlerThread作为解析线程
- 在触发了setVisiable方法之后,会触发自身start方法开始解析第一张图片
- start方法调用scheduleDecodeLocked开始解析
- mDecodeRunnable的run方法执行,解析下一张要展示的图片,调用Drawable自身的scheduleSelf方法,参数when会设置为当前图片的展示时间
- scheduleSelf 会调用FrameSequenceDrawable所实现Runnable的run方法,并且导致draw,在draw方法中会首先调用解析线程去解析下一张图片,然后在继续绘制当前图片
- 反复执行绘制和解析步骤,知道循环次数达到设置状态或者无限循环
效果示意图
源码分析
现在我们对整个流程上的源码进行一些分析
- 首先第一步我们先看看FrameSequenceDrawable的构造函数,可以发现源码中一共有两个构造函数,我为了方便在我分享的github项目里增加了第三个构造,下面我们来一起看一看
//这个是我自己添加的,利用了FrameSequence可以通过InputStream方法创建FrameSequence功能
public FrameSequenceDrawable(InputStream inputStream){
this(FrameSequence.decodeStream(inputStream));
}
public FrameSequenceDrawable(FrameSequence frameSequence) {
this(frameSequence, sAllocatingBitmapProvider);
}
public FrameSequenceDrawable(FrameSequence frameSequence, BitmapProvider bitmapProvider) {
if (frameSequence == null || bitmapProvider == null) throw new IllegalArgumentException();
mFrameSequence = frameSequence;
mFrameSequenceState = frameSequence.createState();
final int width = frameSequence.getWidth();
final int height = frameSequence.getHeight();
mBitmapProvider = bitmapProvider;
mFrontBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
mBackBitmap = acquireAndValidateBitmap(bitmapProvider, width, height);
mSrcRect = new Rect(0, 0, width, height);
mPaint = new Paint();
mPaint.setFilterBitmap(true);
mFrontBitmapShader
= new BitmapShader(mFrontBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mBackBitmapShader
= new BitmapShader(mBackBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mLastSwap = 0;
mNextFrameToDecode = -1;
mFrameSequenceState.getFrame(0, mFrontBitmap, -1);
initializeDecodingThread();
}
我们可以看到在构造方法中,创建了mFrontBitmap和mBackBitmap两个对象,它俩的作用就是mFrontBitmap用于绘制,mBackBitmap用于解析线程下一张要展示的图片,在每次draw方法之前会把它俩所指向的实际bitmap交换,FrameSequence就是抽象出去的帧序列对象,它内部封装了动画的长、宽、透明度、循环次数、帧数等属性,它的内部所有解析和获取帧的方法都是native,我们来看看initializeDecodingThread这个方法做了哪些事情
private static void initializeDecodingThread() {
synchronized (sLock) {
if (sDecodingThread != null) return;
sDecodingThread = new HandlerThread("FrameSequence decoding thread",
Process.THREAD_PRIORITY_BACKGROUND);
sDecodingThread.start();
sDecodingThreadHandler = new Handler(sDecodingThread.getLooper());
}
}
这里也很简单,就是创建了一个HandlerThread,后续所有调用线程调度解析都是通过sDecodingThreadHandler这个去实现的
- setVisible,动画的开始
FrameSequenceDrawable的setVisible重载了父类的setVisible,这个会在设置动画的时候被调用,这里也是动画调度开始的地方,我们来看一下它的实现
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean changed = super.setVisible(visible, restart);
if (!visible) {
stop();
} else if (restart || changed) {
stop();
start();
}
return changed;
}
@Override
//Animatable中的方法
public void start() {
if (!isRunning()) {
synchronized (mLock) {
checkDestroyedLocked();
if (mState == STATE_SCHEDULED) return; // already scheduled
mCurrentLoop = 0;
scheduleDecodeLocked();
}
}
}
private void scheduleDecodeLocked() {
mState = STATE_SCHEDULED;
mNextFrameToDecode = (mNextFrameToDecode + 1) % mFrameSequence.getFrameCount();
sDecodingThreadHandler.post(mDecodeRunnable);
}
可以看到,setVisible会调用start方法,start方法会调用到scheduleDecodeLocked方法,这个方法会计算下一张需要解析的index,然后通过sDecodingThreadHandler调用mDecodeRunnable去在子线程进行解析,下面我们来看看mDecodeRunnable干了一些什么事情
/**
* Runs on decoding thread, only modifies mBackBitmap's pixels
*/
private Runnable mDecodeRunnable = new Runnable() {
@Override
public void run() {
int nextFrame;
Bitmap bitmap;
synchronized (mLock) {
if (mDestroyed) return;
//下一张要解析的index
nextFrame = mNextFrameToDecode;
if (nextFrame < 0) {
return;
}
//后台解析时用mBackBitmap
bitmap = mBackBitmap;
mState = STATE_DECODING;
}
int lastFrame = nextFrame - 2;
boolean exceptionDuringDecode = false;
long invalidateTimeMs = 0;
try {
//解析下一张图片到bitmap,并且返回nextFrame-1的展示时间
invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
} catch (Exception e) {
// Exception during decode: continue, but delay next frame indefinitely.
Log.e(TAG, "exception during decode: " + e);
exceptionDuringDecode = true;
}
if (invalidateTimeMs < MIN_DELAY_MS) {
invalidateTimeMs = DEFAULT_DELAY_MS;
}
boolean schedule = false;
Bitmap bitmapToRelease = null;
//计算是否满足交换普片的条件
synchronized (mLock) {
if (mDestroyed) {
bitmapToRelease = mBackBitmap;
mBackBitmap = null;
} else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
schedule = true;
//计算下次调度的时间,上一张图片的展示时间加上上次调度的时间(mLastSwap就是上次调度的时间)
mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap;
mState = STATE_WAITING_TO_SWAP;
}
}
if (schedule) {
//在mNextSwap时调度自己的run方法
scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
}
if (bitmapToRelease != null) {
// destroy the bitmap here, since there's no safe way to get back to
// drawable thread - drawable is likely detached, so schedule is noop.
mBitmapProvider.releaseBitmap(bitmapToRelease);
}
}
};
在上面的代码中比较关键的部分我已经加了注释,整段代码的逻辑可以分为三个部分,第一个部分是设置条件判断以及设置mState为STATE_DECODING
synchronized (mLock) {
if (mDestroyed) return;
//下一张要解析的index
nextFrame = mNextFrameToDecode;
if (nextFrame < 0) {
return;
}
//后台解析时用mBackBitmap
bitmap = mBackBitmap;
mState = STATE_DECODING;
}
第二部分是解析nextFrame并且获取nextFrame上一张图片的展示时间,并且修改mState和计算mNextSwap时间
...
try {
//解析下一张图片到bitmap,并且返回lastFrame的展示时间
invalidateTimeMs = mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame);
} catch (Exception e) {
// Exception during decode: continue, but delay next frame indefinitely.
Log.e(TAG, "exception during decode: " + e);
exceptionDuringDecode = true;
}
....
synchronized (mLock) {
if (mDestroyed) {
bitmapToRelease = mBackBitmap;
mBackBitmap = null;
} else if (mNextFrameToDecode >= 0 && mState == STATE_DECODING) {
schedule = true;
//计算下次调度的时间,上一张图片的展示时间加上上次调度的时间(mLastSwap就是上次调度的时间)
mNextSwap = exceptionDuringDecode ? Long.MAX_VALUE : invalidateTimeMs + mLastSwap;
mState = STATE_WAITING_TO_SWAP;
}
}
关于 mFrameSequenceState.getFrame(nextFrame, bitmap, lastFrame)这个方法的返回值到底是哪一帧的时间,我一开始也不是很明确,但是后来通过思考和后面的逻辑来看,这个返回值应该是nextFrame的上一张图片的时间,因为下次调度的时间是这个返回值+ mLastSwap,后来看了一下native的代码,证实了这个想法,getFrame调用了native的nativeGetFrame方法,nativeGetFrame方法又调用了drawFrame,c++层的代码如下
static jlong JNICALL nativeGetFrame(
... 省略
jlong delayMs = frameSequenceState->drawFrame(frameNr,
(Color8888*) pixels, pixelStride, previousFrameNr);
AndroidBitmap_unlockPixels(env, bitmap);
return delayMs;
}
long FrameSequenceState_webp::drawFrame(int frameNr,
Color8888* outputPtr, int outputPixelStride, int previousFrameNr) {
... 省略
WebPIterator currIter;
WebPIterator prevIter;
int ok = WebPDemuxGetFrame(demux, start, &currIter); // Get frame number 'start - 1'.
ALOG_ASSERT(ok, "Could not retrieve frame# %d", start - 1);
// Use preserve buffer only if needed.
Color8888* prevBuffer = (frameNr == 0) ? outputPtr : mPreservedBuffer;
int prevStride = (frameNr == 0) ? outputPixelStride : canvasWidth;
Color8888* currBuffer = outputPtr;
int currStride = outputPixelStride;
for (int i = start; i <= frameNr; i++) {
prevIter = currIter;
ok = WebPDemuxGetFrame(demux, i + 1, &currIter); // Get ith frame.
ALOG_ASSERT(ok, "Could not retrieve frame# %d", i);
...省略
// Return last frame's delay.
const int frameCount = mFrameSequence.getFrameCount();
const int lastFrame = (frameNr + frameCount - 1) % frameCount;
//这里虽然+1应该是计算值可能从1开始,因为上面for循环计算第ith时也加了1
ok = WebPDemuxGetFrame(demux, lastFrame + 1, &currIter);
ALOG_ASSERT(ok, "Could not retrieve frame# %d", lastFrame);
const int lastFrameDelay = currIter.duration;
WebPDemuxReleaseIterator(&currIter);
WebPDemuxReleaseIterator(&prevIter);
return lastFrameDelay;
}
可以看到最后的返回值是lastFrameDelay它的计算帧lastFrame是(frameNr + frameCount - 1) % frameCount计算出来的,可以看到确实是frameNr的上一张,frameNr就是我们这里的nextFrame,为什么要纠结于这一块的?因为我们只要理解了这个方法,就可以抽象FrameSequence,然后使用自己或者其他的解析代码来解析帧,可以灵活的使用解析库,还可以同时支持gif和webp
继续代码第三部分,这部分就是在调度了,在nextSwap的时间
if (schedule) {
//在mNextSwap时调度自己的run方法
scheduleSelf(FrameSequenceDrawable.this, mNextSwap);
}
if (bitmapToRelease != null) {
// destroy the bitmap here, since there's no safe way to get back to
// drawable thread - drawable is likely detached, so schedule is noop.
mBitmapProvider.releaseBitmap(bitmapToRelease);
}
- scheduleSelf调用自身的run方法触发了绘制
通过上面的流程,到达了时间后,就会触发scheduleSelf调用FrameSequenceDrawable自身的run方法并且会触发绘制,下面我们就来看看这部分代码
@Override
public void run() {
// set ready to swap as necessary
boolean invalidate = false;
synchronized (mLock) {
if (mNextFrameToDecode >= 0 && mState == STATE_WAITING_TO_SWAP) {
mState = STATE_READY_TO_SWAP;
invalidate = true;
}
}
if (invalidate) {
invalidateSelf();
}
}
@Override
public void draw(Canvas canvas) {
synchronized (mLock) {
checkDestroyedLocked();
if (mState == STATE_WAITING_TO_SWAP) {
// may have failed to schedule mark ready runnable,
// so go ahead and swap if swapping is due
if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
mState = STATE_READY_TO_SWAP;
}
}
if (isRunning() && mState == STATE_READY_TO_SWAP) {
// Because draw has occurred, the view system is guaranteed to no longer hold a
// reference to the old mFrontBitmap, so we now use it to produce the next frame
Bitmap tmp = mBackBitmap;
mBackBitmap = mFrontBitmap;
mFrontBitmap = tmp;
BitmapShader tmpShader = mBackBitmapShader;
mBackBitmapShader = mFrontBitmapShader;
mFrontBitmapShader = tmpShader;
mLastSwap = SystemClock.uptimeMillis();
boolean continueLooping = true;
if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
mCurrentLoop++;
if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) ||
(mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
continueLooping = false;
}
}
if (continueLooping) {
scheduleDecodeLocked();
} else {
scheduleSelf(mFinishedCallbackRunnable, 0);
}
}
}
if (mCircleMaskEnabled) {
final Rect bounds = getBounds();
final int bitmapWidth = getIntrinsicWidth();
final int bitmapHeight = getIntrinsicHeight();
final float scaleX = 1.0f * bounds.width() / bitmapWidth;
final float scaleY = 1.0f * bounds.height() / bitmapHeight;
canvas.save();
// scale and translate to account for bounds, so we can operate in intrinsic
// width/height (so it's valid to use an unscaled bitmap shader)
canvas.translate(bounds.left, bounds.top);
canvas.scale(scaleX, scaleY);
final float unscaledCircleDiameter = Math.min(bounds.width(), bounds.height());
final float scaledDiameterX = unscaledCircleDiameter / scaleX;
final float scaledDiameterY = unscaledCircleDiameter / scaleY;
// Want to draw a circle, but we have to compensate for canvas scale
mTempRectF.set(
(bitmapWidth - scaledDiameterX) / 2.0f,
(bitmapHeight - scaledDiameterY) / 2.0f,
(bitmapWidth + scaledDiameterX) / 2.0f,
(bitmapHeight + scaledDiameterY) / 2.0f);
mPaint.setShader(mFrontBitmapShader);
canvas.drawOval(mTempRectF, mPaint);
canvas.restore();
} else {
mPaint.setShader(null);
canvas.drawBitmap(mFrontBitmap, mSrcRect, getBounds(), mPaint);
}
}
这里代码的主要就可以分成两个部分了,下面绘制的部分我们就不说了,主要看上面的获取当前需要绘制的图片和解析下一张图片的部分
if (mState == STATE_WAITING_TO_SWAP) {
// may have failed to schedule mark ready runnable,
// so go ahead and swap if swapping is due
if (mNextSwap - SystemClock.uptimeMillis() <= 0) {
mState = STATE_READY_TO_SWAP;
}
}
if (isRunning() && mState == STATE_READY_TO_SWAP) {
//因为交换时间到了,所以应该绘制mBackBitmap的内容了,而mFrontBitmap所指向的内存可以用于解析下一张图片使用了
//所以交换它们所指向的bitmap
Bitmap tmp = mBackBitmap;
mBackBitmap = mFrontBitmap;
mFrontBitmap = tmp;
BitmapShader tmpShader = mBackBitmapShader;
mBackBitmapShader = mFrontBitmapShader;
mFrontBitmapShader = tmpShader;
mLastSwap = SystemClock.uptimeMillis();
boolean continueLooping = true;
//如果绘制到了最后一张,就需要我们根据条件判断是否继续loop了
if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
mCurrentLoop++;
//第一个判断的条件是,LoopBehavior是LOOP_FINITE时,根据是否达到我们设置的loopCount为依据,如果达到就结束
//第二个判断的条件是,LoopBehavior是LOOP_DEFAULT时,根据Sequence自身的LoopCount来决定,如果达到就结束
if ((mLoopBehavior == LOOP_FINITE && mCurrentLoop == mLoopCount) ||
(mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
continueLooping = false;
}
}
if (continueLooping) {
//继续调度下张
scheduleDecodeLocked();
} else {
scheduleSelf(mFinishedCallbackRunnable, 0);
}
}
同样关键的部分我已经注释在上面了,主要就是达到了交换的时间会产生调度,然后重新绘制,在重新绘制时,需要绘制的图片是mBackBitmap,然后mFrontBitmap可以用于解析下一张图片,所以把它俩做了一次交换,后面主要就是判断是否播放到了最后一张,如果播放到了最后一张,那么就会根据条件判断是否继续循环播放,最后满足条件的话调用scheduleDecodeLocked,这个方法上面有介绍,就是让解析线程解析下一张图片,这样反复的进行,整个webp动画就播放起来了,整个解析的过程中也不会造成内存的飙升,因为使用的内存只有mFrontBitmap和mBackBitmap,这种思想还是很好的,如果我们想在节约内存,只用一个bitmap,解一张播一张的话会没有这么流畅,别问我为什么知道,因为我们项目里现在就是播一张解析一张的。。。
好了,到这里整体代码逻辑的介绍就完成了,如果你觉得我说的不是那么清晰,可以留言说出你的疑问,也可以直接阅读源码看看到底是咋回事
预告
其实我看了这个源码以后,想了一下我们之前播放webp用的库,我通过抽象了FrameSequence这个类,在保持了FrameSequenceDrawable几乎所有的源码后,使用了facebook 的 Fresco库对FrameSequence这个类进行了抽象和实现,达到了一个Drawable可以通过简单的修改可以同时支持webp和gif的功能,介绍的文章在我写完这个之后会马上开始~
更新
预告中的文章已经写完Android播放webp和gif的一种方法(接上篇),欢迎批评指正