滚动具有大量单元格(250,000 或更多)的两种方式滚动 UICollectionView 时出现可见滞后

Posted

技术标签:

【中文标题】滚动具有大量单元格(250,000 或更多)的两种方式滚动 UICollectionView 时出现可见滞后【英文标题】:Visible lag while scrolling two way scrolling UICollectionView with large number of cells (250,000 or more) 【发布时间】:2016-05-30 08:15:27 【问题描述】:

我将UICollectionViewFlowLayout 子类化,以便在UICollectionView 中进行两种滚动。滚动适用于较少数量的行和节数(100-200 行和节),但是当我将行数和节数增加到 500 以上时,滚动时会出现明显的滞后,即UICollectionView 中的单元格数为 250,000 或更多。我已经在layoutAttributesForElementsInRect 中追踪了延迟的来源。我正在使用Dictionary 来保存每个单元格的UICollectionViewLayoutAttributes,以避免重新计算它并循环遍历它以从layoutAttributesForElementsInRect 返回单元格的属性

import UIKit

class LuckGameCollectionViewLayout: UICollectionViewFlowLayout 

    // Used for calculating each cells CGRect on screen.
    // CGRect will define the Origin and Size of the cell.
    let CELL_HEIGHT = 70.0
    let CELL_WIDTH = 70.0

    // Dictionary to hold the UICollectionViewLayoutAttributes for
    // each cell. The layout attribtues will define the cell's size
    // and position (x, y, and z index). I have found this process
    // to be one of the heavier parts of the layout. I recommend
    // holding onto this data after it has been calculated in either
    // a dictionary or data store of some kind for a smooth performance.
    var cellAttrsDictionary = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
    // Defines the size of the area the user can move around in
    // within the collection view.
    var contentSize = CGSize.zero

    override func collectionViewContentSize() -> CGSize 
        return self.contentSize
    

    override func prepareLayout() 

        // Cycle through each section of the data source.
        if collectionView?.numberOfSections() > 0 
            for section in 0...collectionView!.numberOfSections()-1 

                // Cycle through each item in the section.
                if collectionView?.numberOfItemsInSection(section) > 0 
                    for item in 0...collectionView!.numberOfItemsInSection(section)-1 

                        // Build the UICollectionVieLayoutAttributes for the cell.
                        let cellIndex = NSIndexPath(forItem: item, inSection: section)
                        let xPos = Double(item) * CELL_WIDTH
                        let yPos = Double(section) * CELL_HEIGHT

                        let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: cellIndex)
                        cellAttributes.frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT)

                        // Save the attributes.
                        cellAttrsDictionary[cellIndex] = cellAttributes
                    
                

            
        

        // Update content size.
        let contentWidth = Double(collectionView!.numberOfItemsInSection(0)) * CELL_WIDTH
        let contentHeight = Double(collectionView!.numberOfSections()) * CELL_HEIGHT
        self.contentSize = CGSize(width: contentWidth, height: contentHeight)

    

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? 
        // Create an array to hold all elements found in our current view.
        var attributesInRect = [UICollectionViewLayoutAttributes]()

        // Check each element to see if it should be returned.
        for (_,cellAttributes) in cellAttrsDictionary 
            if CGRectIntersectsRect(rect, cellAttributes.frame) 
                attributesInRect.append(cellAttributes)
            
        

        // Return list of elements.
        return attributesInRect
    

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? 
        return cellAttrsDictionary[indexPath]!
    

    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool 
        return false
    

编辑: 以下是我在layoutAttributesForElementsInRect 方法中提出的更改。

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?         
    // Create an array to hold all elements found in our current view.
    var attributesInRect = [UICollectionViewLayoutAttributes]()

    let xOffSet = self.collectionView?.contentOffset.x
    let yOffSet = self.collectionView?.contentOffset.y
    let totalColumnCount = self.collectionView?.numberOfSections()
    let totalRowCount = self.collectionView?.numberOfItemsInSection(0)

    let startRow = Int(Double(xOffSet!)/CELL_WIDTH) - 10    //include 10 rows towards left
    let endRow = Int(Double(xOffSet!)/CELL_WIDTH + Double(Utils.getScreenWidth())/CELL_WIDTH) + 10 //include 10 rows towards right
    let startCol = Int(Double(yOffSet!)/CELL_HEIGHT) - 10 //include 10 rows towards top
    let endCol = Int(Double(yOffSet!)/CELL_HEIGHT + Double(Utils.getScreenHeight())/CELL_HEIGHT) + 10 //include 10 rows towards bottom

    for(var i = startRow ; i <= endRow; i = i + 1)
        for (var j = startCol ; j <= endCol; j = j + 1)
            if (i < 0 || i > (totalRowCount! - 1) || j < 0 || j > (totalColumnCount! - 1))
                continue
            

            let indexPath: NSIndexPath = NSIndexPath(forRow: i, inSection: j)
            attributesInRect.append(cellAttrsDictionary[indexPath]!)
        
    

    // Return list of elements.
    return attributesInRect

我已经计算了 collectionView 的偏移量,并用它来计算将在屏幕上可见的单元格(使用每个单元格的高度/宽度)。我必须在每一侧添加额外的单元格,以便在用户滚动时不会丢失单元格。我对此进行了测试,性能很好。

【问题讨论】:

显然遍历字典是昂贵的部分。我想知道是否有某种方法可以键入您的字典,这样您就可以获得与 rect 相交的所有内容,而无需遍历 250k 值。 @KevinDiTraglia 我也想过这个问题,但问题是layoutAttributesForElementsInRect 不一定只返回可见元素。来自文档 (developer.apple.com/library/ios/documentation/WindowsViews/…) Based on the current scroll position, the collection view then calls your layoutAttributesForElementsInRect: method to ask for the attributes of the cells and views in a specific rectangle, which may or may not be the same as the visible rectangle. 【参考方案1】:

