自定义相册CollectionView加载缓慢问题及优化咨询
嘿,这个卡顿问题我之前帮不少开发者踩过坑,核心就是把那些耗时的宽高比计算从主线程挪走,同时避免重复计算。给你几个优先级从高到低的实用方案:
1. 直接利用PHAsset的内置尺寸,跳过图片加载计算
这是最立竿见影的优化!你根本不需要加载图片文件来获取宽高比——相册里的PHAsset本身就提供了pixelWidth和pixelHeight属性,直接用这两个值计算就行,省掉了大量的图片解码和内存开销。
示例代码:
// 获取对应的PHAsset let asset = yourAssetArray[indexPath.item] let aspectRatio = CGFloat(asset.pixelWidth) / CGFloat(asset.pixelHeight) // 假设你要的item宽度是固定的(比如屏幕宽度/2) let targetWidth = UIScreen.main.bounds.width / 2 let itemSize = CGSize(width: targetWidth, height: targetWidth / aspectRatio)
2. 缓存已计算的尺寸,避免重复计算
不管用哪种方式计算尺寸,都要把结果缓存起来,不然每次滚动或者刷新都会重复计算,浪费资源。可以用字典来存,键可以用PHAsset的localIdentifier(唯一标识)或者indexPath的字符串形式。
示例代码:
// 类里定义缓存字典 private var sizeCache: [String: CGSize] = [:] // 计算尺寸时先查缓存 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let asset = yourAssetArray[indexPath.item] let key = asset.localIdentifier if let cachedSize = sizeCache[key] { return cachedSize } // 计算尺寸(用上面的PHAsset属性方法) let aspectRatio = CGFloat(asset.pixelWidth) / CGFloat(asset.pixelHeight) let targetWidth = UIScreen.main.bounds.width / 2 let itemSize = CGSize(width: targetWidth, height: targetWidth / aspectRatio) // 存入缓存 sizeCache[key] = itemSize return itemSize }
注意:如果用户编辑了照片,记得清空对应asset的缓存值哦!
3. 异步预计算所有尺寸,主线程刷新布局
不要在sizeForItemAt这种主线程调用的方法里做计算,最好在初始化相册数据的时候,就把所有asset的尺寸在后台队列一次性计算完,存到缓存里,然后再刷新CollectionView。
示例代码:
// 加载相册数据后异步计算尺寸 DispatchQueue.global(qos: .userInitiated).async { for asset in self.yourAssetArray { let aspectRatio = CGFloat(asset.pixelWidth) / CGFloat(asset.pixelHeight) let targetWidth = UIScreen.main.bounds.width / 2 let itemSize = CGSize(width: targetWidth, height: targetWidth / aspectRatio) self.sizeCache[asset.localIdentifier] = itemSize } // 回到主线程刷新 DispatchQueue.main.async { self.collectionView.reloadData() } }
4. 优化自定义布局的prepare()方法
针对你用的GreedoSizeCalculator,可以把尺寸计算逻辑移到自定义布局的prepare()方法里,而不是每次请求item尺寸时才计算。在prepare()中异步预计算所有item的尺寸,存到布局缓存中,这样布局系统请求尺寸时直接返回缓存值即可。
示例代码(自定义布局中):
override func prepare() { super.prepare() guard let dataSource = self.dataSource as? GreedoSizeCalculatorDataSource else { return } DispatchQueue.global(qos: .userInitiated).async { // 预计算所有item的尺寸 var itemSizes: [IndexPath: CGSize] = [:] for section in 0..<self.collectionView?.numberOfSections ?? 0 { for item in 0..<self.collectionView?.numberOfItems(inSection: section) ?? 0 { let indexPath = IndexPath(item: item, section: section) let asset = dataSource.greedoSizeCalculator(self, originalImageSizeAt: indexPath) let aspectRatio = CGFloat(asset.pixelWidth) / CGFloat(asset.pixelHeight) let targetWidth = UIScreen.main.bounds.width / 2 itemSizes[indexPath] = CGSize(width: targetWidth, height: targetWidth / aspectRatio) } } // 主线程更新布局缓存并刷新 DispatchQueue.main.async { self.itemSizeCache = itemSizes self.invalidateLayout() } } }
5. 懒加载+限制初始加载数量
如果相册里照片特别多,初期不要一次性加载所有asset。可以先加载前20-30张,计算好尺寸显示出来,剩下的等用户滚动到接近底部时再加载计算,这样初期的计算量会大幅降低,不会出现明显卡顿。
6. 图片加载优化
最后,图片本身的加载也要异步处理,用成熟的图片加载库(比如SDWebImage、Kingfisher)来加载缩略图,不要直接加载原图。这样不仅能减少内存占用,还能避免因为图片解码导致的主线程卡顿。
内容的提问来源于stack exchange,提问作者TheRedCamaro3.0 3.0

