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

掌握Objective-C中NSOperation与NSOperationQueue的结合应用

2024-01-017.2k 阅读

1. NSOperation 与 NSOperationQueue 基础概念

1.1 NSOperation 是什么

NSOperation 是一个抽象类,它代表了一个异步操作。它为我们提供了一种面向对象的方式来管理和执行任务。我们可以通过继承 NSOperation 类并实现其 main 方法来定义自己的操作。例如:

@interface MyOperation : NSOperation
@end

@implementation MyOperation
- (void)main {
    // 这里写具体的操作逻辑
    NSLog(@"My operation is running.");
}
@end

此外,NSOperation 还有一些属性可以帮助我们管理操作的状态。比如 isFinished 属性,当操作完成时,这个属性会被设置为 YES。我们可以通过检查这个属性来判断操作是否结束。例如:

MyOperation *operation = [[MyOperation alloc] init];
[operation start];
while (!operation.isFinished) {
    // 等待操作完成
}
NSLog(@"Operation has finished.");

1.2 NSOperationQueue 是什么

NSOperationQueue 是一个队列,用于管理 NSOperation 对象。它允许我们将多个 NSOperation 对象添加到队列中,队列会按照一定的规则来执行这些操作。NSOperationQueue 提供了一种方便的方式来实现多任务处理。我们可以创建一个 NSOperationQueue 对象,然后将 NSOperation 对象添加到这个队列中,如下所示:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
MyOperation *operation = [[MyOperation alloc] init];
[queue addOperation:operation];

NSOperationQueue 有两种类型:主队列(main queue)和自定义队列(custom queue)。主队列与主线程相关联,添加到主队列的操作会在主线程中执行。而自定义队列则会在后台线程中执行操作。我们可以通过以下方式获取主队列:

NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];

2. NSOperation 的子类

2.1 NSInvocationOperation

NSInvocationOperation 是 NSOperation 的一个子类,它允许我们通过一个 NSInvocation 对象来定义操作。NSInvocation 可以封装一个方法调用,包括方法的选择器、目标对象以及参数。下面是一个使用 NSInvocationOperation 的示例:

// 定义一个方法
- (void)printMessage:(NSString *)message {
    NSLog(@"%@", message);
}

// 创建 NSInvocationOperation
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(printMessage:) object:@"Hello from NSInvocationOperation"];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:invocationOperation];

在这个示例中,我们创建了一个 NSInvocationOperation,它会调用 printMessage: 方法,并传递一个字符串参数。然后将这个操作添加到自定义队列中执行。

2.2 NSBlockOperation

NSBlockOperation 也是 NSOperation 的一个子类,它允许我们通过一个代码块(block)来定义操作。这使得我们可以更方便地创建操作,尤其是对于一些简单的任务。例如:

NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"This is a NSBlockOperation.");
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:blockOperation];

NSBlockOperation 还支持添加多个代码块,这些代码块会并发执行。例如:

NSBlockOperation *multiBlockOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"First block in NSBlockOperation.");
}];
[multiBlockOperation addExecutionBlock:^{
    NSLog(@"Second block in NSBlockOperation.");
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:multiBlockOperation];

3. NSOperation 之间的依赖关系

3.1 设置依赖关系的方法

NSOperation 允许我们设置操作之间的依赖关系,这意味着一个操作必须在另一个或多个操作完成后才能开始执行。我们可以通过 addDependency: 方法来设置依赖关系。例如:

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 1 is running.");
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 2 is running.");
}];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 3 is running.");
}];
// 设置 operation2 依赖于 operation1
[operation2 addDependency:operation1];
// 设置 operation3 依赖于 operation2
[operation3 addDependency:operation2];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];

在这个示例中,operation2 会在 operation1 完成后开始执行,operation3 会在 operation2 完成后开始执行。

3.2 依赖关系的注意事项

