Как избежать занятого вращения паузы-продюсера? - PullRequest
0 голосов
/ 09 сентября 2018

Фон

Я нашел некоторую библиотеку анимации GIF , которая имеет фоновый поток, который постоянно декодирует текущий кадр в растровое изображение, являясь производителем для другого потока:

@Volatile
private var mIsPlaying: Boolean = false

...
while (mIsRunning) {
    if (mIsPlaying) {
        val delay = mGifDecoder.decodeNextFrame()
        Thread.sleep(delay.toLong())
        i = (i + 1) % frameCount
        listener.onGotFrame(bitmap, i, frameCount)
    }
}

Образец POC, который я сделал для этого, доступен здесь .

Проблема

Это неэффективно, потому что, когда поток достигает точки, которая mIsPlaying ложна, он просто ждет и постоянно проверяет это. Фактически, это заставляет этот поток как-то больше загружать процессор (я проверял через профилировщик).

Фактически, он увеличивается с 3–5% ЦП до 12–14% ЦП.

Что я пробовал

В прошлом я хорошо знал о потоках, и я знаю, что просто ставить wait и notify опасно, так как это может привести к тому, что поток будет ждать в некоторых редких случаях. Например, когда он определил, что он должен ждать, а затем, прежде чем он начнет ждать, внешний поток помечает его как ожидающий.

Это поведение называется «занятое вращение» или «Ожидание при занятости», и на самом деле есть несколько решений об этом, в случае нескольких потоков, которые должны работать вместе, здесь .

Но здесь я думаю, что это немного по-другому. Ожидание не для некоторого потока, чтобы закончить свою работу. Это для временного ожидания.

Другая проблема заключается в том, что потребительский поток является потоком пользовательского интерфейса, поскольку именно он должен получить растровое изображение и просмотреть его, поэтому он не может просто ждать работу, как решение потребительского производителя (пользовательский интерфейс никогда не должен ждать , как это может вызвать "джанк").

Вопрос

Какой правильный способ избежать вращения здесь?

1 Ответ

0 голосов
/ 10 сентября 2018

Поэтому я решил использовать механизм ожидания-уведомления, так как не смог найти ни одного хорошего класса для обработки этого случая. Это требует деликатного мышления, поскольку неправильное использование потоков может привести (в очень редких случаях) к бесконечному ожиданию и другим странным вещам.

Я решил использовать synchronized даже в потоке пользовательского интерфейса, но я использую его, обещая, что он не будет длинным, никогда. Это связано с тем, что поток пользовательского интерфейса вообще не должен ждать других потоков. Я мог бы использовать для этого пул потоков (размером 1), чтобы избежать ожидания потока пользовательского интерфейса на синхронизированной части, но я думаю, что это достаточно хорошо.

Вот мой модифицированный код для gifPlayer:

class GifPlayer(private val listener: GifListener) : Runnable {
    private var playThread: Thread? = null
    private val gifDecoder: GifDecoder = GifDecoder()
    private var sourceType: SourceType? = null
    private var filePath: String? = null
    private var sourceBuffer: ByteArray? = null
    private var isPlaying = AtomicBoolean(false)

    interface GifListener {
        fun onGotFrame(bitmap: Bitmap, frame: Int, frameCount: Int)

        fun onError()
    }

    @UiThread
    fun setFilePath(filePath: String) {
        sourceType = SourceType.SOURCE_PATH
        this.filePath = filePath
    }

    @UiThread
    fun setBuffer(buffer: ByteArray) {
        sourceType = SourceType.SOURCE_BUFFER
        sourceBuffer = buffer
    }

    @UiThread
    fun start() {
        if (sourceType != null) {
            playThread = Thread(this)
            synchronized(this) {
                isPlaying.set(true)
            }
            playThread!!.start()
        }
    }

    @UiThread
    fun stop() {
        playThread?.interrupt()
    }

    @UiThread
    fun pause() {
        synchronized(this) {
            isPlaying.set(false)
            (this as java.lang.Object).notify()
        }
    }

    @UiThread
    fun resume() {
        synchronized(this) {
            isPlaying.set(true)
            (this as java.lang.Object).notify()
        }
    }

    @UiThread
    fun toggle() {
        synchronized(this) {
            isPlaying.set(!isPlaying.get())
            (this as java.lang.Object).notify()
        }
    }

