【问题标题】:downloading and caching images from url asynchronously从 url 异步下载和缓存图像
【发布时间】:2017-12-24 00:02:43
【问题描述】:

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

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

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

这是处理下载的代码:

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 我只是不知道在哪里做。

有什么建议吗?

编辑: 这是调用函数的地方;我的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 会更新并正确显示新注册的用户以及他们的个人资料图像,以及正确下载它的图像的最后一个旧单元格。然而,其余的仍然没有个人资料图片。

【问题讨论】:

  • 你能展示你的 CollectionViewController 吗?
  • @javimuu 我包含了调用函数的位置。
  • 使用 sdwebimage 库轻松加载图片。
  • 使用 sdwebImage 库缓存图片。
  • 但是异步检索机制基本上没问题(尽管我会在后台实例化UIImage 并更新缓存,并且只将self.image = image 分派到主队列。)您的问题可能与此无关。当然,这个扩展有很多改进(例如,如果用户快速滚动浏览集合视图,你会看到图像闪烁,显示以前索引路径的结果,你会积压对索引路径的请求不再可见等)。但在我们着手改进之前,让我们先弄清楚当前的问题是什么。

标签: swift uiimageview uicollectionviewcell nsurl


【解决方案1】:

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

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

}

还请注意,您引用了一些 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。这些人花了很多时间来解决上述问题和其他问题,并且相当强大。但如果你要“自己动手”,我建议至少像上面那样。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-07-01
    • 1970-01-01
    • 2020-05-25
    • 1970-01-01
    • 1970-01-01
    • 2019-02-05
    • 2020-03-03
    • 2016-02-10
    相关资源
    最近更新 更多