通过利用已知单元格大小的layoutAttributesForElementsInRect(rect: CGRect),您无需缓存属性,只需在collectionView 请求时为给定的rect 计算它们。您仍然需要检查 0 的边界情况和最大部分/行数,以避免计算不需要或无效的属性,但这可以在循环周围的 where 子句中轻松完成。这是一个我用 1000 个部分 x 1000 行测试的工作示例,它工作得很好,不会在设备上滞后:

编辑:我添加了更大的矩形,以便可以在滚动到达之前预先计算属性。从您的编辑看来,您仍在缓存我认为性能不需要的属性。此外,随着滚动次数的增加,它会导致更大的内存占用。还有一个原因是您不想使用回调中提供的CGRect 而不是从 contentOffset 手动计算一个?

class LuckGameCollectionViewLayout: UICollectionViewFlowLayout 

let CELL_HEIGHT = 50.0
let CELL_WIDTH = 50.0

override func collectionViewContentSize() -> CGSize 
    let contentWidth = Double(collectionView!.numberOfItemsInSection(0)) * CELL_WIDTH
    let contentHeight = Double(collectionView!.numberOfSections()) * CELL_HEIGHT
    return CGSize(width: contentWidth, height: contentHeight)


override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? 
    let biggerRect = rect.insetBy(dx: -2048, dy: -2048)
    let startIndexY = Int(Double(biggerRect.origin.y) / CELL_HEIGHT)
    let startIndexX = Int(Double(biggerRect.origin.x) / CELL_WIDTH)
    let numberOfVisibleCellsInRectY = Int(Double(biggerRect.height) / CELL_HEIGHT) + startIndexY
    let numberOfVisibleCellsInRectX = Int(Double(biggerRect.width) / CELL_WIDTH) + startIndexX
    var attributes: [UICollectionViewLayoutAttributes] = []

    for section in startIndexY..<numberOfVisibleCellsInRectY
        where section >= 0 && section < self.collectionView!.numberOfSections() 
            for item in startIndexX..<numberOfVisibleCellsInRectX
                where item >= 0 && item < self.collectionView!.numberOfItemsInSection(section) 
                    let cellIndex = NSIndexPath(forItem: item, inSection: section)
                    if let attrs = self.layoutAttributesForItemAtIndexPath(cellIndex) 
                        attributes.append(attrs)
                    
            
    
    return attributes


override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? 
    let xPos = Double(indexPath.row) * CELL_WIDTH
    let yPos = Double(indexPath.section) * CELL_HEIGHT
    let cellAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
    cellAttributes.frame = CGRect(x: xPos, y: yPos, width: CELL_WIDTH, height: CELL_HEIGHT)
    return cellAttributes


【讨论】:

您能否检查我的更新以及我提出的解决方案以及对此的任何反馈。 关于让它适用于不同单元格宽度的任何建议?谢谢! @Unikorn 如果您的意思是为每个单元格设置不同的单元格宽度,那肯定会更难。您可以像上面问题中的原始示例一样将属性保存在 Dictionary 中,但这会增加内存占用,或者存储每个部分(在本例中为行)的 x 偏移量,当用户向右拖动时会增加或向左拖动时递减,并根据可见单元格的总宽度加上单元格间距计算。我敢肯定这方面会有问题需要进一步探索,但这可能是一个开始。【参考方案2】:

您需要为您的单元格提出一个按维度排序的数据结构,以便提出一些使用这些维度来缩小搜索范围的算法。

让我们以表格为例,其中单元格高 100 像素(按视图的全宽)和 250_000 个元素,要求与 0, top, 320, bottom 相交的单元格。那么你的数据结构将是一个按顶坐标排序的数组,伴随的算法就像

let start: Int = top / 100
let end: Int = bottom / 100 + 1
return (start...end).map  cellAttributes[$0] 

根据实际布局的需要添加尽可能多的复杂性。

【讨论】:

【参考方案3】:

cellAttrsDictionary 不适合存储UICollectionViewLayoutAttributes。它由NSIndexPath 索引,但在layoutAttributesForElementsInRect 中,您正在搜索一个区域。如果您需要 cellAttrsDictionary 来做其他事情,那么您可以保留它,但将每个 cellAttribute additionally 存储在另一个搜索速度更快的数据结构中。

例如嵌套数组:

var allCellAttributes = [[UICollectionViewLayoutAttributes]]()

第一级数组描述了一个区域,假设它是 1000 像素高和全屏宽度的块。所以所有与矩形 0, 0, screenwidth, 1000 相交的 cellAttributes 都进入:

allCellAttributes[0].append(cellAttributes)

所有与矩形 0, 1000, screenwidth, 2000 相交的 cellAttributes 进入:

allCellAttributes[1].append(cellAttributes)

等等……

然后,在layoutAttributesForElementsInRect 中,您可以根据给定的 CGRect 直接跳转到数组中来搜索此数据结构。如果矩形是例如0, 5500, 100, 6700 那么你只需要在这个范围内搜索:

allCellAttributes[5]
allCellAttributes[6]

这应该给你基本的想法,我希望你明白我的意思。

【讨论】:

以上是关于滚动具有大量单元格(250,000 或更多)的两种方式滚动 UICollectionView 时出现可见滞后的主要内容,如果未能解决你的问题,请参考以下文章

滚动或重新加载后单元格高度增加

当我向下滚动时,为什么我看到更多单元格而不是numberOfRowsInSection返回值?

带有 bing 数字和百分比单元格的奇怪结果

UITableView 滚动非常大的单元格

限制tableView中显示的单元格数量,滚动到最后一个单元格时加载更多单元格

在具有不同单元格高度的列表视图中滚动