Jetpack Compose使用rememberLauncherForActivityResult触发TransactionTooLargeException如何解决?
修复TransactionTooLargeException方案
根因定位
崩溃的核心原因有两个:
Picture组件使用rememberSaveable存储Bitmap对象:rememberSaveable会将持有的数据序列化后存入系统SavedInstanceStateBundle,切后台、配置变更时系统会通过Binder传输这个Bundle,而单个Bitmap体积就可能达到几十KB,多个Bitmap累加后极易超过Binder事务1MB的大小限制,触发崩溃。- 自行实现的图片加载逻辑无缓存,每次页面重组都会重新调用
loadThumbnail加载所有可见图片,同时ActivityResultlauncher的声明位置错误导致不必要的额外重组,进一步放大了资源占用。
具体修复步骤
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
相关产品推荐
相关产品推荐