当设置依赖关系时,需要注意避免循环依赖。例如,如果 operationA 依赖于 operationB,而 operationB 又依赖于 operationA,这就会导致死锁。在实际应用中,我们需要仔细规划操作之间的依赖关系,确保程序的正确性和稳定性。此外,依赖关系的设置也会影响操作的执行顺序和并发性能。合理设置依赖关系可以提高程序的执行效率,而不合理的依赖关系可能会导致不必要的等待和性能瓶颈。

4. NSOperationQueue 的属性与方法

4.1 最大并发数(maxConcurrentOperationCount)

NSOperationQueue 的 maxConcurrentOperationCount 属性用于设置队列中可以同时执行的最大操作数。默认情况下,这个值是 -1,表示不限制并发数。例如,如果我们希望队列中最多同时执行 3 个操作,可以这样设置:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 3;
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 1 is running.");
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 2 is running.");
}];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 3 is running.");
}];
NSBlockOperation *operation4 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 4 is running.");
}];
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];
[queue addOperation:operation4];

在这个示例中,当 operation1、operation2 和 operation3 开始执行后,operation4 会等待,直到有一个操作完成,才会开始执行。

4.2 暂停与恢复(suspended)

NSOperationQueue 的 suspended 属性可以用于暂停或恢复队列的执行。当 suspended 属性为 YES 时,队列会暂停执行新的操作,但已经在执行的操作会继续执行直到完成。例如:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 1 is running.");
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 2 is running.");
}];
[queue addOperation:operation1];
[queue addOperation:operation2];
// 暂停队列
queue.suspended = YES;
// 模拟一些其他操作
// 恢复队列
queue.suspended = NO;

4.3 取消操作(cancelAllOperations)

NSOperationQueue 提供了 cancelAllOperations 方法,用于取消队列中所有尚未开始执行的操作。已经在执行的操作不会被立即取消,但可以通过在操作内部检查 isCancelled 属性来决定是否提前结束操作。例如:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    while (!self.isCancelled) {
        // 执行操作逻辑
        NSLog(@"Operation 1 is running.");
    }
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Operation 2 is running.");
}];
[queue addOperation:operation1];
[queue addOperation:operation2];
// 取消所有操作
[queue cancelAllOperations];

在这个示例中,operation1 在执行过程中会不断检查 isCancelled 属性,如果发现被取消,就会结束操作。operation2 如果还未开始执行,就会被取消。

5. 实际应用场景

5.1 网络请求与数据处理

在开发应用程序时,经常需要进行网络请求并处理返回的数据。我们可以将网络请求封装成一个 NSOperation,将数据处理封装成另一个 NSOperation,并设置它们之间的依赖关系。例如:

// 网络请求操作
NSBlockOperation *networkOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSURL *url = [NSURL URLWithString:@"http://example.com/api/data"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 模拟网络请求成功,返回数据
    self.receivedData = data;
}];
// 数据处理操作
NSBlockOperation *dataProcessOperation = [NSBlockOperation blockOperationWithBlock:^{
    if (self.receivedData) {
        NSError *error;
        NSDictionary *json = [NSJSONSerialization JSONObjectWithData:self.receivedData options:NSJSONReadingMutableContainers error:&error];
        if (!error) {
            NSLog(@"Processed data: %@", json);
        }
    }
}];
[dataProcessOperation addDependency:networkOperation];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:networkOperation];
[queue addOperation:dataProcessOperation];

在这个示例中,网络请求操作先执行,获取到数据后,数据处理操作才会执行。这样可以确保数据处理操作在有数据的情况下进行,提高程序的稳定性和可靠性。

5.2 图片加载与缓存

在显示图片的应用中,我们可以使用 NSOperation 和 NSOperationQueue 来优化图片加载和缓存。例如,我们可以创建一个操作来从网络加载图片,另一个操作来将图片缓存到本地。同时,我们可以设置操作之间的依赖关系,确保缓存操作在图片加载完成后执行。

