通过使用协程改善APngDrawable
背景
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()
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