Objective-C中的NSOperationQueue操作队列应用
1. 认识NSOperationQueue
在Objective-C的开发世界中,NSOperationQueue
扮演着至关重要的角色,它是一种用于管理和执行NSOperation
对象的机制。NSOperation
代表一个独立的任务,而NSOperationQueue
则负责调度这些任务的执行顺序和并发情况。
从本质上来说,NSOperationQueue
提供了一种异步执行任务的方式。它内部维护了一个任务队列,开发者可以将多个NSOperation
对象添加到这个队列中,队列会按照一定的规则(比如添加顺序、优先级等)来决定任务的执行顺序。这种机制使得我们可以方便地管理复杂的任务流程,同时利用多核处理器的优势来提高应用的性能。
1.1 NSOperationQueue的类型
NSOperationQueue
主要有两种类型:主队列(main queue)和自定义队列(custom queue)。
主队列:主队列与应用的主线程相关联,在主队列中执行的任务会在主线程上运行。由于主线程通常用于处理用户界面更新等操作,所以在主队列中执行的任务应该尽量简短,避免阻塞主线程,导致界面卡顿。可以通过[NSOperationQueue mainQueue]
来获取主队列。
自定义队列:开发者可以创建多个自定义队列,这些队列可以并行或串行执行任务,取决于队列的配置。自定义队列适用于处理一些耗时的后台任务,比如网络请求、数据处理等,这样不会影响主线程的流畅运行。可以通过[[NSOperationQueue alloc] init]
来创建一个自定义队列。
2. NSOperation基础
在深入了解NSOperationQueue
之前,我们需要先掌握NSOperation
的基本概念。NSOperation
是一个抽象类,它定义了任务的基本属性和行为。实际使用中,我们通常会使用它的子类或者创建继承自NSOperation
的自定义类。
2.1 NSOperation的子类
NSOperation
有两个主要的子类:NSInvocationOperation
和NSBlockOperation
。
NSInvocationOperation:NSInvocationOperation
允许我们将一个已有的方法包装成一个操作。它的初始化方法接受一个目标对象和一个选择器,在执行操作时,会调用目标对象的指定方法。例如:
// 创建一个目标对象
id target = [[MyClass alloc] init];
// 创建一个NSInvocationOperation,调用目标对象的myMethod方法
NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:target selector:@selector(myMethod) object:nil];
// 将操作添加到队列中执行
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:invocationOp];
NSBlockOperation:NSBlockOperation
则更加灵活,它允许我们将一个代码块(block)包装成一个操作。可以通过addExecutionBlock:
方法为NSBlockOperation
添加多个代码块,这些代码块会在操作执行时并发执行(如果队列允许并发)。示例代码如下:
NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
// 第一个代码块的任务
NSLog(@"Block 1 execution");
}];
[blockOp addExecutionBlock:^{
// 第二个代码块的任务
NSLog(@"Block 2 execution");
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:blockOp];
2.2 自定义NSOperation
除了使用NSOperation
的子类,我们还可以创建继承自NSOperation
的自定义类来实现特定的任务逻辑。在自定义NSOperation
类中,我们需要重写main
方法,该方法包含了任务的具体执行代码。
例如,我们创建一个简单的下载任务类:
@interface DownloadOperation : NSOperation
@property (nonatomic, strong) NSString *urlString;
- (instancetype)initWithURLString:(NSString *)urlString;
@end
@implementation DownloadOperation
- (instancetype)initWithURLString:(NSString *)urlString {
self = [super init];
if (self) {
_urlString = urlString;
}
return self;
}
- (void)main {
NSURL *url = [NSURL URLWithString:self.urlString];
NSData *data = [NSData dataWithContentsOfURL:url];
if (data) {
// 处理下载的数据
NSLog(@"Downloaded data of length: %lu", (unsigned long)data.length);
} else {
NSLog(@"Download failed");
}
}
@end
使用这个自定义操作的方式如下:
DownloadOperation *downloadOp = [[DownloadOperation alloc] initWithURLString:@"http://example.com"];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:downloadOp];
3. 操作队列的基本操作
了解了NSOperation
的基础后,我们来看看如何在NSOperationQueue
中进行一些基本操作,包括添加操作、设置队列属性等。
3.1 添加操作到队列
将NSOperation
对象添加到NSOperationQueue
非常简单,只需要调用队列的addOperation:
方法即可。例如:
NSOperation *operation = [[NSBlockOperation alloc] init];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
我们也可以一次添加多个操作,使用addOperations:waitUntilFinished:
方法,这个方法接受一个NSArray
包含多个NSOperation
对象,以及一个布尔值来决定是否等待所有操作执行完毕。例如:
NSArray<NSOperation *> *operations = @[operation1, operation2, operation3];
[queue addOperations:operations waitUntilFinished:NO];
3.2 设置队列属性
NSOperationQueue
有一些重要的属性可以设置,以满足不同的任务执行需求。
最大并发数(maxConcurrentOperationCount):这个属性决定了队列中最多可以同时执行的操作数量。默认情况下,自定义队列的最大并发数为 -1,表示不限制并发数量(实际上会根据系统资源动态调整)。如果将其设置为1,则队列会以串行方式执行任务。例如:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 3; // 设置最大并发数为3
优先级(qualityOfService):可以通过设置队列的qualityOfService
属性来指定任务的优先级。不同的优先级会影响系统对任务的调度策略。例如,NSQualityOfServiceUserInteractive
适用于与用户交互密切的任务,如界面更新;NSQualityOfServiceBackground
适用于后台的低优先级任务,如数据备份。示例代码如下:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.qualityOfService = NSQualityOfServiceUserInitiated;
4. 操作之间的依赖关系
在实际开发中,很多时候任务之间存在依赖关系,即一个任务必须在另一个任务完成后才能执行。NSOperationQueue
提供了强大的机制来管理这种依赖关系。
4.1 设置依赖关系
可以通过addDependency:
方法为NSOperation
对象设置依赖关系。例如,假设有操作operationA
、operationB
和operationC
,我们希望operationC
在operationA
和operationB
都完成后执行,可以这样设置:
NSOperation *operationA = [[NSBlockOperation alloc] init];
NSOperation *operationB = [[NSBlockOperation alloc] init];
NSOperation *operationC = [[NSBlockOperation alloc] init];
[operationC addDependency:operationA];
[operationC addDependency:operationB];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operationA, operationB, operationC] waitUntilFinished:NO];
在这个例子中,operationC
会等待operationA
和operationB
完成后才开始执行。即使operationC
先被添加到队列中,也会遵循依赖关系等待。
4.2 循环依赖检测
NSOperationQueue
会检测并防止出现循环依赖的情况。如果尝试设置一个会导致循环依赖的关系,例如operationA
依赖operationB
,operationB
依赖operationC
,而operationC
又依赖operationA
,系统会抛出异常。这有助于避免程序进入无限等待的死锁状态。
5. 操作的状态与监控
在任务执行过程中,了解操作的状态以及对其进行监控是非常重要的。NSOperation
提供了一些属性和方法来帮助我们实现这一点。
5.1 操作的状态属性
NSOperation
有几个关键的状态属性,如isReady
、isExecuting
、isFinished
和isCancelled
。
isReady:表示操作是否准备好执行。当操作的所有依赖都已完成,并且没有被取消时,isReady
为YES
。
isExecuting:表示操作当前是否正在执行。
isFinished:表示操作是否已经完成执行。
isCancelled:表示操作是否已被取消。
我们可以通过观察这些属性来了解操作的实时状态。例如,在自定义NSOperation
类中,可以重写这些属性的获取方法,以便在状态变化时执行一些自定义逻辑:
@interface MyCustomOperation : NSOperation
//...
@end
@implementation MyCustomOperation
//...
- (BOOL)isFinished {
BOOL finished = [super isFinished];
if (finished) {
// 操作完成时的自定义逻辑
NSLog(@"Operation finished");
}
return finished;
}
@end
5.2 操作完成的通知
除了通过检查状态属性,我们还可以使用通知机制来得知操作的完成情况。NSOperation
在完成时会发送NSOperationDidFinishNotification
通知,我们可以在需要的地方注册这个通知来处理操作完成后的任务。例如:
// 注册通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(operationFinished:) name:NSOperationDidFinishNotification object:nil];
// 通知处理方法
- (void)operationFinished:(NSNotification *)notification {
NSOperation *operation = notification.object;
if ([operation isKindOfClass:[MyCustomOperation class]]) {
// 处理自定义操作完成后的逻辑
}
}
6. 高级应用场景
NSOperationQueue
在实际开发中有许多高级应用场景,下面我们来探讨一些常见的例子。
6.1 网络请求队列
在进行网络编程时,我们经常需要发起多个网络请求。使用NSOperationQueue
可以有效地管理这些请求的并发数量,避免过多请求导致网络拥堵或资源耗尽。
例如,我们创建一个网络请求操作类:
@interface NetworkRequestOperation : NSOperation
@property (nonatomic, strong) NSURL *url;
- (instancetype)initWithURL:(NSURL *)url;
@end
@implementation NetworkRequestOperation
- (instancetype)initWithURL:(NSURL *)url {
self = [super init];
if (self) {
_url = url;
}
return self;
}
- (void)main {
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:self.url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data) {
// 处理响应数据
NSLog(@"Received data of length: %lu", (unsigned long)data.length);
} else if (error) {
NSLog(@"Request error: %@", error);
}
// 标记操作完成
[self completeOperation];
}];
[task resume];
}
- (void)completeOperation {
// 使用KVO来标记操作完成
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
}
@end
然后创建一个网络请求队列,并添加多个请求操作:
NSOperationQueue *networkQueue = [[NSOperationQueue alloc] init];
networkQueue.maxConcurrentOperationCount = 5; // 设置最大并发数为5
NSArray<NSURL *> *urls = @[[NSURL URLWithString:@"http://example1.com"], [NSURL URLWithString:@"http://example2.com"]];
for (NSURL *url in urls) {
NetworkRequestOperation *requestOp = [[NetworkRequestOperation alloc] initWithURL:url];
[networkQueue addOperation:requestOp];
}
6.2 复杂任务流程管理
在一些复杂的应用中,可能涉及到多个相互关联的任务,并且这些任务需要按照特定的顺序和并发策略执行。NSOperationQueue
可以很好地满足这种需求。
比如,我们有一个图像处理的任务流程,包括下载图片、解码图片、调整图片大小和保存图片。每个步骤都可以定义为一个NSOperation
,然后通过设置依赖关系和队列属性来管理整个流程。
@interface ImageDownloadOperation : NSOperation
@property (nonatomic, strong) NSURL *imageURL;
@property (nonatomic, strong) NSData *imageData;
- (instancetype)initWithImageURL:(NSURL *)imageURL;
@end
@implementation ImageDownloadOperation
- (instancetype)initWithImageURL:(NSURL *)imageURL {
self = [super init];
if (self) {
_imageURL = imageURL;
}
return self;
}
- (void)main {
self.imageData = [NSData dataWithContentsOfURL:self.imageURL];
if (self.imageData) {
NSLog(@"Image downloaded");
} else {
NSLog(@"Image download failed");
}
}
@end
@interface ImageDecodeOperation : NSOperation
@property (nonatomic, strong) UIImage *decodedImage;
@property (nonatomic, strong) ImageDownloadOperation *downloadOp;
- (instancetype)initWithDownloadOperation:(ImageDownloadOperation *)downloadOp;
@end
@implementation ImageDecodeOperation
- (instancetype)initWithDownloadOperation:(ImageDownloadOperation *)downloadOp {
self = [super init];
if (self) {
_downloadOp = downloadOp;
}
return self;
}
- (void)main {
if (self.downloadOp.imageData) {
self.decodedImage = [UIImage imageWithData:self.downloadOp.imageData];
NSLog(@"Image decoded");
} else {
NSLog(@"Image decoding failed");
}
}
@end
// 类似地,可以创建ImageResizeOperation和ImageSaveOperation
// 创建操作和队列
NSOperationQueue *imageProcessingQueue = [[NSOperationQueue alloc] init];
ImageDownloadOperation *downloadOp = [[ImageDownloadOperation alloc] initWithImageURL:[NSURL URLWithString:@"http://example.com/image.jpg"]];
ImageDecodeOperation *decodeOp = [[ImageDecodeOperation alloc] initWithDownloadOperation:downloadOp];
[decodeOp addDependency:downloadOp];
// 添加操作到队列
[imageProcessingQueue addOperations:@[downloadOp, decodeOp] waitUntilFinished:NO];
通过这种方式,我们可以清晰地管理复杂的任务流程,提高代码的可维护性和可读性。
7. 与其他异步编程模型的比较
在Objective-C开发中,除了NSOperationQueue
,还有其他一些异步编程模型,如GCD(Grand Central Dispatch)。了解它们之间的差异有助于我们在不同场景下选择最合适的方案。
7.1 NSOperationQueue与GCD的区别
抽象层次:NSOperationQueue
基于NSOperation
对象,是一种面向对象的异步编程模型,更适合处理复杂的任务关系和状态监控。而GCD基于队列和块(block),是一种基于函数式编程的模型,更加轻量级和简洁,适合简单的异步任务。
任务管理:NSOperationQueue
可以方便地设置任务之间的依赖关系,并且可以通过属性和通知来监控任务的状态。GCD虽然也可以通过dispatch_group
等方式实现类似的功能,但相对来说没有NSOperationQueue
那么直观和面向对象。
并发控制:NSOperationQueue
通过设置maxConcurrentOperationCount
来控制并发数量,同时可以根据任务优先级进行调度。GCD则通过不同类型的队列(如并发队列、串行队列)以及dispatch_async
、dispatch_sync
等函数来控制任务的并发和同步执行。
灵活性:NSOperationQueue
在任务的创建和管理上更加灵活,可以自定义NSOperation
类来实现复杂的任务逻辑。GCD则更侧重于简单高效地执行代码块。
例如,对于一个简单的后台数据处理任务,使用GCD可能更简洁:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// 数据处理代码
NSLog(@"Data processing in GCD");
});
而对于一个涉及多个相互依赖任务的复杂流程,NSOperationQueue
可能是更好的选择,如前面提到的图像处理任务流程。
8. 性能优化与注意事项
在使用NSOperationQueue
时,为了确保应用的性能和稳定性,需要注意一些性能优化和相关事项。
8.1 合理设置并发数
设置合适的maxConcurrentOperationCount
对于性能至关重要。如果并发数设置过高,可能会导致资源竞争和系统开销增加,影响应用的整体性能。相反,如果并发数设置过低,可能无法充分利用多核处理器的优势。需要根据任务的性质(如CPU密集型还是I/O密集型)和系统资源来合理调整并发数。
对于CPU密集型任务,由于多个任务同时执行会竞争CPU资源,建议适当降低并发数,例如设置为CPU核心数减1,以避免过度竞争。而对于I/O密集型任务,由于任务在等待I/O操作完成时会释放CPU资源,所以可以适当提高并发数。
8.2 避免主线程阻塞
如前所述,主队列与主线程相关联,在主队列中执行的任务会在主线程上运行。因此,不要在主队列中添加耗时较长的任务,否则会阻塞主线程,导致界面卡顿。对于需要在主线程上执行的任务,尽量确保其执行时间在16ms以内(以60fps的帧率计算)。
如果一个任务既需要在主线程上执行部分操作,又有一些耗时的后台操作,可以将耗时操作放在自定义队列中执行,完成后再回到主线程更新界面。例如:
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
[backgroundQueue addOperationWithBlock:^{
// 耗时的后台操作
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://example.com"]];
// 回到主线程更新界面
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 更新界面的代码
}];
}];
8.3 内存管理
在使用NSOperationQueue
时,要注意内存管理。特别是在自定义NSOperation
类中,如果在操作执行过程中创建了大量的临时对象,要确保在操作完成后及时释放这些对象。另外,如果操作之间存在强引用循环,可能会导致内存泄漏。例如,一个操作持有另一个操作的强引用,而另一个操作又持有这个操作的强引用,这种情况下需要使用弱引用(__weak
)来打破循环。
例如,在设置依赖关系时:
__weak typeof(operationA) weakA = operationA;
[operationB addDependency:operationA];
operationB.completionBlock = ^{
__strong typeof(weakA) strongA = weakA;
if (strongA) {
// 操作完成后的逻辑,这里使用strongA来避免weakA在block执行过程中被释放
}
};
通过注意这些性能优化和事项,可以使我们在使用NSOperationQueue
时开发出更加高效、稳定的应用程序。
在Objective-C开发中,NSOperationQueue
是一个功能强大且灵活的异步编程工具,通过深入理解和熟练运用它,我们能够更好地管理任务流程,提高应用的性能和响应速度。无论是简单的后台任务还是复杂的业务流程,NSOperationQueue
都能为我们提供有效的解决方案。同时,与其他异步编程模型(如GCD)相结合,能够在不同场景下发挥各自的优势,进一步提升开发效率和应用质量。