乐趣区

关于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

退出移动版