我发现了一些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
为假时,它只是在那里等待并不断对其进行检查。实际上,这会导致该线程以某种方式占用更多的CPU使用率(我通过分析器进行了检查)。
实际上,它从CPU的3-5%变为12-14%的CPU。
我过去对线程有很好的了解,我知道简单地放置wait
和notify
是危险的,因为它仍然可能导致线程在某些罕见情况下等待。例如,当它确定应该等待,然后在开始等待之前,外部线程将其标记为不应该等待。
此行为称为“忙纺”或“忙等待”,实际上,在需要多个线程一起工作的情况下,有一些解决方案here。
但是在这里我认为有点不同。等待不是等待某个线程完成其工作。这是暂时的等待。
这里的另一个问题是使用者线程是UI线程,因为它是一个需要获取位图并进行查看的线程,因此它不能像使用者-生产者解决方案那样只是等待工作(UI绝不能等待,因为它可能导致“垃圾”。
避免在这里旋转的正确方法是什么?
答案 0 :(得分:0)
所以我决定使用wait-notify机制,因为找不到任何好的类来处理这种情况。这需要仔细考虑,因为以错误的方式使用线程会导致(在极少数情况下)无限的等待和其他奇怪的事情。
我决定甚至在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
}
}