ios - UICollectionView 上的滞后滚动问题
问题描述
我正在尝试优化 UICollectionView 加载。我有一个数组,其中有 40 张图像,并且在滚动时会滞后。我进行了很多研究,但找不到本地图像异步的合适解决方案。我尝试了 DispatchQueue 全局异步,但效果不佳。我怎么解决这个问题?谢谢。
这是我的问题:
这是我的 CollectionView 快速文件:
extension ThemesVC: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return ThemeManager().themeBackgroundImages().count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ThemesCollectionViewCell", for: indexPath) as! ThemesCollectionViewCell
cell.contentView.layer.cornerRadius = 20
cell.themeCellImageView.image = ThemeManager().themeBackgroundImages()[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.size.width/2.34, height: view.frame.size.height * 0.3)
}
}
UICollectionViewCell Swift 文件:
import UIKit
class ThemesCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var themeCellImageView: UIImageView!
}
这是 ThemeManager().themeBackgroundImages() 代码:
func themeBackgroundImages() -> [UIImage]{
var images = [UIImage]()
images.reserveCapacity(40)
images = [UIImage(named: "original-theme")!, UIImage(named: "blue-theme")!, UIImage(named: "purple-theme")!, UIImage(named: "pink-theme")!, UIImage(named: "red-theme")!, FlyTheme().backgroundImage, DreamTheme().backgroundImage, LoverTheme().backgroundImage, NatureTheme().backgroundImage, InspireTheme().backgroundImage, TimeMachineTheme().backgroundImage, NowhereTheme().backgroundImage, DarkThoughtsTheme().backgroundImage, FutureCityTheme().backgroundImage, LostTheme().backgroundImage, FireTheme().backgroundImage, PatternTheme().backgroundImage, HorizonTheme().backgroundImage, SpelTheme().backgroundImage, WonderlandTheme().backgroundImage, BreatheTheme().backgroundImage, PassionTheme().backgroundImage, TimelapseTheme().backgroundImage, FugitiveTheme().backgroundImage, InfiniteWayTheme().backgroundImage, LonelyTheme().backgroundImage, RelationshipTheme().backgroundImage, MoonlightTheme().backgroundImage, DreamlandTheme().backgroundImage, WayOutTheme().backgroundImage, MistyTheme().backgroundImage, UnpredictableTheme().backgroundImage, VolcanoTheme().backgroundImage, DiscoverTheme().backgroundImage, PurpleInspirationTheme().backgroundImage, ForestTheme().backgroundImage, RainbowTheme().backgroundImage, FlowersTheme().backgroundImage, TheMoonTheme().backgroundImage, LuridTheme().backgroundImage]
return images
}
解决方案
您的代码中存在一些导致故障的问题:
- 每次集合视图需要一个单元格时,您的代码都会从磁盘加载 40 张大图像,从中挑选一张,然后丢弃其余的。
- 即使每个单元格只加载一张图像,它仍然会发生在主队列中。加载图像时,UI 冻结。
- 图像本身非常大,因此
UIImageView
每次需要显示图像时都必须调整它们的大小。
为了减轻这种情况,以下是我的建议:
- 不要
ThemeManager
在collectionView(:cellForItemAt:)
. 相反,让它成为视图控制器的属性。 - 与其
ThemeManager
一次返回所有图像,不如让它只获取一个具有特定索引的图像。 - 使图像加载异步,将工作移出主队列。
- 从磁盘加载图像后,适当调整它的大小,这样
UIImageView
就不必处理巨大的分辨率。 - 在内部实现调整大小图像的图像缓存,
ThemeManager
以便立即加载先前的图像。NSCache
是一门可以做到这一点的好课。
这是一个示例,其中我标记了上述每个建议的实现:
import UIKit
final class ViewController: UIViewController {
@IBOutlet private var collectionView: UICollectionView!
// Recommendation #1: make ThemeManager a property
// instead of recreating it each time
private let themeManager = ThemeManager()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.setCollectionViewLayout(UICollectionViewFlowLayout(), animated: false)
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
themeManager.imageKeys.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ThemesCollectionViewCell", for: indexPath) as? ThemesCollectionViewCell else {
fatalError("Unexpected cell class dequeued")
}
cell.currentIndexPath = indexPath
let cellSize = self.collectionView(collectionView, layout: collectionView.collectionViewLayout, sizeForItemAt: indexPath)
themeManager.fetchImage(atIndex: indexPath.item, resizedTo: cellSize) { [weak cell] image, itemIndex in
guard let cell = cell, let image = image else { return }
DispatchQueue.main.async {
guard let cellIndexPath = cell.currentIndexPath, cellIndexPath.item == itemIndex else {
print("⚠️ Discarding fetched image for item \(itemIndex) because the cell is no longer being used for that index path")
return
}
print("Fetched image for \(indexPath) of size \(image.size) and scale \(image.scale)")
cell.imageView.image = image
cell.textLabel.text = "\(indexPath)"
}
}
return cell
}
}
// MARK: - UICollectionViewDelegate and UICollectionViewDelegateFlowLayout
extension ViewController: UICollectionViewDelegate {}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let sectionInsets = self.collectionView(collectionView, layout: collectionViewLayout, insetForSectionAt: indexPath.section)
return CGSize(width: collectionView.frame.width - sectionInsets.left - sectionInsets.right, height: 128)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
10
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
0
}
}
// MARK: - ThemesCollectionViewCell
final class ThemesCollectionViewCell: UICollectionViewCell {
@IBOutlet var textLabel: UILabel!
@IBOutlet var imageView: UIImageView!
var currentIndexPath: IndexPath?
override func awakeFromNib() {
super.awakeFromNib()
layer.cornerRadius = 20
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
currentIndexPath = nil
}
}
// MARK: - ThemeManager
final class ThemeManager {
// Recommendation #5: Implement an image cache of resized images inside `ThemeManager`
private let imageCache = NSCache<NSString, UIImage>()
private(set) var imageKeys: [NSString]
init() {
imageKeys = []
for i in 1...40 {
imageKeys.append(NSString(string: "image\(i)"))
}
}
// Recommendation #2: make ThemeManager fetch just one image with a particular index.
func fetchImage(atIndex index: Int, resizedTo size: CGSize, completion: @escaping (UIImage?, Int) -> Void) {
guard 0 <= index, index < imageKeys.count else {
assertionFailure("Image with invalid index requested")
completion(nil, index)
return
}
let imageKey = imageKeys[index]
if let cachedImage = imageCache.object(forKey: imageKey) {
completion(cachedImage, index)
return
}
// Recommendation #3: Make image loading asynchronous, moving the work off the main queue.
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else {
completion(nil, index)
return
}
guard let image = UIImage(named: String(imageKey)) else {
assertionFailure("Image is missing from the asset catalog")
completion(nil, index)
return
}
// Recommendation #4: After loading an image from disk, resize it appropriately
let resizedImage = image.resized(to: size)
self.imageCache.setObject(resizedImage, forKey: imageKey)
completion(resizedImage, index)
}
}
}
// MARK: - Image Resize Extension
extension UIImage {
func resized(to targetSize: CGSize) -> UIImage {
let widthRatio = (targetSize.width / size.width)
let heightRatio = (targetSize.height / size.height)
let effectiveRatio = max(widthRatio, heightRatio)
let newSize = CGSize(width: size.width * effectiveRatio, height: size.height * effectiveRatio)
let rect = CGRect(origin: .zero, size: newSize)
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
self.draw(in: rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}
我已将整个示例项目放在 Github 上,您可以随意克隆它,看看它是如何工作的。
推荐阅读
- vim - .vimrc 中的正则表达式抛出“不是编辑器命令”
- machine-learning - 形状属性中的神经网络密集层错误
- python - 可配置的整数序列
- python - 如何从文件夹一次读取一个文件并将数据作为字符串传递到 API,同时将响应写回文件?
- sql - 从存储过程中获取查询结果
- ajax - 在 Django 上使用 AJAX 防止 Gunicorn 30 秒超时
- firebase - 有关实时数据库中数据传输的一般问题
- delphi - Delphi:如何创建一个独立于主应用程序的分离进程?
- java - 如何打印出正确的输出
- python - MATLAB ActiveX 与 Python Win32COM