为什么在这个 table 视图中滚动如此不稳定?
Why is the scrolling so choppy in this table view?
我的 tvOS 应用中的 table 视图出现一些奇怪的行为。我的应用程序只是显示一些电视 shows/movies 的图像列表以及系列名称和剧集标题。我创建了一个名为 BMContentCell 的自定义 table 视图单元格 class。这是整个文件:
import UIKit
class BMContentCell: UITableViewCell
{
private var imageCache: NSCache<NSString, UIImage>? // key: string url pointing to the image on the internet, value: UIImage object
private let kCellContentFormat = "VOD: %@ | DRM: %@ | Auth: %@"
private let kPosterContent = "poster"
private var contentImageView: UIImageView =
{
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 15
imageView.clipsToBounds = true
imageView.layer.borderColor = UIColor.clear.cgColor
imageView.layer.borderWidth = 1
return imageView
}()
private let contentTitle: UILabel =
{
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 32)
label.textColor = UIColor.black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let contentSubtitle: UILabel =
{
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 26)
label.textColor = UIColor.black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let contentAttributes: UILabel =
{
let label = UILabel()
label.backgroundColor = .white
label.font = UIFont.systemFont(ofSize: 20)
label.textColor = UIColor.black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func awakeFromNib()
{
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool)
{
super.setSelected(selected, animated: animated)
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?)
{
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .white
addSubview(contentImageView)
addSubview(contentTitle)
addSubview(contentSubtitle)
addSubview(contentAttributes)
contentImageView.anchor(
top: self.topAnchor,
leading: self.leadingAnchor,
bottom: self.bottomAnchor,
trailing: contentTitle.leadingAnchor,
padding: .init(top: 0, left: 0, bottom: 0, right: 0),
size: .init(width: 180, height: 180)
)
contentTitle.anchor(
top: self.topAnchor,
leading: contentImageView.trailingAnchor,
bottom: contentSubtitle.topAnchor,
trailing: self.trailingAnchor,
size: CGSize.init(width: 0, height: 60)
)
contentSubtitle.anchor(
top: contentTitle.bottomAnchor,
leading: contentImageView.trailingAnchor,
bottom: contentAttributes.topAnchor,
trailing: self.trailingAnchor,
padding: .zero,
size: CGSize.init(width: 0, height: 60)
)
contentAttributes.anchor(
top: contentSubtitle.bottomAnchor,
leading: contentImageView.trailingAnchor,
bottom: self.bottomAnchor,
trailing: self.trailingAnchor,
padding: .zero,
size: CGSize.init(width: 0, height: 60)
)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
internal func configureCell(content: BMContent)
{
self.contentTitle.text = content.media.name
self.contentSubtitle.text = content.name
self.contentAttributes.text = self.contentCellAttributes(
String(!content.isLiveStream()),
String(content.isDrm),
String(content.authentication.required)
)
var posterImageContent = content.media.images.filter { [=11=].type == kPosterContent }
DispatchQueue.global(qos: .userInitiated).async
{
if posterImageContent.count > 1
{
posterImageContent.sort { [=11=].url < .url }
self.loadImageURLFromCache(imageURLStr: posterImageContent[0].url)
}
else if posterImageContent.count == 1
{
self.loadImageURLFromCache(imageURLStr: posterImageContent[0].url)
}
}
}
private func contentCellAttributes(_ isLiveStream: String, _ isDRM: String, _ isAuthRequired: String) -> String
{
return String(format: kCellContentFormat, isLiveStream, isDRM, isAuthRequired)
}
private func loadImageURLFromCache(imageURLStr: String)
{
// check if the url (key) exists in the cache
if let imageFromCache = imageCache?.object(forKey: imageURLStr as NSString)
{
// the image exists in cache, load the image and return
DispatchQueue.main.async {self.contentImageView.image = imageFromCache as UIImage}
return
}
guard let url = URL(string: imageURLStr) else
{
// if the url is not valid, load a blank UIImage
self.imageCache?.setObject(UIImage(), forKey: imageURLStr as NSString)
DispatchQueue.main.async {self.contentImageView.image = UIImage()}
return
}
let session = URLSession.shared
let task = session.dataTask(with: url)
{
data, response, error in
if let data = data
{
guard let imageFromURL = UIImage(data: data) else
{
// if we cannot create an image from the data for some reason, load a blank UIImage
let placeholderImage = UIImage()
self.imageCache?.setObject(placeholderImage, forKey: imageURLStr as NSString)
DispatchQueue.main.async {self.contentImageView.image = placeholderImage}
return
}
// load the image and set the key and value in the cache
DispatchQueue.main.async {self.contentImageView.image = imageFromURL}
self.imageCache?.setObject(imageFromURL, forKey: imageURLStr as NSString)
}
}
task.resume()
}
}
如果您查看 configureCell 方法,它正在寻找图像 url(这是此 class 中 NSCache object 的键)并且它会检查缓存是否url 已经存在。如果是,它会简单地加载 UIImage,否则它会从互联网上获取它。
table视图所在的视图控制器叫做BMContentViewController。 class里面的cellForRowAt方法如下:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: kReuseIdentifier, for: indexPath) as! BMContentCell
if self.viewModel.currentContent.count > 0
{
cell.configureCell(content: self.viewModel.currentContent[indexPath.row])
}
return cell
}
每当我在 tvOS 应用程序的 table 视图中向上或向下滚动时,它都非常不稳定。而且对于任何给定的电视,图像最初似乎都是错误的 show/movie 并且在几毫秒后更新为正确的图像。
基本上,我的问题是在我的应用程序内的 table 视图中解决缓存和检索图像问题的更好方法是什么?
谢谢!
你绝对是在正确的轨道上。您的图像显示半秒不正确的原因是滚动时单元格被重复使用,因此它们保留了它们 "prepared" 所在的最后状态。这通常不会有问题,但在您的configureCell
方法,将 loadImageURLFromCache
包装在不在主队列上的线程中,然后每个 UI 更新都(正确地)在主队列上进行。这是 async
行为,也是图像需要一小部分更新的原因。在我看来,你根本不需要 DispatchQueue.global(qos: .userInitiated).async
。由于您的缓存对象未使用异步查找,因此您可以安全地将图像设置为当前线程上的 imageView,而无需包装在主线程异步调用中。此外,如果您要滚动浏览大量图像,您真的应该考虑使用写入磁盘并具有清理方法的缓存库,因为您的内存使用量只会继续增长。我还建议不要在 loadImageURLFromCache
方法中间使用 "dangling return"。我在下面稍微调整了你的方法。我还没有验证它是否被正确输入或编译,所以它可能不是一个粘贴和构建的解决方案。
private func loadImageURLFromCache(imageURLStr: String)
{
// check if the url (key) exists in the cache
if let imageFromCache = imageCache?.object(forKey: imageURLStr) as? UIImage
{
// the image exists in cache, load the image and return
self.contentImageView.image = imageFromCache
return
} else if let url = URL(string: imageURLStr) {
loadImageFromURL(url)
} else {
loadBlankImage()
}
}
private func loadImageFromURL(_ url: URL) {
let session = URLSession.shared
let task = session.dataTask(with: url)
{
[weak self] (data, response, error) in
if let data = data
{
guard let imageFromURL = UIImage(data: data) else
{
// if we cannot create an image from the data for some reason, load a blank UIImage
self?.loadBlankImage()
return
}
// load the image and set the key and value in the cache
DispatchQueue.main.async {self?.contentImageView.image = imageFromURL}
self?.imageCache?.setObject(imageFromURL, forKey: imageURLStr as NSString)
}
}
task.resume()
}
private func loadBlankImage() {
contentImageView.image = UIImage()
}
我的 tvOS 应用中的 table 视图出现一些奇怪的行为。我的应用程序只是显示一些电视 shows/movies 的图像列表以及系列名称和剧集标题。我创建了一个名为 BMContentCell 的自定义 table 视图单元格 class。这是整个文件:
import UIKit
class BMContentCell: UITableViewCell
{
private var imageCache: NSCache<NSString, UIImage>? // key: string url pointing to the image on the internet, value: UIImage object
private let kCellContentFormat = "VOD: %@ | DRM: %@ | Auth: %@"
private let kPosterContent = "poster"
private var contentImageView: UIImageView =
{
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 15
imageView.clipsToBounds = true
imageView.layer.borderColor = UIColor.clear.cgColor
imageView.layer.borderWidth = 1
return imageView
}()
private let contentTitle: UILabel =
{
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 32)
label.textColor = UIColor.black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let contentSubtitle: UILabel =
{
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 26)
label.textColor = UIColor.black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let contentAttributes: UILabel =
{
let label = UILabel()
label.backgroundColor = .white
label.font = UIFont.systemFont(ofSize: 20)
label.textColor = UIColor.black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func awakeFromNib()
{
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool)
{
super.setSelected(selected, animated: animated)
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?)
{
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .white
addSubview(contentImageView)
addSubview(contentTitle)
addSubview(contentSubtitle)
addSubview(contentAttributes)
contentImageView.anchor(
top: self.topAnchor,
leading: self.leadingAnchor,
bottom: self.bottomAnchor,
trailing: contentTitle.leadingAnchor,
padding: .init(top: 0, left: 0, bottom: 0, right: 0),
size: .init(width: 180, height: 180)
)
contentTitle.anchor(
top: self.topAnchor,
leading: contentImageView.trailingAnchor,
bottom: contentSubtitle.topAnchor,
trailing: self.trailingAnchor,
size: CGSize.init(width: 0, height: 60)
)
contentSubtitle.anchor(
top: contentTitle.bottomAnchor,
leading: contentImageView.trailingAnchor,
bottom: contentAttributes.topAnchor,
trailing: self.trailingAnchor,
padding: .zero,
size: CGSize.init(width: 0, height: 60)
)
contentAttributes.anchor(
top: contentSubtitle.bottomAnchor,
leading: contentImageView.trailingAnchor,
bottom: self.bottomAnchor,
trailing: self.trailingAnchor,
padding: .zero,
size: CGSize.init(width: 0, height: 60)
)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
internal func configureCell(content: BMContent)
{
self.contentTitle.text = content.media.name
self.contentSubtitle.text = content.name
self.contentAttributes.text = self.contentCellAttributes(
String(!content.isLiveStream()),
String(content.isDrm),
String(content.authentication.required)
)
var posterImageContent = content.media.images.filter { [=11=].type == kPosterContent }
DispatchQueue.global(qos: .userInitiated).async
{
if posterImageContent.count > 1
{
posterImageContent.sort { [=11=].url < .url }
self.loadImageURLFromCache(imageURLStr: posterImageContent[0].url)
}
else if posterImageContent.count == 1
{
self.loadImageURLFromCache(imageURLStr: posterImageContent[0].url)
}
}
}
private func contentCellAttributes(_ isLiveStream: String, _ isDRM: String, _ isAuthRequired: String) -> String
{
return String(format: kCellContentFormat, isLiveStream, isDRM, isAuthRequired)
}
private func loadImageURLFromCache(imageURLStr: String)
{
// check if the url (key) exists in the cache
if let imageFromCache = imageCache?.object(forKey: imageURLStr as NSString)
{
// the image exists in cache, load the image and return
DispatchQueue.main.async {self.contentImageView.image = imageFromCache as UIImage}
return
}
guard let url = URL(string: imageURLStr) else
{
// if the url is not valid, load a blank UIImage
self.imageCache?.setObject(UIImage(), forKey: imageURLStr as NSString)
DispatchQueue.main.async {self.contentImageView.image = UIImage()}
return
}
let session = URLSession.shared
let task = session.dataTask(with: url)
{
data, response, error in
if let data = data
{
guard let imageFromURL = UIImage(data: data) else
{
// if we cannot create an image from the data for some reason, load a blank UIImage
let placeholderImage = UIImage()
self.imageCache?.setObject(placeholderImage, forKey: imageURLStr as NSString)
DispatchQueue.main.async {self.contentImageView.image = placeholderImage}
return
}
// load the image and set the key and value in the cache
DispatchQueue.main.async {self.contentImageView.image = imageFromURL}
self.imageCache?.setObject(imageFromURL, forKey: imageURLStr as NSString)
}
}
task.resume()
}
}
如果您查看 configureCell 方法,它正在寻找图像 url(这是此 class 中 NSCache object 的键)并且它会检查缓存是否url 已经存在。如果是,它会简单地加载 UIImage,否则它会从互联网上获取它。
table视图所在的视图控制器叫做BMContentViewController。 class里面的cellForRowAt方法如下:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: kReuseIdentifier, for: indexPath) as! BMContentCell
if self.viewModel.currentContent.count > 0
{
cell.configureCell(content: self.viewModel.currentContent[indexPath.row])
}
return cell
}
每当我在 tvOS 应用程序的 table 视图中向上或向下滚动时,它都非常不稳定。而且对于任何给定的电视,图像最初似乎都是错误的 show/movie 并且在几毫秒后更新为正确的图像。
基本上,我的问题是在我的应用程序内的 table 视图中解决缓存和检索图像问题的更好方法是什么?
谢谢!
你绝对是在正确的轨道上。您的图像显示半秒不正确的原因是滚动时单元格被重复使用,因此它们保留了它们 "prepared" 所在的最后状态。这通常不会有问题,但在您的configureCell
方法,将 loadImageURLFromCache
包装在不在主队列上的线程中,然后每个 UI 更新都(正确地)在主队列上进行。这是 async
行为,也是图像需要一小部分更新的原因。在我看来,你根本不需要 DispatchQueue.global(qos: .userInitiated).async
。由于您的缓存对象未使用异步查找,因此您可以安全地将图像设置为当前线程上的 imageView,而无需包装在主线程异步调用中。此外,如果您要滚动浏览大量图像,您真的应该考虑使用写入磁盘并具有清理方法的缓存库,因为您的内存使用量只会继续增长。我还建议不要在 loadImageURLFromCache
方法中间使用 "dangling return"。我在下面稍微调整了你的方法。我还没有验证它是否被正确输入或编译,所以它可能不是一个粘贴和构建的解决方案。
private func loadImageURLFromCache(imageURLStr: String)
{
// check if the url (key) exists in the cache
if let imageFromCache = imageCache?.object(forKey: imageURLStr) as? UIImage
{
// the image exists in cache, load the image and return
self.contentImageView.image = imageFromCache
return
} else if let url = URL(string: imageURLStr) {
loadImageFromURL(url)
} else {
loadBlankImage()
}
}
private func loadImageFromURL(_ url: URL) {
let session = URLSession.shared
let task = session.dataTask(with: url)
{
[weak self] (data, response, error) in
if let data = data
{
guard let imageFromURL = UIImage(data: data) else
{
// if we cannot create an image from the data for some reason, load a blank UIImage
self?.loadBlankImage()
return
}
// load the image and set the key and value in the cache
DispatchQueue.main.async {self?.contentImageView.image = imageFromURL}
self?.imageCache?.setObject(imageFromURL, forKey: imageURLStr as NSString)
}
}
task.resume()
}
private func loadBlankImage() {
contentImageView.image = UIImage()
}