// 图片加载操作
NSBlockOperation *imageLoadOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSURL *imageURL = [NSURL URLWithString:@"http://example.com/image.jpg"];
    NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
    UIImage *image = [UIImage imageWithData:imageData];
    self.loadedImage = image;
}];
// 图片缓存操作
NSBlockOperation *imageCacheOperation = [NSBlockOperation blockOperationWithBlock:^{
    if (self.loadedImage) {
        NSData *imageData = UIImageJPEGRepresentation(self.loadedImage, 0.8);
        NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"image.jpg"];
        [imageData writeToFile:cachePath atomically:YES];
    }
}];
[imageCacheOperation addDependency:imageLoadOperation];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:imageLoadOperation];
[queue addOperation:imageCacheOperation];

通过这种方式,我们可以有效地管理图片加载和缓存的过程,提高应用的性能和用户体验。

6. 性能优化与注意事项

6.1 合理设置并发数

在使用 NSOperationQueue 时,合理设置 maxConcurrentOperationCount 非常重要。如果并发数设置过高,可能会导致系统资源过度消耗,影响应用的性能。例如,在一个内存有限的设备上,如果同时执行过多的操作,可能会导致内存不足,应用崩溃。相反,如果并发数设置过低,可能无法充分利用系统资源,导致操作执行时间过长。我们需要根据应用的具体需求和运行环境来调整并发数,以达到最佳的性能。

6.2 避免死锁

如前文所述,设置 NSOperation 之间的依赖关系时要避免循环依赖,以免造成死锁。在复杂的应用中,操作之间的依赖关系可能会变得比较复杂,因此需要仔细规划和检查。可以通过绘制依赖关系图等方式来辅助分析,确保依赖关系的正确性。

6.3 线程安全

当多个操作并发执行时,需要注意线程安全问题。例如,如果多个操作同时访问和修改同一个共享资源,可能会导致数据不一致。我们可以使用锁(如 NSLock、NSConditionLock 等)来保证共享资源的线程安全。例如:

NSLock *lock = [[NSLock alloc] init];
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    [lock lock];
    // 访问和修改共享资源
    [lock unlock];
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    [lock lock];
    // 访问和修改共享资源
    [lock unlock];
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation1];
[queue addOperation:operation2];

在这个示例中,通过使用 NSLock,确保了在同一时间只有一个操作可以访问和修改共享资源,从而保证了线程安全。

6.4 内存管理

在使用 NSOperation 和 NSOperationQueue 时,要注意内存管理。如果操作中使用了大量的内存,并且操作长时间运行,可能会导致内存泄漏。例如,在操作完成后,要及时释放不再使用的资源。对于一些需要长时间持有数据的操作,可以考虑使用弱引用(weak reference)来避免循环引用导致的内存泄漏。

7. 与其他多线程技术的比较

7.1 与 GCD 的比较

Grand Central Dispatch(GCD)是另一种常用的多线程编程技术。与 NSOperation 和 NSOperationQueue 相比,GCD 更加轻量级和简单。GCD 使用队列(dispatch queue)来管理任务,任务以代码块的形式提交到队列中执行。而 NSOperation 和 NSOperationQueue 提供了更面向对象的方式,支持操作之间的依赖关系、优先级设置等功能。例如,在处理复杂的任务依赖关系时,NSOperation 和 NSOperationQueue 会更加灵活和易于管理。但在一些简单的多线程任务场景中,GCD 的代码可能会更加简洁。

7.2 与 pthread 的比较

POSIX Threads(pthread)是一套标准的多线程 API,它提供了底层的线程控制功能。与 pthread 相比,NSOperation 和 NSOperationQueue 位于更高的抽象层次,使用起来更加方便。pthread 需要手动管理线程的创建、销毁、同步等操作,而 NSOperation 和 NSOperationQueue 则封装了这些细节,使开发者可以更专注于业务逻辑。但在一些对性能要求极高、需要精细控制线程的场景中,pthread 可能会更合适。

