Jetpack Compose从网络获取视频帧致卡顿的优化方案
解决远程视频帧加载卡顿、性能低下的问题
你当前的代码存在几个核心问题导致性能差:
MediaMetadataRetriever的操作是耗时的网络/IO任务,即使在LaunchedEffect中,默认协程池处理大量此类任务时仍会挤占UI线程资源- 直接获取的原始Bitmap未做压缩,内存占用过高,频繁触发GC导致卡顿
- 没有缓存机制,每次重组或重新进入都会重复下载解析
下面给出两种高效的解决方案:
方案一:使用Coil(推荐)
Coil是Jetpack Compose友好的图片加载库,内置线程调度、缓存、Bitmap压缩等优化,还原生支持视频帧加载,无需自己造轮子。
步骤1:添加依赖
implementation("io.coil-kt:coil-compose:2.5.0")
优化后的代码
@Composable private fun ContentItem( modifier: Modifier = Modifier, content: Content, onClick: (Content) -> Unit ) { when (content.type) { ContentType.Image -> { AsyncImage( modifier = modifier, model = content.url, contentDescription = null, placeholder = { CircularProgressIndicator() }, error = { Icon(Icons.Default.Error, contentDescription = "加载失败") } ) } ContentType.Video -> { AsyncImage( modifier = modifier, model = ImageRequest.Builder(LocalContext.current) .data(content.url) .videoFrameMillis(1000) // 指定获取1秒处的帧 .crossfade(true) .build(), contentDescription = null, placeholder = { CircularProgressIndicator() }, error = { Icon(Icons.Default.Error, contentDescription = "加载失败") } ) } } }
方案二:手动实现优化(不依赖第三方库)
如果不想引入第三方库,可通过以下几点手动优化:
1. 将耗时任务移到IO线程
使用withContext(Dispatchers.IO)将MediaMetadataRetriever的操作放到IO线程,避免阻塞UI。
2. 压缩Bitmap到目标尺寸
根据展示控件的大小压缩Bitmap,减少内存占用。
3. 添加Lru缓存
避免重复下载解析同一视频帧。
优化后的代码
首先创建缓存工具类:
object BitmapCache { private const val MAX_CACHE_SIZE = 10 * 1024 * 1024 // 10MB缓存上限 private val cache = LruCache<String, Bitmap>(MAX_CACHE_SIZE) { bitmap -> bitmap.byteCount } fun get(key: String): Bitmap? = cache.get(key) fun put(key: String, bitmap: Bitmap) = cache.put(key, bitmap) }
然后修改Composable:
@Composable private fun ContentItem( modifier: Modifier = Modifier, content: Content, onClick: (Content) -> Unit ) { val imageSize = remember { // 提前获取控件的预期尺寸,可根据实际布局调整 IntSize(200, 200) } val bitmap = remember { mutableStateOf<Bitmap?>(null) } when (content.type) { ContentType.Image -> { // 图片处理逻辑 } ContentType.Video -> { LaunchedEffect(content, imageSize) { // 先查缓存 BitmapCache.get(content.url)?.let { bitmap.value = it return@LaunchedEffect } // IO线程执行耗时操作 val result = withContext(Dispatchers.IO) { runCatching { val retriever = MediaMetadataRetriever() retriever.setDataSource(content.url) val originalBitmap = retriever.getFrameAtTime( 1000000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC ) retriever.release() originalBitmap?.let { scaleBitmap(it, imageSize.width, imageSize.height) } }.getOrNull() } result?.let { scaledBitmap -> BitmapCache.put(content.url, scaledBitmap) bitmap.value = scaledBitmap } } // 展示占位符或加载后的帧 bitmap.value?.let { Image( modifier = modifier, bitmap = it.asImageBitmap(), contentDescription = null ) } ?: run { Box(modifier = modifier, contentAlignment = Alignment.Center) { CircularProgressIndicator() } } } } } // Bitmap压缩工具方法 private fun scaleBitmap(bitmap: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap { val width = bitmap.width val height = bitmap.height val scale = min(targetWidth.toFloat() / width, targetHeight.toFloat() / height) val matrix = Matrix().apply { postScale(scale, scale) } return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true) }
额外注意事项
- 如果是在列表中加载大量视频帧,建议分页加载,避免一次性触发过多IO任务
- 可添加错误重试逻辑,提升稳定性
- 若控件尺寸不固定,可使用
onSizeChanged获取实际渲染尺寸后再压缩Bitmap
内容的提问来源于stack exchange,提问作者Annon
相关产品推荐
相关产品推荐

