You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 13:45:32