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?
UI Stuttering:
- Your custom
LoadOperationlikely isn't properly implementing asynchronous operation state management. OperationQueue relies onisExecuting/isFinishedflags 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.
- Your custom
Index Out-of-Bounds Crashes:
- Your
allItemscomputed 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 wheregetNumberOfRows()returns a count, but by the timegetCellContentaccesses the array, its length has changed.
- Your
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

