背景

之前写了一篇对于如何自定义APngDrawable的文章,过后通过提交工作到线程池来解码apng 文件。其中帧播放的逻辑管制也过于简单,须要一直的计算帧延时刷新。并且APngDrawable在播放apng文件的过程中,解码线程会常常的产生挂起。为了充沛的利用线程,防止挂起线程,并且简化帧播放逻辑。所以咱们思考应用协程来解决这些问题。

协程

协程能够挂起执行,这里的挂起执行与线程的挂起不同。它没有阻塞线程,而是记录以后执行的地位。当异步执行完结后从记录的执行地位继续执行,挂起前后的执行线程有可能不同。利用协程的非阻塞个性能够无效优化apng文件的解码过程。

协程在解码过程中的应用

启动播放apng的过程就是启动协程工作的过程。协程的协程体中进行循环播放管制,帧解码管制,帧渲染管制。上面看下具体的代码:

playJob = launch(Dispatchers.IO) {            /**             * for decode the apng file.             */            var aPngDecoder: APngDecoder? = null            frameBuffer = FrameBuffer(columns, rows)            try {                // send start event.                sendEvent(PlayEvent.START)                //Loop playback.                repeat(plays) { playCounts ->                    log { "play start play count : $playCounts" }                    if (playCounts > 0) {                        //send repeat event.                        sendEvent(PlayEvent.REPEAT)                    }                    //init apng decoder and frame buffer.                    if (aPngDecoder == null) {                        aPngDecoder = APngDecoder(streamCreator.invoke())                        frameBuffer!!.reset()                    }                    aPngDecoder?.let { decoder ->                        log { "decode start decoder ${decoder.hashCode()} skipFrameCount $skipFrameCount" }                        //seek to the last played frame.                        repeat(skipFrameCount) {                            decoder.advance(frameBuffer!!.bgFrameData)                        }                        //decode the left frames                        repeat(frames - skipFrameCount) {                            var time = System.currentTimeMillis()                            decoder.advance(frameBuffer!!.bgFrameData)                            time = System.currentTimeMillis() - time                            //compute the delay time. We need to minus the decode time.                            val delay = frameBuffer!!.fgFrameData.delay - time                            skipFrameCount = frameBuffer!!.bgFrameData.index + 1                            logFrame { "decode frame index ${frameBuffer!!.bgFrameData.index} skipFrameCount $skipFrameCount time $time delay $delay" }                            delay(delay)                            //swap the frame between fg frame and bg frame.                            frameBuffer?.swap()                            //send frame event.                            sendEvent(PlayEvent.FRAME)                        }                        //close the apng decoder.                        decoder.close()                        skipFrameCount = 0                        aPngDecoder = null                        log { "decode end release decoder ${decoder.hashCode()}" }                    }                    log { "play end play count : $playCounts" }                }                //play end, reset the start state for next time to restart again.                isStarted = false                sendEvent(PlayEvent.END)            } catch (e: Exception) {                log { "launch  Exception ${e.message}" }                //send cancel event.                sendEvent(PlayEvent.CANCELED)            } finally {                log { "release decoder and frameBuffer in finally" }                aPngDecoder?.close()                lastFrameData?.release()                lastFrameData = frameBuffer?.cloneFgBuffer()                frameBuffer?.release()            }        }

这里利用到了协程的repeat办法管制循环播放和循环解码frame,同时配合协程的delay办法管制帧的渲染工夫。通过协程革新后的逻辑简略清晰,更加容易了解。
渲染的delay工夫须要思考到解码frame的工夫,这里的delay工夫是将解码工夫排除掉后的工夫。通过上面的图能够不便了解:

图片反映的是一帧的解码和渲染过程,因为draw frame的速度很快,所以它的执行工夫忽略不计。所以draw frame的开始点也是下一帧解码的开始点。每一帧都是依照这样的逻辑重复执行。
因为解码的协程执行在IO Dispatcher中,而渲染帧是在UI 线程,所以这里须要思考多线程协同的问题。也就是说draw frame执行在main ui线程。描绘时应用的帧数据和解码的帧数据须要保障不是同一个数据。为了解决这个问题,咱们定义了一个FrameBuffer用于管制解码与渲染,让他们能够协调工作。

FrameBuffer的应用

上面是FrameBuffer的残缺代码,代码还是比较简单的。它通过定义前台frame和后盾frame来达到解码与渲染的协同工作。前台frame只用于渲染图像,后盾frame只用于解码应用。这样他们两个就各自工作而互相不影响。当后盾frame解码实现并且delay工夫曾经到时,程序会通过调用swap办法切换前后台frame。

internal class FrameBuffer(w: Int, h: Int) {    var prFrameData: FrameData = FrameData(Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888))    var fgFrameData: FrameData = FrameData(Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888))    var bgFrameData: FrameData = FrameData(Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888))    fun swap() {        val temp = prFrameData        prFrameData = fgFrameData        fgFrameData = bgFrameData        bgFrameData = temp    }    fun reset() {        fgFrameData.reset()        prFrameData.reset()        bgFrameData.reset()    }    fun release() {        fgFrameData.release()        prFrameData.release()        bgFrameData.release()    }    fun cloneFgBuffer() = FrameData(Bitmap.createBitmap(fgFrameData.bitmap))}

