You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS OperationQueue并发异步操作致界面卡顿及数组越界问题求助

Hey there! Let's break down the two key issues you're facing—UI stuttering and UITableView index out-of-bounds crashes—and fix them step by step.

First, Why Are You Seeing These Problems?

  1. UI Stuttering:

    • Your custom LoadOperation likely isn't properly implementing asynchronous operation state management. OperationQueue relies on isExecuting/isFinished flags to track task completion, so if these aren't set correctly, the queue can't properly control concurrency. This leads to more concurrent requests than intended, hogging CPU resources and causing lag.
    • Recursively calling startSearching() in every completion block creates unnecessary overhead and can flood the queue with rapid-fire operation creation.
    • Frequent full reloadTable() calls force the UITableView to redraw everything every time, which is inefficient for dynamic data.
  2. Index Out-of-Bounds Crashes:

    • Your allItems computed property and array modification methods (addToPending, removeFromPending, etc.) are not thread-safe. Even though you use a serial queue in the completion block, direct calls to these methods outside the serial queue mean multiple threads can modify the arrays simultaneously. This causes race conditions where getNumberOfRows() returns a count, but by the time getCellContent accesses the array, its length has changed.

Step-by-Step Fixes

1. Fix Thread Safety for Shared Data

Update your Queue class to force all access/modification of shared arrays through the serial queue. No direct array access allowed outside this queue:

class Queue<Item: Equatable> {
    let loadOperationQueue = OperationQueue()
    let serialOperationQueue = OperationQueue()
    
    var maxNumberOfItems: Int = 1
    private var pendingItems: [Item] = []
    private var loadingItems: [Item] = []
    private var processedItems: [Item] = []
    
    init() {
        serialOperationQueue.maxConcurrentOperationCount = 1
        loadOperationQueue.maxConcurrentOperationCount = 3 // Set a reasonable limit (3-5 works well)
        loadOperationQueue.qualityOfService = .utility
        serialOperationQueue.qualityOfService = .utility
    }
    
    // Async get all items (returns on main thread)
    func getAllItems(completion: @escaping ([Item]) -> Void) {
        serialOperationQueue.addOperation {
            let allItems = self.processedItems + self.loadingItems + self.pendingItems
            OperationQueue.main.addOperation {
                completion(allItems)
            }
        }
    }
    
    func addToPending(_ item: Item, completion: (() -> Void)? = nil) {
        serialOperationQueue.addOperation {
            guard self.processedItems.count + self.loadingItems.count + self.pendingItems.count < self.maxNumberOfItems else {
                completion?()
                return
            }
            self.pendingItems.append(item)
            completion?()
        }
    }
    
    func removeFromPending(completion: @escaping (Item?) -> Void) {
        serialOperationQueue.addOperation {
            guard !self.pendingItems.isEmpty else {
                OperationQueue.main.addOperation {
                    completion(nil)
                }
                return
            }
            let item = self.pendingItems.removeFirst()
            self.loadingItems.append(item)
            OperationQueue.main.addOperation {
                completion(item)
            }
        }
    }
    
    func addToProcessed(_ item: Item, completion: (() -> Void)? = nil) {
        serialOperationQueue.addOperation {
            self.loadingItems = self.loadingItems.filter { $0 != item }
            self.processedItems.append(item)
            completion?()
        }
    }
    
    func isFull(completion: @escaping (Bool) -> Void) {
        serialOperationQueue.addOperation {
            let isFull = self.processedItems.count + self.loadingItems.count + self.pendingItems.count >= self.maxNumberOfItems
            OperationQueue.main.addOperation {
                completion(isFull)
            }
        }
    }
    
    func containsItem(_ item: Item, completion: @escaping (Bool) -> Void) {
        serialOperationQueue.addOperation {
            let allItems = self.processedItems + self.loadingItems + self.pendingItems
            let contains = allItems.contains(item)
            OperationQueue.main.addOperation {
                completion(contains)
            }
        }
    }
}

2. Correctly Implement Asynchronous LoadOperation

OperationQueue needs proper state tracking to manage concurrency. Update your custom operation:

class LoadOperation: Operation {
    private let webPage: WebPage
    private let searchString: String
    var output: WebPage?
    
