处理 UITableViewCells 的图像拼贴 – 可以扩展到数百个单元格的方法?
Processing image collage for UITableViewCells – Method that would scale for hundreds of cells?
问题:我正在使用通过 Apple API(所有本地)访问的音乐中的艺术资产创建一个 4 图像拼贴作为 table 视图中播放列表单元格中的图像。我已经尝试了一些方法,我将通过这些方法使性能达到可接受的table 水平,但总会有一些问题。
方法1)运行处理代码return每个单元格的拼贴图像。这对于我设备上的几个播放列表来说总是很好,但我收到重度用户的报告说他们无法滚动。
方法2)在viewdidload()中,遍历所有播放列表并将拼贴图像和其中一个UUID保存在mu[=27中=] 数组然后使用 UUID 获取单元格的图像。这似乎在延迟加载视图时工作正常 - 但由于某些原因,后续加载需要更长的时间。
因此,在我尝试对方法 2 进行更多故障排除之前,我想知道我是否直接以完全错误的方式解决这个问题?我已经研究过 GCD 和 NSCache,就 GCD 而言,我不知道如何制定适当的设计模式来利用它,如果有可能的话,因为像 UI 更新和存储访问可能是什么阻碍了。
import UIKit
import MediaPlayer
class playlists: UITableViewController, UISearchBarDelegate, UISearchControllerDelegate {
...
var compositedCellImages:[(UIImage, UInt64)] = []
...
override func viewDidLoad() {
super.viewDidLoad()
let cloudFilter:MPMediaPropertyPredicate = MPMediaPropertyPredicate(value: false, forProperty: MPMediaItemPropertyIsCloudItem, comparisonType: MPMediaPredicateComparison.equalTo)
playlistsQuery.addFilterPredicate(cloudFilter)
playlistQueryCollections = playlistsQuery.collections?.filter{[=10=].value(forProperty: MPMediaPlaylistPropertyName) as? String != "Purchased"} as NSArray?
var tmpArray:[MPMediaPlaylist] = []
playlists = playlistQueryCollections as! [MPMediaPlaylist]
for playlist in playlists {
if playlist.value(forProperty: "parentPersistentID") as! NSNumber! == playlistFolderID {
tmpArray.append(playlist)
compositedCellImages.append(playlistListImage(inputPlaylistID: playlist.persistentID))
}
}
playlists = tmpArray
...
}
// MARK: - Table view data source
...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "playlistCell", for: indexPath) as! playlistCell
...
let currentItem = playlists[indexPath.row]
...
cell.playlistCellImage.image = compositedCellImages[indexPath.row].0
}
}
return cell
}
...
func playlistListImage(inputPlaylistID:MPMediaEntityPersistentID) -> (UIImage,UInt64) {
var playlistData:[MPMediaItem] = []
var pickedArtwork:[UIImage] = []
var shuffledIndexes:[Int] = []
let playlistDetailImage:collageImageView = collageImageView()
playlistDetailImage.frame = CGRect(x: 0, y: 0, width: 128, height: 128)
let playlistDataPredicate = MPMediaPropertyPredicate(value: NSNumber(value: inputPlaylistID as UInt64), forProperty: MPMediaPlaylistPropertyPersistentID, comparisonType:MPMediaPredicateComparison.equalTo)
let playlistDataQuery = MPMediaQuery.playlists()
let cloudFilter:MPMediaPropertyPredicate = MPMediaPropertyPredicate(value: false, forProperty: MPMediaItemPropertyIsCloudItem, comparisonType: MPMediaPredicateComparison.equalTo)
playlistDataQuery.addFilterPredicate(cloudFilter)
playlistDataQuery.addFilterPredicate(playlistDataPredicate)
playlistData = playlistDataQuery.items!
playlistData = playlistData.filter{[=10=].mediaType == MPMediaType.music}
for (index,_) in playlistData.enumerated() {
shuffledIndexes.append(index)
}
shuffledIndexes.shuffleInPlace()
for (_,element) in shuffledIndexes.enumerated() {
if playlistData[element].artwork != nil {
pickedArtwork.append(playlistData[element].artwork!.image(at: CGSize(width: 64, height: 64))!)
}
if pickedArtwork.count == 4 { break }
}
while pickedArtwork.count < 4 {
if pickedArtwork.count == 0 {
pickedArtwork.append(UIImage(named: "missing")!)
} else {
pickedArtwork.shuffleInPlace()
pickedArtwork.append(pickedArtwork[0])
}
}
pickedArtwork.shuffleInPlace()
playlistDetailImage.drawInContext(pickedArtwork, matrixSize: 2)
return ((playlistDetailImage.image)!,inputPlaylistID)
}
...
}
...
class collageImageView: UIImageView {
var inputImages:[UIImage] = []
var rows:Int = 1
var cols:Int = 1
func drawInContext(_ imageSet: [UIImage], matrixSize: Int) {
let frameLeg:Int = Int(self.frame.width/CGFloat(matrixSize))
var increment:Int = 0
UIGraphicsBeginImageContextWithOptions(self.frame.size, false, UIScreen.main.scale)
self.image?.draw(in: self.frame)
for col in 1...matrixSize {
for row in 1...matrixSize {
imageSet[increment].draw(in: CGRect(x: (row - 1) * frameLeg, y: (col-1) * frameLeg, width: frameLeg, height: frameLeg))
increment += 1
}
}
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
一种方法是修改原来的方法 1,但添加延迟加载和缓存。
有关延迟加载的示例,请参阅 https://developer.apple.com/library/content/samplecode/LazyTableImages/Introduction/Intro.html
基本思想是您仅在滚动完成时才尝试计算图像(在上面的示例中,他们从 URL 加载图像,但您会用计算替换它)。
此外,您可以缓存每一行的计算结果,这样当用户来回滚动时,您可以先检查缓存的值。还要清除 didReceiveMemoryWarning 中的缓存。
所以在 tableView(_: UITableView, cellForRowAt: IndexPath)
if <cache contains image for row> {
cell.playlistCellImage.image = <cached image>
} else if tableView.isDragging && !tableView.isDecelerating {
let image = <calculate image for row>
<add image to cache>
cell.playlistCellImage.image = image
} else {
cell.playlistCellImage.image = <placeholder image>
}
然后覆盖滚动视图的委托方法
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
loadImagesForOnScreenRows()
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
loadImagesForOnScreenRows()
}
并实施 loadImagesForOnScreenRows()
for path in tableView.indexPathsForVisibleRows {
let image = <calculate image for row>
<add image to cache>
if let cell = tableView.cellForRow(at: path) {
cell.playlistCellImage.image = image
}
}
最后一个优化是将实际计算推送到后台线程,但您应该会发现上面的延迟加载可能就足够了。
更新 - 在后台线程上计算。
总体思路是 运行 在后台队列上使用 DispatchQueue 进行计算,然后当结果准备好时,在 UI 线程上更新显示。
类似于以下(未经测试的)代码
func loadImageInBackground(inputPlaylistID:MPMediaEntityPersistentID, completion:@escaping (UIImage, UInt64))
{
let backgroundQueue = DispatchQueue.global(dos: DispatchQoS.QoSClass.background)
backgroundQueue.async {
let (image,n) = playlistListImage(inputPlaylistID:inputPlaylistID)
DispatchQueue.main.async {
completion(image,n)
}
}
}
并且在tableView(_: UITableView, cellForRowAt: IndexPath)中不直接计算图片,调用后台方法:
if <cache contains image for row> {
cell.playlistCellImage.image = <cached image>
} else if tableView.isDragging && !tableView.isDecelerating {
cell.playlistCellImage.image = <placeholder image>
loadImageInBackground(...) {
(image, n) in
if let cell = tableView.cellForRow(at:indexPath) {
cell.playlistCellImage.image = image
}
<add image to cache>
}
} else {
cell.playlistCellImage.image = <placeholder image>
}
和 loadImagesForOnScreenRows() 中的类似更新。
请注意在回调处理程序中再次检索单元格的额外代码。由于此更新可以异步发生,因此很有可能会重复使用原始单元格,因此您需要确保更新的是正确的单元格
问题:我正在使用通过 Apple API(所有本地)访问的音乐中的艺术资产创建一个 4 图像拼贴作为 table 视图中播放列表单元格中的图像。我已经尝试了一些方法,我将通过这些方法使性能达到可接受的table 水平,但总会有一些问题。
方法1)运行处理代码return每个单元格的拼贴图像。这对于我设备上的几个播放列表来说总是很好,但我收到重度用户的报告说他们无法滚动。
方法2)在viewdidload()中,遍历所有播放列表并将拼贴图像和其中一个UUID保存在mu[=27中=] 数组然后使用 UUID 获取单元格的图像。这似乎在延迟加载视图时工作正常 - 但由于某些原因,后续加载需要更长的时间。
因此,在我尝试对方法 2 进行更多故障排除之前,我想知道我是否直接以完全错误的方式解决这个问题?我已经研究过 GCD 和 NSCache,就 GCD 而言,我不知道如何制定适当的设计模式来利用它,如果有可能的话,因为像 UI 更新和存储访问可能是什么阻碍了。
import UIKit
import MediaPlayer
class playlists: UITableViewController, UISearchBarDelegate, UISearchControllerDelegate {
...
var compositedCellImages:[(UIImage, UInt64)] = []
...
override func viewDidLoad() {
super.viewDidLoad()
let cloudFilter:MPMediaPropertyPredicate = MPMediaPropertyPredicate(value: false, forProperty: MPMediaItemPropertyIsCloudItem, comparisonType: MPMediaPredicateComparison.equalTo)
playlistsQuery.addFilterPredicate(cloudFilter)
playlistQueryCollections = playlistsQuery.collections?.filter{[=10=].value(forProperty: MPMediaPlaylistPropertyName) as? String != "Purchased"} as NSArray?
var tmpArray:[MPMediaPlaylist] = []
playlists = playlistQueryCollections as! [MPMediaPlaylist]
for playlist in playlists {
if playlist.value(forProperty: "parentPersistentID") as! NSNumber! == playlistFolderID {
tmpArray.append(playlist)
compositedCellImages.append(playlistListImage(inputPlaylistID: playlist.persistentID))
}
}
playlists = tmpArray
...
}
// MARK: - Table view data source
...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "playlistCell", for: indexPath) as! playlistCell
...
let currentItem = playlists[indexPath.row]
...
cell.playlistCellImage.image = compositedCellImages[indexPath.row].0
}
}
return cell
}
...
func playlistListImage(inputPlaylistID:MPMediaEntityPersistentID) -> (UIImage,UInt64) {
var playlistData:[MPMediaItem] = []
var pickedArtwork:[UIImage] = []
var shuffledIndexes:[Int] = []
let playlistDetailImage:collageImageView = collageImageView()
playlistDetailImage.frame = CGRect(x: 0, y: 0, width: 128, height: 128)
let playlistDataPredicate = MPMediaPropertyPredicate(value: NSNumber(value: inputPlaylistID as UInt64), forProperty: MPMediaPlaylistPropertyPersistentID, comparisonType:MPMediaPredicateComparison.equalTo)
let playlistDataQuery = MPMediaQuery.playlists()
let cloudFilter:MPMediaPropertyPredicate = MPMediaPropertyPredicate(value: false, forProperty: MPMediaItemPropertyIsCloudItem, comparisonType: MPMediaPredicateComparison.equalTo)
playlistDataQuery.addFilterPredicate(cloudFilter)
playlistDataQuery.addFilterPredicate(playlistDataPredicate)
playlistData = playlistDataQuery.items!
playlistData = playlistData.filter{[=10=].mediaType == MPMediaType.music}
for (index,_) in playlistData.enumerated() {
shuffledIndexes.append(index)
}
shuffledIndexes.shuffleInPlace()
for (_,element) in shuffledIndexes.enumerated() {
if playlistData[element].artwork != nil {
pickedArtwork.append(playlistData[element].artwork!.image(at: CGSize(width: 64, height: 64))!)
}
if pickedArtwork.count == 4 { break }
}
while pickedArtwork.count < 4 {
if pickedArtwork.count == 0 {
pickedArtwork.append(UIImage(named: "missing")!)
} else {
pickedArtwork.shuffleInPlace()
pickedArtwork.append(pickedArtwork[0])
}
}
pickedArtwork.shuffleInPlace()
playlistDetailImage.drawInContext(pickedArtwork, matrixSize: 2)
return ((playlistDetailImage.image)!,inputPlaylistID)
}
...
}
...
class collageImageView: UIImageView {
var inputImages:[UIImage] = []
var rows:Int = 1
var cols:Int = 1
func drawInContext(_ imageSet: [UIImage], matrixSize: Int) {
let frameLeg:Int = Int(self.frame.width/CGFloat(matrixSize))
var increment:Int = 0
UIGraphicsBeginImageContextWithOptions(self.frame.size, false, UIScreen.main.scale)
self.image?.draw(in: self.frame)
for col in 1...matrixSize {
for row in 1...matrixSize {
imageSet[increment].draw(in: CGRect(x: (row - 1) * frameLeg, y: (col-1) * frameLeg, width: frameLeg, height: frameLeg))
increment += 1
}
}
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
一种方法是修改原来的方法 1,但添加延迟加载和缓存。
有关延迟加载的示例,请参阅 https://developer.apple.com/library/content/samplecode/LazyTableImages/Introduction/Intro.html
基本思想是您仅在滚动完成时才尝试计算图像(在上面的示例中,他们从 URL 加载图像,但您会用计算替换它)。
此外,您可以缓存每一行的计算结果,这样当用户来回滚动时,您可以先检查缓存的值。还要清除 didReceiveMemoryWarning 中的缓存。
所以在 tableView(_: UITableView, cellForRowAt: IndexPath)
if <cache contains image for row> {
cell.playlistCellImage.image = <cached image>
} else if tableView.isDragging && !tableView.isDecelerating {
let image = <calculate image for row>
<add image to cache>
cell.playlistCellImage.image = image
} else {
cell.playlistCellImage.image = <placeholder image>
}
然后覆盖滚动视图的委托方法
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
loadImagesForOnScreenRows()
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
loadImagesForOnScreenRows()
}
并实施 loadImagesForOnScreenRows()
for path in tableView.indexPathsForVisibleRows {
let image = <calculate image for row>
<add image to cache>
if let cell = tableView.cellForRow(at: path) {
cell.playlistCellImage.image = image
}
}
最后一个优化是将实际计算推送到后台线程,但您应该会发现上面的延迟加载可能就足够了。
更新 - 在后台线程上计算。
总体思路是 运行 在后台队列上使用 DispatchQueue 进行计算,然后当结果准备好时,在 UI 线程上更新显示。
类似于以下(未经测试的)代码
func loadImageInBackground(inputPlaylistID:MPMediaEntityPersistentID, completion:@escaping (UIImage, UInt64))
{
let backgroundQueue = DispatchQueue.global(dos: DispatchQoS.QoSClass.background)
backgroundQueue.async {
let (image,n) = playlistListImage(inputPlaylistID:inputPlaylistID)
DispatchQueue.main.async {
completion(image,n)
}
}
}
并且在tableView(_: UITableView, cellForRowAt: IndexPath)中不直接计算图片,调用后台方法:
if <cache contains image for row> {
cell.playlistCellImage.image = <cached image>
} else if tableView.isDragging && !tableView.isDecelerating {
cell.playlistCellImage.image = <placeholder image>
loadImageInBackground(...) {
(image, n) in
if let cell = tableView.cellForRow(at:indexPath) {
cell.playlistCellImage.image = image
}
<add image to cache>
}
} else {
cell.playlistCellImage.image = <placeholder image>
}
和 loadImagesForOnScreenRows() 中的类似更新。
请注意在回调处理程序中再次检索单元格的额外代码。由于此更新可以异步发生,因此很有可能会重复使用原始单元格,因此您需要确保更新的是正确的单元格