具有调整大小的大图像的 UITableView 单元格中的初始滚动滞后?

Posted

技术标签:

【中文标题】具有调整大小的大图像的 UITableView 单元格中的初始滚动滞后?【英文标题】:Initial scroll lag in UITableView cells with resized large images? 【发布时间】:2017-04-29 23:56:46 【问题描述】:

我基本上和这个问题有同样的问题:

Slow scroll on UITableView images

我的 UITableView 包含一个大小为 87x123 的 UIImageView

当我的UITableViewController 被加载时,它首先调用一个循环遍历图像数组的函数。这些图像是从照片库中存储的高分辨率图像。在每次迭代中,它检索图像并将每个图像的大小调整为 87x123,然后将其存储回数组中的原始图像。

当所有图像都调整大小并存储后,它会调用self.tableView.reloadData 将数组中的数据填充到单元格中。

但是,就像提到的问题一样,如果我在所有图像都调整大小并存储在数组中之前快速滚动,我的UITablView 会不稳定并且滞后。

这是有问题的代码:

extension UIImage

    func resizeImage(originalImage: UIImage, scaledTo size: CGSize) -> UIImage
    
        // Avoid redundant drawing
        if originalImage.size.equalTo(size)
        
            return originalImage
        

        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        originalImage.draw(in: CGRect(x: CGFloat(0.0), y: CGFloat(0.0), width: CGFloat(size.width), height: CGFloat(size.height)))

        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()

        return image
    


func loadImages()
   
    DispatchQueue.global(qos: .background).async 

        for index in 0..<self.myArray.count
        
            if let image = self.myArray[index].image
            
                self.myArray[index].image = image.resizeImage(originalImage: image, scaledTo: CGSize(width: 87, height: 123) )
            

            if index == self.myArray.count - 1
            
                print("FINISHED RESIZING ALL IMAGES")
            
        
    

    tableView.reloadData()


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

    ...

    // Size is 87x123
    let thumbnailImage = cell.viewWithTag(1) as! UIImageView

    DispatchQueue.main.async

        thumbnailImage.image = self.myArray[indexPath.row]

    

    thumbnailImage.contentMode = UIViewContentMode.scaleAspectFill

    thumbnailImage.layer.borderColor = UIColor.black.cgColor
    thumbnailImage.layer.borderWidth = 1.0
    thumbnailImage.clipsToBounds = true

    return cell

我知道在后台线程中执行任何非 UI 操作,这就是我所做的。然后我在cellForRowAt 中所做的就是使用其indexPath.row 将图像加载到单元格中。

问题是,如前所述,如果我开始滚动 UITableView BEFORE FINISHED RESIZING ALL IMAGES 打印出来,即在所有图像调整大小之前,会出现明显的滞后和缓慢.

但是,如果我等到所有图像都已调整大小并在滚动 UITableView 之前调用 FINISHED RESIZING ALL IMAGES,则滚动很顺畅,没有任何延迟。

我可以放置一个加载指示器,让用户等到所有图像都已调整大小并加载到单元格中,然后再进行用户交互,但这会很烦人,因为调整所有高分辨率图像的大小大约需要 8 秒(18 张图片可调整大小)。

有没有更好的方法可以解决这个延迟问题?

更新:在@iWheelBuy 的第二个示例之后,我实现了以下内容:

final class ResizeOperation: Operation 

    private(set) var image: UIImage
    let index: Int
    let size: CGSize

    init(image: UIImage, index: Int, size: CGSize) 
        self.image = image
        self.index = index
        self.size = size
        super.init()
    

    override func main() 
        image = image.resizeImage(originalImage: image, scaledTo: size)
    


class MyTableViewController: UITableViewController

    ...

    lazy var resizeQueue: OperationQueue = self.getQueue()

    var myArray: [Information] = []

    internal struct Information
    
        var title: String?
        var image: UIImage?

        init()
        

        
    

    override func viewDidLoad()
    
        ....

        loadImages()

        ...
    

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    
        ...

        // Size is 87x123
        let thumbnailImage = cell.viewWithTag(1) as! UIImageView

        DispatchQueue.main.async

            thumbnailImage.image = self.myArray[indexPath.row].image

        

        thumbnailImage.contentMode = UIViewContentMode.scaleAspectFill

        return cell
    

    func loadImages()
    
        let size = CGSize(width: 87, height: 123)

        for item in myArray
        
            let operations = self.myArray.enumerated().map( ResizeOperation(image: item.image!, index: $0.offset, size: size) )

            operations.forEach  [weak queue = resizeQueue, weak controller = self] (operation) in
                operation.completionBlock =  [operation] in
                    DispatchQueue.main.async  [image = operation.image, index = operation.index] in

                        self.update(image: image, index: index)
                    
                
                queue?.addOperation(operation)
            
        
    

    func update(image: UIImage, index: Int)
    
        myArray[index].image = image

        tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: UITableViewRowAnimation.fade)

        

但是,在调用 tableView.reloadRows 时,我收到了一个错误提示:

尝试从第 0 节中删除第 0 行,但只有 0 节 更新前

我对它的含义以及如何解决它有点困惑。

【问题讨论】:

【参考方案1】:

很难确定滞后的原因。但是有些想法可能会帮助您提高代码的性能。

尝试在开始时使用一些小图像,看看是否是原始图像的大小会影响性能不佳。还可以尝试隐藏这些行,看看是否有任何变化:

