I have a status application, it's long to post it all so I'll describe it and post only part of the code:
In the xib file there are two objects: AboutController and PreferencesController;
The app delegate is able to launch AboutController's and PreferencesController's panels;
The panels are in the xib file too;
The user by selecting a status menu item is able to launch these two panels;
There is a timer that constantly downloads a HTML page and reads it;
When the page is downloaded, the stringValue of a label is changed.But the stringValue may also be changed from the PreferencesController.The page is downloaded from a background thread, but it's changed through the main queue.
Now I've got some questions:
Do I have to invalidate the timer when the application starts sleeping (computer goes in standby),and create another one when it returns on?
The label is updated in the main queue, so I still have to protect the label access with a mutex?
Sometimes the panels are missing: at the start of the application I am able to launch panels by clicking a menu item, but sometimes they are not launching.I don't know how to reproduce this bug always, it just happens randomly when the application is active for 2/3 hours usually, I have to relaunch the application to go around this.
The code is too long, that's only a piece of code:
- (void) checkPosts: (id ) sender
{
NSOperationQueue* queue=[NSOperationQueue new];
queue.maxConcurrentOperationCount=1;
[queue addOperationWithBlock:^
{
NSNumber* newPosts= [self updatePosts];
NSNumber* posts= [controller posts];
if([posts integerValue]!=[newPosts integerValue])
{
NSOperationQueue* queue=[NSOperationQueue mainQueue];
posts= newPosts;
[queue addOperationWithBlock:^
{
// This is where I may have a race condition
item.attributedTitle=[[NSAttributedString alloc]initWithString: [formatter stringFromNumber: posts] attributes: #{NSForegroundColorAttributeName : [controller color], NSFontAttributeName : [NSFont userFontOfSize: 12.5]}];
}];
// That's not so relevant:
NSUserNotification* notification=[NSUserNotification new];
notification.title= [NSString stringWithFormat: #"posts Changed to %#",posts];
notification.deliveryDate=[NSDate date];
notification.soundName= NSUserNotificationDefaultSoundName;
NSUserNotificationCenter* center=[NSUserNotificationCenter defaultUserNotificationCenter];
[center deliverNotification: notification];
center.delegate= self;
[controller setPosts: posts];
}
}];
}
A little background information:
This method works in a background thread;
[self updatePosts] downloads the HTML page and returns the number of posts;
[controller posts] reads the previous number of posts using NSUserDefaults;
item is a status menu's menu item.
More Details
This is how I get the timer:
// In the applicationDidFinishLaunching method
timer=[NSTimer scheduledTimerWithTimeInterval: [interval integerValue ] target: self selector: #selector(checkReputation:) userInfo: nil repeats: YES];
timer is a property:
#property (nonatomic, strong) NSTimer* timer;
interval is a NSNumber, for sure it's integer value is greater or equal than 1.
It's not entirely clear what's happening here. You've provided plenty of information but not everything needed to give a definitive answer. I'll try to address your questions first:
Do I have to invalidate the timer when the application starts sleeping
(computer goes in standby),and create another one when it returns on?
Have to? No. Should for cleanliness and certainty of the state? Yes, probably. You should probably specify exactly how you set up the timer since you can run into problems with it interacting with the run loop ... but I don't think this is your problem.
The label is updated in the main queue, so I still have to protect the
label access with a mutex?
As long as you update the UI from the main thread/queue, you should be fine. This is a standard design approach with blocks.
Sometimes the panels are missing: at the start of the application I am
able to launch panels by clicking a menu item, but sometimes they are
not launching.I don't know how to reproduce this bug always, it just
happens randomly when the application is active for 2/3 hours usually,
I have to relaunch the application to go around this.
If you don't know how to reproduce it, I'm not sure we can help you beyond "places to look." The first thought I've got is that you may be recreating multiple copies of your primary controllers when the app becomes active (since you asked about this earlier, I assume you've tried doing something with it). Make sure the same controllers are being reused.
Now on to the code.
NSOperationQueue* queue=[NSOperationQueue new];
The queue variable is local to the scope of the method. I see no retain/release, so I assume you're using ARC. In that case, you're not retaining the new queue you're creating and its life span is not guaranteed to survive as long as you need it to once the method completes and you've left its scope. You should make queue an instance variable so it sticks around. This way the queue can be reused every time the method is fired and it'll stay around long enough for other queues/threads to use.
I think it's likely this is your biggest culprit. Adjust it and update your question to reflect how it affects the condition of your app.
Related
My AppDelegate maintains a list of active window controllers to avoid ARC deallocating them too early. So I have a notification handler like this:
- (void) windowWillClose: (NSNotification*) notification {
[self performSelectorOnMainThread: #selector(removeWindowControllerInMainThread:)
withObject: windowController
waitUntilDone: NO];
}
- (void) removeWindowControllerInMainThread: (id) windowController {
[windowControllers removeObject: windowController];
}
I use the main thread because doing the handling on the notification thread risks deallocating the controller before it's ready.
Now, this works pretty well — except when there are animators currently running. I use animators in some places, through NSAnimationContext. I have looked at this QA, and the answer just isn't acceptable. Waiting for a while, just to get animation done, is really shoddy and not guaranteed to work; indeed it doesn't. I tried using performSelector:withObject:afterDelay, even with a larger delay than the current animation duration, and it still results in the animator running against nil objects.
What is the preferred way of doing controller cleanup like this? Not use NSAnimationContext but using NSAnimation instead, which has a stopAnimation method?
First, if some of your animations run indefinitely -- or for a very long time -- you're going to have to have a way to stop them.
But for things like implicit animations on views, you could simply use a completion method.
self.animating=YES;
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
[[v animator] setAlphaValue: 1];
} completionHandler:^{
self.animating=NO;
}];
Now, you only need to poll whether your animation is running and, if it's not running, proceed to close your window.
One nice way to do the polling is to set a timer with a fixed delay. If the animation is still running, just reset the timer and wait another interval.
Alternatively, you could send a notificaton from the completion handler.
I haven't used NSAnimationContext (always did this with NSAnimation, but mostly for historical reasons). But the typical way I like to managed things similar to this is to create short-lived retain loops.
Mark's answer is exactly the right kind of idea, but the polling is not required. The fact that you reference self in the completion handler means that self cannot deallocate prior to the completion handler running. It doesn't actually matter whether you ever read animating. ARC has to keep you around until the completion block runs because the block made a reference to you.
Another similar technique is to attach yourself to the animation context using objc_setAssociatedObject. This will retain you until the completion block runs. In the completion block, remove self as an associated object, and then you'll be free to deallocate. The nice thing about that approach is that it doesn't require a bogus extra property like animating.
And of course the final, desperate measure that is occasionally appropriate is to create short-lived self-references. For instance:
- (void)setImmortal:(BOOL)imortal {
if (immortal) {
_immortalReference = self;
}
else {
_immortalReference = nil;
}
}
I'm not advocating this last option. But it's good to know that it exists, and more importantly to know why it works.
I've looked at a lot of topics but I still can't figure it out.
I have a UITableview which downloads its content online. Each cell has an image, and I use GCD to let the image download. The downloaded image will be saved to disk, and before each time a cell is loaded there is checked if the file already exist, if not -> gcd, nsdata etc.
All goes well if someone has a good internet connection (wifi), but if I'm going to hop from View to View (back and forth), with my crappy 3G connection, what happens is that it wants to finish its queue (about 4 cells), but already gets assigned a new one, and a new one, and a new one and eventually the user has to wait a looong time before the others are executed (which he doesnt see) before the actual UITableview gets populated. With NSLog I can see that even I'm in a different view, it's still downloading and making uiimages that were visible on the screen. Each task is approximately 100 kb, and with a slow (or even no internet connection?!) it can take a while if you have a lot.
I know it's not possible to cancel it, but I read in other topics about using a BOOL variable but I don't really get it. Even if the BOOL variable change when the user leaves the screen, the cells are already in queue right?
Is it possible that when a user taps the back button in my Navigationcontroller, so he leaves the view, I change the data the blocks in queue use (empty it), so there is nothing to download and the blocks will be executed right away (there is nothing to do). So something like, making every value in array newsitems nil? Is it possible to change the datasource, or will the blocks that are waiting already have their datasource with them while waiting?
Then there is another problem, this doesn't have effect on the the currently executed block.
Can someone point me in a good direction?
Thank you.
Prastow
You can make use of NSBlockOperation and NSOperationQueue to create a cancellable download task. You create an NSBlockOperation by giving it a block which performs some work. In your case the block would download the contents of the URL.
In your view controller, you would store a list of the operations that have been submitted to the queue. If the user decides to leave the current view, you can then call cancel on each of the pending operations to prevent any needless work from taking place. The currently running operation will run to completion however. In order to cancel the currently running operation, you need to store a weak reference to the NSOperation object in the block doing teh work. Then at appropriate intervals within the body of the block, you can check to see if the operation has been cancelled and exit early.
// Create a queue on which to run the downloads
NSOperationQueue* queue = [NSOperationQueue new];
// Create an operation without any work to do
NSBlockOperation* downloadImageOperation = [NSBlockOperation new];
// Make a weak reference to the operation. This is used to check if the operation
// has been cancelled from within the block
__weak NSBlockOperation* operation = downloadImageOperation;
// The url from which to download the image
NSURL* imageURL = [NSURL URLWithString:#"http://www.someaddress.com/image.png"];
// Give the operation some work to do
[downloadImageOperation addExecutionBlock: ^() {
// Download the image
NSData* imageData = [NSData dataWithContentsOfURL:imageURL];
// Make sure the operation was not cancelled whilst the download was in progress
if (operation.isCancelled) {
return;
}
// Do something with the image
}];
// Schedule the download by adding the download operation to the queue
[queue addOperation:imageDownloadOperation];
// As necessary
// Cancel the operation if it is not already running
[imageDownloadOperation cancel];
A good talk on this exact topic was given at WWDC this year entitled "Building Concurrent User Interfaces on iOS". You can find the video and slides here
I faced similar issues with an app I developed a while back and found that the best way to do everything you require, and more, is to use https://github.com/MugunthKumar/MKNetworkKit
It took me the best part of a day to learn and understand the conversion and then a couple more days to tweak it to exactly what I needed.
If you do decide to use it or would like a thorough overview of the capabilities start here
http://blog.mugunthkumar.com/products/ios-framework-introducing-mknetworkkit/
I have pre allocated the mail and messaging controllers on startup in my app delegate to save the initialisation time (over 10 secs) when the user is using my application...
__mailController = [[MFMailComposeViewController alloc] init];
__messageController = [[MFMessageComposeViewController alloc] init];
It works fine the first time the controller is displayed then the next time the message is not changed and the old message is still displayed ?? ... Is it likely that the controller is being deallocated ??? Strange as the views work correctly just that the message is not correct ?
- (IBAction)actionSMS:(id)sender {
if([MFMessageComposeViewController canSendText])
{
self.messageController.body = self.MessageDetail.text;
// controller.recipients = [NSArray arrayWithObjects:#"+919999999999", nil];
[self presentModalViewController:self.messageController animated:YES];
}
}
Once MFMailComposeViewController and MFMessageComposeViewController are presented to the user you can't make changes to the content they display.
MFMailComposeViewController Class Reference:
Important The mail composition interface itself is not customizable
and must not be modified by your application. In addition, after
presenting the interface, your application is not allowed to make
further changes to the email content. The user may still edit the
content using the interface, but programmatic changes are ignored.
Thus, you must set the values of content fields before presenting the
interface.
That means those values are somehow locked in the implementation of the MFM*ViewController at the moment you present the controller. So you can't reuse these viewControllers. iOS doesn't care if the controller is, like in your case, invisible or not. If it is presented the content is locked.
I would figure out why it takes 10 seconds to allocate them. And then dump that whole pre-allocation thingie. 10 seconds are definitely to much.
I had the same problem. Not only are the MF controllers only good for one time use, as you discovered, they also cannot be init-ed in the background because their UI elements need to be init-ed in the main thread.
In the end, I just present a UIActivityIndicatorView over a HUD, so the users will know the app is responding.
I have an IBAction for when a button is clicked:
- (IBAction)importButtonClicked:(id)sender
And I want a series of events to take place like:
[_progressLabel becomeFirstResponder]; // I tried this but to no effect
_progressLabel.stringValue = BEGIN_IMPORT_STRING;
[_importButton setEnabled:FALSE];
_fileField.stringValue = #"";
[_progressIndicator startAnimation:nil];
But what ends up happening is the _progressIndicator animation takes place before the _progressLabel text appears. And often times the text won't appear untili the _progressIndicator animation has stopped. How do I fix that?
Put the work you're doing which takes time (I assume that's what the progress indicator is for) on a separate thread. You don't have to do this manually in Cocoa, but instead, use Grand Central Dispatch (GCD), NSOperationQueue or such a construct available. You'll find lots of resources on GCD.
following is .m code:
#import "ThreadLabAppDelegate.h"
#interface ThreadLabAppDelegate()
- (void)processStart;
- (void)processCompleted;
#end
#implementation ThreadLabAppDelegate
#synthesize isProcessStarted;
- (void)awakeFromNib {
//Set levelindicator's maximum value
[levelIndicator setMaxValue:1000];
}
- (void)dealloc {
//Never called while debugging ????
[super dealloc];
}
- (IBAction)startProcess:(id)sender {
//Set process flag to true
self.isProcessStarted=YES;
//Start Animation
[spinIndicator startAnimation:nil];
//perform selector in background thread
[self performSelectorInBackground:#selector(processStart) withObject:nil];
}
- (IBAction)stopProcess:(id)sender {
//Stop Animation
[spinIndicator stopAnimation:nil];
//set process flag to false
self.isProcessStarted=NO;
}
- (void)processStart {
int counter = 0;
while (counter != 1000) {
NSLog(#"Counter : %d",counter);
//Sleep background thread to reduce CPU usage
[NSThread sleepForTimeInterval:0.01];
//set the level indicator value to showing progress
[levelIndicator setIntValue:counter];
//increment counter
counter++;
}
//Notify main thread for process completed
[self performSelectorOnMainThread:#selector(processCompleted) withObject:nil waitUntilDone:NO];
}
- (void)processCompleted {
//Stop Animation
[spinIndicator stopAnimation:nil];
//set process flag to false
self.isProcessStarted=NO;
}
#end
I need to clear following things as per the above code.
How to interrupt/cancel processStart while loop from UI control?
I also need to show the counter value in main UI, which i suppose to do with performSelectorOnMainThread and passing argument. Just want to know, is there anyother way to do that?
When my app started it is showing 1 thread in Activity Monitor, but when i started the processStart() in background thread its creating two new thread,which makes the total 3 thread until or unless loop get finished.After completing the loop i can see 2 threads.
So, my understanding is that, 2 thread created when i called performSelectorInBackground, but what about the thrid thread, from where it got created?
What if thread counts get increases on every call of selector.How to control that or my implementation is bad for such kind of requirements?
Thanks
how to update UI controls in cocoa application from background thread
Simple: Don't.
How to interrupt/cancel processStart while loop from UI control?
Outside of processStart, set a flag variable. Inside of processStart, check that flag and exit the loop if it is set.
Don't try to “kill” a thread from another thread. It's always a bad idea. Tell the thread it's time to stop by setting the flag, and have the thread check that flag and stop at an appropriate time.
I also need to show the counter value in main UI, which i suppose to do with performSelectorOnMainThread and passing argument. Just want to know, is there anyother way to do that?
Yes.
When my app started it is showing 1 thread in Activity Monitor, but when i started the processStart() in background thread its creating two new thread,which makes the total 3 thread until or unless loop get finished.After completing the loop i can see 2 threads. So, my understanding is that, 2 thread created when i called performSelectorInBackground, but what about the thrid thread, from where it got created?
Profile your app using Instruments or Shark and look. It's probably the heartbeat thread for the progress indicator.
What if thread counts get increases on every call of selector.How to control that or my implementation is bad for such kind of requirements?
Every performSelectorInBackground:withObject: message starts a thread. If your thread count isn't going down, it's because your thread method didn't exit. If your thread count is too high, it's (probably) because you started too many threads.
There is a much better way to do this.
First, the general rule in Cocoa is never sleep. Think of this as special ultra-caffeinated Cocoa. For anything you might sleep for in another framework, there is almost always a better, usually easier, way in Cocoa.
With that in mind, look at processStart. All it does is do something every centisecond. How best to do that?
Cocoa has a class for this specific purpose: NSTimer. Create a timer that sends yourself a message at the desired interval, and respond to that message by updating the progress bar—that is, your timer callback method should essentially just be the loop body from processStart, without the loop.
By the way, 100 updates per second is overkill. First off, the user does not care that you have made 1/5th of a pixel's worth of progress since the last time you updated the bar. Second, the screen only updates about 60 times per second anyway, so updating anything visible faster than that is pointless.
- (void)dealloc {
//Never called while debugging ????
[super dealloc];
}
Assuming you put your app delegate in the MainMenu nib, the application object owns it because of that—but it doesn't know that, because it only knows about the app delegate as its delegate, which is a non-owning relationship. (And even if it were an owning relationship, that would just be two ownerships, of which the app would release one, which wouldn't help.)
However, the lifetime of the app delegate doesn't really matter. Its purpose as the delegate of the application means that it needs to last about as long as the application does, but when the application goes away, the process is exiting, which means the delegate will be deallocated as well, as part of the reclamation of the process's memory space. That's why dealloc isn't called—the whole process space goes away at once, instead of objects being deallocated one at a time.
So, in principle, yeah, the app delegate not getting explicitly cleaned up is kind of dodgy. In practice, don't put any temporary-files clean-up in its dealloc (use applicationWillTerminate: instead) and you'll be fine.
I typically work around the problem by putting all my real work in one or more other objects which the app delegate owns. The app delegate creates these other controllers in applicationWillFinishLaunching: and releases them in applicationWillTerminate:, so those objects do get dealloc messages. Problem solved.