关于android:通过使用协程改善APngDrawable

背景

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理