背景

之前写了一篇关于如何自定义APngDrawable的文章,当时通过提交任务到线程池来解码apng 文件。其中帧播放的逻辑控制也过于复杂,需要不断的计算帧延时刷新。并且APngDrawable在播放apng文件的过程中,解码线程会经常的发生挂起。为了充分的利用线程,避免挂起线程,并且简化帧播放逻辑。所以我们考虑使用协程来解决这些问题。

协程

协程可以挂起执行,这里的挂起执行与线程的挂起不同。它没有阻塞线程,而是记录当前执行的位置。当异步执行结束后从记录的执行位置继续执行,挂起前后的执行线程有可能不同。利用协程的非阻塞特性可以有效优化apng文件的解码过程。

协程在解码过程中的使用

启动播放apng的过程就是启动协程任务的过程。协程的协程体中进行循环播放控制,帧解码控制,帧渲染控制。下面看下具体的代码:

playJob = launch(Dispatchers.IO) {/*** for decode the apng file.*/var aPngDecoder: APngDecoder? = nullframeBuffer = 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 framesrepeat(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 - timeskipFrameCount = frameBuffer!!.bgFrameData.index + 1logFrame { "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 = 0aPngDecoder = nulllog { "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 = falsesendEvent(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方法控制循环播放和循环解码frame,同时配合协程的delay方法控制帧的渲染时间。通过协程改造后的逻辑简单清晰,更加容易理解。
渲染的delay时间需要考虑到解码frame的时间,这里的delay时间是将解码时间排除掉后的时间。通过下面的图可以方便理解:

图片反映的是一帧的解码和渲染过程,由于draw frame的速度很快,所以它的执行时间忽略不计。所以draw frame的开始点也是下一帧解码的开始点。每一帧都是按照这样的逻辑反复执行。
由于解码的协程执行在IO Dispatcher中,而渲染帧是在UI 线程,所以这里需要考虑多线程协同的问题。也就是说draw frame执行在main ui线程。描画时使用的帧数据和解码的帧数据需要保证不是同一个数据。为了解决这个问题,我们定义了一个FrameBuffer用于控制解码与渲染,让他们可以协调工作。

FrameBuffer的使用

下面是FrameBuffer的完整代码,代码还是比较简单的。它通过定义前台frame和后台frame来达到解码与渲染的协同工作。前台frame只用于渲染图像,后台frame只用于解码使用。这样他们两个就各自工作而相互不影响。当后台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 = prFrameDataprFrameData = fgFrameDatafgFrameData = bgFrameDatabgFrameData = 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的方式来解决这个问题。在库中我们定义了一个APngHolderPool用于管理共享的APngHolder。下面是这个类的代码:

class APngHolderPool(private val lifecycle: Lifecycle) : LifecycleObserver {private val holders = mutableMapOf<String, APngHolder>()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] = thisif (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

我的公众号已经开通,公众号会同步发布。
欢迎关注我的公众号

Android 高效播放apng文件(支持在RecycleView、ListView中显示)相关推荐

  1. Android WebView播放视频并支持全屏

    1. 前言 支持视频在线播放的网页随处可见,前端开发不需要做太多的工作就可以实现大多数需求.因为播放视频的具体功能是由浏览器核心在处理的.我以为Android的WebView应该是默认支持视频播放的, ...

  2. android swf webview,android webview播放swf文件

    今天做了一个通过webview播放flash的文件,以前一直以为可以使用videoview视频播放播放flash的东西,今天才发现不行,好像现在除了自己做一个flash播放器,就是用webview去加 ...

  3. android swf webview,android webview播放swf文件

    今天做了一个通过webview播放flash的文件,以前一直以为可以使用videoview视频播放播放flash的东西,今天才发现不行,好像现在除了自己做一个flash播放器,就是用webview去加 ...

  4. Android AudioTrack播放PCM文件

    上篇文章写了使用AudioRecord采集音频,为了测试采集音频是否正确,可以通过AudioTrack播放音频试下. AudioTrack只能播放PCM格式的文件.PCM全称是Pulse Code M ...

  5. 微信android自动播放视频文件,vue-player或TcPlayer在微信内自动播放video和audio

    不管是IOS框互理.各近架跳机蓝种近架跳机蓝种近架跳和Android,当video和audio初始src为空,根据点击不同的媒体源(非播放器控件),比如多个章节的视频列表,动态给src赋值并执行pla ...

  6. 在html中加入pdf文件吗,如何在网页中显示PDF文件

    我们是不是对百度文库能直接在网页上显示PDF文件感到好奇,你是否也想实现这样的功能?很多朋友认为可以直接在网页中插入代码就可以实现这个功能,其实要在网页中完整地显示PDF文件,需要把PDF文件转换成S ...

  7. 在Linux上使用ffmpeg摆脱DTS / AC3音频,以在iOS或Android上播放MKV文件

    I encountered the problem on iPhone that MKV video files with AC3 are played with no sound. The OPla ...

  8. android mediaplay 播放AAC文件时,无法正常播放完成就回调完成。

    今天在处理文件时,发现一个问题,某个AAC文件总是无法播放到结尾,但是播放前或者播放过程中拖动进度条后却能正常播放完成.系统播放器也是如此.解决方法如下 mMediaPlayer.start();mM ...

  9. android MP3播放器(支持歌词滚动等功能)

    大二课余时间写的音乐播放器,发现目前网上很多android播放器都缺胳膊少腿的, 于是便分享出来给才接触android的同学作参考,(Tomcat服务器功能已删除) 实现了启动动画,引导界面,appw ...

最新文章

  1. Comet OJ - Contest #2题解
  2. Python进阶8——字典与散列表,字符串编解码
  3. VHDL+Verilog良好的代码编写风格(转载)
  4. Redis 在真实世界的 5 个用法
  5. Redis源码分析(二)redis-cli.c
  6. php压缩zip文件类
  7. 预约购票 php,正式上线!预约购票、参访攻略...你最关心的都在这!
  8. 手机黑圆点怎么打_手机能「打快板」是怎么回事?浅谈手机的光学防抖
  9. c语言智能指针是什么,C++ 智能指针深入解析
  10. ultraedit 运行的是试用模式_原来用Unittest框架写接口测试用例这么简单!
  11. 深入理解JavaScript系列(13):This? Yes,this!
  12. 使用allegro画PCB的基本流程:
  13. 地理空间数据云DEM数据解压失败_解决了
  14. Java项目:博客系统西瓜社区(springboot+mybatis-plus+thymeleaf)
  15. js会员头像上传拖动处理头像类
  16. PS PhotoShop CS5 CS6 序列号 安装
  17. 杂谈:创业公司的产品开发与团队管理
  18. 【用户运营】滴滴出行活动策划、用户成长体系、用户增长逻辑分析
  19. 仿天猫商城html网页源码
  20. 回溯法和分支限界法解决旅行商问题

热门文章

  1. python三行情书_不知道怎么向女神表白?Python三大神技分分钟带你成功逆袭!
  2. 【强化学习论文合集】三十.2021AAAI人工智能大会论文(AAAI2021)
  3. 5 按示例查询(QueryByExample,QBE)
  4. 为什么会看到IP地址相同的两台电脑?附查询自己公网IP的方法
  5. C# 利用linq获取一组数中几个连续数中最大的一个数字
  6. WordPress主题 大前端 阿里百秀 XIU 小清新CMS高级主题[更新v5.1]
  7. pycharm 输入中文变成繁体字的解决方法
  8. 为ListView添加分段标头
  9. java 获得 大盘 开盘_教你利用开盘十分钟判定当日大盘强弱(建议收藏!)
  10. Failed to configure a DataSource: ‘url‘ attribute is not specified and no embedded 。。。的解决办法