8. 高级应用技巧

8.1 操作优先级(queuePriority)

NSOperation 有一个 queuePriority 属性,用于设置操作在队列中的优先级。优先级分为 NSOperationQueuePriorityVeryLowNSOperationQueuePriorityLowNSOperationQueuePriorityNormalNSOperationQueuePriorityHighNSOperationQueuePriorityVeryHigh 几个级别。例如:

NSBlockOperation *highPriorityOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"High priority operation is running.");
}];
highPriorityOperation.queuePriority = NSOperationQueuePriorityHigh;
NSBlockOperation *lowPriorityOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Low priority operation is running.");
}];
lowPriorityOperation.queuePriority = NSOperationQueuePriorityLow;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:highPriorityOperation];
[queue addOperation:lowPriorityOperation];

在这个示例中,highPriorityOperation 会比 lowPriorityOperation 更有可能先执行,尽管不能保证绝对的顺序,因为队列的执行顺序还受到其他因素的影响,如依赖关系等。

8.2 自定义操作的状态管理

当我们继承 NSOperation 类创建自定义操作时,除了 isFinished 属性外,还可以自定义一些状态属性来更好地管理操作的状态。例如,我们可以添加一个 isInProgress 属性来表示操作是否正在进行中。

@interface MyCustomOperation : NSOperation
@property (nonatomic, assign, readonly) BOOL isInProgress;
@end

@implementation MyCustomOperation
{
    BOOL _isInProgress;
}

- (BOOL)isInProgress {
    return _isInProgress;
}

- (void)main {
    _isInProgress = YES;
    // 执行操作逻辑
    NSLog(@"My custom operation is running.");
    _isInProgress = NO;
}
@end

通过这种方式,我们可以在外部更方便地了解操作的状态,例如在界面上根据操作状态显示不同的提示信息等。

8.3 操作的进度跟踪

在一些长时间运行的操作中,我们可能需要跟踪操作的进度。我们可以在自定义操作中添加一个进度属性,并在操作执行过程中更新这个属性。例如:

@interface MyLongRunningOperation : NSOperation
@property (nonatomic, assign, readonly) float progress;
@end

@implementation MyLongRunningOperation
{
    float _progress;
}

- (float)progress {
    return _progress;
}

- (void)main {
    for (int i = 0; i < 100; i++) {
        // 模拟操作执行
        [NSThread sleepForTimeInterval:0.1];
        _progress = (float)i / 100.0;
        NSLog(@"Progress: %.2f%%", _progress * 100);
    }
}
@end

然后在外部,我们可以通过监听这个进度属性来更新界面上的进度条等,给用户提供操作进度的反馈。

9. 调试与错误处理

9.1 调试 NSOperation 和 NSOperationQueue

在调试使用 NSOperation 和 NSOperationQueue 的代码时,我们可以使用 NSLog 输出操作的状态和执行过程中的关键信息。例如,在操作的 main 方法中添加 NSLog 语句来查看操作何时开始、何时结束等。另外,Xcode 的调试工具也可以帮助我们查看操作队列的状态,比如暂停队列后查看哪些操作在等待执行等。

9.2 错误处理

在操作执行过程中,可能会出现各种错误。例如,网络请求操作可能会因为网络问题失败,数据处理操作可能会因为数据格式不正确而失败。我们可以在操作内部通过 NSError 来处理错误。例如:

NSBlockOperation *networkOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSURL *url = [NSURL URLWithString:@"http://example.com/api/data"];
    NSError *error;
    NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error];
    if (error) {
        NSLog(@"Network request error: %@", error);
        // 可以设置一个属性表示操作失败,供外部检查
        self.operationFailed = YES;
    } else {
        self.receivedData = data;
    }
}];

在这个示例中,网络请求操作捕获到错误后,通过 NSLog 输出错误信息,并设置一个属性表示操作失败,以便外部代码根据这个属性进行相应的处理,如提示用户网络错误等。