2017-07-18 53 views
1

我试图从我的firebase数据库下载图像并将它们加载到collectionviewcells中。图像下载,但我有麻烦让他们都下载和异步加载。异步下载和缓存来自url的图像

当前当我运行我的代码最后图像下载加载。但是,如果我更新数据库,集合视图更新和新的最后一个用户配置文件图像也会加载,但其余部分不存在。

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

下面是处理下载的代码:

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会更新并正确显示新注册用户的个人资料图像,以及正确下载图像的最后一个单元格。然而,其余的,没有配置文件图像。

+0

你可以显示你的CollectionViewController吗? – javimuu

+0

@javimuu我包括函数被调用的地方。 – Stefan

+0

使用sdwebimage库轻松加载图像。 –

回答

6

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

  1. 如果您快速滚动到第100行,你将不得不等待第一个99行的图像检索你看到的图像的可见单元格之前。在图像开始弹出之前,这可能会导致真正的长时间延迟。

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

,您可能不会立即注意到无论这些都是问题,因为他们只会表现自己,当图像检索也很难跟上用户的滚动(速度较慢的网络和快速滚动的组合)。另外,您应该始终使用网络链接调节器测试您的应用程序,该网络链接调节器可以模拟不良连接,这使得更容易显示这些错误。

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

但是,编写扩展时,你不能只添加新的存储属性。因此,您必须使用关联的对象API,因此您可以将这两个新存储的值与UIImageView对象相关联。我个人将这个关联的值包装在一个计算属性中,以便检索图像的代码不会被这种东西掩盖。无论如何,这得到:

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。这些人花了很多时间处理上述问题和其他问题,并且相当健壮。但是如果你打算“自己动手”,那么我会在最低限度内提出类似上述的建议。