swift - 从 url 异步下载和缓存图像

标签 swift uiimageview uicollectionviewcell nsurl

我正在尝试从我的 firebase 数据库下载图像并将它们加载到 collectionviewcells 中。图片下载,但我无法让它们全部异步下载和加载。

目前,当我运行我的代码时,最后 下载的图像加载。但是,如果我更新我的数据库, Collection View 会更新并且新的最后一个用户配置文件图像也会加载,但其余部分会丢失。

我不想使用第 3 方库,因此我们将不胜感激任何资源或建议。

这是处理下载的代码:

func loadImageUsingCacheWithUrlString(_ urlString: String) {

    self.image = nil

//        checks cache
    if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
        self.image = cachedImage
        return
    }

    //download
    let url = URL(string: urlString)
    URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in

        //error handling
        if let error = error {
            print(error)
            return
        }

        DispatchQueue.main.async(execute: {

            if let downloadedImage = UIImage(data: data!) {
                imageCache.setObject(downloadedImage, forKey: urlString as NSString)

                self.image = downloadedImage
            }

        })

    }).resume()
}

我相信解决方案在于重新加载 collectionview 我只是不知 Prop 体在哪里做。

有什么建议吗?

编辑: 这是函数被调用的地方;我的 cellForItem at indexpath

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: userResultCellId, for: indexPath) as! FriendCell

    let user = users[indexPath.row]

    cell.nameLabel.text = user.name

    if let profileImageUrl = user.profileImageUrl {

            cell.profileImage.loadImageUsingCacheWithUrlString(profileImageUrl)
    }

    return cell
}

我认为唯一可能影响图像加载的是我用来下载用户数据的函数,该函数在 viewDidLoad 中调用,但是所有其他数据都能正确下载。

func fetchUser(){
    Database.database().reference().child("users").observe(.childAdded, with: {(snapshot) in

        if let dictionary = snapshot.value as? [String: AnyObject] {
            let user = User()
            user.setValuesForKeys(dictionary)

            self.users.append(user)
            print(self.users.count)

             DispatchQueue.main.async(execute: {
            self.collectionView?.reloadData()
              })
        }


    }, withCancel: nil)

}

当前行为:

至于当前行为,最后一个单元格是唯一显示下载的个人资料图像的单元格;如果有 5 个单元格,则第 5 个单元格是唯一显示个人资料图像的单元格。此外,当我更新数据库时,即向其中注册一个新用户时,除了正确下载其图像的旧最后一个单元格之外,collectionview 还会更新并正确显示新注册的用户及其个人资料图像。然而,其余的仍然没有个人资料图片。

最佳答案

我知道你发现了你的问题,它与上面的代码无关,但我仍然有一个观察。具体来说,您的异步请求将继续进行,即使单元格(以及 ImageView )随后被重新用于另一个索引路径。这会导致两个问题:

  1. 如果您快速滚动到第 100 行,您将不得不等待前 99 行的图像被检索,然后才能看到可见单元格的图像。在图像开始弹出之前,这可能会导致非常长的延迟。

  2. 如果第 100 行的单元格被多次重复使用(例如第 0 行、第 9 行、第 18 行等),您可能会看到图像从一个图像闪烁到下一个图像,直到你得到第 100 行的图像检索。

现在,您可能不会立即注意到这些问题中的任何一个,因为它们只会在图像检索难以跟上用户滚动(慢速网络和快速滚动的组合)时才会显现出来。另外,您应该始终使用网络链接调节器测试您的应用,它可以模拟不良连接,从而更容易显示这些错误。

无论如何,解决方案是跟踪 (a) 与上次请求关联的当前 URLSessionTask; (b) 当前请求的 URL。然后,您可以 (a) 在开始新请求时,确保取消任何先前的请求; (b) 在更新 ImageView 时,确保与图像关联的 URL 与当前 URL 匹配。

不过,诀窍是在编写扩展时,您不能只添加新的存储属性。因此,您必须使用关联对象 API 将这两个新存储值与 UIImageView 对象相关联。我个人用计算属性包装了这个关联值 API,这样检索图像的代码就不会被这类东西淹没。无论如何,这会产生:

