问题
Long time lurker, first time poster. I'm relatively new to objective-C so my apologies if I'm asking something fairly simple. My google & stack overflow-fu has let me down here, so I figured somebody could maybe help.
I have a synchronous process executing, say, three functions in a row - call it A -> B-> C , where task A executes, followed by B, followed by C.
Now, B involves an asynchronous process with a delegate callback for completion. But B must complete before C is executed, so I need some mechanism such that C is not triggered before B has finished. I imagine there must be a common design pattern for this problem?
Initially naive solution would be -
execute A
execute B
while (!B finished) {}
execute C
...but this seems really lame.
I suspect I can do this with some kind of block, but for the life of me I just can't figure it out. Could anyone help?
appreciate any assistance!
Guillaume
回答1:
Thanks for all the feeback - apologies for not responding sooner. I've now resolved this in a slightly different way to the suggestions:
Firstly, I extended NSObject to have the following method -
#import "NSObject+LTExtensions.h"
@implementation NSObject (Testing)
- (void) performSelectorWithBlock: (SEL) selector withSemaphore:(dispatch_semaphore_t)semaphore
{
[self performSelector:selector]; // This selector should complete the semaphore
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
}
@end
This allows me to execute a block via a selector. When the block executes, the thread on which it is executed will wait until signaled to proceed by a specific dispatch semaphore.
What we can then do is as follows:
- Call A
- Create a dispatch semaphore and define a selector which executes B
- Call the method defined above to execute B and wait for the selector to complete
- When B is completed (via a delegate callback), it signals the dispatch semaphore to suspend the wait
- I then execute C
So we have
A
B -> Asynchronous with delegate callback
C
Here's a simple example of how the above is implemented
-(void) methodA {
// ... do something
// Assign your semaphore (this is a dispatch_semaphore_t)
self.semaphore = dispatch_semaphore_create(0);
[self performSelectorWithBlock:@selector(methodB) withSemaphore:semaphore];
[self methodC];
}
-(void) methodB {
// ... do whatever needs to be done asynchronously
CFRunLoopRun();
}
-(void) methodBDelegateCallBack {
// This is called when B completes
// Signal completion
dispatch_semaphore_signal(self.semaphore);
CFRunLoopStop(CFRunLoopGetCurrent());
}
-(void) methodC {
...
}
Works very well without any issues (but I am new to Obj C, so there may be glaring issues with my approach).
回答2:
Another approach to this problem might be the following: create an helper object for the async task and copy a completion block when the task is called. Call the completion block using the delegate methods once the async task is finished. As a result we might execute the tasks in order like the following:
FSTask *taskA = [FSTask taskWithName:@"Task A"];
FSAsyncTask *taskB = [FSAsyncTask asyncTaskWithName:@"Task B"];
FSTask *taskC = [FSTask taskWithName:@"Task C"];
[taskA performTaskWithCompletionBlock:^ (NSString *result) {
NSLog(@"%@", result);
[taskB performTaskWithCompletionBlock:^ (NSString *result) {
NSLog(@"%@", result);
[taskC performTaskWithCompletionBlock:^ (NSString *result) {
NSLog(@"%@", result);
}];
}];
}];
So how is this achieved? Well, look at the task objects below ...
FSTask.m - synchronous work on main thread ...
@interface FSTask ()
@property (nonatomic, copy) NSString *name;
@end
@implementation FSTask
@synthesize name = _name;
+ (FSTask *)taskWithName:(NSString *)name
{
FSTask *task = [[FSTask alloc] init];
if (task)
{
task.name = name;
}
return task;
}
- (void)performTaskWithCompletionBlock:(void (^)(NSString *taskResult))block
{
NSString *message = [NSString stringWithFormat:@"%@: doing work on main thread ...", _name];
NSLog(@"%@", message);
if (block)
{
NSString *result = [NSString stringWithFormat:@"%@: result", _name];
block(result);
}
}
@end
FSAsyncTask.m - asynchronous work on background thread ...
@interface FSAsyncTask ()
@property (nonatomic, copy) void (^block)(NSString *taskResult);
@property (nonatomic, copy) NSString *name;
- (void)performAsyncTask;
@end
@implementation FSAsyncTask
@synthesize block = _block;
@synthesize name = _name;
+ (FSAsyncTask *)asyncTaskWithName:(NSString *)name
{
FSAsyncTask *task = [[FSAsyncTask alloc] init];
if (task)
{
task.name = name;
}
return task;
}
- (void)performTaskWithCompletionBlock:(void (^)(NSString *taskResult))block
{
self.block = block;
// the call below could be e.g. a NSURLConnection that's being opened,
// in this case a NSURLConnectionDelegate method will return the result
// in this delegate method the completion block could be called ...
dispatch_queue_t queue = dispatch_queue_create("com.example.asynctask", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^ {
[self performAsyncTask];
});
}
#pragma mark - Private
- (void)performAsyncTask
{
for (int i = 0; i < 5; i++)
{
NSString *message = [NSString stringWithFormat:@"%d - %@: doing work on background thread ...", i, _name];
NSLog(@"%@", message);
[NSThread sleepForTimeInterval:1];
}
// this completion block might be called from your delegate methods ...
if (_block)
{
dispatch_async(dispatch_get_main_queue(), ^ {
NSString *result = [NSString stringWithFormat:@"%@: result", _name];
_block(result);
});
}
}
@end
回答3:
You can assign a block property to B where it would be used to execute a block of code before calling the delegate method. something like:
@property (nonatomic, copy)void(^yourBlock)(id blockParameter);
So, after calling B's delegate, you could call upon this block and execute it. Inside this block, you can call C's method.
回答4:
the way I handled this is.
I created a NSMutableDictionary before the async call.
Then i make the async call. and do a check for the value I am waiting for
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[AsyncCallClass asyncCall:^{
@synchronized(dictionary) {
[dictionary setValue:myValue forKey:@"result"];
}
}];
while (true){
@synchronized(dictionary){
if ([dictionary valueForKey:@"resultValue"] != nil){
break;
}
}
[NSThread sleepForTimeInterval:.25];
}
MyResultClass *result = [dictionary valueForKey:@"resultValue"];
you can add time out for this too to stop it from being an infinite loop. but this is my solution. and it seems to work pretty well.
回答5:
Here is the typical code I use to do such things (adapt the completionBlock signature and method names to your needs of course)
typedef void (^BCompletionBlock)(void);
@interface B : NSObject <BDelegate>
@property(nonatomic, copy) BCompletionBlock completionBlock;
-(void)doAsynchronousActionWithCompletion:(BCompletionBlock)aCompletionBlock;
@end
@implementation B
-(void)doAsynchronousActionWithCompletion:(BCompletionBlock)aCompletionBlock
{
// Store the completion block for later use
self.completionBlock = aCompletionBlock;
// Then execute your asynchronous action, that will call some delegate method when done
[self doYourAsynchronousActionWithDelegate:self];
}
-(void)yourBDelegateMethodCalledWhenDone
{
// Upon your async task completion, call your completion block then
if (self.completionBlock) self.completionBlock();
}
@end
Then here is an example usage:
-(void)doActions
{
[a doSynchronousAction];
[b doAsynchronousActionWithCompletion:^{
[c doSynchronousAction];
// A,B,C are now done
}];
}
I do this quite all the time to "convert" actions that uses delegate methods (to tell me when they are done) to actions that uses completionBlocks (have some classes to do this for UIAlertViews, UIActionsSheets, and many more cases for example) and it works like a charm.
I find it much more easier to use completionBlocks than the delegate mechanism in such cases.
回答6:
You can also pass C in a block like so...
define a custom block
typedef void(^myCompletion)(BOOL complete);
Create your B method
-(void)performBWithCompletionBlock:(myCompletion)complete;
{
// do your things
[self.delegate delegateCallback];
complete(YES);
}
then create BG / async ABC
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // now we're on a BG queue to perform our async tasks
[self performA];
[self performBWithCompletionBlock:^(BOOL complete) {
if (complete == YES)
[self performC];
}];
});
If you want C to be on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self performC];
});
来源:https://stackoverflow.com/questions/12184068/completion-blocks-asynchronous-processes-embedded-in-synchronous-workflow