背景
之前写了一篇对于如何自定义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