//    thumbnailImage.contentMode = UIViewContentMode.scaleAspectFill
//    thumbnailImage.layer.borderColor = UIColor.black.cgColor
//    thumbnailImage.layer.borderWidth = 1.0
//    thumbnailImage.clipsToBounds = true

您的代码有点老派,通过将mapforEach 应用于您的图像数组,只需几行代码,loadImages 中的for 循环就变得更具可读性。

另外,关于您的图像数组。您从主线程读取它并从后台线程对其进行修改。你同时做。我建议只在背景上调整图像大小...除非您确定不会有不良后果

查看下面的代码示例 #1 您当前的代码可能是什么样子。

另一方面,你可以走其他路。例如,您可以在开始时设置一些占位符图像,并在某些特定单元格的图像准备好时更新单元格。不是一次所有的图像!如果您使用一些串行队列,您将每 0.5 秒获得一次图像更新,并且 UI 更新会很好。

检查代码示例 #2。它没有经过测试,只是为了展示你可以走的路。

顺便说一句,您是否尝试将 QualityOfService 从 background 更改为 userInitiated?它可能会减少调整大小的时间......或者不会(:

DispatchQueue.global(qos: .userInitiated).async 
    // code

示例 #1

extension UIImage 

    func resize(to size: CGSize) -> UIImage 
        guard self.size.equalTo(size) else  return self 
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        draw(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return image
    

    static func resize(images: [UIImage], size: CGSize, completion: @escaping ([UIImage]) -> Void) 
        DispatchQueue.global(qos: .background).async 
            let newArray = images.map( $0.resize(to: size) )
            DispatchQueue.main.async 
                completion(newArray)
            
        
    


final class YourController: UITableViewController 

    var myArray: [UIImage] = []

    override func viewDidLoad() 
        super.viewDidLoad()
        self.loadImages()
    


fileprivate extension YourController 

    func loadImages() 
        UIImage.resize(images: myArray, size: CGSize(width: 87, height: 123))  [weak controller = self] (newArray) in
            guard let controller = controller else  return 
            controller.myArray = newArray
            controller.tableView.reloadData()
        
    

示例 #2

final class ResizeOperation: Operation 

    private(set) var image: UIImage
    let index: Int
    let size: CGSize

    init(image: UIImage, index: Int, size: CGSize) 
        self.image = image
        self.index = index
        self.size = size
        super.init()
    

    override func main() 
        image = image.resize(to: size)
    


final class YourController: UITableViewController 

    var myArray: [UIImage] = []
    lazy var resizeQueue: OperationQueue = self.getQueue()

    override func viewDidLoad() 
        super.viewDidLoad()
        self.loadImages()
    

    private func getQueue() -> OperationQueue 
        let queue = OperationQueue()
        queue.qualityOfService = .background
        queue.maxConcurrentOperationCount = 1
        return queue
    


fileprivate extension YourController 

    func loadImages() 
        let size = CGSize(width: 87, height: 123)
        let operations = myArray.enumerated().map( ResizeOperation(image: $0.element, index: $0.offset, size: size) )
        operations.forEach  [weak queue = resizeQueue, weak controller = self] (operation) in
            operation.completionBlock =  [operation] in
                DispatchQueue.main.async  [image = operation.image, index = operation.index] in
                    controller?.update(image: image, index: index)
                
            
            queue?.addOperation(operation)
        
    

    func update(image: UIImage, index: Int) 
        myArray[index] = image
        tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: UITableViewRowAnimation.fade)
    

【讨论】:

感谢您的建议...userInitiated 确实将时间缩短了一半...我会尝试其他建议! @Pangu 太棒了!顺便说一句,我在示例中使用了很多捕获列表。如果您不熟悉它们,那么您绝对应该阅读它们 我喜欢你的第二个建议,即拥有一个占位符图像并在图像准备好时更新单元格......目前我设置它的方式是在情节提要中,我已经有了单元格的占位符图像,然后使用thumbnailImage.image = self.myArray[indexPath.row] 代码更新单元格的图像,但如前所述,当我在出现UITableViewController 时快速滚动时会出现明显的滞后......我正在尝试实施您的第二个建议,但在跟进时遇到了麻烦:( 也许这个:goo .gl/OGNmss 另外,AsyncDisplayKit 是为了消除 UI 滞后而开发的:youtu .be/wrctPJxskhI 无论如何,这是一个非常复杂的问题。即使是最流行的应用程序也经常在表格滚动时遇到延迟。 第一个链接正是我想要实现的!..这与 eBay 应用程序在您进一步向下滚动列表时异步加载列表中的图像的方式非常相似,没有任何滞后。 ..谢谢,我会调查NSOperation and NSOperationQueue

以上是关于具有调整大小的大图像的 UITableView 单元格中的初始滚动滞后?的主要内容,如果未能解决你的问题,请参考以下文章

枕头 - 调整 GIF 的大小

具有自定义 UINavigationBar 大小的 UITableView“自动调整大小”

调整 Shiny RMarkdown 报告中的大图大小

具有自调整大小 insertRowsAtIndexPaths 的 UITableView 更改 contentOffset

UITableView:如何知道 TableView 的所有具有动态单元格高度的单元格何时被调整大小?

如何自动调整图像大小以适合“flex-box”容器?