我正在努力了解如何更好地使用Expressc/await来处理新的Swift并发.很久以前,我就学会了如何使用Objective-C编写iOS应用程序,并且仍然在使用这个参考框架来处理事情.

使用传统的完成处理程序风格逻辑,您可以编写一个图像提取器函数,其中可以判断缓存,如果图像存在,则立即执行完成处理程序以返回图像.如果图像不存在,您可以调用某个后台线程来执行获取它所需的任何工作(磁盘、网络等),然后稍后返回主线程上的完成处理程序.这在您想要立即使用UI Image而不是在下一个运行循环中使用的情况下非常有用,以避免图像加载被延迟或导致闪烁(例如,在UI CollectionView Cell中设置图像时)

func fetchImage(url: URL, completion: ((UIImage) -> Void)) {
    if let image = self.images[url] {
        completion(image)
        return
    }

    DispatchQueue.global(qos: .background).async {
        // Code to do the network fetch or whatever
        DispatchQueue.main.async {
            completion(...)
        }
    }
}

// On the main thread somewhere else
fetchImage(url: url) { image in
    cell.imageView.image = image
}

我不确定如何使用Deliverc/await执行同样的行为,而不必单独执行非Deliverc调用来判断缓存.

func fetchImageSync(url: Url) -> UIImage? {
    return self.images[url]
}

func fetchImageAsync(url: Url) async -> UIImage {
    if let image = self.images[url] {
        return image
    }

    // Do some async stuff to get the image
    self.images[url] = image
    return image
}

// On the main thread
if let image = fetchImageSync(url: url) {
    cell.imageView.image = image
} else {
    Task {
        if image = await fetchImageAsync(url: url) {
            await MainActor.run {
                  cell.imageView.image = image
            }
        }
    }
}

上述方法的缺点是,无法在图像已存储在缓存中的快乐路径中的主线程上执行同步代码.

推荐答案

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,我们消除了不当使用导致竞争条件的可能性.

Swift相关问答推荐

SwiftUI:为什么@State在子视图中持续存在?

查找数组中 ** 元素 ** 的属性的最小值和最大值

在不传递参数的情况下使用Init方法?

文件命名&NumberForMatter+扩展名&:含义?

expose 不透明的底层类型

Swift String.firstIndex(of: "\n") 返回 nil,即使字符串有 \n

如何在 SwiftUI 中为过渡动画保持相同的标识

可以使 Swift init 仅可用于 Objective C 吗?

在 RealmSwift 中表示范围值?

URL appendPathComponent(_:) 已弃用?

ScreenCaptureKit/CVPixelBuffer 格式产生意外结果

.onTapGesture 不适用于 Stepper,仅适用于其文本

如何使被拖动的对象成为前景(foreground)对象

找不到接受提供的参数的/的重载

swift 如何从 Int 转换?到字符串

快速的 AES 加密

是否可以在 Swift 中创建通用闭包?

为什么'nil' 与 Swift 3 中的 'UnsafePointer' 不兼容?

如何在 SwiftUI 中以编程方式滚动列表?

使用 ARAnchor 插入 node 和直接插入 node 有什么区别?