背景
我发现了一些GIF animation library,它有一个后台线程不断地将当前帧解码为位图,它是其他线程的生成器:
@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示例是可用的here。
问题所在
这是低效的,因为当线程到达mIsPlaying为false的点时,它只是在那里等待并不断地检查它。事实上,它会导致这个线程以某种方式做更多的CPU使用(我通过分析器检查过)。
事实上,它从占CPU的3-5%增加到了12%-14%。
我试过的
过去我对线程有很好的了解,我知道简单地放置一个wait和notify是危险的,因为它仍然会导致线程等待一些罕见的情况。例如,当它确定它应该等待,然后在它开始等待之前,外部线程标记它不应该等待。
这种行为被称为“忙碌旋转”或“忙碌等待”,实际上对于需要协同工作的多个线程,here有一些解决方案。
但在这里我觉得有点不一样。等待不是等待某个线程完成它的工作。这是暂时的等待。
这里的另一个问题是使用者线程是UI线程,因为它需要获取位图并查看它,所以它不能像消费者生产者解决方案那样等待工作(用户界面绝不能等待,因为它会导致"jank")。
问题是
避免在这里旋转的正确方法是什么?
发布于 2018-09-10 05:17:58
所以我决定使用等待通知机制,因为我找不到任何好的类来处理这个案例。这需要精细的思考,因为以错误的方式使用线程可能导致(在非常罕见的情况下)无限等待和其他奇怪的事情。
即使在UI线程上,我也决定使用synchronized,但我使用它的同时也保证它不会出现很长时间。这是因为UI线程一般不应该等待其他线程。为此,我可以使用一个线程池(大小为1),以避免UI线程等待同步部分,但我认为它足够好。
下面是我为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
}
}https://stackoverflow.com/questions/52245590
复制相似问题