从URL异步下载和缓存图像

downloading and caching images from url asynchronously(从URL异步下载和缓存图像)

本文介绍了从URL异步下载和缓存图像的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试从我的Firebase数据库下载图像,并将它们加载到集合视图单元中。图像已下载,但我无法让它们全部异步下载和加载。

目前,当我运行代码时,会加载下载的最后图像。但是,如果我更新我的数据库,集合视图也会更新,新的最后一个用户配置文件映像也会加载进来,但其余的都会丢失。

我不喜欢使用第三方图书馆,所以任何资源或建议都会非常感谢。

以下是处理下载的代码:

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()
}

我认为解决方案在于重新加载集合视图,我只是不知道具体到哪里去做。

有什么建议吗?

编辑: 下面是调用该函数的位置;MycellForItem 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个单元格,则第五个单元格是唯一显示配置文件图像的单元格。另外,当我更新数据库时,即注册一个新用户到其中,集合视图更新并正确显示新注册的用户以及他们的配置文件图像,以及正确下载其图像的旧的最后一个单元格。但是,其余部分仍没有配置文件图像。

推荐答案

我知道您找到了问题,而且它与上面的代码无关,但我仍然有一个观察结果。具体地说,即使该单元格(以及图像视图)随后已被重用于另一个索引路径,您的异步请求仍将继续。这会导致两个问题:

  1. 如果您快速滚动到第100行,则必须等待检索到前99行的图像,然后才能看到可见单元格的图像。这可能会导致在图像开始弹出之前出现很长时间的延迟。

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

现在,您可能不会立即注意到这两个问题中的任何一个,因为只有当图像检索很难跟上用户的滚动(慢速网络和快速滚动的组合)时,它们才会显现出来。顺便说一句,你应该总是使用网络链接调节器来测试你的应用程序,它可以模拟糟糕的连接,从而更容易显示这些错误。

无论如何,解决方案是跟踪(A)与最后一个请求相关联的当前URLSessionTask;以及(B)被请求的当前URL。然后,您可以(A)在启动新请求时,确保取消以前的任何请求;以及(B)在更新图像视图时,确保与图像关联的URL与当前URL匹配。

不过,诀窍是在编写扩展时,不能只添加新的存储属性。因此,您必须使用关联的对象API,以便可以将这两个新存储的值与UIImageView对象关联起来。我亲自用一个计算属性包装了这个关联的Value 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 NSError).domain == NSURLErrorDomain && (error as NSError).code == NSURLErrorCancelled {
                    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)
    }
}

更大、更具架构性的观察:人们真的应该将图像检索与图像视图分离。假设您有一个表格,其中碰巧有十几个单元格使用相同的图像。您真的想仅仅因为第二个图像视图在第一个图像视图完成检索之前滚动到视图中就检索同一图像十几次吗?编号

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

底线是,获取图像不是图像视图的一种方法,而是图像视图希望利用的一种通用机制。异步图像检索/高速缓存机制通常应合并在单独的"图像管理器"对象中。它可以检测冗余请求,并从图像视图以外的上下文中使用。


如您所见,异步检索和缓存开始变得有点复杂,这就是为什么我们通常建议考虑已建立的异步图像检索机制,如AlamofireImage、Kingfisher或SDWebImage。这些人花了很多时间来解决上述问题和其他问题,并且相当健壮。但如果你打算"自己滚",我会建议你至少做一些类似于上面的事情。

这篇关于从URL异步下载和缓存图像的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!

本文标题为:从URL异步下载和缓存图像

基础教程推荐