I have made a subclass of NSOperation called ˚ to achieve multiple movie downloads . In the appDelegate.m , I have made an object of NSOperationQueue .
- (void)applicationDidFinishLaunching:(UIApplication *)application {
queue = [[NSOperationQueue alloc] init];
[queue setMaximumConcurrentOperationCount:5]
}
MovieDownloadOperation depends on a class called Downloader which actually downloads the movie
and gives callback movieCompletelyDownloadedWithUrl: .
Then , I have made a property called downloadState in MovieDownloadOperation . It has different values like "STARTED" , "DOWNLOADING" , "COMPLETED" , "ERROR".
MyDownloadOperation looks like
-(id)initWithUrl:(NSURL *)url
{
if (self = [super init])
{
_downloader = [[Downloader alloc] initWithUrl:url];
_downloadState = #"STARTED" ;
}
}
-(void)main
{
while(1)
{
if ([_downloadState isEqualToString:#"COMPLETED"])
{
NSLog(#"movie downloaded successfully");
break ;
}
}
}
-(void)movieCompletelyDownloadedWithUrl:(NSURL *)url
{
_downloadState = #"COMPLETED" ;
}
This works well for one movie , but when I try to download more than one movie , the UI freezes until the first is downloaded . I think the the problem is the while loop inside the main method , is there a better way to check if the _downloadState is changed to "COMPLETED" ??
It's unclear why the UI freezes with multiple operations, but not with only one download. But, your code sample provokes a couple of thoughts:
Concurrent Operation:
Rather than having a while loop in main, and you'd generally would define your operation to be concurrent (i.e. return YES from isConcurrent). Then movieCompletelyDownloadedWithUrl would post the isFinished event, which would trigger the completion of the operation.
In terms of how to make a concurrent operation, you might define properties for executing and finished:
#property (nonatomic, readwrite, getter = isFinished) BOOL finished;
#property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
You'd probably want to have a strong property for the URL and the downloader:
#property (nonatomic, strong) NSURL *url;
#property (nonatomic, strong) Downloader *downloader;
And then you might have the following code in the operation subclass:
#synthesize finished = _finished;
#synthesize executing = _executing;
- (id)init
{
self = [super init];
if (self) {
_finished = NO;
_executing = NO;
}
return self;
}
- (id)initWithUrl:(NSURL *)url
{
self = [self init];
if (self) {
// Note, do not start downloader here, but just save URL so that
// when the operation starts, you have access to the URL.
_url = url;
}
return self;
}
- (void)start
{
if ([self isCancelled]) {
self.finished = YES;
return;
}
self.executing = YES;
[self main];
}
- (void)main
{
// start the download here
self.downloader = [[Downloader alloc] initWithUrl:self.url];
}
- (void)completeOperation
{
self.executing = NO;
self.finished = YES;
}
// you haven't shown how this is called, but I'm assuming you'll fix the downloader
// to call this instance method when it's done
- (void)movieCompletelyDownloadedWithUrl:(NSURL *)url
{
[self completeOperation];
}
#pragma mark - NSOperation methods
- (BOOL)isConcurrent
{
return YES;
}
- (void)setExecuting:(BOOL)executing
{
[self willChangeValueForKey:#"isExecuting"];
_executing = executing;
[self didChangeValueForKey:#"isExecuting"];
}
- (void)setFinished:(BOOL)finished
{
[self willChangeValueForKey:#"isFinished"];
_finished = finished;
[self didChangeValueForKey:#"isFinished"];
}
So, with these methods, you might then have movieCompletelyDownloadedWithUrl call completeOperation like above, which will ensure that isExecuting and isFinished notifications get posted. You'd also want to respond to cancellation event, too, making sure to cancel the download if the operation is canceled.
See Configuring Operations for Concurrent Execution section of the Concurrency Programming Guide for more details.
Don't initiate download until main:
I don't see your main method initiating the download. That makes me nervous that your Downloader initialization method, initWithURL, might be initiating the download, which would be bad. You don't want downloads initiating when you create the operation, but rather you shouldn't do that until the operation starts (e.g. start or main). So, in my above example, I only have initWithURL save the URL, and then main is what starts the download.
Using NSURLConnectionDataDelegate methods in NSOperation:
As an aside, you didn't share how your operation is doing the network request. If you're using NSURLConnectionDataDelegate methods, when you get rid of that while loop in main, you might have problems if you don't schedule the NSURLConnection in a particular run loop. For example, you might do:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
If you're not using NSURLConnectionDataDelegate methods, or if you've already addressed this run loop issue, then ignore this counsel, but, bottom line, when you fix the main method in your operation, you might expose the NSURLConnection issue that your old main might have hidden from you.
How does Downloader invoke moveCompleteDownloadedWithUrl?
BTW, you're not showing how Downloader could possibly invoke moveCompleteDownloadedWithUrl. That looks suspicious, but I'm just hoping you simplified your code when you posted it. But if you're not using a protocol-delegate pattern or completion block pattern, then I'd be very nervous about how your multiple Downloader objects are informing the respective MyDownloadOperation objects that the download is done. Personally, I might be inclined to refactor these two differ classes into one, but that's a matter of personal taste.
You can use NSTimer to check whether your download is completed or not. It'll not freezes your UI
NSTimer *localTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(checkDownloadStatus) userInfo:nil repeats:YES];
-(void)checkDownloadStatus
{
if ([_downloadState isEqualToString:#"COMPLETED"])
{
NSLog(#"movie downloaded successfully");
[localTimer invalidate];
}
}
Related
Following is the subclass implementation of NSOperation Subclass
The operation will be use to asynchronously download Image from server.
-(void) main{
#autoreleasepool {
//NSURLConnection
NSURLRequest *request=[NSURLRequest requestWithURL:[NSURL URLWithString:#"A URl"]];
_downloadConnection=[[NSURLConnection alloc] initWithRequest:request delegate:self];
}
}
-(BOOL)isConcurrent{
return YES;
}
-(BOOL)isExecuting{
return _isExecuting;
}
-(BOOL)isFinished{
return _isFinished;
}
-(void)finish{
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_isExecuting = NO;
_isFinished =YES;
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
}
-(void)cancel{
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_isExecuting = NO;
_isFinished =YES;
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[connection cancel];
connection=nil;
[self finish];
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
//Other Code
[connection cancel];
connection=nil;
[self finish];
}
Please let me know if there might be anything that I might have missed out within the code, so as to avoid leaks and check all KVO are handled properly.
I see a couple of issues:
Your finish and cancel routines are calling willChangeValueForKey twice for each key. Obviously that second call should be didChangeValueForKey.
I would advise against implementing cancel method. The default implementation does some other stuff. Do not implement that method. If you really want to, there are changes I'd advise here (at very least, cancel connection, too; call super; some other things, too), but I'd simply advise against implementing it at all and detect cancellation in didReceiveData (see point #5).
This code does not appear to be setting _isExecuting (nor do appropriate KVO) when the operation starts. Maybe that's in a start method you neglected to share with us?
Likewise, that start method should check to see if the operation has been canceled already and if so, stop the operation immediately.
In didReceiveData, are you checking isCancelled, as well? Making operations cancelable is one of the main reasons you use operation queues.
You're starting a NSURLConnection within an operation (presumably with the intent to add this operation to some random queue). But NSURLConnection will not work correctly unless you either schedule it on the main run loop (the easy solution) or you create your own run loop alive (there are a variety of techniques of that).
For example, to tell the operation to schedule the connection in the main run loop, you could do something like:
_downloadConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:FALSE];
[_downloadConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] runLoopModes:NSRunLoopCommonModes];
[_downloadConnection start];
I'm typing that without benefit of Xcode, so if I mistyped a method forgive me, but it illustrates the idea: Create connection with startImmediately of FALSE, schedule it on the main run loop, and only then should you start it.
If connectionDidFinishLoading is called, it is entirely unnecessary to call [connection cancel].
As of iOS 7, isConcurrent has been deprecated in favor of isAsynchronous. If you need to support earlier iOS versions, though, keep isConcurrent.
By the way, while I think it might work the way you've laid it out, it is generally advised to implement properties called executing and finished:
#property (nonatomic, readwrite, getter=isExecuting) BOOL executing;
#property (nonatomic, readwrite, getter=isFinished) BOOL finished;
I then manually synthesize:
#synthesize finished = _finished;
#synthesize executing = _executing;
And then I implement manual setters (but rely upon the synthesized getters and ivars):
- (void)setExecuting:(BOOL)executing
{
if (_executing != executing) {
[self willChangeValueForKey:#"isExecuting"];
_executing = executing;
[self didChangeValueForKey:#"isExecuting"];
}
}
- (void)setFinished:(BOOL)finished
{
if (_finished != finished) {
[self willChangeValueForKey:#"isFinished"];
_finished = finished;
[self didChangeValueForKey:#"isFinished"];
}
}
But if you do that, you can now just set self.executing = FALSE (or whatever) and it (a) does appropriate KVO, saving you from littering your code with all sorts of KVO calls; and (b) but saves you from having to implement properties and getters manually.
Im trying to subclass a NSOperation, and read some sample from,
they say: when the task finished, using KVO of NSOperation, to finish the operation,
code here:
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"]
finished = YES;
executing = NO;
[self didChangeValueForKey:#"isFinished"];
[self didChangeValueForKey:#"isExecuting"];
then isFinished get called
- (BOOL) isFinished{
return(finished);
}
anyone could explain this to me? why isFinished gets called, will the isFinished finish the operation? as I understanded, do KVO manually need [self didChangeValueForKey:#"isExecuting"]; and I didnt see code like addobserver: and observeValueForKeyPath:
I write
-(void)call
{
[self willChangeValueForKey:#"isVip"];
[self didChangeValueForKey:#"isVip"];
}
-(void)isVip
{
NSLog(#"Im vip");
}
isVip is not called when do [self call];
The NSOperationQueue implementation will observe the "isFinished" property of your operation (using KVO) so it knows when to remove it from the queue. isFinished is most likely being called by internal Apple code after it has been told of the change to its value.
Adding to quellish answer, this is how you would override executing, finished, cancelled.
//.m
#interface MyOperation () //class extension, make these otherwise read-only properties read-write, we must synthesize
#property(atomic, assign, readwrite, getter=isExecuting) BOOL executing;
#property(atomic, assign, readwrite, getter=isFinished) BOOL finished;
#property(atomic, assign, readwrite, getter=isCancelled) BOOL cancelled;
#end
#implementation CoreLocationOperation
#synthesize executing, finished, cancelled;
+ (BOOL)automaticallyNotifiesObserversForKey {
return YES;
}
+ (NSSet *)keyPathsForValuesAffectingIsCancelled {
NSSet *result = [NSSet setWithObject:#"cancelled"];
return result;
}
+ (NSSet *)keyPathsForValuesAffectingIsExecuting {
NSSet *result = [NSSet setWithObject:#"executing"];
return result;
}
+ (NSSet *)keyPathsForValuesAffectingIsFinished {
NSSet *result = [NSSet setWithObject:#"finished"];
return result;
}
- (void)start {
//..
//You can use self.executing = YES; (note we can change executing which would otherwise be read-only because we synthesized our own ivar.
[self setExecuting:YES];
...
}
- (void)cancel {
//..
//super will change the properties executing/finished for us or we can do it manually
[super cancel];
...
}
#end
I think this is clearer than having
[self willChangeValueForKey:_NSURLOperationIsFinished];
[self setIsFinished:YES];
[self didChangeValueForKey:_NSURLOperationIsFinished];
First, you should not need to do manual KVO notifications. For an NSOperation subclass KVO notifications should be sent automatically unless your class has opted out of automatic KVO notifications by implementing +automaticallyNotifiesObserversForKey or +automaticallyNotifiesObserversOf<Key> to return NO.
NSOperation subclasses are not very useful unless they are added to an NSOperationQueue. When an operation is added to a queue, the queue uses KVO to observe the properties that indicate state changes, such as finished, executing, cancelled, etc. Note that these are not isFinished, isExecuting, or isCancelled - those are the names of the synthesized get accessors for those properties.
In your question you include this code:
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"]
finished = YES;
executing = NO;
[self didChangeValueForKey:#"isFinished"];
[self didChangeValueForKey:#"isExecuting"];
What you're doing here is sending manual KVO notifications for the get accessors, not the properties being observed. Instead, this would accomplish what you seem to be trying to do:
[self setFinished:YES];
[self setExecuting:NO];
Rather than accessing instance variables directly, use the accessor methods. This will correctly send automatic change notifications for these properties.
If you are truly paranoid about KVO and want to send notifications for the get accessor key paths such as isFinished, register your property as a dependency of the key path:
+ (NSSet *) keyPathsForValuesAffectingIsFinished {
NSSet *result = [NSSet setWithObject:#"finished"];
return result;
}
Registering dependencies is part of KVO compliance.
i make a AdView extends UIView like this
AdView:
//nerver call dealloc when adview release
-(void)dealloc
{
//stop thread
bStart = NO;
//...
[super dealloc];
}
-(id)init
{
//.....
bStart = YES;
//the self will retain by NSThread,i try to call [self performBackground..:onThrad] or timer the same too.
NSThread* thead = [[NSThread alloc] initWithTagert:self ...:#select(onThread)];
[thread start];
[thread release];
}
-(void)onThread
{
while(bStart)
{
//....
}
}
the controller
{
AdView* view = [[AdView alloc] init];
view.delegate = self;// i am ture delegate is not retain
[self.view addSubView:view];
[view release]
}
Adview has never to call dealloc when contoller release,
who konws how to fix it.
As others noted you are passing self to the target initialization which retains it. That's why you have an extra retain causing the object not being deallocated.
That said, let me give you two pieces of advice here:
Use ARC. It's 2013, we suffered with manual reference counting for about enough time.
Use GCD. It's 2013, we suffered with manual threads management for about enough time.
A modern version of your code would look like
- (instancetype)init {
//...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doStuffAsynchronously];
});
//...
}
- (void)doStuffAsynchronously { ... }
EDIT
As JFS advices in the comments, if you need to start and stop the background execution you should consider using a NSOperation within a NSOperationQueue. A naive (but still functional) implementation would be:
#property (nonatomic, strong) NSOperationQueue * operationQueue;
//...
- (instancetype)init {
//...
self.operationQueue = [NSOperationQueue new];
[operationQueue addOperationWithBlock:^{
[self doStuffAsynchronously];
}];
//...
}
- (void)doStuffAsynchronously { ... }
- (void)stopDoingStuff {
[self.operationQueue cancelAllOperations];
}
A neater approach, though, would be to subclass NSOperation, starting it by adding it to a queue and stopping it by invoking stop.
The thread retains the target self in start. So the object can not go away as long as the thread runs.
The controller should stop the thread by calling something like adView.bStart = NO; (which of course you have to implement).
I've got class:
ClassX.m
#property (assign) BOOL wasProcessed;
-(void) methodA { //<- this can be called many times in short period of time
dispatch_async(dispatch_get_main_queue(), ^{
[self methodB];
});
}
- (void) methodB {
if (!self.wasProcessed) {
self.wasProcessed = YES;
//... some code
}
}
Since dispatch_async is used so a few calls to methodB can be processed concurrently at the same time and following code needs to be atomic:
if (!self.wasProcessed) {
self.wasProcessed = YES; //e.g two calls can enter here before setting YES and it would be bad because I want to process it only one time
How can those 2 lines be made atomic (checking and setting variable)? I dont want to make atomic code that is after "self.wasProcessed = YES;" so moving whole if to #synchronize(self) won't be good solution. If there is anything wrong with my thinking please point it out as I'm not very experienced in those topics, Thank you.
Try #synchronized. While the enclosed code is being executed on a thread, it will block other threads from executing it.
- (void) methodB {
#synchronized(self) {
if (!self.wasProcessed) {
self.wasProcessed = YES;
//... some code
}
}
}
-(void) methodA {
dispatch_async(dispatch_get_main_queue(), ^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^(){
[self methodB];
}];
});
}
Your's methodB will be only called in main thread, so it will be never performed simultaneously.
I have recently been debugging a zombie issue with operations and found out that calling cancelAllOperations on the queue didn't cancel the operation in question, and in fact, the operation queue was empty even though the operation was still running.
The structure was a viewcontroller asynchronously loading a set of images off the web and perform some changes on them. Relevant (anonymised) exerpts follow:
#implementation MyViewController
- (id) init
{
(...)
mOperationQueue = [[NSOperationQueue alloc] init];
(...)
}
- (void) viewDidAppear:(BOOL)animated
{
(...)
MyNSOperation * operation = [[MyNSOperation alloc] initWithDelegate:self andData:data];
[mOperationQueue addOperation:operation];
[operation release];
(...)
}
- (void) dealloc
{
(...)
[mOperationQueue cancelAllOperations];
[mOperationQueue release];
(...)
}
- (void) imagesLoaded:(NSArray *)images
{
(...)
}
And the operation in question:
#implementation MyNSOperation
- (id) initWithDelegate:(id)delegate andData:(NSDictionary *)data
{
self = [super init];
if (self)
{
mDelegate = delegate; // weak reference
mData = [data retain];
(...)
}
return self;
}
- (void) main
{
NSAutoReleasePool * pool = [[NSAutoReleasePool alloc] init];
mImages = [[NSMutableArray alloc] init];
// load and compose images
mAlteredImages = (...)
[self performSelectorOnMainThread:#selector(operationCompleted) withObject:nil waitUntilDone:YES];
[pool release];
}
- (void)operationCompleted
{
if (![self isCancelled])
{
[mDelegate imagesLoaded:mAlteredImages];
}
}
The observed flow is as follows:
The viewcontroller is shown, calling init and viewDidAppear starting the operation.
[mOperationQueue operations] contains exactly one element;
Shortly after, the operation enters main and
The viewcontroller is exited by the user before the operation completes.
dealloc is called on the viewcontroller (because the operation keeps a weak reference)
[mOperationQueue operations] contains zero (!) elements
cancelAllOperations is sent to the operation queue
[NSOperation cancel] is not called, resulting in an app-visible bogus state.
dealloc finishes
the operation completes
isCancelled returns false, resulting in a zombie call
The documentation of NSOperationQueue however explicitly states that "Operations remain queued until they finish their task." which looks like a breach of contract.
I've fixed the crash by keeping a reference to the operation and manually sending cancel, but I would like to know why the original approach isn't working to prevent further problems. Can someone shed some light on this?
Thanks in advance.
cancelAllOperations does not cancel an already started operation. It only informs the operation about that fact and let the operation cancel themself, whenever it want. Thus, you can get a raise condition. Proceed with deallocacion, after you are sure that the operation is canceled.