从url异步下载和缓存图像

时间:2017-07-18 22:46:50

标签: swift uiimageview uicollectionviewcell nsurl

我试图从我的firebase数据库下载图像并将它们加载到collectionviewcells中。图像下载,但是我无法异步下载和加载它们。

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

我不想使用第三方图书馆,因此我们将非常感谢任何资源或建议。

这是处理下载的代码:

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

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

有什么建议吗?

编辑: 这是函数被调用的地方;我的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个单元格是唯一显示轮廓图像的单元格。此外,当我更新数据库,即将新用户注册到其中时,除了正确下载其图像的旧的最后一个单元之外,集合视图还更新并正确显示新注册的用户及其配置文件图像。然而,其余部分仍然没有个人资料图片。

1 个答案:

答案 0 :(得分:11)

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

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

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

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

    无论如何,解决方案是跟踪(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()
        }
    
    }
    

    另请注意,您引用了一些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)
        }
    }
    

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