如何共享APng播放

有的时候咱们须要在同一个画面下播放多个同一个APng 文件,如果为每个播放都创立一个解码用的APngHolder,那么内存应用就会减少。咱们能够通过共享APngHolder的形式来解决这个问题。在库中咱们定义了一个APngHolderPool用于治理共享的APngHolder。上面是这个类的代码:

class APngHolderPool(private val lifecycle: Lifecycle) : LifecycleObserver {    private val holders = mutableMapOf<String, APngHolder>()    init {        lifecycle.addObserver(this)    }    @OnLifecycleEvent(Lifecycle.Event.ON_START)    fun onStart() {        holders.forEach {            it.value.resume(true)        }    }    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)    fun onStop() {        holders.forEach {            it.value.pause(true)        }    }    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)    fun onDestroy() {        holders.clear()        lifecycle.removeObserver(this)    }    internal fun require(scope: CoroutineScope, file: String, streamCreator: () -> InputStream) =        holders[file] ?: APngHolder(file, true, scope, streamCreator)            .apply {                holders[file] = this                if (lifecycle.currentState >= Lifecycle.State.STARTED) {                    resume(true)                }            }}

通过代码咱们也能发现通过APngHolderPool治理的APngHolder的播放进行等动作只与lifecycle绑定,共享的APngHolder不会因为APngDrawable的暗藏和销毁而进行播放并开释。所以大家在应用共享的APngHolder的时候要思考是否真正须要它。上面的代码展现了如何应用APngHolderPool。

val sharedAPngHolderPool = APngHolderPool()    fun onClickView(view: View) {        when (view.id) {            R.id.image1 -> {                imageView.playAPngAsset(this, "google.png", sharedHolders = sharedAPngHolderPool)            }            R.id.image2 -> {                imageView.playAPngAsset(this, "blued.png")            }            R.id.imageView -> (imageView.drawable as? APngDrawable)?.let {                if (it.isRunning) {                    it.stop()                } else {                    it.start()                }            }        }    }

总结

通过协程革新过的解码过程和渲染过程更加简洁清晰了,也达到了最后的革新目标。并且通过kotlin的扩大反对,使得播放APng的调用也更加简略。上面我分享了整个的代码,其中也包含了革新前的代码。大家能够对照下,置信协程实现的长处不言而喻。

Git

大家能够通过上面的git地址下载到残缺的代码。
https://github.com/mjlong123123/PlayAPng/releases/tag/1.0.1