    override fun run() {
        try {
            val isLoadOk: Boolean = if (sourceType == SourceType.SOURCE_PATH) {
                gifDecoder.load(filePath)
            } else {
                gifDecoder.load(sourceBuffer)
            }
            val bitmap = gifDecoder.bitmap
            if (!isLoadOk || bitmap == null) {
                listener.onError()
                gifDecoder.recycle()
                return
            }
            var i = -1
            val frameCount = gifDecoder.frameCount
            gifDecoder.setCurIndex(i)
            while (true) {
                if (isPlaying.get()) {
                    val delay = gifDecoder.decodeNextFrame()
                    Thread.sleep(delay.toLong())
                    i = (i + 1) % frameCount
                    listener.onGotFrame(bitmap, i, frameCount)
                } else {
                    synchronized(this@GifPlayer) {
                        if (!isPlaying.get())
                            (this@GifPlayer as java.lang.Object).wait()
                    }
                }
            }
        } catch (interrupted: InterruptedException) {
        } catch (e: Exception) {
            e.printStackTrace()
            listener.onError()
        } finally {
        }
    }


    internal enum class SourceType {
        SOURCE_PATH, SOURCE_BUFFER
    }

}

После некоторой работы у меня есть хороший способ сделать это с помощью HandlerThread. Я думаю, что это лучше и, вероятно, имеет лучшую стабильность. Вот код:

open class GifPlayer(private val listener: GifListener) {
        private val uiHandler = Handler(Looper.getMainLooper())
        private var playerHandlerThread: HandlerThread? = null
        private var playerHandler: Handler? = null
        private val gifDecoder: GifDecoder = GifDecoder()
        private var currentFrame: Int = -1
        var state: State = State.IDLE
            private set
        private val playRunnable: Runnable

        enum class State {
            IDLE, PAUSED, PLAYING, RECYCLED, ERROR
        }

        interface GifListener {
            fun onGotFrame(bitmap: Bitmap, frame: Int, frameCount: Int)

            fun onError()
        }

        init {
            playRunnable = object : Runnable {
                override fun run() {
                    val frameCount = gifDecoder.frameCount
                    gifDecoder.setCurIndex(currentFrame)
                    currentFrame = (currentFrame + 1) % frameCount
                    val bitmap = gifDecoder.bitmap
                    val delay = gifDecoder.decodeNextFrame().toLong()
                    uiHandler.post {
                        listener.onGotFrame(bitmap, currentFrame, frameCount)
                        if (state == State.PLAYING)
                            playerHandler!!.postDelayed(this, delay)
                    }
                }
            }
        }

        @Suppress("unused")
        protected fun finalize() {
            stop()
        }

        @UiThread
        fun start(filePath: String): Boolean {
            if (state != State.IDLE)
                return false
            currentFrame = -1
            state = State.PLAYING
            playerHandlerThread = HandlerThread("GifPlayer")
            playerHandlerThread!!.start()
            playerHandler = Handler(playerHandlerThread!!.looper)
            playerHandler!!.post {
                gifDecoder.load(filePath)
                val bitmap = gifDecoder.bitmap
                if (bitmap != null) {
                    playRunnable.run()
                } else {
                    gifDecoder.recycle()
                    uiHandler.post {
                        state = State.ERROR
                        listener.onError()
                    }
                    return@post
                }
            }
            return true
        }

        @UiThread
        fun stop(): Boolean {
            if (state == State.IDLE)
                return false
            state = State.IDLE
            playerHandler!!.removeCallbacks(playRunnable)
            playerHandlerThread!!.quit()
            playerHandlerThread = null
            playerHandler = null
            return true
        }

        @UiThread
        fun pause(): Boolean {
            if (state != State.PLAYING)
                return false
            state = State.PAUSED
            playerHandler?.removeCallbacks(playRunnable)
            return true
        }

        @UiThread
        fun resume(): Boolean {
            if (state != State.PAUSED)
                return false
            state = State.PLAYING
            playerHandler?.removeCallbacks(playRunnable)
            playRunnable.run()
            return true
        }

        @UiThread
        fun toggle(): Boolean {
            when (state) {
                State.PLAYING -> pause()
                State.PAUSED -> resume()
                else -> return false
            }
            return true
        }

    }
...