Cocoa Tutorial: NSOperation and NSOperationQueue

史承福
2023-12-01

 

Threading is hard in any language. And what is worse, when it goes wrong, it usually goes wrong in a very bad way. Because of this, programmers either avoid threading completely (and refer to it as the spawn of the devil) or spend a LOT of time making sure that everything is perfect.

Fortunately, Apple has made a lot of progress in OS X 10.5 Leopard. NSThread itself has received a number of very useful new methods that make threading easier. In addition, they have introduced two new objects: NSOperation and NSOperationQueue.

In this Tutorial I will walk through a basic example that shows how to use these new Objects and how they make multi-threading your application nearly trivial.

You can get the example project here: Async Downloader Example Project

In this tutorial, I will demonstrate one way in which to use NSOperation and NSOperationQueue to handle tasks that are best performed on background threads. The intent of this tutorial is to demonstrate a basic use of these classes and is not intentioned to be the only way to use them.

If you are familiar with Java, or one of its variants, the NSOperation object is very similar to the java.lang.Runnable interface. Like, in Java’s Runnable interface, the NSOperation object is designed to be extended. Also like Java’s Runnable, there is a minimum of one method to override. For NSOperation that method is -(void)main. One of the easiest ways to use an NSOperation is to load it into an NSOperationQueue. As soon as the operation is loaded into the queue, the queue will kick it off and begin processing. As soon as the operation is complete the queue will release it.

NSOperation Example

In this example, I have written an NSOperation that fetches a webpage as a string, parses it into an NSXMLDocument and then passes that NSXMLDocument back to the main thread in the application before completing.

PageLoadOperation.h

1
2
3
4
5
6
7
8
9
10
11
12
#import <Cocoa/Cocoa.h>
 
 
@interface PageLoadOperation : NSOperation {
    NSURL *targetURL;
}
 
@property(retain) NSURL *targetURL;
 
- (id)initWithURL:(NSURL*)url;
 
@end

PageLoadOperation.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import "PageLoadOperation.h"
#import "AppDelegate.h"
 
@implementation PageLoadOperation
 
@synthesize targetURL;
 
- (id)initWithURL:(NSURL*)url;
{
    if (![super init]) return nil;
    [self setTargetURL:url];
    return self;
}
 
- (void)dealloc {
    [targetURL release], targetURL = nil;
    [super dealloc];
}
 
- (void)main {
    NSString *webpageString = [[[NSString alloc] initWithContentsOfURL:[self targetURL]] autorelease];
 
    NSError *error = nil;
    NSXMLDocument *document = [[NSXMLDocument alloc] initWithXMLString:webpageString 
                                                              options:NSXMLDocumentTidyHTML 
                                                                error:&error];
    if (!document) {
        NSLog(@"%s Error loading document (%@): %@", _cmd, [[self targetURL] absoluteString], error);
        return;
    }	
 
    [[AppDelegate shared] performSelectorOnMainThread:@selector(pageLoaded:)
                                           withObject:document
                                        waitUntilDone:YES];
    [document release];
}
 
@end

As you can see, this class is very simple. It accepts a URL in the init and stores it. When the main method is called it constructs a string from the URL and then passes that string into the init of an NSXMLDocument. If there is no error with the loading of the xml document, it is then passed back to the AppDelegate, on the main thread, and the task is complete. When the main method of the NSOperation ends the queue will automatically release the object.

AppDelegate.h

1
2
3
4
5
6
7
8
9
10
#import <Cocoa/Cocoa.h>
 
@interface AppDelegate : NSObject {
	NSOperationQueue *queue;
}
 
+ (id)shared;
- (void)pageLoaded:(NSXMLDocument*)document;
 
@end

AppDelegate.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#import "AppDelegate.h"
#import "PageLoadOperation.h"
 
@implementation AppDelegate
static AppDelegate *shared;
static NSArray *urlArray;
 
- (id)init
{
    if (shared) {
        [self autorelease];
        return shared;
    }
    if (![super init]) return nil;
 
    NSMutableArray *array = [[NSMutableArray alloc] init];
    [array addObject:@"http://www.google.com"];
    [array addObject:@"http://www.apple.com"];
    [array addObject:@"http://www.yahoo.com"];
    [array addObject:@"http://www.zarrastudios.com"];
    [array addObject:@"http://www.macosxhints.com"];
    urlArray = array;
 
    queue = [[NSOperationQueue alloc] init];
    shared = self;
    return self;
}
 
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    for (NSString *urlString in urlArray) {
        NSURL *url = [NSURL URLWithString:urlString];
        PageLoadOperation *plo = [[PageLoadOperation alloc] initWithURL:url];
        [queue addOperation:plo];
        [plo release];
    }
}
 
- (void)dealloc
{
    [queue release], queue = nil;
    [super dealloc];
}
 
+ (id)shared;
{
    if (!shared) {
        [[AppDelegate alloc] init];
    }
    return shared;
}
 
- (void)pageLoaded:(NSXMLDocument*)document;
{
    NSLog(@"%s Do something with the XMLDocument: %@", _cmd, document);
}
 
@end

In this example AppDelegate, two things are occurring. First, in the init method, the NSOperationQueue is being initialized and an array of urls is being loaded. Then when the application has completed its load, the applicationDidFinishLaunching: method is called by the NSApplication instance and the AppDelegate loops over the urls, creating a task for each one and loading those tasks into the NSOperationQueue. As soon as each item is loaded into the queue the queue will kick it off by assigning it to a NSThread and the thread will then run the main method of the operation. Once the operation is complete the thread will report back to the queue and the queue will release the operation.

NSOperationQueue Concurrency

In this very simple example, it is quite difficult to load up the queue with enough objects to actually see it running them in parallel. However, if you run tasks that take more time than these, you will see that the queue will run all of the tasks at the same time. Fortunately, if you want to limit how many tasks are running in parallel you can alter the init method in the App Delegate as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (id)init
{
    if (shared) {
        [self autorelease];
        return shared;
    }
    if (![super init]) return nil;
 
    NSMutableArray *array = [[NSMutableArray alloc] init];
    [array addObject:@"http://www.google.com"];
    [array addObject:@"http://www.apple.com"];
    [array addObject:@"http://www.yahoo.com"];
    [array addObject:@"http://www.zarrastudios.com"];
    [array addObject:@"http://www.macosxhints.com"];
    urlArray = array;
    queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:2];
    shared = self;
    return self;
}

In this updated init method, the queue is throttled down to 2 operations running at the same time. The rest of the operations will wait until one of the first two is completed and then they will get an opportunity to run until the queue is empty.

Conclusion

That is the NSOperation and NSOperationQueue in its most basic form. You will note that most of the code in this example has nothing to do with the building up or using of the NSOperation or the NSOperationQueue. The actual code required to use the NSOperation is amazingly small. Yet with this small amount of code you can easily start using multiple threads in your application and provide a better experience for the user and be able to fine tune those complicated tasks.

 

 类似资料:

相关阅读

相关文章

相关问答