Lazy Loading(懒加载)是一种延迟加载数据、图像和组件的策略,其中资源不是在应用程序启动时请求,而是在用户真正需要它们时才请求。根据Android Paging 3指南,在处理大数据集时,列表的懒加载可降低60-80%的内存消耗。延迟初始化是所有Lazy Loading实现背后的关键原则。
要点
Lazy Loading(懒加载、延迟加载) — 一种设计和优化模式,其中应用程序资源不是在启动时加载,而是在使用前直接加载。在移动开发中,Lazy Loading适用于三大类别:数据(列表分页)、图像(滚动时加载)和组件(懒堆栈和视图)。
与Lazy Loading相反的是Eager Loading(急切加载),即所有资源在屏幕启动时加载。Eager Loading实现更简单,但消耗更多内存并增加首次显示时间。对于包含数千个元素的列表,Eager Loading会在内存有限的设备上导致OOM(内存不足)。Lazy Loading通过仅加载屏幕上可见的内容并在滚动时添加其余内容来解决此问题。
在iOS和Android环境中,Lazy Loading在不同级别实现。SwiftUI提供LazyVStack和LazyHStack用于延迟渲染。UIKit使用带dequeueReusableCell的UITableView。Android — 带ViewHolder池的RecyclerView。在数据层面 — 带Paging 3的Room和带NSFetchedResultsController的Core Data。具体技术的选择取决于技术栈和性能要求。
Lazy Loading的核心原则 — 加载当前屏幕状态所需的确切数据量,加上用于流畅滚动的预加载缓冲区。这种方法基于两种机制:可见性跟踪和元素虚拟化。
跟踪机制确定哪些元素位于屏幕的可视区域(视口)内。在Android中,LinearLayoutManager或GridLayoutManager通过findFirstVisibleItemPosition和findLastVisibleItemPosition方法实现。在iOS中,UIScrollView提供bounds.origin.y和contentOffset.height来计算可视区域。当元素进入视口(或预取缓冲区)时,其加载开始。当元素离开屏幕时,其资源可以被释放或移至缓存。
// Android — 在RecyclerView中跟踪可见性
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisible = layoutManager.findLastVisibleItemPosition()
val totalCount = layoutManager.itemCount
// 如果剩余少于5个元素,我们加载下一页 < 5 个元素
if (lastVisible >= totalCount - 5) {
loadMoreItems()
}
}
})
预取缓冲区 — 预加载即将出现在屏幕上的元素。RecyclerView通过layoutManager.setItemPrefetchEnabled(true)支持GapWorker.Prefetch。iOS UITableView通过UITableViewDataSourcePrefetching支持预取。预取缓冲区的大小通常为前方1-2个屏幕,在滚动流畅性和内存消耗之间提供折衷。太大的预取缓冲区会抵消Lazy Loading的优势,太小则会在快速滚动时产生空白区域。
图像 — 移动应用中最重的资源类型。一张12兆像素的照片在未压缩状态下可能占用3-5MB。图像的Lazy Loading可防止数百个不可见图像加载到内存中,这对于用户评论列表或产品目录将是灾难性的。
Glide — 最流行的Android图像加载库,支持缓存、转换和动画。Coil — 一种更轻的替代方案,使用Kotlin协程编写。两个库都在ImageView离开屏幕时自动停止加载,并在重复使用ViewHolder时取消请求。Coil使用协程,大小约1.5MB(而Glide约4MB),使其成为专注于APK大小的项目的首选。
// Coil — 延迟加载图像
imageView.load("https://example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.placeholder)
size(512, 512)
memoryCachePolicy(CachePolicy.ENABLED)
}
Kingfisher — iOS库,支持Swift Concurrency、磁盘和内存缓存,以及UICollectionView的预取。SDWebImage — 较旧的库,有Objective-C背景,但支持Swift。两个库都与UIImageView集成并自动管理加载生命周期:在重复使用单元格时取消请求,仅在单元格可见时加载图像,并在收到内存不足通知时释放内存。
大数据列表 — Lazy Loading在移动应用中的主要应用领域。新闻源、产品目录、聊天、交易历史 — 每个具有潜在无限列表的屏幕都需要分页和延迟加载。
Paging 3 — 来自Android Jetpack的库,实现了完整的懒加载循环:从RemoteMediator(API + 数据库)请求数据、在Room中缓存、通过PagingData分页交付以及通过AsyncPagingDataAdapter显示。Paging 3支持三种分页类型:基于页面的(Page-based)、基于项目的(Item-based,offset/limit)和基于键的(Key-based,来自API的分页键)。Separator — 内置支持页面之间的分隔符,用于加载指示器。
// Paging 3 — 从API延迟加载
class ArticlePagingSource(
private val api: ArticleApi
) : PagingSource<Int, Article>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Article> {
return try {
val page = params.key ?: 1
val response = api.getArticles(page)
LoadResult.Page(
data = response.items,
prevKey = page.takeIf { it > 1 }?.dec(),
nextKey = page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
LazyVStack — SwiftUI的内置容器,仅在元素出现在屏幕上时创建和渲染元素。与立即计算所有子元素布局的VStack不同,LazyVStack延迟视图的创建,直到元素变为可见或进入预取范围。LazyHStack — 用于轮播的水平对应物。对于大量列表,Apple建议使用List,其内部工作方式类似于LazyVStack,并带有额外的内置回收机制。
// SwiftUI — 带延迟加载的LazyVStack
ScrollView {
LazyVStack(spacing: 8) {
ForEach(articles) { article in
ArticleRow(article: article)
.onAppear {
if article == articles.last {
viewModel.loadMore()
}
}
}
}
}
UI组件的懒加载 — 一种技术,其中界面部分(页眉、页脚、设置部分、选项卡)不是在屏幕启动时创建,而是在首次访问时创建。这加速了初始渲染并减少了主线程的负载。
ViewStub — Android中的轻量级View占位符,不占用布局空间,在调用inflate()之前不创建子View。适用于很少使用的部分:搜索面板、高级设置、广告块。Fragment懒加载 — 一种技术,其中Fragment.onCreateView延迟到用户切换到该选项卡时。通过ViewPager中的isVisible或UserVisibleHint实现。
AndroidX提供SplitInstallManager用于按需延迟加载模块。设置、诊断或附加功能模块仅在用户首次请求时作为Dynamic Feature模块加载。这可将应用的基础大小减少30-50%,同时在数据层面和代码层面实现Lazy Loading原则。
SwiftUI中的TabView延迟加载每个选项卡的内容 — 仅在选项卡激活时。UIKit UITabBarController默认在启动时创建所有子控制器,但可以通过不立即将它们添加到tabBarController.viewControllers来更改此行为,而是在切换时添加。具有动态添加的arrangedSubviews的UIStackView也遵循Lazy Loading原则 — 仅在用户执行需要该界面部分的操作时才添加SubView。
要优化屏幕的整体加载,在所有级别组合Lazy Loading:很少使用的部分使用ViewStub、数据使用Paging 3、图像使用Glide/Coil以及通过Hilt/Dagger Scopes或Swinject延迟初始化ViewModel。这种方法即使在具有3GB RAM的预算设备上也能在200-400毫秒内加载屏幕。
常见问题
如果屏幕保证显示少量元素(最多20个)且所有这些都需要立即显示 — Lazy Loading是多余的。对于很少滚动的列表,Eager Loading可能更简单且实现更快,而不会显著损失性能。
对于大型列表,峰值内存消耗降低3-10倍,因为内存中仅存储可见元素加上预取缓冲区。然而,添加预取和图像缓存会产生需要管理的适度内存消耗。
List更适合具有滑动、拖放和内置选择功能的同质数据。LazyVStack — 适用于具有不同单元格类型、部分和非标准间距的自定义布局。List在内部工作方式类似于LazyVStack,并具有附加功能。
在Android中使用Layout Inspector查看View层次结构:在Lazy Loading下,大多数元素应在树中缺失。在iOS中 — Xcode Debug View Hierarchy。如果滚动时所有元素都存在 — Lazy Loading未起作用。
两个参数都很重要,但优先级取决于平台。在具有ARC和高效内存管理的iOS上,加载速度是优先考虑的。在具有JVM和GC的Android上,节省内存是优先考虑的,因为主线程中的每次分配都可能导致GC冻结。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。