【问题标题】:Custom UICollectionViewCell not loading from cache自定义 UICollectionViewCell 未从缓存加载
【发布时间】:2018-03-23 05:33:01
【问题描述】:

我有一个自定义 UICollectionViewCell 定义如下:

class MomentsCell: UICollectionViewCell {
@IBOutlet weak var imageView: UIImageView!}

我的委托方法如下所示:

    override func collectionView(_ collectionView: UICollectionView,
                             cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier,
                                                  for: indexPath) as! MomentsCell
    let imageURL = imageURLS[indexPath.row]
    self.updateImageForCell(cell: cell,
                            inCollectionView: collectionView,
                            withImageURL: imageURL,
                            atIndexPath: indexPath)
    return cell

方法“updateImageForCell”如下所示:

    func updateImageForCell(cell: MomentsCell,
                        inCollectionView collectionView: UICollectionView,
                        withImageURL: String,
                        atIndexPath indexPath: IndexPath) {
    /*if let url = URL(string: withImageURL) {
        cell.imageView.setImageWith(url, placeholderImage: UIImage(named: "placeholder"))
    }*/
     cell.imageView.image = UIImage(named: "placeholder")
     ImageManager.shared.downloadImageFromURL(withImageURL) {
        (success, image) -> Void in
        if success && image != nil {
            // checks that the view did not move before setting the image to the cell!
            if collectionView.cellForItem(at: indexPath) == cell {
                cell.imageView.image = image
            }
        }
     }
}

ImageManager 是一个单例,其中包含一组图像的缓存。如果图像 URL 在缓存中,则返回缓存的图像。如果没有,它会启动一个 URLSession 从 Firebase 下载图像。

第一次加载视图时,图像确实会显示出来,这向我表明,此时一切都或多或少地正常工作。但是,当我滚动时,会加载随机图像,有些最终没有加载,最终所有单元格都变为空白并且无论如何都不会加载,即使所有内容都保存在缓存中。

这很糟糕,但这是我的 ImageManager 类:

class ImageManager: NSObject {
static var shared: ImageManager { return _singletonInstance }
var imageCache = [String : UIImage]()

// checks the local variable for url string to see if the UIImage was already downloaded
func cachedImageForURL(_ url: String) -> UIImage? {
    return imageCache[url]
}

// saves a downloaded UIImage with corresponding URL String
func cacheImage(_ image: UIImage, forURL url: String) {
    // First check to see how many images are already saved in the cache
    // If there are more images than the max, we have to clear old images
    if imageCache.count > kMaxCacheImageSize {
        imageCache.remove(at: imageCache.startIndex)
    }
    // Adds the new image to the END of the local image Cache array
    imageCache[url] = image
}

func downloadImageFromURL(_ urlString: String,
                          completion: ((_ success: Bool,_ image: UIImage?) -> Void)?) {

    // First, checks for cachedImage
    if let cachedImage = cachedImageForURL(urlString) {
         completion?(true, cachedImage)
    } else {
        guard let url = URL(string: urlString) else {
            completion?(false,nil)
            return
        }
        print("downloadImageFromURL")

        let task = URLSession.shared.downloadTask(with: url,
            completionHandler: { (url, response, error) in

                print("downloadImageFromURL complete")


                if error != nil {
                    print("Error \(error!.localizedDescription)")
                } else {
                    if let url = URL(string: urlString),
                        let data = try? Data(contentsOf: url) {
                        if let image = UIImage(data: data) {
                            self.cacheImage(image, forURL: url.absoluteString)
                            DispatchQueue.main.async(execute: { completion?(true, image) })
                        }
                    }
                }
        })
        task.resume()
    }
}

func prefetchItem(url urlString: String) {
    guard let url = URL(string: urlString) else {
        return
    }

    let task = URLSession.shared.downloadTask(with: url,
            completionHandler: { (url, response, error) in
                if error != nil {
                    print("Error \(error!.localizedDescription)")
                } else {
                    if let url = URL(string: urlString),
                    let data = try? Data(contentsOf: url) {
                    if let image = UIImage(data: data) {
                    self.cacheImage(image, forURL: url.absoluteString)
                }
            }
        }
        })
        task.resume()
    }

}

