如何优化Swift中含大量图片的ViewController加载耗时?
看起来你遇到的核心问题是同步预加载了所有65MB的图片资源——在初始化大量AnatomyView实例时,你直接用UIImage(named:)同步加载了所有图片,这不仅会阻塞主线程导致长达10秒的卡顿,还会一次性占用大量内存,完全拖垮了App的启动和前台恢复体验。结合你提到的ScrollView最多同时显示8-10张图的场景,我给你整理了几个针对性的优化方案:
1. 延迟加载图片,仅在需要显示时加载
把AnatomyView里的图片实例换成文件名存储,不要在初始化时加载UIImage。这样初始化AnatomyView的时间会趋近于0,因为只是存储字符串和元数据,完全不涉及磁盘IO和图片解码。
修改后的AnatomyView示例:
class AnatomyView { var viewName: String = "" // 存储图片文件名而非UIImage实例 var normalImageName: String = "" var markedImageName: String = "" var stepImageNames: [String] = [] var attribution: String = "" init(viewName: String, normalImageName: String, markedImageName: String, stepImageNames: [String], attribution: String) { self.viewName = viewName self.normalImageName = normalImageName self.markedImageName = markedImageName self.stepImageNames = stepImageNames self.attribution = attribution } // 异步加载图片的方法,后台线程加载+解码,主线程返回结果 func loadNormalImage(completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { // UIImage(named:)会缓存图片,适合常驻资源;大图片可用contentsOfFile避免缓存 guard let image = UIImage(named: self.normalImageName) else { DispatchQueue.main.async { completion(nil) } return } // 提前解码图片,避免主线程解码卡顿 _ = image.cgImage DispatchQueue.main.async { completion(image) } } } // 同理实现markedImage和step图片的加载方法 func loadMarkedImage(completion: @escaping (UIImage?) -> Void) { // 重复类似逻辑 } }
对应的SectionViewController实例初始化:
let lateralYShoulder = AnatomyView( viewName: "Lateral Y", normalImageName: "Lateral Y Normal Unmarked.jpg", markedImageName: "Lateral Y Normal Marked.jpg", stepImageNames: ["Lateral Y Step 1.jpg", "Lateral Y Step 2.jpg"], attribution: "Case courtesy of Mr Andrew Murphy\nRadiopaedia.org, rID: 48080" )
之后在ScrollView的可见区域变化时(比如scrollViewDidScroll或使用UICollectionView的cellForItemAt),调用这些加载方法,只有当某个AnatomyView即将进入屏幕时才加载对应的图片。
2. 利用异步图片加载库简化逻辑
如果不想自己写异步加载的代码,可以用成熟的第三方库,比如SDWebImage或Kingfisher。这些库不仅能自动处理异步加载、缓存,还能优化图片解码、内存管理,甚至支持WebP/HEIC等高效图片格式。
以SDWebImage为例,你可以直接给UIImageView设置本地图片的路径:
// 假设你有一个显示normalImage的UIImageView if let imagePath = Bundle.main.path(forResource: lateralYShoulder.normalImageName, ofType: nil) { let imageURL = URL(fileURLWithPath: imagePath) imageView.sd_setImage(with: imageURL, placeholderImage: UIImage(named: "Logo.jpg")) }
这些库会自动在后台加载、解码图片,加载完成后更新UI,完全不阻塞主线程。
3. 优化图片资源本身
65MB的图片体积是导致加载慢的重要原因之一,你可以从这几个方面优化:
- 转换为高效格式:把PNG转成HEIC或WebP,这两种格式的压缩率是PNG的2-3倍,质量损失几乎可以忽略。Xcode支持直接导入HEIC图片,WebP需要借助库(比如SDWebImage支持WebP解码)。
- 适配屏幕分辨率:为不同设备提供@2x、@3x的图片,不要用远超显示需求的大图。比如如果图片在屏幕上只显示500px宽,就不要用2000px宽的原图。
- 移除不必要的Alpha通道:如果图片不需要透明背景,把Alpha通道去掉,可以显著减小体积并加快解码速度。
4. 采用Apple官方的按需资源(On-Demand Resources)
如果你的图片资源不是启动时必须的,可以用On-Demand Resources把图片分成多个资源包。App启动时只会下载核心资源,当用户滚动到需要特定图片的区域时,系统会自动后台下载对应的资源包。这不仅能减少启动时的加载时间,还能减小App的初始安装包大小。
设置方法:在Xcode中把图片拖到"On Demand Resources"标签下,标记为不同的标签(比如按身体部位分组),然后在代码中请求对应的资源标签:
let resourceRequest = NSBundleResourceRequest(tags: ["shoulder-images"]) resourceRequest.beginAccessingResources { error in if let error = error { // 处理资源加载错误 return } // 资源已下载,可以加载图片了 }
5. 优化ScrollView的图片加载范围
因为你的ScrollView最多同时显示8-10张图,所以可以只加载当前可见区域+前后各1-2张的图片,其他图片在滚动出屏幕时释放内存(比如把UIImageView的image设为nil)。
如果用的是UIScrollView,可以在scrollViewDidScroll方法中计算可见区域,判断哪些子视图需要加载图片;如果换成UICollectionView会更方便,它自带的复用机制会自动管理单元格的创建和销毁,你只需要在cellForItemAt中加载当前单元格需要的图片即可。
6. 绝对避免主线程阻塞
一定要确保所有图片加载、解码操作都在后台线程执行,绝对不要在主线程批量调用UIImage(named:)——之前的代码就是在主线程同步加载所有图片,直接导致了10秒的卡顿,主线程被阻塞时App完全无法响应用户操作,看起来就像崩溃了一样。
按照这些方案优化后,你的SectionViewController初始化时间会降到毫秒级,图片加载会分散到用户滚动的过程中,完全不会出现10秒的卡顿。优先实施前两个方案(延迟加载+异步加载),这两个是见效最快的。
内容的提问来源于stack exchange,提问作者Nate

