免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

iOS并发编程进阶:NSOperation与NSOperationQueue实战指南

iOS并发编程进阶:NSOperation与NSOperationQueue实战指南 1. 从 GCD 到 NSOperation为什么我们需要更“高级”的队列管理如果你在 iOS 开发中处理过并发任务那么DispatchQueueGCD 一定是你最先接触的工具。它简单、高效一个DispatchQueue.global().async就能轻松把任务丢到后台线程。但当你面对更复杂的场景时比如需要取消一个正在排队的下载任务、限制同时进行的网络请求数量或者让几个任务按照特定的依赖关系顺序执行时单纯使用 GCD 就会显得有些力不从心。这时NSOperation和NSOperationQueue的价值就凸显出来了。简单来说NSOperation和NSOperationQueue是构建在 GCD 之上的一个更高层次的抽象。它们把“任务”这个概念对象化了。在 GCD 的世界里任务是一个闭包Block它一旦被提交其生命周期和状态管理就变得相对隐晦。而NSOperation则是一个代表单个计算单元的类它封装了任务代码和与之相关的数据并且最重要的是它拥有明确的生命周期状态isReady、isExecuting、isFinished、isCancelled。这种状态机模型正是实现任务取消、依赖和优先级等高级特性的基石。NSOperationQueue则相当于一个智能的任务调度器。它管理着一个或多个NSOperation对象的执行。你可以设置队列的最大并发操作数maxConcurrentOperationCount来轻松实现“线程池”的效果避免系统资源被耗尽你可以通过添加操作之间的依赖addDependency来构建复杂的执行流程图所有操作的状态变化开始、结束、取消都有清晰的 KVO 通知便于监控。所以当你需要以下功能时就该考虑使用NSOperation和NSOperationQueue了可取消的任务用户离开了当前界面需要取消所有未完成的图片加载。任务依赖任务 B 必须在任务 A 成功完成后才能开始例如先登录再获取用户数据。限制并发数控制同时发起的网络请求不超过 3 个避免服务器压力过大或客户端资源竞争。监听任务状态需要精确知道每个后台任务的开始、结束或失败并更新 UI。封装可复用的任务单元将某个特定业务逻辑如数据预处理、文件上传封装成一个独立的NSOperation子类方便在不同地方复用和组合。接下来我们就深入这个强大组合的内部看看如何从基础使用到高级定制彻底掌握它们。2. NSOperation 核心状态、子类化与取消机制NSOperation是一个抽象基类你不能直接使用它而是需要继承它或者使用系统提供的两个具体子类NSInvocationOperation在 Swift 中已不推荐使用和BlockOperation。理解它的核心在于理解其状态机和start、main方法。2.1 状态机与 KVO 合规性一个NSOperation对象在其生命周期中会经历一系列状态这些状态通过几个布尔属性来体现并且这些属性的变化是 KVO 合规的。这是整个框架设计的精髓。isReady: 操作是否已经准备好被执行。当所有依赖它的操作都已完成且它自身满足执行条件时此属性为true。队列会等待操作变为ready状态才考虑执行它。isExecuting: 操作是否正在执行其任务。当你重写start方法手动管理执行时必须在开始执行前将此属性设为true并在结束时设为false。isFinished: 操作是否已经完成执行。无论是成功完成还是被取消最终都必须将此属性设为true。这是最关键的一点一个未完成isFinishedfalse的操作会一直留在队列中导致队列无法释放或继续执行其他操作。isCancelled: 操作是否已被取消。这是一个只读信号调用cancel()方法会将其设置为true。你的任务代码应该定期检查这个属性以便在可能的时候提前退出。在自定义子类中当你手动管理这些状态时必须在改变它们的前后发送相应的 KVO 通知。例如class MyOperation: Operation { private var _executing false private var _finished false override var isExecuting: Bool { return _executing } override var isFinished: Bool { return _finished } override func start() { // 1. 在开始前检查是否已取消 guard !isCancelled else { // 如果已取消仍需标记为完成 willChangeValue(forKey: isFinished) _finished true didChangeValue(forKey: isFinished) return } // 2. 标记为正在执行 willChangeValue(forKey: isExecuting) _executing true didChangeValue(forKey: isExecuting) // 3. 执行主任务 main() } override func main() { // 你的任务逻辑 // 必须频繁检查 isCancelled for item in largeArray { if isCancelled { break } // 响应取消 process(item) } // 4. 任务结束更新状态 completeOperation() } private func completeOperation() { willChangeValue(forKey: isExecuting) willChangeValue(forKey: isFinished) _executing false _finished true didChangeValue(forKey: isFinished) didChangeValue(forKey: isExecuting) } }注意对于绝大多数情况你不需要重写start方法。直接继承Operation并重写main方法是更简单和安全的选择。系统会自动处理状态转换。只有当你需要完全控制操作的执行环境例如确保操作在特定线程启动时才需要重写start。2.2 使用 BlockOperation 快速创建任务对于简单的任务使用BlockOperation是最高效的方式。它可以包含一个或多个执行闭包。// 创建包含单个闭包的操作 let blockOp BlockOperation { print(这是在后台线程执行的任务: \(Thread.current)) } // 创建包含多个闭包的操作这些闭包会并发执行但仍在同一个操作的生命周期内 let multiBlockOp BlockOperation() multiBlockOp.addExecutionBlock { print(任务块 1: \(Thread.current)) } multiBlockOp.addExecutionBlock { print(任务块 2: \(Thread.current)) } multiBlockOp.addExecutionBlock { print(任务块 3: \(Thread.current)) } // 设置完成闭包 multiBlockOp.completionBlock { print(所有 executionBlock 都执行完毕了) }BlockOperation的addExecutionBlock添加的多个闭包默认会并发执行可能在不同的线程但它们同属一个操作。只有当所有添加的executionBlock都执行完毕后该操作才会被标记为isFinished继而触发completionBlock。这一点在理解任务完成时机时很重要。2.3 实现可响应的取消Cancellation“取消”不是强制的。调用operation.cancel()只是将操作的isCancelled属性标记为true并不会强制终止正在执行的代码。操作本身必须在main方法中定期检查这个标志并主动退出。class DownloadOperation: Operation { let url: URL private var task: URLSessionDataTask? init(url: URL) { self.url url } override func main() { // 在开始任何昂贵操作前检查 if isCancelled { return } // 模拟一个长时间运行的任务比如网络请求 let semaphore DispatchSemaphore(value: 0) var cancelled false // 在实际项目中这里会是 URLSessionDataTask // 为了演示我们用 DispatchQueue 模拟 DispatchQueue.global().asyncAfter(deadline: .now() 2) { [weak self] in guard let self self else { return } // 在执行过程中也要检查 for i in 1...10 { if self.isCancelled { print(操作在步骤 \(i) 被取消) cancelled true break } // 模拟工作单元 Thread.sleep(forTimeInterval: 0.5) print(处理单元 \(i)) } semaphore.signal() } semaphore.wait() // 等待模拟任务完成 // 根据是否被取消来决定最终结果 if !cancelled { print(下载操作成功完成) } else { print(下载操作被取消) } } // 提供一个更积极的取消方式可以取消关联的 NSURLSessionTask override func cancel() { super.cancel() task?.cancel() // 同时取消底层的网络任务 print(主动取消了底层网络任务) } }实操心得对于网络请求这类依赖于系统框架的任务最佳实践是在自定义操作的cancel()方法中不仅调用super.cancel()还要调用底层资源如URLSessionTask.cancel()的取消方法。这样可以实现更及时的资源释放。3. NSOperationQueue 实战调度、依赖与并发控制NSOperationQueue是NSOperation的舞台。它负责管理操作的执行顺序、并发数量并提供了主队列和自定义队列。3.1 队列创建与基本使用// 1. 获取主队列关联主线程 let mainQueue OperationQueue.main // 2. 创建自定义后台队列 let backgroundQueue OperationQueue() backgroundQueue.name com.example.myBackgroundQueue // 建议命名便于调试 // 3. 添加操作到队列 let op1 BlockOperation { print(操作 1) } let op2 BlockOperation { print(操作 2) } backgroundQueue.addOperation(op1) backgroundQueue.addOperation(op2) // 4. 直接添加闭包便捷方式 backgroundQueue.addOperation { print(这是一个直接添加的闭包任务) }3.2 控制并发操作数量maxConcurrentOperationCount这是NSOperationQueue最实用的特性之一。通过设置maxConcurrentOperationCount属性你可以轻松实现一个固定大小的“线程池”。默认值OperationQueue.defaultMaxConcurrentOperationCount这个值由系统根据当前设备状况动态决定。设置为 1队列变为串行队列操作按添加顺序考虑依赖依次执行。设置为特定数值 (如 3)队列中最多同时有 3 个操作处于isExecuting状态。设置为.max(或NSOperationQueue.defaultMaxConcurrentOperationCount)不限制并发数但系统仍会进行全局调度并非无限创建线程。let downloadQueue OperationQueue() downloadQueue.name 图片下载队列 downloadQueue.maxConcurrentOperationCount 3 // 最多同时下载3张图片 let imageUrls [URL](repeating: URL(string: https://example.com/image.jpg)!, count: 10) for (index, url) in imageUrls.enumerated() { let downloadOp DownloadOperation(url: url) downloadOp.completionBlock { print(图片 \(index) 下载任务结束) } downloadQueue.addOperation(downloadOp) } // 即使添加了10个操作同一时刻也只会执行3个注意事项maxConcurrentOperationCount控制的是同时处于执行状态的操作数而不是线程数。一个操作内部可能使用 GCD 创建多个线程但这不影响队列对操作数量的控制。另外改变运行中队列的此属性是即时生效的。3.3 建立操作间的依赖关系addDependency依赖关系让你可以构建一个有向无环图DAG来定义任务执行的先后顺序。操作 A 依赖于操作 BA.addDependency(B)意味着 A 必须等到 B成功完成isFinished为true后才会变为isReady状态。// 模拟数据处理流水线 let fetchDataOp BlockOperation { print(1. 从网络获取数据) } let parseDataOp BlockOperation { print(2. 解析数据) } let saveToDBOp BlockOperation { print(3. 保存到数据库) } let updateUIOp BlockOperation { print(4. 更新UI) } // 建立依赖解析依赖获取保存依赖解析更新UI依赖保存 parseDataOp.addDependency(fetchDataOp) saveToDBOp.addDependency(parseDataOp) updateUIOp.addDependency(saveToDBOp) // 注意更新UI的操作必须放在主队列 let backgroundQueue OperationQueue() backgroundQueue.addOperations([fetchDataOp, parseDataOp, saveToDBOp], waitUntilFinished: false) OperationQueue.main.addOperation(updateUIOp)重要陷阱小心循环依赖。如果操作 A 依赖 B同时 B 又依赖 A那么这两个操作都将永远无法变为ready状态导致死锁。队列会检测到这种情况但你的程序逻辑会停滞。实操心得依赖关系是相对于操作对象的与它们被添加到哪个队列无关。你可以让一个在队列 A 中的操作依赖于一个在队列 B 中的操作。这为跨队列的任务协调提供了极大的灵活性。3.4 暂停队列与取消所有操作你可以控制整个队列的暂停与继续以及一键取消所有操作。let queue OperationQueue() // 暂停队列不会停止正在执行的操作但会阻止新操作开始执行 queue.isSuspended true // ... 此时添加的操作会处于等待状态 // 恢复队列 queue.isSuspended false // 取消队列中的所有操作 queue.cancelAllOperations()注意cancelAllOperations()会向队列中所有未完成的操作发送cancel()消息包括那些正在等待isReady的和正在执行isExecuting的。对于正在执行的操作如前所述需要操作自身响应取消。已完成的isFinished操作不受影响。4. 高级特性与性能优化实践掌握了基础之后我们来看看如何利用一些高级特性和模式来构建更健壮、高效的应用。4.1 操作优先级queuePriority每个NSOperation都有一个queuePriority属性其类型是Operation.QueuePriority包含.veryLow,.low,.normal,.high,.veryHigh等级别。注意优先级影响的是同一队列中、都已处于ready状态的操作的执行顺序。它不能跨越依赖关系。如果一个低优先级的操作 A 依赖高优先级的操作 B那么 B 仍然会先执行。let opLow BlockOperation { print(低优先级操作) } opLow.queuePriority .low let opHigh BlockOperation { print(高优先级操作) } opHigh.queuePriority .high let queue OperationQueue() queue.maxConcurrentOperationCount 1 // 设为串行以便观察顺序 queue.addOperation(opLow) queue.addOperation(opHigh) // 输出大概率是“高优先级操作”先于“低优先级操作”尽管 opLow 先被添加。不要过度依赖优先级优先级是提示而非保证。复杂的依赖关系或动态添加的操作可能使优先级的效果不如预期。设计良好的依赖链通常比依赖优先级更可靠。4.2 等待操作完成waitUntilFinished 与 addOperations有时你需要同步等待一个或一组操作完成。let queue OperationQueue() let longOp BlockOperation { Thread.sleep(forTimeInterval: 2) print(长时间操作完成) } queue.addOperation(longOp) print(即将等待操作完成...) longOp.waitUntilFinished() // **当前线程会阻塞**直到 longOp 完成 print(等待结束继续执行。) // 等待多个操作 let ops [BlockOperation { print(Op1) }, BlockOperation { print(Op2) }] queue.addOperations(ops, waitUntilFinished: true) // 阻塞直到 ops 数组中的所有操作完成 print(所有操作完成)警告waitUntilFinished会阻塞当前线程。绝对不要在主线程上调用此方法否则会导致界面卡死。它通常用于在后台线程协调任务或者在程序退出前进行清理工作。4.3 使用 DispatchSemaphore 在操作内部进行精细同步虽然操作依赖可以控制操作间的顺序但有时你需要在一个操作内部等待多个异步回调都完成后才标记操作结束。这时可以结合DispatchSemaphore。class ConcurrentAsyncOperation: Operation { override func main() { let semaphore DispatchSemaphore(value: 0) var successCount 0 let totalTasks 3 for i in 0..totalTasks { // 模拟并发发起三个网络请求 someAsyncAPI.call { result in // 处理结果 if result.isSuccess { successCount 1 } semaphore.signal() // 每个异步回调完成时发送信号 } } // 等待所有三个异步回调都完成 for _ in 0..totalTasks { semaphore.wait() } // 所有异步任务完成后根据 successCount 决定最终操作状态 if successCount totalTasks { print(所有子任务成功) } else { print(部分子任务失败) } // 注意这里不需要手动修改 isFinished因为 main() 方法正常返回父类会处理。 // 但如果你重写了 start则必须在最后调用 completeOperation()。 } }4.4 避免常见陷阱与性能考量内存循环引用在操作的completionBlock或main方法中捕获self操作对象本身或其所属的控制器时容易形成强引用循环。务必使用[weak self]。// 错误示例completionBlock 强引用了 op而 op 又被 queue 持有 let op BlockOperation { /* ... */ } op.completionBlock { print(op) } // op 被闭包捕获 // 正确示例 op.completionBlock { [weak op] in guard let op op else { return } print(操作 \(op) 完成了) }操作对象复用一个NSOperation对象只能执行一次。一旦其isFinished为true就不能再被添加到队列中。如果需要重复执行相同逻辑请创建新的操作实例。队列的线程爆炸虽然NSOperationQueue管理并发数但如果你在每个操作的main方法中都大量使用DispatchQueue.global().async来创建新任务实际上仍然可能创建大量线程。尽量将操作内部的任务也组织好或者考虑使用基于DispatchSemaphore或OperationQueue嵌套的内部并发控制。监控与调试为你的队列设置一个有意义的name属性。在 Xcode 的调试导航器中你可以看到这些名字这对于诊断复杂的多线程问题非常有帮助。你也可以利用OperationQueue的operations和operationCount属性来监控队列状态。与 GCD 的混合使用NSOperationQueue底层可能使用 GCD 实现但这并不冲突。你完全可以在一个操作的main方法里使用DispatchQueue进行更细粒度的并行。例如一个图片处理操作内部可以使用DispatchQueue.concurrentPerform来并行处理图片的多个区域。5. 实战案例构建一个可取消的图片下载管理器让我们综合运用所学构建一个简化但实用的图片下载管理器。它需要支持并发下载限制、单个下载任务取消、全部下载任务取消、下载结果缓存内存/磁盘。import UIKit class ImageDownloadManager { static let shared ImageDownloadManager() private let downloadQueue: OperationQueue private let cache NSCacheNSURL, UIImage() private var operations: [URL: ImageDownloadOperation] [:] private init() { downloadQueue OperationQueue() downloadQueue.name ImageDownloadQueue downloadQueue.maxConcurrentOperationCount 4 // 限制并发下载数 downloadQueue.qualityOfService .userInitiated } func downloadImage(from url: URL, completion: escaping (UIImage?, URL, Error?) - Void) - ImageDownloadOperation? { // 1. 检查内存缓存 if let cachedImage cache.object(forKey: url as NSURL) { DispatchQueue.main.async { completion(cachedImage, url, nil) } return nil } // 2. 检查磁盘缓存这里简化为伪代码 // if let diskImage loadFromDisk(url) { ... } // 3. 检查是否已有相同 URL 的下载正在进行 if let existingOperation operations[url] { // 可以在这里为 existingOperation 添加额外的完成回调避免重复下载 // 为了简化我们直接返回现有操作调用者可以监听它的状态 return existingOperation } // 4. 创建新的下载操作 let downloadOperation ImageDownloadOperation(url: url) downloadOperation.completionHandler { [weak self] image, error in guard let self self else { return } // 下载完成从记录中移除 self.operations.removeValue(forKey: url) if let image image { // 存入内存缓存 self.cache.setObject(image, forKey: url as NSURL) // 异步存入磁盘伪代码 // self.saveToDisk(image, for: url) } DispatchQueue.main.async { completion(image, url, error) } } // 5. 记录操作并加入队列 operations[url] downloadOperation downloadQueue.addOperation(downloadOperation) return downloadOperation } func cancelDownload(for url: URL) { operations[url]?.cancel() operations.removeValue(forKey: url) } func cancelAllDownloads() { downloadQueue.cancelAllOperations() operations.removeAll() cache.removeAllObjects() } } class ImageDownloadOperation: Operation { let url: URL var completionHandler: ((UIImage?, Error?) - Void)? private var task: URLSessionDataTask? init(url: URL) { self.url url super.init() } override func main() { // 在开始前检查取消状态 if isCancelled { return } // 使用 URLSession 进行下载 let semaphore DispatchSemaphore(value: 0) var downloadedImage: UIImage? var downloadError: Error? let session URLSession.shared let request URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30) task session.dataTask(with: request) { [weak self] data, response, error in guard let self self else { return } // 再次检查取消状态因为网络请求是异步的 if self.isCancelled { semaphore.signal() return } if let error error { downloadError error } else if let data data, let image UIImage(data: data) { downloadedImage image } else { downloadError NSError(domain: ImageDownload, code: -1, userInfo: [NSLocalizedDescriptionKey: Invalid image data]) } semaphore.signal() } task?.resume() semaphore.wait() // 等待网络请求完成 // 请求完成后检查取消状态 guard !isCancelled else { task nil return } // 调用完成回调 completionHandler?(downloadedImage, downloadError) completionHandler nil // 防止循环引用 } override func cancel() { super.cancel() task?.cancel() // 取消底层的 URLSession 任务 task nil } } // 使用示例 class ViewController: UIViewController { var imageView: UIImageView! var currentDownloadOperation: ImageDownloadOperation? override func viewDidLoad() { super.viewDidLoad() let url URL(string: https://example.com/large-image.jpg)! currentDownloadOperation ImageDownloadManager.shared.downloadImage(from: url) { [weak self] image, url, error in if let image image { self?.imageView.image image } else { print(下载失败: \(error?.localizedDescription ?? )) } } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // 如果视图控制器即将消失取消正在进行的下载 currentDownloadOperation?.cancel() } }这个案例展示了如何将NSOperation的子类化、状态管理、取消机制与NSOperationQueue的并发控制结合起来构建一个功能完整的模块。通过将每个下载任务封装成独立的ImageDownloadOperation我们可以轻松地管理它们的生命周期并通过管理器统一控制并发数和缓存策略。6. 与 GCD 的对比与选型建议最后我们来明确一下NSOperation/NSOperationQueue和 Grand Central Dispatch (GCD) 的适用场景这能帮助你在实际项目中做出更合适的选择。特性NSOperation/NSOperationQueueGrand Central Dispatch (GCD)抽象层级更高层的面向对象抽象。将任务封装为对象。更低层的 C API。基于队列和闭包。任务状态有明确的状态isReady,isExecuting,isFinished,isCancelled可通过 KVO 观察。无内置状态。闭包执行后即结束。任务依赖原生支持。通过addDependency轻松建立操作间的依赖关系。不支持。需通过DispatchGroup、信号量或嵌套回调手动实现代码复杂。任务取消原生支持。调用cancel()方法操作内部可响应取消。不支持。一旦将闭包提交到队列无法取消。需自行实现标志位检查。并发控制通过maxConcurrentOperationCount轻松设置队列的最大并发操作数。需要通过信号量 (DispatchSemaphore) 或创建有限数量的串行队列来手动控制较为繁琐。优先级支持操作级别的优先级 (queuePriority)。支持队列级别的服务质量 (QoS)如.userInitiated,.background。执行目标操作默认在后台线程执行但可以指定到主队列。通过选择不同的队列主队列.main或全局队列.global()来指定线程。适用场景需要复杂任务管理取消、依赖、状态监听、限制并发数的场景。如批量文件下载/上传、有依赖关系的数据处理流水线。简单的后台任务、一次性异步执行、延迟执行、简单的线程同步。代码更简洁性能开销极低。复杂度相对较高需要理解状态机和 KVO。相对较低API 简单直观。选型建议首选 GCD 的情况你的任务非常简单只是一个独立的、不需要取消、不依赖其他任务、也不关心其执行状态的闭包。例如在后台线程处理一段数据后更新 UIDispatchQueue.global().async { ...; DispatchQueue.main.async { ... } }。或者需要简单的延迟执行DispatchQueue.main.asyncAfter(deadline: .now() 0.5) { ... }。GCD 的代码更简洁性能也足够好。首选 NSOperation 的情况你需要取消任务例如用户滑动列表时取消离开屏幕的单元格的图片加载。你有一系列有依赖关系的任务例如先验证用户令牌再获取用户信息最后获取用户列表。你需要精确控制同时执行的任务数量例如限制同时发起的网络请求不超过 5 个。你需要监听每个任务的生命周期状态例如在 UI 上显示每个后台任务的进度。你希望将任务封装成可复用的组件并在不同上下文中组合使用。混合使用在实际项目中两者并非互斥。完全可以在一个NSOperation的main方法内部使用 GCD 来进行更细粒度的并行计算。例如一个视频编码操作NSOperation内部可以使用DispatchQueue.concurrentPerform来并行编码视频的不同片段。我个人在项目中的体会是对于任何涉及网络请求、文件 I/O 或复杂数据处理链路的模块我都会优先考虑使用NSOperationQueue来构建。它提供的状态管理和依赖控制能让异步代码的逻辑清晰很多尤其是在处理错误和取消逻辑时。而对于那些“一锤子买卖”式的工具函数或者简单的 UI 异步更新GCD 的轻量级 API 则是更快捷的选择。理解两者的优劣并在合适的场景运用它们是写出高效、健壮并发代码的关键。
返回列表