MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的NSOperationQueue操作队列应用

2023-12-044.0k 阅读

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有两个主要的子类:NSInvocationOperationNSBlockOperation

NSInvocationOperationNSInvocationOperation允许我们将一个已有的方法包装成一个操作。它的初始化方法接受一个目标对象和一个选择器,在执行操作时,会调用目标对象的指定方法。例如:

// 创建一个目标对象
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];

NSBlockOperationNSBlockOperation则更加灵活,它允许我们将一个代码块(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对象设置依赖关系。例如,假设有操作operationAoperationBoperationC,我们希望operationCoperationAoperationB都完成后执行,可以这样设置:

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会等待operationAoperationB完成后才开始执行。即使operationC先被添加到队列中,也会遵循依赖关系等待。

4.2 循环依赖检测

NSOperationQueue会检测并防止出现循环依赖的情况。如果尝试设置一个会导致循环依赖的关系,例如operationA依赖operationBoperationB依赖operationC,而operationC又依赖operationA,系统会抛出异常。这有助于避免程序进入无限等待的死锁状态。

5. 操作的状态与监控

在任务执行过程中,了解操作的状态以及对其进行监控是非常重要的。NSOperation提供了一些属性和方法来帮助我们实现这一点。

5.1 操作的状态属性

NSOperation有几个关键的状态属性,如isReadyisExecutingisFinishedisCancelled

isReady:表示操作是否准备好执行。当操作的所有依赖都已完成,并且没有被取消时,isReadyYESisExecuting:表示操作当前是否正在执行。 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_asyncdispatch_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)相结合,能够在不同场景下发挥各自的优势,进一步提升开发效率和应用质量。