任何帮助将不胜感激。如果我错过了任何重要信息,请告诉我。

【问题讨论】:

    标签: ios swift uicollectionview uicollectionviewcell


    【解决方案1】:

    这里有多个问题,但我相信只有 #1 会导致您的图像根本不显示的问题。

    1. 设置图像前检查单元格是否相同时的行:collectionView.cellForItem(at: indexPath) == cellcellForItem 实际上返回 nil,因为它在屏幕外。它在下载图像时工作,因为有时间将其显示在屏幕上,但是当将图像从缓存中拉出时,您的 completionHandler 会立即被调用,因此单元格尚未返回到 collectionView!

      对此有多种解决方案,但也许最简单的方法是添加一个返回到您的 completionHandler 的 wasCached 标志(请参阅此答案末尾的代码)。

    2. 您实际上是在下载每个图像两次:首先,使用URLSession.shared.downloadTask,然后在您从下载任务的 url 获取数据时再次在 completionHandler 中。您传递给 try? Data(contentsOf: url) 的 URL 是来自服务器的图像 URL,而不是在 completionHandler 中返回的文件 URL。

    3. Apple's documentation of downloadTask(with:completionHandler:) 传递到您正在读取的 completionHandler 块的 URL:

      存储服务器响应的临时文件的位置。您必须在完成处理程序返回之前移动此文件或打开它以供阅读。否则,文件被删除,数据丢失。

    如果您不需要磁盘缓存,那么要修复 #2 和 #3,请改用 dataTask(with:completionHandler:) 函数,它会为您提供内存中已经存在的图像数据,您可以从中构造图像。

    func downloadImageFromURL(
        _ urlString: String,
        completion: ((_ success: Bool,_ image: UIImage?, _ wasCached: Bool) -> Void)?) {
        
        // First, checks for cachedImage
        if let cachedImage = cachedImageForURL(urlString) {
            print("Found cached image for URL \(urlString)")
            completion?(true, cachedImage, true)
        } else {
            guard let url = URL(string: urlString) else {
                completion?(false,nil, false)
                return
            }
            print("downloadImageFromURL \(urlString)")
            
            let task = URLSession.shared.dataTask(with: url) { data, response, error in
                print("downloadImageFromURL complete \(urlString)")
                if let error = error {
                    print("Error \(urlString) \(error.localizedDescription)")
                } else if let data = data, let image = UIImage(data: data) {
                    self.cacheImage(image, forURL: url.absoluteString)
                    DispatchQueue.main.async { completion?(true, image, false) }
                }
            }
            task.resume()
        }
    }
    

    及其用法:

    ImageManager.shared.downloadImageFromURL(withImageURL) { success, image, wasCached in
        if success && image != nil {
            // checks that the view did not move before setting the image to the cell!
            if wasCached || collectionView.cellForItem(at: indexPath) == cell {
                cell.imageView.image = image
            }
        }
    }
    

    【讨论】:

    • 杰克,感谢您提供详细而正确的答案。我应该更加小心我对异步和主线程调用的假设。调试后,我看到您所描述的正是正在发生的事情-比较始终为零,因为尚未返回单元格!我现在还看到我将每个图像下载了两次。修复该部分使图像输入的速度提高了一倍。我必须更加小心我如何使用网络功能,因为我有时仍然会将本地文件路径与外部地址混淆。感谢您的帮助!
    猜你喜欢
    • 1970-01-01
    • 2018-01-14
    • 2017-07-22
    • 2018-06-03
    • 1970-01-01
    • 2016-12-16
    • 1970-01-01
    • 1970-01-01
    • 2012-10-17
    相关资源
    最近更新 更多