Asynchronous methods in NSOperation
I'm fetching some data from Facebook Connect (using the FBConnect Objective-C 2.0 framework) and I'm doing all that in an NSOperation. It is in an NSOperation because I开发者_运维问答 have several other operations that run as well and this is one of them.
The problem is that all the FBConnect calls are asynchronous. Because of this, the main method of the NSOperation quickly finishes and the operation is marked as completed.
Is there some way to overcome this? It would appear there are no synchronous options in FBConnect!
Many thanks,
Mike
Below is a full example. In your subclass, after your async method completes, call [self completeOperation]
to transition to the finished state.
@interface AsynchronousOperation()
// 'executing' and 'finished' exist in NSOperation, but are readonly
@property (atomic, assign) BOOL _executing;
@property (atomic, assign) BOOL _finished;
@end
@implementation AsynchronousOperation
- (void) start;
{
if ([self isCancelled])
{
// Move the operation to the finished state if it is canceled.
[self willChangeValueForKey:@"isFinished"];
self._finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
self._executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void) main;
{
if ([self isCancelled]) {
return;
}
}
- (BOOL) isAsynchronous;
{
return YES;
}
- (BOOL)isExecuting {
return self._executing;
}
- (BOOL)isFinished {
return self._finished;
}
- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
self._executing = NO;
self._finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
@end
put your FBConnect
calls in 'start
', not 'main
', and manage the 'isFinished
' 'isExecuting
' properties. (and return YES
for 'isConcurrent
')
For more details, see Apple's documentation on writing concurrent NSOperations.
Please understand this if nothing else: There's nothing magic about NSOperation
's behaviour. NSOperationQueue
just uses Key Value Observation to monitor operations. The only reason why this isn't painfully easy is that the keys used aren't the same as what Objective-C 2.0 conventions say they should be, so the standard synthesized setters won't work.
The result is that when you define your NSOperation
subclass, you need to provide asynchronous
, executing
and finished
. And those last two need a bit of help on your part to work properly.
Sound complicated? It's not, it's just details. Each step along the way is simple and makes sense, but it won't actually work until you get all of them right.
First, the header:
//
// MyOperation.h
#import <Foundation/Foundation.h>
@interface MyOperation : NSOperation
@property(readonly, getter=isAsynchronous) BOOL asynchronous;
@property(readonly, getter=isExecuting) BOOL executing;
@property(readonly, getter=isFinished) BOOL finished;
@end
You could, of course, define executing
and finished
as readwrite
here so you don't need to redefine them as readwrite
in the implementation. But I like to know only my operations can change their state.
Now the implementation. There's a few steps here:
- redefine
finished
andexecuting
properties as read/write. - fully provide an implementation of
executing
andfinished
that manually provides the correct KVO messaging (soisExecuting
,setExecuting:
,isFinished
andsetFinished:
). - provide storage for
executing
andfinished
ivars using@synthesize
. - provide the implementation of
asynchronous
(Note that this code will probably scroll a bit.)
//
// MyOperation.m
#import "MyOperation.h"
@interface MyOperation()
@property(readwrite) BOOL executing;
@property(readwrite) BOOL finished;
@end
@implementation MyOperation
// Provide your own start.
- (void)start {
if (self.cancelled) {
self.finished = YES;
return;
}
NSLog(@"Starting %@", self);
self.executing = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSLog(@"Finished %@", self);
self.executing = NO;
self.finished = YES;
});
}
// The rest of this is boilerplate.
- (BOOL)isAsynchronous {
return YES;
}
@synthesize executing = _executing;
- (BOOL)isExecuting {
@synchronized(self) {
return _executing;
}
}
- (void)setExecuting:(BOOL)executing {
@synchronized(self) {
if (executing != _executing) {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
}
}
@synthesize finished = _finished;
- (BOOL)isFinished {
@synchronized(self) {
return _finished;
}
}
- (void)setFinished:(BOOL)finished {
@synchronized(self) {
if (finished != _finished) {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
}
}
@end
It's not really necessary to check (for example) executing != _executing
in the setter. The correct behaviour is provided automatically by calling willChangeValueForKey
, blindly changing the value, then calling didChangeValueForKey
. But the condition means you can put a breakpoint down on the assignment and only stop when the value is changed, and I've found that incredibly useful for debugging my operations in practice.
I've also seen this implemented by providing a custom state on top of the executing
and finished
properties. This works perfectly well, of course, and is in some ways better… but it also requires more knowledge of KVO than this example, and this is already enough.
Finally, note that I have not added support for cancel once the operation starts. To do that, you'd have to override cancel
(or maybe, more correctly, observe the value of isCancelled
) and handle it. That would complicate my simple start
example a lot.
I ran this code in a command line console app by adding 15 operations to a queue with a maxConcurrentOperationCount
of 5 then waiting on the queue to finish using waitUntilAllOperationsAreFinished
(this is why I used a background queue for dispatch_after
in my start
). This is the output:
2019-01-22 13:29:32.897893-0800 test[86762:4812871] Starting <MyOperation: 0x10058d2d0>
2019-01-22 13:29:32.897893-0800 test[86762:4812872] Starting <MyOperation: 0x10058d710>
2019-01-22 13:29:32.897903-0800 test[86762:4812873] Starting <MyOperation: 0x100589930>
2019-01-22 13:29:32.898161-0800 test[86762:4812871] Starting <MyOperation: 0x10058edc0>
2019-01-22 13:29:32.898166-0800 test[86762:4812873] Starting <MyOperation: 0x10058ed50>
2019-01-22 13:29:37.898487-0800 test[86762:4812872] Finished <MyOperation: 0x100589930>
2019-01-22 13:29:37.898489-0800 test[86762:4812870] Finished <MyOperation: 0x10058ed50>
2019-01-22 13:29:37.898548-0800 test[86762:4812874] Finished <MyOperation: 0x10058edc0>
2019-01-22 13:29:37.898797-0800 test[86762:4812870] Starting <MyOperation: 0x100590000>
2019-01-22 13:29:37.899160-0800 test[86762:4812870] Finished <MyOperation: 0x10058d710>
2019-01-22 13:29:37.899651-0800 test[86762:4812870] Starting <MyOperation: 0x1005901a0>
2019-01-22 13:29:37.899933-0800 test[86762:4812874] Starting <MyOperation: 0x100590340>
2019-01-22 13:29:37.900133-0800 test[86762:4812871] Finished <MyOperation: 0x10058d2d0>
2019-01-22 13:29:37.900504-0800 test[86762:4812871] Starting <MyOperation: 0x100590680>
2019-01-22 13:29:37.900583-0800 test[86762:4812874] Starting <MyOperation: 0x1005904e0>
2019-01-22 13:29:42.899325-0800 test[86762:4812871] Finished <MyOperation: 0x100590000>
2019-01-22 13:29:42.899541-0800 test[86762:4812874] Starting <MyOperation: 0x100590820>
2019-01-22 13:29:43.393291-0800 test[86762:4812871] Finished <MyOperation: 0x1005901a0>
2019-01-22 13:29:43.393298-0800 test[86762:4812874] Finished <MyOperation: 0x100590340>
2019-01-22 13:29:43.394531-0800 test[86762:4812874] Finished <MyOperation: 0x1005904e0>
2019-01-22 13:29:43.395380-0800 test[86762:4812874] Finished <MyOperation: 0x100590680>
2019-01-22 13:29:43.396359-0800 test[86762:4812874] Starting <MyOperation: 0x1005909c0>
2019-01-22 13:29:43.397440-0800 test[86762:4812872] Starting <MyOperation: 0x100590b60>
2019-01-22 13:29:43.397891-0800 test[86762:4812874] Starting <MyOperation: 0x100590d00>
2019-01-22 13:29:43.399711-0800 test[86762:4812872] Starting <MyOperation: 0x100590ea0>
2019-01-22 13:29:47.900058-0800 test[86762:4812984] Finished <MyOperation: 0x100590820>
2019-01-22 13:29:48.892953-0800 test[86762:4812872] Finished <MyOperation: 0x100590d00>
2019-01-22 13:29:48.892970-0800 test[86762:4812871] Finished <MyOperation: 0x100590b60>
2019-01-22 13:29:48.893019-0800 test[86762:4813163] Finished <MyOperation: 0x100590ea0>
2019-01-22 13:29:48.893562-0800 test[86762:4812984] Finished <MyOperation: 0x1005909c0>
Program ended with exit code: 0
How about this?
//
// Operation.m
#import "Operation.h"
@interface Operation()
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@end
@implementation Operation
- (void)main {
[self doWorkWithCompletion:^{
dispatch_semaphore_signal(self.semaphore);
}];
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
}
....
@end
精彩评论