Downloading images in UITableViewCell: Data vs URLSession?

你。 提交于 2019-12-24 08:46:38

问题


I have a UITableView and I need each of its cells to download an image from a provided URL and show it. This looks to be a quite common scenario and indeed I found several posts and questions related to this, but I am not still clear about which the best approach should be.

First one, I have one using Data(contentsOf:). This is the code I have in my UITableViewCell:

class MyCell: UITableViewCell {

@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var imageView: UIImageView!

var model: Model? {
    willSet {
        activityIndicator.startAnimating()
        configureImage(showImage: false, showActivity: true)
    }
    didSet {
        guard let imageSmallUrlStr = model?.imageSmallUrl, let imageUrl = URL(string: imageSmallUrlStr) else {
            activityIndicator.stopAnimating()
            configureImage(showImage: false, showActivity: false)
            return
        }

        DispatchQueue.global(qos: .userInitiated).async {
            var image: UIImage?
            let imageData = try? Data(contentsOf: imageUrl)
            if let imageData = imageData {
                image = UIImage(data: imageData)
            }

            DispatchQueue.main.async {
                self.imageView.image = image
                self.configureImage(showImage: true, showActivity: false)
            }
        }
}

override func awakeFromNib() {
    super.awakeFromNib()
    configureImage(showImage: false, showActivity: false)
}

override func prepareForReuse() {
    super.prepareForReuse()
    model = nil
}

// Other methods
}

This approach looks quite straightforward and fast, at least when I test it running on a simulator. Though, I have some questions regarding it:

  1. Is it actually appropriate to download an image from an HTTP URL by using Data(contentsOf:)? It works, but maybe it is more suitable for getting files you have or another kind of stuff, and it is not the best solution for networking.
  2. I guess that performing that performing that in a global async queue is the correct thing to do, right?
  3. Is it possible with this approach to cancel that image download if the image URL is updated and I need to download a different one? Also, should I cancel the download when a cell is dequeued from the table, or is this automatically done?
  4. Also, I'm not sure about this: since several cells are going to be shown in the table at same time, those cells need to download and show their image concurrently. And could also happen that, when an image has finished being downloaded, the corresponding cell has been dequeued from the table because the user has scrolled. Does this approach ensure that several cells are concurrently downloading the image, and that each downloaded image is correctly set to its corresponding cell?

Now, this is the other approach I have, the one using URLSession:

class MyCell: UITableViewCell {

@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var imageView: UIImageView!

var imageTask: URLSessionDownloadTask?

var model: Model? {
    willSet {
        activityIndicator.startAnimating()
        configureImage(showImage: false, showActivity: true)
    }
    didSet {
        imageTask?.cancel()

        guard let imageSmallUrlStr = model?.imageSmallUrl, let imageUrl = URL(string: imageSmallUrlStr) else {
            activityIndicator.stopAnimating()
            configureImage(showImage: false, showActivity: false)
            return
        }

        imageTask = NetworkManager.sharedInstance.getImageInBackground(imageUrl: imageUrl, completion: { [weak self] (image, error) in
            DispatchQueue.main.async {
                guard error == nil else {
                    self?.activityIndicator.stopAnimating()
                    self?.configureImage(showImage: false, showActivity: false)
                    return
                }

                self?.imageView.image = image
                self?.activityIndicator.stopAnimating()
                self?.configureImage(showImage: true, showActivity: false)
            }
        })
    }
}

// Other methods

}

With this approach, I can manually cancel the task, but when I run the app on simulator it looks like it is slower. In fact, I am getting some "cancelled" errors when trying to download many of the images. But this way I could be able to handle background downloads.

Questions about this approach:

  1. How could I ensure that a downloaded image is shown in its corresponding cell? Same issue as described earlier: the cell could have been dequeued from the table at that moment.

This question is related to both approaches:

  1. I dont want to save the images to files, but I dont want to download again if the cell is shown again and I already did. What should be the best way to cache them?

回答1:


You should never use NSData's contentsOfURL method to retrieve non-local data, for two reasons:

  • The documentation explicitly says that doing so is unsupported.
  • The name resolution and retrieval happens synchronously, blocking the current thread.

In your case, doing it on a concurrent queue, the last one isn't quite as bad as it otherwise would be, but if you're fetching enough resources, I suspect it would result in a fair amount of overhead.

One reason why your NSURLSession approach appears slower is that I think you're probably fetching all the resources simultaneously (hitting the server really hard) with your other approach. Changing the concurrency limit in NSURLSession might improve its performance a bit, but only do that if you know the server can handle it.

To answer the other questions:

  • You can tie the image to a particular cell in any number of ways, but the most common approach is to have a singleton that manages all the requests, and in that singleton, either:

    • keep a dictionary that maps a data task to a unique identifier. Set that as the cell's accessibility identifier, and use the built-in lookup methods to find the cell with that accessibility identifier. If it no longer exists, throw away the response.
    • keep a dictionary that maps a data task to a block. When the request completes, run the block. Let the block keep a strong reference to the cell.

    In either case, store the URL in a property on the cell, and make sure it matches the request's original URL before setting the image, so that if the cell got reused, you won't overwrite its existing image with the wrong image from a stale request.

  • NSURLCache is your friend. Create an on-disk cache of a reasonable size, and it should take care of avoiding unnecessary network requests (assuming your server supports caching properly).


来源:https://stackoverflow.com/questions/43716279/downloading-images-in-uitableviewcell-data-vs-urlsession

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!