コンシステントの使用によるAPngDrawableの改善

8115 ワード

背景
以前、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法を適用して,フレームのレンダリング時間をコヒーレントなdelay法と組み合わせて制御しながら,ループ再生とループ復号frameを制御した.協程改造後の論理は簡単で明確で、より理解しやすい.レンダリングされたdelay時間はframeを復号する時間を考慮する必要があります.ここでdelay時間は復号時間を排除した時間です.次の図で簡単に理解できます.
ピクチャは、draw frameの速度が速いため、実行時間が無視されるフレームの復号およびレンダリングプロセスを反映します.したがってdraw frameの開始点も次のフレーム復号の開始点である.各フレームは、このような論理に従って繰り返し実行される.復号化されたコパスはIO Dispatcherで実行され、レンダリングフレームはUIスレッドであるため、ここではマルチスレッド連携の問題を考慮する必要がある.すなわちdraw frameはmain uiスレッドで実行される.描画時に使用されるフレームデータと復号されるフレームデータは、同一のデータでないことを保証する必要がある.この問題を解決するために,復号化とレンダリングを制御するためのFrameBufferを定義し,動作を調整できるようにした.
FrameBufferの使用
次はFrameBufferの完全なコードですが、コードは簡単です.フロントフレームとバックグラウンドフレームを定義することで、復号とレンダリングのコラボレーションを実現します.フロントフレームは画像のレンダリングにのみ使用され、バックグラウンドフレームは復号化にのみ使用されます.このように彼ら2人はそれぞれ仕事をして互いに影響しない.バックグラウンド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を共有することでこの問題を解決することができます.ライブラリでは、共有されたAPngHolderを管理するためのAPngHolderPoolを定義します.次はこのクラスのコードです.
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