如何在Jetpack Compose中用PdfRenderer高效渲染PDF并在LazyColumn展示
性能问题核心原因
- 所有PDF页面一次性全量渲染:下载成功后直接循环遍历所有页面生成Bitmap,页数较多时会占用巨量内存,且整个渲染过程默认在主线程执行,直接导致页面长时间冻结
- 逻辑无重组保护:下载、渲染逻辑直接写在Composable根作用域,每次组件重组都会触发重复下载、重复渲染,额外消耗大量系统资源
- 无Bitmap复用与缓存机制:大量Bitmap频繁创建销毁会触发频繁GC,是滚动卡顿的核心诱因
- 固定尺寸渲染不灵活:如果预设的Constants宽高远大于实际显示尺寸,会造成不必要的内存浪费
优化方案
基础优化(原生实现)
- 用
LaunchedEffect包裹下载、渲染逻辑,绑定obraId、autorId作为唯一key,避免逻辑重复执行 - 放弃全量预渲染,改为懒加载:只在LazyColumn对应项即将进入可视区域时,才触发对应PDF页的渲染,渲染逻辑放到后台协程执行,不阻塞主线程
- 接入LruCache实现Bitmap复用,限制内存占用上限,避免频繁GC
- 按实际Image组件的显示尺寸渲染Bitmap,不要使用固定的全局宽高参数,减少单张Bitmap的内存占用
推荐Compose适配库
优先使用Coil图片加载库,3.0及以上版本内置支持PDF页面解码,自动处理后台渲染、内存缓存、懒加载逻辑,无需自己封装PdfRenderer相关逻辑,改造成本极低。
优化后代码示例
Coil实现方案
@Composable fun PDFScreen(obraId: ObraId, autorId: AuthorId) { val storageRef = FirebaseStorage.getInstance().reference val pathReference = storageRef.child("obras/${autorId}/${obraId}.pdf") // 记录本地PDF文件路径,下载成功后再渲染 var localPdfFile by remember { mutableStateOf<File?>(null) } // 总页数,读取PDF后赋值 var pageCount by remember { mutableIntStateOf(0) } // 仅当obraId、autorId变化时才触发下载 LaunchedEffect(obraId, autorId) { val tempFile = File.createTempFile("obra", "pdf") pathReference.getFile(tempFile).await() // 用协程await替代回调,后台执行 if (tempFile.exists()) { localPdfFile = tempFile // 读取PDF总页数 val input = ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_ONLY) val renderer = PdfRenderer(input) pageCount = renderer.pageCount renderer.close() input.close() } } LazyColumn(modifier = Modifier.fillMaxSize()) { items(pageCount) { pageIndex -> localPdfFile?.let { file -> AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(file) .decoderFactory(PdfDecoder.Factory(page = pageIndex)) .size(Size.ORIGINAL) // 可按需设置尺寸降低内存占用 .build(), contentDescription = "PDF页${pageIndex+1}", modifier = Modifier.fillMaxWidth() ) } ?: run { // 自定义下载中占位UI } } } }
原生自定义实现核心逻辑
如果需要自己实现PdfRenderer逻辑,核心懒加载渲染逻辑如下:
// 单个PDF页项的Composable @Composable fun PdfPageItem(file: File, pageIndex: Int, modifier: Modifier = Modifier) { var bitmap by remember { mutableStateOf<ImageBitmap?>(null) } // 仅当项进入组合时才触发渲染,后台执行 LaunchedEffect(pageIndex, file) { withContext(Dispatchers.IO) { val input = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) val renderer = PdfRenderer(input) val page = renderer.openPage(pageIndex) // 按需计算实际需要的Bitmap尺寸,不要硬编码 val targetBitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888) page.render(targetBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) page.close() renderer.close() input.close() bitmap = targetBitmap.asImageBitmap() } } bitmap?.let { Image(bitmap = it, contentDescription = null, modifier = modifier) } ?: // 自定义占位UI }
内容的提问来源于stack exchange,提问作者Komito
相关产品推荐
相关产品推荐

