SwiftUI:如何在LazyVGrid单元格加载图片且不阻塞主线程?
SwiftUI网格图片加载滚动卡顿优化方案
问题描述
我编写了一个简单的网格组件,用于测试在SwiftUI中正确下载图片并加载到单元格的方法,以避免性能下降。目前无法实现滚动保持响应的效果,目标是在图片加载过程中仍能获得流畅的滚动体验。
测试代码如下:
struct DView: View { var columnGridItems = [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ] @State var stuff = [Int]() var body: some View { NavigationView { GeometryReader { geo in let w = geo.size.width / 4 ScrollView { LazyVGrid(columns: columnGridItems, alignment: .center, spacing: 2) { ForEach(stuff, id:\.self) { s in TestView1(i: s) .frame(width:w, height:w) .clipped() } } } .background(.red) .onAppear { for i in 0..<100 { stuff.append(i) } } } } .navigationViewStyle(StackNavigationViewStyle()) } } let localImageUrl = Bundle.main.url(forResource: "f1", withExtension: "jpg")! let localImage = UIImage(contentsOfFile: localImageUrl.path)! let f1Image = "https://images.igdb.com/igdb/image/upload/t_original/t7kg7iu6e2sv8h0vjcip.jpg" let tileImageURL = URL(string: f1Image)! struct TestView1:View { @State var i:Int var body: some View { ZStack { Color.black AsyncImage(url: tileImageURL, scale:2) // AsyncImage(url: localImageUrl) // Image(uiImage: localImage) Text("Some Text") .foregroundColor(.white) } } }
加载图片时滚动卡顿效果:
优化方案
卡顿核心原因是图片下载和解码操作阻塞主线程,系统默认AsyncImage在部分场景下会将解码逻辑放在主线程执行,导致滚动时UI响应延迟。以下是针对性优化措施:
1. 自定义图片加载逻辑,后台处理下载与解码
手动实现图片的后台下载、解码流程,确保仅最终图片渲染操作在主线程执行,避免主线程被阻塞。
2. 实现内存缓存
避免重复下载相同图片,减少网络请求和重复解码的性能开销。
3. 绑定视图生命周期管理异步任务
用.task修饰符绑定图片加载任务到视图生命周期,自动管理任务的创建与销毁,避免无效任务占用资源。
修改后的完整代码
import SwiftUI // 简易内存图片缓存工具 class ImageCache { static let shared = ImageCache() private let cache = NSCache<NSString, UIImage>() func get(forKey key: String) -> UIImage? { cache.object(forKey: key as NSString) } func set(_ image: UIImage, forKey key: String) { cache.setObject(image, forKey: key as NSString) } } struct DView: View { var columnGridItems = [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ] @State var stuff = [Int]() var body: some View { NavigationView { GeometryReader { geo in let w = geo.size.width / 4 ScrollView { LazyVGrid(columns: columnGridItems, alignment: .center, spacing: 2) { ForEach(stuff, id:\.self) { _ in TestView1(imageURL: tileImageURL) .frame(width:w, height:w) .clipped() } } } .background(.red) .onAppear { stuff = Array(0..<100) } } } .navigationViewStyle(StackNavigationViewStyle()) } } let f1Image = "https://images.igdb.com/igdb/image/upload/t_original/t7kg7iu6e2sv8h0vjcip.jpg" let tileImageURL = URL(string: f1Image)! struct TestView1: View { let imageURL: URL @State private var loadedImage: UIImage? var body: some View { ZStack { Color.black if let image = loadedImage { Image(uiImage: image) .resizable() .scaledToFill() } Text("Some Text") .foregroundColor(.white) } .task { await loadImage() } } private func loadImage() async { let cacheKey = imageURL.absoluteString // 优先读取缓存 if let cachedImage = ImageCache.shared.get(forKey: cacheKey) { loadedImage = cachedImage return } // 后台下载图片数据 guard let (data, _) = try? await URLSession.shared.data(from: imageURL) else { return } // 后台解码图片,避免阻塞主线程 await Task.detached(priority: .userInitiated) { guard let image = UIImage(data: data) else { return } ImageCache.shared.set(image, forKey: cacheKey) // 回到主线程更新UI await MainActor.run { loadedImage = image } }.value } }
关键优化点说明
- 后台解码:通过
Task.detached将图片解码操作放到后台线程,主线程仅负责最终UI更新,避免滚动时卡顿。 - 内存缓存:用
NSCache实现图片内存缓存,重复单元格无需重复下载解码相同图片。 - 生命周期绑定:
.task修饰符自动在视图出现时启动加载任务,消失时取消任务,避免资源浪费。
内容的提问来源于stack exchange,提问作者zumzum
相关产品推荐
相关产品推荐