extension UIImageView {
    private static var taskKey = 0
    private static var urlKey = 0

    private var currentTask: URLSessionTask? {
        get { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
        set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
    }

    private var currentURL: URL? {
        get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
        set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
    }

    func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
        // cancel prior task, if any

        weak var oldTask = currentTask
        currentTask = nil
        oldTask?.cancel()

        // reset image view’s image

        self.image = placeholder

        // allow supplying of `nil` to remove old image and then return immediately

        guard let urlString = urlString else { return }

        // check cache

        if let cachedImage = ImageCache.shared.image(forKey: urlString) {
            self.image = cachedImage
            return
        }

        // download

        let url = URL(string: urlString)!
        currentURL = url
        let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            self?.currentTask = nil

            // error handling

            if let error = error {
                // don't bother reporting cancelation errors

                if (error as? URLError)?.code == .cancelled {
                    return
                }

                print(error)
                return
            }

            guard let data = data, let downloadedImage = UIImage(data: data) else {
                print("unable to extract image")
                return
            }

            ImageCache.shared.save(image: downloadedImage, forKey: urlString)

            if url == self?.currentURL {
                DispatchQueue.main.async {
                    self?.image = downloadedImage
                }
            }
        }

        // save and start new task

        currentTask = task
        task.resume()
    }
}

另外,请注意您引用了一些 imageCache 变量(全局变量?)。我建议使用图像缓存单例,它除了提供基本的缓存机制外,还可以观察内存警告并在内存压力情况下自行清除:

class ImageCache {
    private let cache = NSCache<NSString, UIImage>()
    private var observer: NSObjectProtocol?

    static let shared = ImageCache()

    private init() {
        // make sure to purge cache on memory pressure

        observer = NotificationCenter.default.addObserver(
            forName: UIApplication.didReceiveMemoryWarningNotification,
            object: nil,
            queue: nil
        ) { [weak self] notification in
            self?.cache.removeAllObjects()
        }
    }

    deinit {
        NotificationCenter.default.removeObserver(observer!)
    }

    func image(forKey key: String) -> UIImage? {
        return cache.object(forKey: key as NSString)
    }

    func save(image: UIImage, forKey key: String) {
        cache.setObject(image, forKey: key as NSString)
    }
}

一个更大、更架构化的观察:人们真的应该将图像检索与 ImageView 分离。想象一下,您有一个表格,其中有十几个使用相同图像的单元格。你真的想要检索同一张图像十几次只是因为第二个 ImageView 在第一个 ImageView 完成检索之前滚动到 View 中吗?没有。

此外,如果您想在 ImageView 的上下文之外检索图像怎么办?也许是一个按钮?或者可能出于其他原因,例如下载图像以存储在用户的照片库中。除了 ImageView 之外,还有大量可能的图像交互。

最重要的是,获取图像不是 ImageView 的一种方法,而是 ImageView 希望利用的一种通用机制。异步图像检索/缓存机制通常应合并到单独的“图像管理器”对象中。然后它可以检测冗余请求并在 ImageView 以外的上下文中使用。


如您所见,异步检索和缓存开始变得有点复杂,这就是为什么我们通常建议考虑已建立的异步图像检索机制,如 AlamofireImageKingfisherSDWebImage .这些人花了很多时间来解决上述问题和其他问题,并且相当健壮。但是,如果您要“自己动手”,我建议您至少采用上述方法。

关于swift - 从 url 异步下载和缓存图像,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45178305/

相关文章:

ios - UICollection View 根据屏幕大小调整单元格大小

ios - 有没有一种更有效的方法可以在不手动指定每个 View 的情况下向 UICollectionViewCell 添加相同的 View ?

arrays - 如何检查值是否介于存储在数组中的两个值之间

ios - swift 。为自定义区域设置设置特定时间格式(12/24 小时)

swift - 在 uiimageview 上显示上一张图像

ios - 合并两个不同大小的 UIImageView

ios - 带有动画的不同图像之间的 UIImageView 转换

ios - 如何将 UICollectionViewCell 嵌套在另一个 UICollectionViewCell 中?

swift - 在 AR 资源组中保存 ARKit 屏幕截图

swift - 在swift中找到字符串的最后一个索引