Compose Multiplatform LazyGrid 图片网格性能基准:跨 Android、iOS 与 Desktop 的三端同构实现
【免费下载链接】compose-multiplatformCompose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.项目地址: https://gitcode.com/GitHub_Trending/co/compose-multiplatform
本篇文章以仓库中 LazyGridImageView 基准示例 为主线,系统拆解如何用同一套 Compose Multiplatform 代码在 Android、iOS、Desktop(以及 JS/Wasm 目标)上渲染 999 张图片的 3 列懒加载网格,并与原生 Android(Jetpack Compose + Coil)和原生 iOS(SwiftUI + AsyncImage)两套纯原生实现进行逐平台对比。读者在读完本文后,将掌握 Compose Multiplatform 资源目录(composeResources)、LazyVerticalGrid懒加载网格、Coil 异步图片加载与自动滚动压力测试脚本的完整落地写法,以及一套可复现的三端基准测量工作流(大小、启动时间、FPS、CPU/GPU 占用等),可直接照搬到自己的性能评测或图片类应用项目中。
项目定位:为什么需要三个平台同一套功能
LazyGridImageView是仓库benchmarks/showcases目录下专门用于横向对比 Compose Multiplatform 与纯原生实现性能的示例工程。它用三份独立实现完成完全相同的用户场景:
- Kotlin Multiplatform + Compose:位于
composeApp模块,共享一份commonMain代码,同时产出 Android、iOS、Desktop、JS 与 Wasm 目标; - Native Android:位于
nativeAndroidApp模块,纯 Android 实现(Jetpack Compose + Coil),图片来自 Android assets; - Native iOS:位于
nativeiosApp模块,纯 SwiftUI 实现,图片来自 Bundle 资源。
三个实现都渲染999 张 512×512 的图片,排列成3 列的懒加载网格,并提供一个Auto Scroll 自动滚动开关,用于在基准测试时制造持续的滚动负载。这个"同一功能、三份代码"的设计,使得对比指标(应用体积、启动时间、FPS、CPU/GPU 占用、内存等)具有可对照的公平基础,正如 README 所述:"The project is used to compare Compose Multiplatform performance metrics with native counter-parts such as size, startup time, FPS, CPU/GPU usage, etc."
仓库还额外提供了 compare_sizes.main.kts 与 measure_sizes.main.kts 两个 Kotlin 脚本,分别用于对比三端产物体积与测量各产物大小,支撑"应用体积"这一项基准维度。
项目结构与三端模块划分
整个工程位于benchmarks/showcases/LazyGridImageView/,目录组织如下:
| 目录 / 文件 | 职责 |
|---|---|
composeApp/ | Kotlin Multiplatform 实现,含commonMain、androidMain、iosMain、desktopMain、wasmJsMain、jsMain等源集 |
nativeAndroidApp/ | 原生 Android(Jetpack Compose),图片资源放在app/src/main/assets/downloaded_images/ |
nativeiosApp/ | 原生 iOS(SwiftUI),图片放在nativeiosApp/downloaded_images/,通过 Xcode 工程构建 |
download_images.sh | 从 Picsum Photos 批量下载 999 张测试图片并分发到三端资源目录 |
compare_sizes.main.kts/measure_sizes.main.kts | 产物体积对比 / 测量脚本 |
gradle/libs.versions.toml | 版本目录(Version Catalog),统一管理三方库版本 |
settings.gradle.kts | 声明:composeApp模块并启用TYPESAFE_PROJECT_ACCESSORS特性 |
composeApp:一份代码,五个平台目标
从 composeApp/build.gradle.kts 可以看到完整的平台矩阵:
androidTarget:Android 应用(JVM 11);iosX64 / iosArm64 / iosSimulatorArm64:三个 iOS 目标,统一产出名为ComposeApp的静态 framework(isStatic = true);jvm("desktop"):桌面 JVM 目标;wasmJs与js:浏览器目标,均配置browser下的commonWebpackConfig输出composeApp.js。
commonMain的依赖中既有compose.runtime / foundation / material3 / ui全套 Compose 库,也有compose.components.resources(资源组件,用于访问composeResources)和coil.compose(Coil 3 的 Compose 集成)。桌面端额外引入kotlinx.coroutines.swing,Android 端引入androidx.activity:activity-compose与compose.preview。桌面入口在 main.kt,iOS 入口在 MainViewController.kt(ComposeUIViewController { App() }),Android 入口在 MainActivity.kt(setContent { App() })——三个平台的界面代码全部收敛到commonMain的同一个App()。
资源放置差异:三套体系,同一批图片
| 平台 | 资源目录 | 加载方式 |
|---|---|---|
| Compose Multiplatform | composeApp/src/commonMain/composeResources/files/ | Res.getUri("files/xxx.jpg")+ Coil |
| 原生 Android | nativeAndroidApp/app/src/main/assets/downloaded_images/ | file:///android_asset/前缀 + Coil |
| 原生 iOS | nativeiosApp/nativeiosApp/downloaded_images/(Bundle 内) | Bundle.main.url(...)+ SwiftUIAsyncImage |
准备数据:一键下载 999 张测试图片
项目要求在任何构建之前先运行下载脚本(README 明确说明"Please run the script before building the project")。脚本位于 download_images.sh:
./download_images.sh脚本核心逻辑如下:
- 预创建三个目标目录:
composeApp/src/commonMain/composeResources/files、nativeiosApp/nativeiosApp/downloaded_images、nativeAndroidApp/app/src/main/assets/downloaded_images; - 循环
1..999,构造文件名downloaded_image%03d.jpg(即downloaded_image001.jpg~downloaded_image999.jpg); - 每个编号用固定 ID 从
https://picsum.photos/id/$i/512/512.jpg下载,失败时自动回退到随机图片https://picsum.photos/512/512.jpg; - 下载成功(文件非空)后,
cp复制到 iOS 与 Android 两个目录,保证三端使用完全相同的图片数据; - 每张图片之间
sleep 0.1防止请求过快被限流。
注意两点:
- 图片统一为512×512 的 JPG,与后续 UI 中
aspectRatio(1f)的正方形卡片一一对应; - 由于某些编号在 Picsum 上可能不存在(404),脚本会对失败项使用随机图兜底,因此本地实际成功数可能少于 999,代码运行时也会对缺失资源做防御性处理(见下文
Res.getUri的 try/catch)。
仓库中已附带downloaded_image001.jpg等少量示例图片,便于在没有联网环境时先跑通工程;完整数据仍需执行脚本生成。
Compose Multiplatform 实现:commonMain 中的网格与图片加载
资源枚举与防御式解析
App.kt 中首先在remember块里遍历 1..999,用 Compose 资源的类型安全生成器Res.getUri("files/downloaded_imageXXX.jpg")解析每个资源 URI,并用 try/catch 忽略下载失败产生的缺失资源:
val uris = remember { val availableResources: MutableList<String> = mutableListOf() for (index in 1..999) { try { val resUri = Res.getUri("files/downloaded_image${index.toString().padStart(3, '0')}.jpg") availableResources.add(resUri) } catch (e: Exception) { //ignore } } List(999) { index -> availableResources[index % availableResources.size] } }index % availableResources.size的取模写法保证了即使实际可用图片少于 999 张,列表仍能凑满 999 项,从而保证基准负载的一致性。这段代码依赖compose.components.resources提供的Res生成类(对应lazygridimage.composeapp.generated.resources.Res导入),并使用@OptIn(ExperimentalResourceApi::class)声明实验 API。
3 列 LazyVerticalGrid 与自动滚动
网格本体使用LazyVerticalGrid,列数由常量numOfColumns = 3决定:
LazyVerticalGrid( columns = GridCells.Fixed(numOfColumns), contentPadding = PaddingValues(4.dp), modifier = Modifier.fillMaxSize(), state = gridState ) { items(uris) { uri -> ImageCard(uri, modifier = Modifier.padding(4.dp)) } }LazyVerticalGrid只在视口内组合可见条目,这正是"999 张图片仍能流畅滚动"的关键。配合rememberLazyGridState(),代码实现了一个自动往返滚动逻辑:勾选顶部 "Auto Scroll" 复选框后,LaunchedEffect(autoScroll)中每delay(100)毫秒调用一次gridState.animateScrollToItem(index = currentIndex),每次按numOfColumns(3 行)步进;到达底部(currentIndex >= uris.size - numOfColumns - 1)后翻转方向向上滚动,到达顶部再翻转。这样在基准测试时无需人工操作即可持续制造滚动压力。
图片卡片:Coil AsyncImage + 正方形裁剪
每个网格条目封装在ImageCard中:
@Composable fun ImageCard(uri: String, modifier: Modifier = Modifier) { Card( modifier = modifier .aspectRatio(1f) // Square aspect ratio .fillMaxWidth(), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { AsyncImage( model = uri, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } }aspectRatio(1f)让每个卡片呈正方形,与 512×512 的源图比例一致;ContentScale.Crop做居中裁剪填充;- 图片加载使用 Coil 3 的
coil3.compose.AsyncImage,模型直接传Res.getUri解析出的资源 URI——这正是 README 所述"images are loaded using Coil library from the Compose Multiplatform resources"的实现位置。Coil 负责异步解码、缓存与生命周期管理,因此App()中没有任何手写协程加载逻辑。
平台薄壳
三个平台的入口都只是把App()塞进各自宿主:
- Android:MainActivity.kt ——
setContent { App() }; - iOS:MainViewController.kt ——
ComposeUIViewController { App() }; - Desktop:main.kt ——
application { Window(title = "LazyGridImage") { App() } }。
原生 Android 实现:Jetpack Compose + Coil + assets
nativeAndroidApp用纯 Android 技术栈重写同一功能,入口在 nativeAndroidApp/app/src/main/java/org/jetbrains/lazygridimage/MainActivity.kt。
与 Compose Multiplatform 版本最大的差异在资源来源:这里通过context.assets.list("downloaded_images/")枚举 assets 目录下的全部图片文件名,然后拼出file:///android_asset/downloaded_images/xxx.jpg形式的 URI:
val uris = remember { val imagesFolder = "downloaded_images/" val availableResources = context.assets.list(imagesFolder) List(999) { index -> "file:///android_asset/" + imagesFolder + availableResources!![index % availableResources.size] } }其余骨架与共享版本高度一致:LazyVerticalGrid(columns = GridCells.Fixed(3))、rememberLazyGridState()、Auto Scroll 复选框 +LaunchedEffect+animateScrollToItem自动滚动、ImageCard中AsyncImage(model = uri, contentScale = ContentScale.Crop)。也就是说,只要把"资源 URI 的获取方式"这一层换掉,网格与滚动逻辑在 Jetpack Compose 与 Compose Multiplatform 之间可以近乎原样复用——这正是两套实现便于直接对比的前提。
原生 iOS 实现:SwiftUI LazyVGrid + AsyncImage
nativeiosApp是纯 SwiftUI 工程,主要文件:
- ContentView.swift:网格 + 自动滚动 + 图片名加载;
- GridItemView.swift:单个网格条目的
AsyncImage渲染; - App.swift:应用入口。
ContentView 中同样固定 3 列:GridItem(.flexible())× 3,网格本体是LazyVGrid(columns:columns, spacing: 8)。自动滚动用Timer.publish(every: 0.1, on: .main, in: .common)每 100ms 触发一次updateScrollPosition(),配合ScrollViewReader的proxy.scrollTo(newPosition, anchor: .top)完成往返滚动,步长同样是列数 3——与 Compose 版delay(100)+animateScrollToItem的频率与步进完全对齐,保证两边滚动负载等价。
图片名通过loadImageNames()构造downloaded_image001…downloaded_image999,并保留testingMode开关:测试模式下先用UIImage(named:)过滤掉未成功下载的图片(existingImages),再取模补满 999 项,与 Compose 版的防御式处理思路一致。
GridItemView.swift 展示了资源定位与加载的差异点:
let imageUrl = Bundle.main.url(forResource: imageName, withExtension: "jpg") AsyncImage(url: imageUrl) { phase in switch phase { case .success(let image): image .resizable() .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) default: Color.gray } }- 资源来自Bundle(
downloaded_images目录被打包进 app),对应 README 所述"uses iOS asset catalog to load images"; AsyncImage是 SwiftUI 自带的异步图片加载器,加载中/失败时渲染Color.gray占位;- 条目外层
.aspectRatio(1, contentMode: .fill)与.id(index)(供ScrollViewReader定位),背景白色 +cornerRadius(8)圆角卡片,视觉上与 Compose 版的Card保持一致。
三端构建与运行指南
按 README 的说明,三套实现的启动方式如下:
| 实现 | 操作 |
|---|---|
| Compose Multiplatform(Android) | 用 Android Studio 打开工程,运行composeApp配置 |
| Compose Multiplatform(iOS) | 打开iosApp目录中的 Xcode 工程运行(注意composeApp产物是名为ComposeApp的静态 framework) |
| Compose Multiplatform(Desktop) | 在 IntelliJ IDEA 或 Android Studio 中运行desktopApp配置 |
| 原生 Android | 用 Android Studio 打开nativeAndroidApp目录直接运行 |
| 原生 iOS | 用 Xcode 打开nativeiosApp/nativeiosApp.xcodeproj运行 |
前置条件:先执行./download_images.sh下载 999 张图片,否则资源目录为空、网格将无图可显(iOS 端testingMode即为此场景的容错)。
关于 Web 目标需要说明:composeApp的构建脚本虽然声明了wasmJs与js两个浏览器目标,但commonMain中的Res.getUri解析与 Coil 加载在当前资源配置下主要面向桌面/移动端验证,浏览器端运行请以实际构建产物为准,本文档仅记录移动与桌面三端的官方运行路径。
基准测量的配套工具
工程目录下还提供了两个.main.kts脚本,用于自动化"体积"维度的测量:
- measure_sizes.main.kts:测量各平台产物体积;
- compare_sizes.main.kts:对三端产物进行体积对比输出。
结合 README 提到的对比维度(应用大小、启动时间、FPS、CPU/GPU 占用等),建议的完整基准流程是:
- 运行
./download_images.sh准备 999 张 512×512 图片; - 分别构建 Compose Multiplatform、原生 Android、原生 iOS 三个产物;
- 用
measure_sizes.main.kts/compare_sizes.main.kts量化体积差异; - 启动 App 后勾选 "Auto Scroll",让三端以相同的 100ms 步进做往返滚动,再用系统级 Profiler(Android Profiler、Xcode Instruments、桌面性能分析工具)采集 FPS、CPU/GPU 占用与内存;
- 汇总数据并对照三份实现得出结论。
小结:一套 UI、三种落地、可比可测
LazyGridImageView示例的核心价值可以概括为三点:
- 代码复用极限:
App()一份 commonMain 代码同时支撑 Android、iOS、Desktop 三个平台,平台差异被压缩到三个各不超 20 行的入口文件; - 等价负载设计:三端均固定 999 张图、3 列网格、100ms 步进自动往返滚动,保证基准测试输入一致;
- 可复制的测量链路:
download_images.sh统一数据源,measure/compare_sizes.main.kts量化体积,外部 Profiler 采集运行时指标。
无论是想评估 Compose Multiplatform 在真实图片密集型场景下的表现,还是想为自己的项目搭建一套跨平台网格 + Coil 异步图片的参考实现,都可以直接以本示例为起点:复制composeApp目录,替换composeResources/files中的图片数据,即可在最短时间内得到一套可运行的跨平台 LazyGrid 图片浏览器。
【免费下载链接】compose-multiplatformCompose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.项目地址: https://gitcode.com/GitHub_Trending/co/compose-multiplatform
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考