从后台线程添加到 UITableView 中的 UIStackView

Posted

技术标签:

【中文标题】从后台线程添加到 UITableView 中的 UIStackView【英文标题】:Adding to UIStackView in a UITableView from the background thread 【发布时间】:2019-02-07 13:06:30 【问题描述】:

我有一个UITableViewCell,里面有一个水平的UIStackView。创建 UIViews 并将其添加到堆栈视图会导致一些性能问题。在表格视图的滚动中有一个明确的blip。我已经在仪器中对其进行了分析,更新堆栈视图的调用是整个应用程序中第二重的调用。

设置如下。我正在使用 MVP,并且在我的单元演示者中,我在设置单元时调用 updateStackView(with items: [Any]),并在 cellForRowAtIndexPath 方法中设置单元。

我原来的updateStackView(with items: [Any])如下:

func updateStackView(_ items: [Any]) 
    items.forEach  item in
        if let customViewModel = item as? CustomViewModel 
            myStackView.addArrangedSubview(CustomView(customViewModel))
         else if item is Spacing 
            myStackView.addArrangedSubview(PipeSpacerView())
        
    

所以这是瓶颈,我决定将一些东西移到后台,如下所示:

func updateStackView(_ items: [Any]) 
    DispatchQueue.global(qos: .background).async 
        items.forEach  item in
            if let customViewModel = item as? CustomViewModel 
                DispatchQueue.main.async 
                    self.myStackView.addArrangedSubview(CustomView(customViewModel))
                
             else if item is Spacing 
                DispatchQueue.main.async 
                    self.myStackView.addArrangedSubview(PipeSpacerView())
                
            
        
    

有点难看,因为它有一些深层嵌套,但对性能产生了很大的影响。

我对此很满意,直到我向下滚动到表格视图的底部并开始向上滚动。我注意到当单元格被滚动到视图中时,一些CustomViews 在堆栈视图中出现了两次。一旦单元格完全显示在视图中,堆栈视图大部分时间会自行纠正回正确的数字。

在我的prepareForReuse 中,我专门调用了以下函数:

func resetDescriptionStackView() 
    descriptionStackView.arrangedSubviews.forEach  $0.removeFromSuperview() 

以前一直工作正常的是它都在主线程上。

非常感谢任何帮助。我很确定这是一个线程问题,因为它只是在我将东西移到后台时才开始抬头。

【问题讨论】:

为什么要使用堆栈视图?为什么不将其内容放在单独的单元格中? @mag_zbc 抱歉,我不清楚。这是一个水平堆栈视图。我会更新问题 常见的方法是在每个表格单元格内使用水平集合视图。 我会尝试不在主线程上执行此操作,主线程应该只用于 UI 更新。尝试创建一个全局的调度队列。 medium.com/@abhimuralidharan/… 【参考方案1】:

首先 .background 服务质量主要用于大/硬后台工作(例如发送统计数据,与服务器同步数据库等),但是当您想在与主队列不同的队列上做一些快速工作时然后以正确的方式使用 .userInitiated 或 .utility 更新 UI。另外,我建议您将自己的 OperationQueue 与后台 Qos 之一一起使用。 为了使代码更好,您可以使用“defer”语句(documentation)并将代码分离为小函数。

如果您想添加子视图,您可以为您的 CustomView 和 Spacing 添加特殊 id,然后检查它。 完整代码:

//For each our CustomView and Spacing we set uniq id to tag property in init of each class.

        /// Operation queue that work with our UIStackView
        fileprivate lazy var stackViewOperationQueue: OperationQueue = 

            let queue = OperationQueue()
            queue.name = "StackView OperationQueue"
            queue.qualityOfService = .userInitiated
            return queue
        ()

        func updateStackView(_ items: [Any]) 

            stackViewOperationQueue.addOperation  [weak self] in
                items.forEach  self?.addArrangedSubviewToStackView(by: $0) 
            
        

       fileprivate func addArrangedSubviewToStackView(by item: Any) 

            var handler: () -> () = 

            defer 
                DispatchQueue.main.async 
                    handler()
                
            

            if let customViewModel = item as? CustomViewModel 
                handler =  [weak self] in
                  let viewToAdd = CustomView(customViewModel)
                  self?.checkAndAddSubviewIfNeeded(viewToAdd)
            
            else if item is Spacing 
                handler =  [weak self] in
                  self?.checkAndAddSubviewIfNeeded(PipeSpacerView()) 
            
        

       func checkAndAddSubviewIfNeeded(_ subviewToAdd: UIView) 

           if !myStackView.arrangedSubviews.contains(where:  view in return view.tag == subviewToAdd.tag ) 
               self.myStackView.addArrangedSubview(subviewToAdd)
           
       

希望对您有所帮助!

【讨论】:

【参考方案2】:

您遇到的问题是由于表格视图重用了单元格。

发生的情况是,当您向下滚动当前可见单元格时,它们已经运行了它们的后台任务来创建视图。到他们完成时,当前可见单元格已更改为显示当前数据,但它们仍然是表格视图之前使用的相同单元格实例。所以现在两个闭包会将视图添加到同一个堆栈视图实例。 (单元格被重用前一个闭包,单元格被重用后另一个闭包)

您可以为每个单元格创建一个 UUID 并将其存储在单元格中,在后台任务关闭中捕获它并测试是否相等。如果不是,则任务的结果不相关,因为单元格已更改为显示另一个可见行。

例如:

class CustomViewCell: UITableViewCell

    private var cellId: String = ""

    // call this method for each cell in cellForItemAt implementation
    func configure(_ items: [Any])
    
        cellId = UUID() // generates a randomized string

        updateStackView(items)      
    

    private func updateStackView(_ items: [Any]) 
        let curretCellId = cellId
        DispatchQueue.global(qos: .background).async 
            items.forEach  item in
                if let customViewModel = item as? CustomViewModel 
                    DispatchQueue.main.async 
                        if (self.cellId == currentCellId)
                        
                            self.myStackView.addArrangedSubview(CustomView(customViewModel))
                        
                    
                 else if item is Spacing 
                    DispatchQueue.main.async 
                        if (self.cellId == currentCellId)
                        
                            self.myStackView.addArrangedSubview(PipeSpacerView())
                        
                    
                
            
        
    

【讨论】:

有没有机会给个小代码sn-p描述?

以上是关于从后台线程添加到 UITableView 中的 UIStackView的主要内容,如果未能解决你的问题,请参考以下文章

删除核心数据对象并保存在后台线程中

在添加到视图之前在后台线程中更新 UI 元素

无法从 iOS6 中的后台线程调用主线程上的代码

在 ui 线程中执行委托(使用消息泵)

试图了解 iOS 中后台线程行为的幕后情况

如何提高 UITableVIew 的性能?