I found that as predicted when I was writing an image to file that my UI was blocked for the duration, which was not acceptable. When I write the image to file I then post an NS Notification so that I can do some other specific jobs related to that completion. Original working but UI blocking code:
-(void)saveImageToFile {
NSString *imagePath = [self photoFilePath];
BOOL jpgData = [UIImageJPEGRepresentation([[self captureManager] stillImage], 0.5) writeToFile:imagePath atomically:YES];
if (jpgData) {
[[NSNotificationCenter defaultCenter] postNotificationName:kImageSavedSuccessfully object:self];
}
To avoid the UI blocking I have put the writeToFile: into a Grand Central Dispatch queue so it runs as a concurrent thread. But when the write is completed and the thread is done, I want to post an NSNotification. I cannot as the code is shown here because it is in a background thread. But that is the functionality I want to accomplish, realizing this is not workable code:
-(void)saveImageToFile {
NSString *imagePath = [self photoFilePath];
// execute save to disk as a background thread
dispatch_queue_t myQueue = dispatch_queue_create("com.wilddogapps.myqueue", 0);
dispatch_async(myQueue, ^{
BOOL jpgData = [UIImageJPEGRepresentation([[self captureManager] stillImage], 0.5) writeToFile:imagePath atomically:YES];
dispatch_async(dispatch_get_main_queue(), ^{
if (jpgData) {
[[NSNotificationCenter defaultCenter] postNotificationName:kImageSavedSuccessfully object:self];
}
});
});
}
What is the correct mechanism here to post this notification to gain the functionality I want ?
A couple possibilities here.
1)
How about [NSObject performSelectorOnMainThread: ...] ?
E.G.
-(void) doNotification: (id) thingToPassAlong
{
[[NSNotificationCenter defaultCenter] postNotificationName:kImageSavedSuccessfully object:thingToPassAlong];
}
-(void)saveImageToFile {
NSString *imagePath = [self photoFilePath];
// execute save to disk as a background thread
dispatch_queue_t myQueue = dispatch_queue_create("com.wilddogapps.myqueue", 0);
dispatch_async(myQueue, ^{
BOOL jpgData = [UIImageJPEGRepresentation([[self captureManager] stillImage], 0.5) writeToFile:imagePath atomically:YES];
dispatch_async(dispatch_get_main_queue(), ^{
if (jpgData) {
[self performSelectorOnMainThread: #selector(doNotification:) withObject: self waitUntilDone: YES];
}
});
});
}
More details at http://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSObject_Class/Reference/Reference.html#//apple_ref/occ/instm/NSObject/performSelectorOnMainThread:withObject:waitUntilDone:
or 2)
Completion Callbacks
as seen at How can I be notified when a dispatch_async task is complete?
-(void)saveImageToFile {
NSString *imagePath = [self photoFilePath];
// execute save to disk as a background thread
dispatch_queue_t myQueue = dispatch_queue_create("com.wilddogapps.myqueue", 0);
dispatch_async(myQueue, ^{
BOOL jpgData = [UIImageJPEGRepresentation([[self captureManager] stillImage], 0.5) writeToFile:imagePath atomically:YES];
dispatch_async(dispatch_get_main_queue(), ^{
if (jpgData) {
[[NSNotificationCenter defaultCenter] postNotificationName:kImageSavedSuccessfully object:self];
}
});
});
}
That is already correct. However why do you need to use notification if you already dispatch_get_main_queue()?
Just use
dispatch_async(dispatch_get_main_queue(), ^{
if (jpgData) {
//Do whatever you want with the image here
}
});
Anyway your original solution is fine. It's not blocking. Basically it'll save the file at other thread and once it's done it'll do what it takes.
What ever you do will be done on the same thread with the thread that call [[NSNotificationCenter defaultCenter] postNotificationName:kImageSavedSuccessfully object:self]; namely main thread.
Related
I have a settings panel in my app. Whenever the user presses a button, the object is updated on another thread as the UI updates. I have a separate label on the main view that is supposed to update the object count when the object has finished updating (which I want to happen regardless of whether the settings panel is up or down). I've tried following the apple documentation regarding this very topic, but it doesn't seem to work out for me - that is, it seems that the main view controller never receives the notification for some reason. Does anyone have any suggestions on how to alert the main view controller that an object passed to another thread has finished updating? Here's the code I'm using (most of which was copied from that doc):
Object Class:
[[NSNotificationCenter defaultCenter] postNotificationName: #"ScaleCountUpdated" object: self];
Main View Controller
- (void)setUpThreadingSupport
{
if (self.notifications) {
return;
}
self.notifications = [[NSMutableArray alloc] init];
self.notificationLock = [[NSLock alloc] init];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
[self.notificationPort setDelegate: self];
[[NSRunLoop currentRunLoop] addPort: self.notificationPort
forMode: (NSString *)kCFRunLoopCommonModes];
}
- (void)handleMachMessage:(void *)msg
{
[self.notificationLock lock];
while ([self.notifications count]) {
NSNotification *notification = [self.notifications objectAtIndex: 0];
[self.notifications removeObjectAtIndex: 0];
[self.notificationLock unlock];
[self processNotification: notification];
[self.notificationLock lock];
};
[self.notificationLock unlock];
}
- (void)processNotification:(NSNotification *)notification{
if ([NSThread currentThread] != self.notificationThread) {
// Forward the notification to the correct thread.
[self.notificationLock lock];
[self.notifications addObject: notification];
[self.notificationLock unlock];
[self.notificationPort sendBeforeDate: [NSDate date]
components: nil
from: nil
reserved: 0];
} else {
[self updateScaleCount];
}
}
- (void)updateScaleCount
{
NSLog(#"[ScalesViewController - updateScaleCount]: Scales updated from notification center.");
if([UserDefinedScales areScalesGrouped] == YES){
self.groupCountLabel.text = [NSString stringWithFormat: #"Group Count: %i", [[UserDefinedScales sortedKeys] count]];
} else {
self.groupCountLabel.text = #"Group Count: 1";
}
self.scaleCountLabel.text = [NSString stringWithFormat: #"Scale Count: %i", [UserDefinedScales scaleCount]];
}
Main View Controller - View Did Load:
[self setUpThreadingSupport];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(processNotification:)
name: #"ScaleCountUpdated"
object: nil];
If you have any suggestions on how to alter this code to make it function correctly, or have another solution to offer for achieving this, it would be greatly appreciated! Thank you.
It looks to me like you are doing it correctly, i.e. register for notification and send it.
As far as I can see from your code and the information you give, you can basically completely forget about the setupThreadingSupport. You should definitely test it without it. Not sure what you want to achieve, but looks like overkill where probably a simple block would suffice. Is there a compelling reason to listen to the notification on a background thread? Why not let the notification center decide?
Log the sending and receiving of the notifications - addObserver and postNotification is really all this mechanism needs to work as expected.
I would go to a simpler implementation. The NSNotificationCenter already provides all the mechanisms you need to broadcast and receive messages across your app.
If you fire the notification from a background thread you can use a GCD dispatch_async to make make it delivered on the main thread.
Object Class
dispatch_async(dispatch_get_main_queue(), ^{
// You don't need to pass the object itself here as you are not using it later.
[[NSNotificationCenter defaultCenter] postNotificationName:#"ScaleCountUpdated"];
}
MainViewController
-(void)viewDidLoad
{
[super viewDidLoad];
// Register your controller as an observer for a specific message name
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(updateScaleCount)
name: #"ScaleCountUpdated"
object: nil];
}
- (void)updateScaleCount
{
NSLog(#"[ScalesViewController - updateScaleCount]: Scales updated from notification center.");
if([UserDefinedScales areScalesGrouped] == YES){
self.groupCountLabel.text = [NSString stringWithFormat: #"Group Count: %i", [[UserDefinedScales sortedKeys] count]];
} else {
self.groupCountLabel.text = #"Group Count: 1";
}
self.scaleCountLabel.text = [NSString stringWithFormat: #"Scale Count: %i", [UserDefinedScales scaleCount]];
}
I want to display a progress HUD right before starting a long task, and dismiss it once the task is done. Right now the UI freezes for some time, then the HUD shows for a second before disappearing. After another 4-5 seconds the task is finished and it displays its results, but the progress HUD is already gone.
- (void) addProcess:(NSString *)searchTerm
{
dispatch_sync(dispatch_get_main_queue(), ^{
UIApplication *app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = YES;
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient];
});
//DO SOME VERY LONG STUFF HERE
dispatch_sync(dispatch_get_main_queue(), ^{
UIApplication *app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = NO;
[SVProgressHUD dismiss];
});
}
I use SVProgressHUD for the progress HUD. I am calling the method addProcess using dispatch_async()
If I use the old +[NSThread detach...], it's working flawlessly, but I don't want to use it anymore.
A couple of things:
The key observation is to always use dispatch_async unless you need dispatch_sync. You don't need synchronous operation here, so just use dispatch_async for your UI updates.
If you're running addProcess from the main queue, it doesn't need to dispatch the first UI update back to the main queue. Obviously, if you're running this from a background queue, you do.
The original question had the dispatch to the background queue within addProcess, which makes more sense to me (keeps all the GCD stuff nicely encapsulated). You've updated your answer to say that you're invoking this via dispatch_async([self addProcess]) (by which I presume you meant to a global queue, not the main queue), which I'm less crazy about. I address both scenarios in my code samples below.
So, in short, if you're invoking this via [self addProcess] (without dispatching that, itself, to the background queue) I'd suggest:
- (void) addProcess:(NSString *)searchTerm
{
UIApplication *app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = YES;
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//DO SOME VERY LONG STUFF HERE
dispatch_async(dispatch_get_main_queue(), ^{
UIApplication *app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = NO;
[SVProgressHUD dismiss];
});
});
}
Or, alternatively,
- (void) addProcess:(NSString *)searchTerm
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
UIApplication *app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = YES;
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient];
});
//DO SOME VERY LONG STUFF HERE
dispatch_async(dispatch_get_main_queue(), ^{
UIApplication *app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = NO;
[SVProgressHUD dismiss];
});
});
}
And if you're doing ...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self addProcess];
});
then it's just:
- (void) addProcess:(NSString *)searchTerm
{
dispatch_async(dispatch_get_main_queue(), ^{
UIApplication *app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = YES;
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient];
});
//DO SOME VERY LONG STUFF HERE
dispatch_async(dispatch_get_main_queue(), ^{
UIApplication *app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = NO;
[SVProgressHUD dismiss];
});
}
I'm using an NSOperation to collect data that should be downloaded (takes 2-5 sec.) and afterwards I download this. I've put a ASINetworkQueue inside this NSOperation to start downloading the previously collected data.
Everything works fine but when I call cancelAllOperations on my ASINetworkQueue, the main thread blocks and the UI Freezes. Why is this happening? Everything else works fine.
Here is my Code:
- (void)main {
//ManagedObjectContext for operations
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
self.managedObjectContext = [[NSManagedObjectContext alloc] init];
[self.managedObjectContext setPersistentStoreCoordinator: [appDelegate persistentStoreCoordinator]];
// Register context with the notification center
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:self.managedObjectContext];
[self startDownload];
if (!self.downloadDidFail) {
[self moveFiles];
[self.managedObjectContext save:nil];
}
}
- (void)startDownload {
self.downloadQueue = [ASINetworkQueue queue];
self.downloadQueue.delegate = self;
[self.downloadQueue setRequestDidFailSelector:#selector(dataRequestFailed:)];
[self.downloadQueue setRequestDidFinishSelector:#selector(dataRequestFinished:)];
[self.downloadQueue setQueueDidFinishSelector:#selector(dataQueueFinished:)];
[self.downloadQueue setShouldCancelAllRequestsOnFailure:YES];
[self.downloadQueue setDownloadProgressDelegate:self.progressView];
for (File *dataFile in self.dataFiles) {
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:dataFile.url]];
[request setDownloadDestinationPath:dataFile.path];
[self.downloadQueue addOperation:request];
}
}
[self.downloadQueue go];
[self.downloadQueue waitUntilAllOperationsAreFinished];
}
- (void)dataRequestFinished:(ASIHTTPRequest *)request {
NSLog(#"DL finished");
}
- (void)dataRequestFailed:(ASIHTTPRequest *)request {
DLog(#"Download failed");
self.downloadDidFail = YES;
}
- (void)dataQueueFinished:(ASINetworkQueue *)queue {
DLog(#"Finished Data Queue");
}
- (void)cancelDownload {
self.canceledDownload = YES;
[self.downloadQueue cancelAllOperations];
}
I had the same problem and solved by calling:
[queue setShouldCancelAllRequestsOnFailure:NO]
before calling:
[queue cancelAllOperations].
ASI requests responses and queue responses are deliberately moved to the main thread for library design purposes.
You have two solution:
-Subclass ASIHTTPRequest and overwrite 2 methods. (Look for in the code something like "subclass for main thread").
-Modify the library. (Easy, but personally I don't like this solution).
What does your failure delegate method do? ASIHTTPRequest will run that on the main thread by default, so if it does a lot of processing (or there are a lot of requests) this could take quite some time.
Could someone please help me spot the leak. I don't really know where my leak is. So I will post my code here and hopefully someone can spot it for me. In the leaks instrument it says responsible frame is dispatch_semaphore_create which I'm not calling?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([defaults boolForKey:#"notFirstRunSeminars"]) {
BOOL isUpdated = self.seminarsParser.seminarsAreUpdated;
if (isUpdated) {
DLog(#"Seminars have been updated");
[[NSNotificationCenter defaultCenter]
postNotificationName:#"updateSeminarsTable"
object:nil];
[[[[[self tabBarController] tabBar] items] objectAtIndex:kSeminarsTabIndex] setBadgeValue:self.seminarsParser.numberOfNewSeminars];
self.seminarsParser.numberOfNewSeminars = nil;
}
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([defaults boolForKey:#"notFirstRunCareers"]) {
BOOL isUpdated = self.careersParser.careersAreUpdated;
if (isUpdated) {
DLog(#"Careers have been updated");
[[NSNotificationCenter defaultCenter]
postNotificationName:#"updateCareersTable"
object:nil];
[[[[[self tabBarController] tabBar] items] objectAtIndex:kCareersTabIndex] setBadgeValue:self.careersParser.numberOfNewCareers];
self.careersParser.numberOfNewCareers = nil;
}
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([defaults boolForKey:#"notFirstRunOffices"]) {
BOOL isUpdated = [officesParser officesAreUpdated];
if (isUpdated) {
DLog(#"Offices have been updated");
[[NSNotificationCenter defaultCenter]
postNotificationName:#"updateOfficesTable"
object:nil];
}
}
});
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
});
});
The zeroeth problem: What is being leaked?
Since I didn't see an obvious leak scanning your program, here's the most likely problem: UIKit objects are not threadsafe and only meant to be updated from the main thread, unless they enter your program from another thread.
Also note that NSNotifications are posted on the calling thread.
That means that all your UIKit type accesses and updates need to be performed on the main thread. And yes, that can introduce leaks or threading errors for which you should consider as undefined behaviour.
Where are you releasing the group after you're done with it, since you're creating a group within the outermost async block? That's the only object I can see you creating which isn't being released properly. Incidently, a group creates a semaphore behind the scenes (all a dispatch group really is, is some syntactic sugar around an internal dispatch semaphore), which is why instruments is reporting it that way.
I load a UIImageView using an NSOperationQueue.
The load fetches an image from the Internet and then adds it to an image view. The problem I have is that the method finishes but it takes about 3 seconds later for the image view to actually either show the image or remove the image view from the superview...
- (void)viewDidLoad { NSLog(#"AirportDetailView: viewDidLoad");
[super viewDidLoad];
[self.activity startAnimating];
self.queue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(loadImage) object:NULL];
[self.queue addOperation:operation];
[operation release];
}
-(void)loadImage {
[myAp ImageForAp];
NSLog(#"ImageFor Ap Ended, %#",myAp.ApDiagram);
[self.activity stopAnimating];
if (myAp.ApDiagram==NULL) {
NSLog(#"Finally Gets Here");
[self.Diagram removeFromSuperview];
}
else {
NSLog(#"Finally Gets Here with Diag");
[self.Diagram setBackgroundImage:myAp.ApDiagram forState:UIControlStateNormal];
}
}
The NSLOG shows a delay between the first two log statements of about 3 seconds can't understand why....
Updated with my Latest Code......
-(void)loadImage {
[myAp ImageForAp];
NSLog(#"ImageFor Ap Ended, %#",myAp.ApDiagram);
[self performSelectorOnMainThread:#selector(UpdateUI) withObject:nil waitUntilDone:NO];
}
-(void)UpdateUI {
[self.activity stopAnimating];
if (myAp.ApDiagram==NULL) {
NSLog(#"Finally Gets Here");
[self.Diagram removeFromSuperview];
}
else {
NSLog(#"Finally Gets Here with Diag");
[self.Diagram setBackgroundImage:myAp.ApDiagram forState:UIControlStateNormal];
}
}
Make sure that the loadImage method is being run on the main thread. All UI operations need to happen on the main thread to work as expected. Your code will need to look similar to this.
-(void)loadImage {
[myAp ImageForAp];
[self performSelectorOnMainThread:#selector(UpdateUI) withObject:nil waitUntilDone:NO];
}
-(void)UpdateUI {
[self.activity stopAnimating];
if (myAp.ApDiagram==NULL) {
[self.Diagram removeFromSuperview];
}
else {
[self.Diagram setBackgroundImage:myAp.ApDiagram forState:UIControlStateNormal];
}
}
There is also a possible memory leak inside of viewDidLoad.
self.queue = [NSOperationQueue new]; should be changed to
self.queue = [[[NSOperationQueue alloc] init] autorelease];