从 url 异步下载和缓存图像
downloading and caching images from url asynchronously
我正在尝试从我的 firebase 数据库下载图像并将它们加载到 collectionviewcells 中。图片已下载,但我无法让它们全部异步下载和加载。
目前,当我 运行 我的代码时 最后 图片下载加载。但是,如果我更新我的数据库,集合视图会更新,并且还会加载新的最后一个用户配置文件图像,但其余部分会丢失。
我不想使用第 3 方库,因此我们将不胜感激任何资源或建议。
这是处理下载的代码:
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 还会更新并正确显示新注册用户及其个人资料图像。然而,其余的仍然没有个人资料图片。
我知道你发现了你的问题,它与上面的代码无关,但我仍然有一个观察。具体来说,您的异步请求将继续进行,即使单元格(以及图像视图)随后被重新用于另一个索引路径。这会导致两个问题:
如果快速滚动到第 100 行,您将不得不等待前 99 行的图像被检索,然后才能看到可见单元格的图像。这可能会导致在图像开始弹出之前出现非常长的延迟。
如果第 100 行的那个单元格被多次重复使用(例如第 0 行、第 9 行、第 18 行等),您可能会看到图像从一张图像开始闪烁到下一个,直到您到达第 100 行的图像检索。
现在,您可能不会立即注意到这些问题中的任何一个,因为只有当图像检索难以跟上用户的滚动(慢速网络和快速滚动的结合)时,它们才会显现出来。顺便说一句,您应该始终使用网络 link 调节器测试您的应用程序,它可以模拟不良连接,从而更容易显示这些错误。
无论如何,解决方案是跟踪 (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 { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()
// reset image view’s image
self.image = placeholder
// 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: UIApplication.didReceiveMemoryWarningNotification,
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)
}
}
一个更大的、更具架构性的观察:人们真的应该将图像检索与图像视图分离。假设您有一个 table,其中碰巧有十几个使用相同图像的单元格。你真的想要检索同一张图像十几次只是因为第二个图像视图在第一个图像视图完成检索之前滚动到视图中吗?号
此外,如果您想在图像视图的上下文之外检索图像怎么办?也许是一个按钮?或者可能出于其他原因,例如下载图像以存储在用户的照片库中。在图像视图之外还有大量可能的图像交互。
最重要的是,获取图像不是图像视图的一种方法,而是图像视图希望利用的一种通用机制。异步图像 retrieval/caching 机制通常应合并到单独的“图像管理器”对象中。它们可以检测冗余请求并在图像视图以外的上下文中使用。
如您所见,异步检索和缓存开始变得有点复杂,这就是为什么我们通常建议考虑已建立的异步图像检索机制,如 AlamofireImage or Kingfisher or SDWebImage。这些人花了很多时间来解决上述问题和其他问题,并且相当健壮。但是,如果您要“自己动手”,我建议您至少要采用类似上述的方法。
我正在尝试从我的 firebase 数据库下载图像并将它们加载到 collectionviewcells 中。图片已下载,但我无法让它们全部异步下载和加载。
目前,当我 运行 我的代码时 最后 图片下载加载。但是,如果我更新我的数据库,集合视图会更新,并且还会加载新的最后一个用户配置文件图像,但其余部分会丢失。
我不想使用第 3 方库,因此我们将不胜感激任何资源或建议。
这是处理下载的代码:
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 还会更新并正确显示新注册用户及其个人资料图像。然而,其余的仍然没有个人资料图片。
我知道你发现了你的问题,它与上面的代码无关,但我仍然有一个观察。具体来说,您的异步请求将继续进行,即使单元格(以及图像视图)随后被重新用于另一个索引路径。这会导致两个问题:
如果快速滚动到第 100 行,您将不得不等待前 99 行的图像被检索,然后才能看到可见单元格的图像。这可能会导致在图像开始弹出之前出现非常长的延迟。
如果第 100 行的那个单元格被多次重复使用(例如第 0 行、第 9 行、第 18 行等),您可能会看到图像从一张图像开始闪烁到下一个,直到您到达第 100 行的图像检索。
现在,您可能不会立即注意到这些问题中的任何一个,因为只有当图像检索难以跟上用户的滚动(慢速网络和快速滚动的结合)时,它们才会显现出来。顺便说一句,您应该始终使用网络 link 调节器测试您的应用程序,它可以模拟不良连接,从而更容易显示这些错误。
无论如何,解决方案是跟踪 (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 { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()
// reset image view’s image
self.image = placeholder
// 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: UIApplication.didReceiveMemoryWarningNotification,
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)
}
}
一个更大的、更具架构性的观察:人们真的应该将图像检索与图像视图分离。假设您有一个 table,其中碰巧有十几个使用相同图像的单元格。你真的想要检索同一张图像十几次只是因为第二个图像视图在第一个图像视图完成检索之前滚动到视图中吗?号
此外,如果您想在图像视图的上下文之外检索图像怎么办?也许是一个按钮?或者可能出于其他原因,例如下载图像以存储在用户的照片库中。在图像视图之外还有大量可能的图像交互。
最重要的是,获取图像不是图像视图的一种方法,而是图像视图希望利用的一种通用机制。异步图像 retrieval/caching 机制通常应合并到单独的“图像管理器”对象中。它们可以检测冗余请求并在图像视图以外的上下文中使用。
如您所见,异步检索和缓存开始变得有点复杂,这就是为什么我们通常建议考虑已建立的异步图像检索机制,如 AlamofireImage or Kingfisher or SDWebImage。这些人花了很多时间来解决上述问题和其他问题,并且相当健壮。但是,如果您要“自己动手”,我建议您至少要采用类似上述的方法。