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

Jetpack Compose使用rememberLauncherForActivityResult触发TransactionTooLargeException如何解决?

修复TransactionTooLargeException方案

根因定位

崩溃的核心原因有两个:

  1. Picture组件使用rememberSaveable存储Bitmap对象:rememberSaveable会将持有的数据序列化后存入系统SavedInstanceState Bundle,切后台、配置变更时系统会通过Binder传输这个Bundle,而单个Bitmap体积就可能达到几十KB,多个Bitmap累加后极易超过Binder事务1MB的大小限制,触发崩溃。
  2. 自行实现的图片加载逻辑无缓存,每次页面重组都会重新调用loadThumbnail加载所有可见图片,同时ActivityResult launcher的声明位置错误导致不必要的额外重组,进一步放大了资源占用。

具体修复步骤

1. 移除Bitmap的rememberSaveable存储

将Picture组件中存储Bitmap的rememberSaveable替换为普通remember,Bitmap属于大对象,绝对不能存入SavedInstanceState:

// 修改前
val bitmap: MutableState<Bitmap?> = rememberSaveable { mutableStateOf(null) }
// 修改后
val bitmap: MutableState<Bitmap?> = remember { mutableStateOf(null) }

如果需要配置变更(如旋转屏幕)时不重新加载图片,可自行实现全局内存缓存,不要依赖SavedInstanceState。

2. 调整ActivityResult launcher的声明位置

将launcher的初始化移到Composable顶层,不要放在条件分支内,避免每次触发目录选择时重复创建launcher带来的额外重组:

@ExperimentalFoundationApi
@Composable
fun DisplayPictures(
    pictures: List<Uri>,
    navController: NavController,
    appBarName: String = stringResource(id = R.string.choose_folder),
    onNewDirectoryUri: (uri: Uri?) -> Unit = { }
) {
    // launcher移到顶层初始化
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.OpenDocumentTree(),
        onResult = {
            onNewDirectoryUri(it)
        })
    var showDirectorySelect by remember { mutableStateOf(false) }
    // 仅在showDirectorySelect为true时触发启动
    LaunchedEffect(showDirectorySelect) {
        if (showDirectorySelect) {
            launcher.launch(null)
            showDirectorySelect = false
        }
    }

    Scaffold(topBar = {
        TopAppBar(title = {
            Text(
                text = appBarName.dropLastWhile { predicate -> predicate == '%' },
                Modifier.padding(8.dp)
            )
        }, Modifier.clickable { showDirectorySelect = true })
    }) {
        // 移除不必要的showDirectorySelect判断,避免每次选择目录后重建整个网格
        LazyVerticalGrid(
            maxColumnWidth = 150.dp,
            paddingDp = 4.dp,
            pictures = pictures
        ) { uri ->
            val route = Screen.Detail.createRoute(
                URLEncoder.encode(uri.toString(), "UTF-8")
            )
            if (navController.currentDestination?.route?.contains("detail") == false) {
                navController.navigate(route)
            }
        }
    }
}

3. 优化图片加载逻辑

建议直接使用Coil、Glide等成熟的Compose图片加载库,它们自带内存/磁盘缓存、Bitmap生命周期管理,无需自行处理加载逻辑。
如果要保留自定义加载逻辑,可增加全局Lru缓存减少重复加载:

// 全局缩略图缓存,最大占用内存为应用可用内存的1/8
object ThumbnailCache : LruCache<String, Bitmap>(
    (Runtime.getRuntime().maxMemory() / 8 / 1024).toInt()
) {
    override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount / 1024
    fun getKey(uri: Uri, size: Size): String = "${uri}_${size.width}_${size.height}"
}

// 修改后的Picture组件加载逻辑
@ExperimentalFoundationApi
@Composable
fun Picture(
    uri: Uri,
    modifier: Modifier,
    size: Size,
    onClick: () -> Unit = {},
) {
    val bitmap: MutableState<Bitmap?> = remember { mutableStateOf(null) }
    val context = LocalContext.current
    val cacheKey = remember(uri, size) { ThumbnailCache.getKey(uri, size) }

    LaunchedEffect(uri, size) {
        // 优先读缓存
        ThumbnailCache.get(cacheKey)?.let {
            bitmap.value = it
            return@LaunchedEffect
        }
        // 缓存不存在再异步加载
        withContext(Dispatchers.IO) {
            try {
                val loaded = context.contentResolver.loadThumbnail(uri, size, null)
                loaded?.let {
                    ThumbnailCache.put(cacheKey, it)
                    bitmap.value = it
                }
            } catch (e: Exception) {
                // 异常处理逻辑
            }
        }
    }

    Box(
        modifier = modifier
            .aspectRatio(1f)
            .placeholder(visible = bitmap.value == null)
    ) {
        bitmap.value?.let {
            Image(
                bitmap = it.asImageBitmap(),
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .clip(RectangleShape)
                    .fillMaxSize()
                    .clickable { onClick() }
            )
        }
    }
}

4. 大列表优化

如果单批次加载的图片Uri数量超过1000条,建议增加分页加载逻辑,每次仅加载部分Uri传入DisplayPictures,减少重组时的数据传输量。


内容的提问来源于stack exchange,提问作者Evgeny Yurin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 08:15:03