    // Track operation state
    private var _isExecuting = false
    override var isExecuting: Bool {
        get { _isExecuting }
        set {
            willChangeValue(forKey: "isExecuting")
            _isExecuting = newValue
            didChangeValue(forKey: "isExecuting")
        }
    }
    
    private var _isFinished = false
    override var isFinished: Bool {
        get { _isFinished }
        set {
            willChangeValue(forKey: "isFinished")
            _isFinished = newValue
            didChangeValue(forKey: "isFinished")
        }
    }
    
    override var isAsynchronous: Bool { true }
    
    init(_ webPage: WebPage, searchString: String) {
        self.webPage = webPage
        self.searchString = searchString
        super.init()
    }
    
    override func start() {
        if isCancelled {
            isFinished = true
            return
        }
        isExecuting = true
        
        // Launch your network request
        guard let url = URL(string: webPage.url) else {
            finish()
            return
        }
        
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard let self = self, !self.isCancelled else {
                self?.finish()
                return
            }
            
            // Process response, validate, extract links
            var result = self.webPage
            result.status = .processed
            
            if let data = data, let html = String(data: data, encoding: .utf8) {
                result.containedLinks = self.extractLinks(from: html)
            }
            
            self.output = result
            self.finish()
        }.resume()
    }
    
    private func finish() {
        isExecuting = false
        isFinished = true
    }
    
    private func extractLinks(from html: String) -> [String] {
        // Add your link extraction logic here
        return []
    }
}

3. Rewrite startSearching to Avoid Recursion

Use async callbacks instead of recursion to feed new tasks to the queue, and only update the UI when necessary:

func startSearching() {
    queue.removeFromPending { [weak self] currentWebPage in
        guard let self = self, let webPage = currentWebPage else { return }
        
        let loadOperation = LoadOperation(webPage, searchString: self.searchString)
        loadOperation.completionBlock = { [weak self, weak loadOperation] in
            guard let self = self, let operation = loadOperation else { return }
            
            let resultWebPage = operation.output ?? webPage
            self.queue.addToProcessed(resultWebPage) {
                // Add new links to pending
                let group = DispatchGroup()
                for urlString in resultWebPage.containedLinks {
                    group.enter()
                    self.queue.isFull { isFull in
                        if isFull {
                            group.leave()
                            return
                        }
                        let newPage = WebPage(url: urlString, containedLinks: [], status: .pending)
                        self.queue.containsItem(newPage) { contains in
                            if !contains {
                                self.queue.addToPending(newPage) {
                                    group.leave()
                                }
                            } else {
                                group.leave()
                            }
                        }
                    }
                }
                
                // Update UI and continue searching after all new links are added
                group.notify(queue: .main) {
                    self.updateUITableView()
                    self.startSearching()
                }
            }
        }
        self.queue.loadOperationQueue.addOperation(loadOperation)
    }
}

private func updateUITableView() {
    queue.getAllItems { [weak self] items in
        self?.mainThreadItems = items // Maintain a main-thread-safe copy
        OperationQueue.main.addOperation {
            self?.viewDelegate?.reloadTable()
            // For better performance, replace full reload with targeted inserts/updates
        }
    }
}

4. Fix UITableView Index Out-of-Bounds

Maintain a main-thread-only copy of your data source in the Presenter to avoid race conditions:

class Presenter {
    private var mainThreadItems: [WebPage] = []
    private let queue: Queue<WebPage>
    
    // ...
    
    func getNumberOfRows() -> Int {
        return mainThreadItems.count
    }
    
    func getCellContent(at index: Int) -> (url: String, status: String) {
        guard index < mainThreadItems.count else {
            return (url: "", status: "Invalid")
        }
        let item = mainThreadItems[index]
        return (url: item.url, status: item.status.description)
    }
}

Bonus: Reduce UI Stuttering Further

Instead of calling reloadTable() every time, track changes to your data (e.g., new pending items, completed tasks) and use UITableView's insertRows(at:with:) or reloadRows(at:with:) methods. This minimizes the amount of UI redraw needed and makes scrolling smoother.

内容的提问来源于stack exchange,提问作者Alina Vas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:20:46