美文网首页View
Android中播放webp动画的一种方式:FrameSeque

Android中播放webp动画的一种方式:FrameSeque

作者: HumorousMan | 来源:发表于2018-03-02 18:24 被阅读0次

    简介

    本篇主要是介绍FrameSequenceDrawable的相关实现原理的文章,FrameSequenceDrawable是Google实现的可以播放Webp动画的Drawable,这个并没有在SDK里面,但是我们可以在googlesource中看到相关的代码,FrameSequenceDrawable相关代码地址

    播放效果

    在介绍之前,我们可以先看一下播放效果:


    webp.gif

    我想直接用

    如果你说我不想看原理,我就想知道咋播放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方法中会首先调用解析线程去解析下一张图片,然后在继续绘制当前图片
    • 反复执行绘制和解析步骤,知道循环次数达到设置状态或者无限循环

    效果示意图

    1.png 2.png 3.png

    源码分析

    现在我们对整个流程上的源码进行一些分析

    • 首先第一步我们先看看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的一种方法(接上篇),欢迎批评指正

    相关文章

      网友评论

        本文标题:Android中播放webp动画的一种方式:FrameSeque

        本文链接:https://www.haomeiwen.com/subject/jfooxftx.html