从url异步下载和缓存图像

Ste*_*fan 1 nsurl uiimageview uicollectionviewcell swift

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

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

我宁愿不使用第三方库,所以任何资源或建议将不胜感激.

这是处理下载的代码:

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()
}
Run Code Online (Sandbox Code Playgroud)

我相信解决方案在重新加载集合视图的某个地方我只是不知道到底要做什么.

有什么建议?

编辑:这是调用函数的地方; 我的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
}
Run Code Online (Sandbox Code Playgroud)

我认为唯一可能影响图像加载的另一件事是我用来下载用户数据的函数,这个函数被调用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)

}
Run Code Online (Sandbox Code Playgroud)

目前的行为:

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

Rob*_*Rob 11

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

  1. 如果快速滚动到第100行,则在查看可见单元格的图像之前,必须等待检索前99行的图像.在图像开始弹出之前,这可能导致非常长的延迟.

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

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

无论如何,解决方案是跟踪(a)URLSessionTask与最后一个请求相关的电流; (b)URL要求的电流.然后您可以(a)在开始新请求时,确保取消任何先前的请求; (b)更新图像视图时,请确保与图像关联的URL与当前URL匹配.

但是,诀窍在于编写扩展时,不能只添加新的存储属性.因此,您必须使用关联的对象API,因此您可以将这两个新存储的值与UIImageView对象相关联.我亲自用一个计算属性包装这个关联值API,这样检索图像的代码就不会被这种东西所掩盖.无论如何,这产生:

extension UIImageView {

    private static var taskKey = 0
    private static var urlKey = 0

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

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

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

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

        // reset imageview's image

        self.image = nil

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

}
Run Code Online (Sandbox Code Playgroud)

另请注意,您正在引用某个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: .UIApplicationDidReceiveMemoryWarning, 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,异步检索和缓存开始变得更加复杂,这就是为什么我们通常建议考虑建立异步图像检索机制,如AlamofireImage或Kingfisher或SDWebImage.这些人花了很多时间来解决上述问题,而其他人则相当强大.但是,如果你要"自己动手",我会提出类似上述内容的建议.