通過使用協(xié)程改善APngDrawable
背景
APngDrawable在播放apng文件的過程中,解碼線程會經(jīng)常的發(fā)生掛起。為了充分的利用線程,避免掛起線程,并且簡化幀播放邏輯。所以我們考慮使用協(xié)程來解決這些問題。
協(xié)程
協(xié)程可以掛起執(zhí)行,這里的掛起執(zhí)行與線程的掛起不同。它沒有阻塞線程,而是記錄當(dāng)前執(zhí)行的位置。當(dāng)異步執(zhí)行結(jié)束后從記錄的執(zhí)行位置繼續(xù)執(zhí)行,掛起前后的執(zhí)行線程有可能不同。利用協(xié)程的非阻塞特性可以有效優(yōu)化apng文件的解碼過程。
協(xié)程在解碼過程中的使用
啟動播放apng的過程就是啟動協(xié)程任務(wù)的過程。協(xié)程的協(xié)程體中進(jìn)行循環(huán)播放控制,幀解碼控制,幀渲染控制。下面看下具體的代碼:
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()
????????????}
????????}
這里應(yīng)用到了協(xié)程的repeat方法控制循環(huán)播放和循環(huán)解碼frame,同時配合協(xié)程的delay方法控制幀的渲染時間。通過協(xié)程改造后的邏輯簡單清晰,更加容易理解。
渲染的delay時間需要考慮到解碼frame的時間,這里的delay時間是將解碼時間排除掉后的時間。通過下面的圖可以方便理解:
圖片反映的是一幀的解碼和渲染過程,由于draw frame的速度很快,所以它的執(zhí)行時間忽略不計。所以draw frame的開始點也是下一幀解碼的開始點。每一幀都是按照這樣的邏輯反復(fù)執(zhí)行。
由于解碼的協(xié)程執(zhí)行在IO Dispatcher中,而渲染幀是在UI 線程,所以這里需要考慮多線程協(xié)同的問題。也就是說draw frame執(zhí)行在main ui線程。描畫時使用的幀數(shù)據(jù)和解碼的幀數(shù)據(jù)需要保證不是同一個數(shù)據(jù)。為了解決這個問題,我們定義了一個FrameBuffer用于控制解碼與渲染,讓他們可以協(xié)調(diào)工作。
FrameBuffer的使用
下面是FrameBuffer的完整代碼,代碼還是比較簡單的。它通過定義前臺frame和后臺frame來達(dá)到解碼與渲染的協(xié)同工作。前臺frame只用于渲染圖像,后臺frame只用于解碼使用。這樣他們兩個就各自工作而相互不影響。當(dāng)后臺frame解碼完成并且delay時間已經(jīng)到時,程序會通過調(diào)用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 文件,如果為每個播放都創(chuàng)建一個解碼用的APngHolder,那么內(nèi)存使用就會增加。我們可以通過共享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)
????????????????}
????????????}
}
通過代碼我們也能發(fā)現(xiàn)通過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()
????????????????}
????????????}
????????}
????}
總結(jié)
經(jīng)過協(xié)程改造過的解碼過程和渲染過程更加簡潔清晰了,也達(dá)到了最初的改造目的。并且通過kotlin的擴(kuò)展支持,使得播放APng的調(diào)用也更加簡單。下面我分享了整個的代碼,其中也包括了改造前的代碼。大家可以對照下,相信協(xié)程實現(xiàn)的優(yōu)點顯而易見。
Git
大家可以通過下面的git地址下載到完整的代碼。
https://github.com/mjlong123123/PlayAPng/releases/tag/1.0.1

