I would like to run a concurrent NSOperation. For this reason, I have extended the NSOperation class and overridden the start and finish methods. It looks something like this:
#import "AVFrameConversionOP.h"
#implementation AVFrameConversionOP //extends NSOperation
- (void)start
{
[self willChangeValueForKey:#"isFinished"];
_isFinished = NO;
[self didChangeValueForKey:#"isFinished"];
// Begin
[self willChangeValueForKey:#"isExecuting"];
_isExecuting = YES;
[self didChangeValueForKey:#"isExecuting"];
if (_isCancelled == YES) {
NSLog(#"** OPERATION CANCELED **");
[self willChangeValueForKey:#"isFinished"];
_isFinished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
[self performTask];
[self finish];
}
- (void)finish
{
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_isExecuting = NO;
_isFinished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
NSLog(#"operationfinished, _isFinished is %d", _isFinished);
if (_isCancelled == YES)
{
NSLog(#"** OPERATION CANCELED **");
}
}
- (void)performTask
{
//The task is performed here
sleep(0.1); //This is just for demonstration purposes
}
#end
I have defined the completion block for this NSOperation in the main view controller in the viewDidLoad method:
- (void)viewDidLoad {
NSLog(#"viewDidLoad!");
[super viewDidLoad];
static int counter = 0;
_frameConvOP = [[AVFrameConversionOP alloc] init]; //Initializing the modified NSOperation class
__weak typeof(self) weakSelf = self;
_frameConvOP.completionBlock = ^ {
counter++;
//NSLogs don't seem to work here
/*if(counter>1){
exit(-1); //Never happens because counter only ever becomes =1
}*/
};
[[VideoPreviewer instance] setNSOperation:_frameConvOP]; //This is where I send the NSOperation object to the class where I'm using it
}
I'm starting the NSOperation by doing [frameConvOP start] inside of a loop. It is called continuously every time a frame is decoded, so that is to say it is called very often. I can see that the finish and start methods of the NSOperation are being called frequently through the logs. And the functions inside these methods are also being correctly called. However, the NSLogs inside the completion blocked aren't logged. I put an exit(-1) inside the completion block and the app crashed, but the NSLogs never show up. So I put a static counter in there and it only increments to 1. So the completion block is only being called once.
Can anyone explain why the completion block is being called only once? Is it because of the way I'm (incorrectly) handling the KVO for _isFinished and _isExecuting? The completion block should be called when _isFinished is set to YES and as far as I can see, I am doing that in the finish block. And the finish block is being called quite frequently as it should be.
Any help, guidance or direction is appreciated. Please let me know if I have made any errors or if I have been less-than-clear in the post.
Thanks,
From the NSOperation class reference:
An operation object is a single-shot object—that is, it executes its task once and cannot be used to execute it again.
You are not allowed to invoke -start multiple times on a single operation object.
Another thing, in your -finish method, you need to properly nest the -willChange... and -didChange... calls. That is, you need to call -didChange... in the reverse order of the -willChange... calls:
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_isExecuting = NO;
_isFinished = YES;
[self didChangeValueForKey:#"isFinished"];
[self didChangeValueForKey:#"isExecuting"];
Related
In my subclass of NSOperation I set 4 flags, and when an operation finishes its execution it is not removed to NSOperation queue, where it was added at the beginning, this thing cause a lot of issues in my app.
I suppose that the way I set these flags is not correct, could you please help with it. cause I really spend a lot of time on identifying this issue.
#property(assign, nonatomic) BOOL isCancelled;
#property(nonatomic, getter=isExecuting) BOOL executing;
#property(nonatomic, getter=isFinished) BOOL finished;
#property(readonly, getter=isAsynchronous) BOOL asynchronous;
//in initialisation
- (id)initWithURL:(NSURL*)url andRaw:(NSInteger)row
{
if (![super init])
return nil;
[self setTargetURL:url];
return self;
}
//the way I override KVO
- (BOOL)isExecuting
{
NSLog(#"Exec");
return (self.defaultSession != nil);//it doesn't work
}
- (BOOL)isFinished
{
NSLog(#"Finished");
return (self.defaultSession == nil); //it doesn't work, so I explicitly set the value
}
- (BOOL)isAsynchronous
{
return YES;
}
- (void)cancel
{
[super cancel];
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
self.isExecuting = NO;
self.isFinished = YES;
[self didChangeValueForKey:#"isFinished"];
[self didChangeValueForKey:#"isExecuting"];
if(self.downloadTask.state == NSURLSessionTaskStateRunning)
[self.downloadTask cancel];
[self finish];
}
- (void)finish
{
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
self.defaultSession = nil; //NSURLSession
self.isFinished = YES;
[self didChangeValueForKey:#"isFinished"];
[self didChangeValueForKey:#"isExecuting"];
}
Thank you in advance
EDIT:
finally I found the issue - it was NSURLSession inside the queue. It kept strong reference to the queue and didn't allow it to be deallocated and removed from NSOperationQueue.
I have done the exact same thing, albiet in Swift.
There are a couple of aspects that I have implemented differently and are listed below:
I have noticed that we do not have to override cancel() method in the
Asynchronous operation. The default behavior of NSOperation's cancel
method is to set the self.cancelled boolean to true.
self.executing should be set to true only inside the overriden start() method of operation, but not in the init ( Not sure if this wil cause any issues). Also before setting the self.executing = true in the start() method, I ensure that the boolean self.cancelled is false. If self.cancelled = true, we should set self.finished = true.
Also , I have created a "didSet" property observer for the isExecuting and isFinished properties where I call the willChangeValueForKey and didChangeValueForKey. I am not 100% sure how to replicate the didSet behavior in Obj C. ( This says to overrride setter)
Please refer to the Ray Wenderlich video tutorials about "Concurrency" where Sam DAvies explains the creation of NSoperation subclass for Asynchronous operation. Please note, that it is only for subscribers and it is explained in Swift. I believe if you fix points 1 and 2, you should see your issues being fixed.
When I first call following code from dispatch queue, completion block not called.
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_queue_t initialize_queue;
initialize_queue = dispatch_queue_create("init", NULL);
dispatch_async(initialize_queue, ^{
_onInit = YES;
_isRunning = NO;
[self startProgress];
_onInit = NO;
});
}
- (void)startProgress
{
if (!_isRunning) {
_isRunning = YES;
NSLog(#"Starting");
[UIView animateWithDuration:0.5 animations:^{
self.progressStatusButton.hidden = NO;
self.activityLeftConstraint.constant = _activityLeftSpace;
self.activityWidthConstraint.constant = _activityWidth;
self.buttonWidthConstraint.constant = _progressStatusButtonWidth;
self.buttonLeftConstraint.constant = _progressStatusButtonLeftSpace;
self.activityView.alpha = 1.0;
}completion:^(BOOL finished){
NSLog(#"Start Animating");
[self.activityView startAnimating];
}];
}
}
When I delete dispatch_aync method and execute startProgress method in viewDidLoad,
completion block called. How should I correct my code?
I tried to change dispatch_async to use dispatch_async(dispatch_get_main_queue(),..),
then completion block called, but I would like to execute asynchronously startProgress method.
Please Let me know.
Everything connected with GUI should be called from main thread/ pushed to main queue. In other case expected behavior is not guaranteed.
So you should use
dispatch_get_main_queue()
Also animation ALWAYS executed asynchronously so I do not think you really need separate queue for it.
I have two methods that both create a random sprite node and add it to the scene. Let's call them spriteMethod1 and spriteMethod2.
I'd like to have a looping method that runs spriteMethod1, 5 times, and then spriteMethod2 once. There also needs to be a delay between each time the spriteMethods are called.
I thought the following might work, but it doesn't:
-(void) addObjects {
for (int i = 0; i < 5; i++) {
[self performSelector:#selector(spriteMethod1) withObject:nil afterDelay:2];
}
[self performSelector:#selector(spriteMethod2) withObject:nil afterDelay:3];
[self performSelector:#selector(addObjects) withObject:nil afterDelay:5];
}
I know this might not be the best solution, but it works for me:
-(void)addObjects {
[self performSelector:#selector(spriteMethod1) withObject:nil afterDelay:2];
[self performSelector:#selector(spriteMethod1) withObject:nil afterDelay:4];
[self performSelector:#selector(spriteMethod1) withObject:nil afterDelay:6];
[self performSelector:#selector(spriteMethod1) withObject:nil afterDelay:8];
[self performSelector:#selector(spriteMethod1) withObject:nil afterDelay:10];
[self performSelector:#selector(spriteMethod2) withObject:nil afterDelay:13];
[self performSelector:#selector(addObjects) withObject:nil afterDelay:18];
}
Add a timer in your interface:
#property (nonatomic, weak) NSTimer *timer;
schedule a timer somewhere in your code
self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(spriteMethod1) userInfo:nil repeats:YES];
do this
int count = 0;
- (void)spriteMethod1 {
count ++;
// do your work here
if(count == 5) {
// stop the timer
[self.timer invalidate];
// call the other methods
[self performSelector:#selector(spriteMethod2) withObject:nil afterDelay:3];
[self performSelector:#selector(addObjects) withObject:nil afterDelay:5];
}
}
The problem is that you won't see the delay because the selector is enqueued on the thread loop. From the documentation:
This method sets up a timer to perform the aSelector message on the
current thread’s run loop. The timer is configured to run in the
default mode (NSDefaultRunLoopMode). When the timer fires, the thread
attempts to dequeue the message from the run loop and perform the
selector. It succeeds if the run loop is running and in the default
mode; otherwise, the timer waits until the run loop is in the default
mode.
Another approach is to make the thread sleep for a few seconds.
[self spriteMethod1];
[NSThread sleepForTimeInterval:2.0f];
In this case your User Interface will hang if you don't execute this code in a separate thread.
This out the top of my head.. not sure if it'll do the trick:
- (void)startAddingObjects
{
NSNumber *counter = #(0);
[self performSelector:#selector(addMoreUsingCounter:)
withObject:counter
afterDelay:2];
}
- (void)addMoreUsingCounter:(NSNumber *)counter
{
int primCounter = [counter intValue];
if (primCounter < 5)
{
[self spriteMethod1];
[self performSelector:#selector(addMoreUsingCounter:)
withObject:#(primCounter++)
afterDelay:2];
}
else if (primCounter < 7)
{
[self spriteMethod2];
[self performSelector:#selector(addMoreUsingCounter:)
withObject:#(primCounter++)
afterDelay:3];
}
}
You'll probably still need to relate the delay to the counter to get the exact results you need.
I think your version doesn't work, because the "afterDelay" param is relative to the moment of invocation. You would need to multiply it with "i", in the for loop, and then use 13 and 18 respectively for the last two selectors.
Look into using an NSOperationQueue. You can set its maxConcurrentOperationCount to 1, to ensure it executes its actions sequentially. E.g.
NSOperationQueue * opQueue = [[NSOperationQueue alloc] init];
opQueue.maxConcurrentOperationCount = 1; // this ensures it'll execute the actions sequentially
NSBlockOperation *spriteMethod1Invoker = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 5; ++i)
{
[self spriteMethod1];
sleep(2); // sleep 2 secs between invoking spriteMethod1 again
}
}];
NSInvocationOperation *spriteMethod2Invoker = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(spriteMethod2) object:nil];
[opQueue addOperation:spriteMethod1Invoker];
[opQueue addOperation:spriteMethod2Invoker];
I have a method that draws a one sprite on the screen with the animation effects,
if I call in init this method
if( (self=[super init]) ) {
// ....
[self myMethod];
// .....
}
Then he does it once on my project
When I call by schedule
-(void)schedulMyMethod:(ccTime)dt {
[self myMethod];
}
if( (self=[super init]) ) {
// ....
[self schedule:#selector(schedulMyMethod:) interval:0.5];
// .....
}
It runs for an unlimited times
I need so that I can call the my method some amount
You mean, you want to have it repeat N times? You'll need to keep state on times remaining for it to run.
#property (nonatomic, assign) NSInteger timesToRunMyMethod;
- (void)beginRunningMyMethod {
self.timesToRunMyMethod = 100; // N==100
[self myMethod];
}
- (void)myMethod {
self.timesToRunMyMethod--;
// do stuff
if (self.timesToRunMyMethod > 0) {
// i used native delayed execution, you can replace it with whatever cocos2d offers if you want
[self performSelector:#selector(myMethod) withObject:nil afterDelay:0.5];
}
}
And it's probably wrong to start this on init. Is it a view controller? Then you can use viewDidAppear or willAppear.
My application will query a database 50 times and will then react accordingly by adding the right UIImageView to another UIImageView, which I then want the application to display immediately at each loop.
Alas, after many nights I have failed to get it to work. The application will not immediately display the UIImageView. Having said that, when I zoom in or zoom out during mid loop the UIImageViews will appear! I must be missing something...
So far every code works except the last part [UIScrollView performSelector....
Please help and thank you in advance.
UIScrollView *firstView;
UIImageView *secondView;
UIImageView *thirdView;
NSOperationQueue *queue;
NSInvocationOperation *operation;
- (void)viewDidAppear:(BOOL)animated
{
queue = [NSOperationQueue new];
operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(getData) object:nil];
[queue addOperation:operation];
}
- (void) getData
{
for (i=0 ; i < 50 ; i++)
{
//All the codes to facilitate XML parsing here
[nsXMLParser setDelegate:parser];
[BOOL success = [nsXMLParser parse];
if (success)
{
if ([parser.currentValue isEqualToString:#"G"])
thirdView.image = greenTick.jpg;
[secondView addSubview:thirdView];
}
else
{
NSLog(#"Error parsing document!");
}
}
[thirdView performSelectorOnMainThread:#selector(setNeedsDisplay) withObject:nil waitUntilDone: YES];
}
I discovered the solution and truly hope this will help someone...
if (success)
{
if ([parser.currentValue isEqualToString:#"G"])
// Make the changes here - start
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(main_queue, ^{
thirdView.image = greenTick.jpg;
[secondView addSubview:thirdView];
});
// Make the changes here - end
}
else
{
NSLog(#"Error parsing document!");
}
}
// Remove performSelectorOnMainThread