Swift并发让我们显式地标记旨在从特定全局参与者调用的函数和类型,并且编译器将保证它们始终与该参与者隔离.在大多数情况下,这足以提供流畅的UI体验并减少不必要的演员 skip .因此,我们可以将该函数和图像缓存重写为一个参与者隔离的类:
@MainActor
final class ImageDownloader {
enum CacheEntry {
case inProgress(Task<UIImage?, Never>)
case complete(UIImage)
}
private var cache: [URL: CacheEntry] = [:]
func fetchImage(from url: URL) async -> UIImage? {
if let cached = cache[url] {
switch cached {
case .complete(let image):
return image
case .inProgress(let task):
return await task.value
}
}
let task = Task {
await downloadImage(from: url)
}
cache[url] = .inProgress(task)
let image = await task.value
if let image {
cache[url] = .complete(image)
}
return image
}
private nonisolated func downloadImage(from url: URL) async -> UIImage? {
do {
let response = try await URLSession.shared.data(from: url)
return UIImage(data: response.0)
} catch {
print(error)
return nil
}
}
}
请注意缓存如何跟踪下载状态.在问题中的基于回调的版本中,可能会在初始下载进行时启动额外的下载.该技术基于https://developer.apple.com/wwdc21/10133?time=536中的代码(缓存示例在页面上,但不在视频中,但值得观看视频.)
现在,在视图控制器中,当您配置单元格时,您可以从顶级任务调用此方法:
final class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
@IBOutlet private weak var collectionView: UICollectionView!
private let items = ListItem.sampleListItems()
private let imageDownloader = ImageDownloader()
...
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let listItem = items[indexPath.item]
let imageCell = collectionView.dequeueReusableCell(
withReuseIdentifier: "ImageCell",
for: indexPath
) as! ImageCell
let title = listItem.title
print("Cell dequeued for: \(title)")
// UIViewControllers are always MainActor-isolated, which
// means this Task is on the MainActor, so there's no need
// to call `MainActor.run`.
Task {
let img = await imageDownloader.fetchImage(from: listItem.url)
imageCell.configure(text: title, image: img)
print("Cell configured for item: \(title)")
}
print("Returning cell for: \(title)")
return imageCell
}
}
只有在downloadImage(from:)
中调用URL会话时,这才会跳下并返回到MainActor.
它还解决了基于回调的版本中潜在的错误来源:基于回调的方法只有在从主队列调用时才能正常工作,但没有什么可以阻止我们从其他队列调用它.如果从另一个线程调用它,代码可能会因同时访问缓存而崩溃.
尽管cellForRowAt
中的任务始终在MainActor上执行,但它可能要在MainActor上执行其他任务之后才会执行.这可能会导致您看到的一些闪烁.如果是这种情况,我们可以在ImageDownloader
中添加一些包装代码:
extension ImageDownloader {
private func cachedImaged(from url: URL) -> UIImage? {
guard case let .complete(img) = cache[url] else {
return nil
}
return img
}
func fetchImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
if let image = cachedImaged(from: url) {
completion(image)
} else {
//No need for explicit MainActor switching, since it's an isolated class.
Task {
let img = await fetchImage(from: url)
completion(img)
}
}
}
}
视图控制器中的一个:
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let listItem = items[indexPath.item]
let imageCell = collectionView.dequeueReusableCell(
withReuseIdentifier: "ImageCell",
for: indexPath
) as! ImageCell
let title = listItem.title
print("Cell dequeued for: \(title)")
imageDownloader.fetchImage(from: listItem.url) { img in
imageCell.configure(text: title, image: img)
print("Cell configured for item: \(title)")
}
print("Returning cell for: \(title)")
return imageCell
}
这最终看起来与您的原始版本相似,但通过将ImageDownloader
及其缓存隔离到MainActor,我们消除了不当使用导致竞争条件的可能性.