I have a UITableView that, when items are selected, loads a viewController, which inside it performs some operations in the background using performSelectorInBackground.
Everything works fine if you slowly tap items in the tableView (essentially allowing the operations preforming in background to finish). But when you select the items quickly, the app quickly returns some memory warnings until it crashes, usually after about 7 or 8 "taps" or selections.
Any idea why this would be? When I move my code from the background thread to the main thread, everything works fine as well. You just can't make the tableView selections as quickly because it's waiting for the operations to finish.
Code snippets:
//this is called from - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
-(void) showLeaseView:(NSMutableDictionary *)selLease
{
LeaseDetailController *leaseController = [[LeaseDetailController alloc] initWithNibName:#"LeaseDetail" bundle:nil];
leaseController.lease = selLease;
//[leaseController loadData];
[detailNavController pushViewController:leaseController animated:NO];
[leaseController release];
}
//this is in LeaseDetailController
- (void)viewDidLoad {
[self performSelectorInBackground:#selector(getOptions) withObject:nil];
[super viewDidLoad];
}
-(void) getOptions
{
NSAutoreleasePool *apool = [[NSAutoreleasePool alloc] init];
NSArray *arrayOnDisk = [[NSArray alloc] initWithContentsOfFile:[appdel.settingsDir stringByAppendingPathComponent:#"optionData"]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(LEASE_ID contains[cd] %#)", [lease leaseId]];
self.options = [NSMutableArray arrayWithArray:[arrayOnDisk filteredArrayUsingPredicate:predicate]];
[arrayOnDisk release];
[apool release];
}
Every time you perform the getOptions selector in the background, what's really happening is a new thread is being created on your behalf, and the work is being done there. When the user taps your table cells a bunch of times in a row, a new thread is created each time to handle the work. If the work done by getOptions takes some time to complete, you will have multiple threads calling getOptions at the same time. That is to say, the system doesn't cancel previous requests to perform getOptions in the background.
If you assume that it takes N bytes of memory to perform the work done by getOptions, then if the user taps on five table cells in a row and getOptions doesn't finish right away, then you'll find that your app is using 5 * N bytes at that point. In contrast, when you change your app to call getOptions on the main thread, it has to wait for each call to getOptions to complete before it can call getOptions again. Thus when you do your work on the main thread you don't run into the situation where you're using 5 * N bytes of memory to do the work of five instances of getOptions simultaneously.
That's why you run out of memory when you do this work in the background and the user taps multiple table cells: you're doing multiple instances of the work, and each instance requires its own amount of memory, and when they all get added up, it's more than the system can spare.
It looks like you're just calling getOptions once when the user selects a table cell and navigates into a new view controller. Since the user will only be looking at one of these view controllers at a time, you don't really need to have multiple instances of getOptions going on simultaneously in the background. Instead, you want to cancel the previously-running instance before starting the new one. You can do this using an NSOperationQueue, like so:
- (NSOperationQueue *)operationQueue
{
static NSOperationQueue * queue = nil;
if (!queue) {
// Set up a singleton operation queue that only runs one operation at a time.
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1];
}
return queue;
}
//this is called from - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
-(void) showLeaseView:(NSMutableDictionary *)selLease
{
LeaseDetailController *leaseController = [[LeaseDetailController alloc] initWithNibName:#"LeaseDetail" bundle:nil];
leaseController.lease = selLease;
// Cancel any pending operations. They'll be discarded from the queue if they haven't begun yet.
// The currently-running operation will have to finish before the next one can start.
NSOperationQueue * queue = [self operationQueue];
[queue cancelAllOperations];
// Note that you'll need to add a property called operationQueue of type NSOperationQueue * to your LeaseDetailController class.
leaseController.operationQueue = queue;
//[leaseController loadData];
[detailNavController pushViewController:leaseController animated:NO];
[leaseController release];
}
//this is in LeaseDetailController
- (void)viewDidLoad {
// Now we use the operation queue given to us in -showLeaseView:, above, to run our operation in the background.
// Using the block version of the API for simplicity.
[queue addOperationWithBlock:^{
[self getOptions];
}];
[super viewDidLoad];
}
-(void) getOptions
{
NSAutoreleasePool *apool = [[NSAutoreleasePool alloc] init];
NSArray *arrayOnDisk = [[NSArray alloc] initWithContentsOfFile:[appdel.settingsDir stringByAppendingPathComponent:#"optionData"]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(LEASE_ID contains[cd] %#)", [lease leaseId]];
NSMutableArray * resultsArray = [NSMutableArray arrayWithArray:[arrayOnDisk filteredArrayUsingPredicate:predicate]];
// Now that the work is done, pass the results back to ourselves, but do so on the main queue, which is equivalent to the main thread.
// This ensures that any UI work we may do in the setter for the options property is done on the right thread.
dispatch_async(dispatch_queue_get_main(), ^{
self.options = resultsArray;
});
[arrayOnDisk release];
[apool release];
}
Related
I am using the following code for nsoperation.The problem is all three tasks run serially.What can I do to make the tasks run in parallel.I tried implementing the start and isconcurrent methods but it doesnt work.please help...
Given is my uaview controller class
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Store *S=[ [Store alloc] init];
S.a=25;
NSOperationQueue *someQueue = [NSOperationQueue currentQueue];
someQueue.MaxConcurrentOperationCount = 3;
NSInvocationOperation *invocationOp2 = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(ymain)
object:nil];
NSInvocationOperation *invocationOp3 = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(ymain2)
object:nil];
NSInvocationOperation *invocationOp4 = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(ymain3)
object:nil];
[someQueue addOperation:invocationOp2];
[someQueue addOperation:invocationOp3];
[someQueue addOperation:invocationOp4];
}
-(void)ymain
{
for (int i = 0 ; i < 10000 ; i++) {
NSLog(#"in the A main"); }
}
This is the other class which was subclassed
#interface A : NSOperation
#end
#implementation A
bool executing;
bool finished;
-(void)main
{
}
- (BOOL)isConcurrent
{
return YES;
}
- (BOOL)isReady
{
return YES;
}
currentQueue is returning the main queue, which is a serial queue that executes on the main runloop. You should create your own NSOperationQueue to run the operations concurrently.
NSOperationQueue manages the number of operations depending on various factors. This is an implementation detail which you cannot effect. You cannot force it to perform operations concurrently.
The only influence you can have is to set operation dependancy, which affects the order in which operations are performed serially (which isn't much use to you!)
Also currentQueue will return nil when it is called from outside of an NSOperation. If you use mainQueue then you'll get the queue which always runs on the main thread and only runs one operation at one. You need to create a new queue.
I am using an NSOperationQueue to get some data for my app:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
GetSUPDataOperation *operation = [[GetDataOperation alloc] init];
operation.context = self;
[queue addOperation:operation];
[operation release];
I want to prevent the user from navigating to certain parts of the app until we have finished getting all the data we need.
Is there some way I can watch for the operation to finish and set a flag then?
You can set a delegate for the operation
#interface YourOperation : NSOperation {
id target;
SEL selector;
}
- (id)initWithTarget:(id)theTarget action:(SEL)action;
#end
At the end of your operation (ie. inside main function), use
- (void)main {
Your code here...
...
[target performSelectorOnMainThread:selector withObject:nil waitUntilDone:NO];
}
to ask your delegate to set a flag
From an architectural point of view you don't want to "monitor" an operation for its running state. You'd want to invoke a method when an operation has finished running.
So just invoke a method that updates the UI (or some other part of the application) when the operation finished.
I have an app, where i use function FSMoveObjectToTrashSync. It works in background thread. I need ability for my app, to click on button to pause it or continue(if it paused) how i can make it?
Example of code:
NSMutableArray *fileArray = [NSMutableArray array withobjects:#"file1url", #"file2", #"file3", nil];
NSMutableArray *threadArray = [[NSMutableArray alloc] init];
-(void)myFunc{
for (NSURL *url in fileArray){
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:#selector(mySelectorWith:) object:url];
[thread start];
[threadArray addObject:thread];
}
}
-(void)mySelectorWith:(NSURL *) url{
FSRef source;
FSPathMakeRef((const UInt8 *)[[url path] fileSystemRepresentation], &source, NULL);
FSMoveObjectToTrashSync(&source, NULL, kFSFileOperationDefaultOptions);
}
PS:sorry for my english, i'm from Belarus... =(
One solution would be to replace the for loop on a single thread with an NSOperation subclass. Each operation should trash exactly one object; you then create one operation for each object you want to trash and put all of the operations on an NSOperationQueue.
The operation queue will run each operation on a thread, and it can even run multiple operations on multiple threads if it sees enough computing power laying around to do it.
An operation queue can be paused and resumed at will; when you suspend the queue, any operations in that queue that are already running will finish, but no more will start until you resume the queue.
You could use an NSConditionLock. An NSConditionLock is similar to a condition variable. It has a couple of basic methods, lockWhenCondition, and unlockWithCondition, and lock. A typical usage is to have your background thread waiting on the condition lock with "lockWhenCondition:", and the in you foreground thread to set the condition, which causes the background thread to wake up. The condition is a simple integer, usually an enumeration.
Here's an example:
enum {
kWorkTodo = 1,
kNoWorkTodo = 0
}
- (id)init {
if ((self = [super init])) {
theConditionLock = [[NSConditionLock alloc] initWithCondition: kNoWorkTodo];
workItems = [[NSMutableArray alloc] init];
}
}
- (void)startDoingWork {
[NSThread detachNewThreadSelector:#selector(doBackgroundWork) toTarget:self withObject:nil];
}
- (void)doBackgroundWork:(id)arg {
while (YES) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSArray *items = nil;
[theConditionLock lockWhenCondition:kWorkTodo]; // Wait until there is work to do
items = [NSArray arrayWithArray:workItems]
[workItems removeAllObjects];
[theConditionLock unlockWithCondition:kNoWorkTodo];
for(id item in items) {
// Do some work on item.
}
[pool drain];
}
}
- (void)notifyBackgroundThreadAboutNewWork {
[theConditionLock lock];
[workItems addObject:/* some unit of work */];
[theConditionLock unlockWithCondition:kWorkTodo];
}
In this example, when startDoingWork is called doBackgroundWork: will start on a background thread, but then stop because there isn't any work to do. Once notifyBackgroundThreadAboutNewWork is called, then doBackgroundWork: will fire up and process the new work, and then go back to sleep waiting for new work to be available, which will happen the next time notifyBackgroundThreadAboutNewWork is called.
I'm contemplating how to offload the drawing of a very large Core Data tree structure to CATiledLayer. CATiledLayer seems to be awesome because it performs drawing on a background thread and then fades in tiles whenever they're drawn. However, because the information of the drawing comes from a Core Data context that is by design not thread safe, I'm running into race condition issues where the drawing code needs to access the CD context.
Normally, if I need to perform background tasks with Core Data, I create a new context in the background thread and reuse the existing model and persistent store coordinator, to prevent threading issues. But the CATiledLayer does all the threading internally, so I don't know when to create the context, and there needs to be some kind of context sharing, or I can't pass the right entities to the CATiledLayer to begin with.
Is there anyone with a suggestion how I can deal with this scenario?
Cheers,
Eric-Paul.
The easiest solution is to use the dispatch API to lock all of your data access onto a single thread, while still allowing the actual drawing to be multi-threaded.
If your existing managed object context can only be accessed on the main thread, then this is what you do:
- (void)drawInContext:(CGContextRef)context // I'm using a CATiledLayer subclass. You might be using a layer delegate instead
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(dispatch_get_main_queue(), ^{
NSManagedObject *record = self.managedObjecToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
This is a quick and easy solution, but it will lock your main thread while fetching the data, which is almost certainly going to create "hitches" whenever a new tile is loaded while scrolling around. To solve this, you need to perform all of your data access on a "serial" dispatch queue.
The queue needs to have it's own managed object context, and you need to keep this context in sync with the context on your main thread, which is (presumably) being updated by user actions. The easiest way to do this is to observe a notification that the context has changed, and throw out the one used for drawing.
Define an instance variable for the queue:
#interface MyClass
{
NSManagedObjectContext *layerDataAccessContext;
dispatch_queue_t layerDataAccessQueue;
}
#end
Create it in your init method:
- (id)init
{
layerDataAccessQueue = dispatch_queue_create("layer data access queue", DISPATCH_QUEUE_SERIAL);
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextDidChange:) name:NSManagedObjectContextDidSaveNotification object:nil]; // you might want to create your own notification here, which is only sent when data that's actually being drawn has changed
}
- (void)contextDidChange:(NSNotification *)notif
{
dispatch_sync(layerDataAccessQueue, ^{
[layerDataAccessContext release];
layerDataAccessContext = nil;
});
[self.layer setNeedsDisplay];
}
And access the context while drawing:
- (void)drawInContext:(CGContextRef)context
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(layerDataAccessQueue, ^{
NSManagedObject record = self.managedObjectToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
- (NSManagedObject *)managedObjectToDraw
{
if (!layerDataAccessContext) {
__block NSPersistentStoreCoordinator *coordinator;
dispatch_sync(dispatch_get_main_queue(), ^{
coordinator = [self persistentStoreCoordinator];
});
layerDataAccessContext = [[NSManagedObjectContext alloc] init];
[layerDataAccessContext setPersistentStoreCoordinator:coordinator];
}
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"Employee"
inManagedObjectContext:layerDataAccessContext];
[request setEntity:entity];
NSPredicate *predicate =
[NSPredicate predicateWithFormat:#"self == %#", targetObject];
[request setPredicate:predicate];
NSError *error = nil;
NSArray *array = [layerDataAccessContext executeFetchRequest:request error:&error];
NSManagedObject *record;
if (array == nil || array.count == 0) {
// Deal with error.
}
return [array objectAtIndex:0];
}
I've given up trying to share managed object context instances between CATiledLayer draws and now just alloc/init a new context at every call of drawLayer:inContext: The performance hit is not noticable, for the drawing is already asynchronous.
If there's anyone out there with a better solution, please share!
So, I am using [NSThread detachNewThreadSelector] to spawn a new thread and I am getting "autoreleased with no pool in place " errors in the console. I know this can happen if you fail to create an auto release pool, but the thing is, I am creating one. I use similar code in other parts of the same app and do NOT get these errors.
Here is the relevant code:
- (void) startThread:(NSString*)strURL
{
// start new thread to load image
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[NSThread detachNewThreadSelector:#selector(loadImageFromURL:) toTarget:self withObject:strURL];
[pool release];
}
- (void) loadImageFromURL:(NSString*)strURL
{
NSNumber* nn = [NSNumber numberWithInt:self.tag];
NSLog(#"loadURL: Tag number == %i", [nn intValue]);
// other code here actually does the work
}
Now, there was more code in loadImageFromURL which actually does the work (of loading an image from a remote server) - but the problem manifests itself without that code, so I've removed it (just so you don't think I have a pointless thread which does nothing!). I left in just one line of code which demonstrates the problem - it creates an autoreleased NSNumber object.
When this code runs, it reports this to the console:
__NSAutoreleaseNoPool(): Object 0x535c0e0 of class NSCFNumber autoreleased with no pool in place - just leaking
Of course, the real code creates many other AR objects and all of them get reported as well.
Would be grateful for any tips or pointers which might help!
Thanks!
When you create a new thread, you need to also create a new autorelease pool for it. In your case, that looks as simple as adding:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
at the beginning of loadImageFromURL: and
[pool drain];
at the end.
You probably don't need or want the pool you're creating in startThread:. Check out the Threading Programming Guide, particularly the "Writing Your Thread Entry Routine" section.
On your code, - (void) startThread:(NSString*)strURL is running in the main thread, while - (void) loadImageFromURL:(NSString*)strURL is running on the background thread you are detaching.
The main thread already has a NSAutoreleasePool, so the one you are creating in startThread: is probably unneeded. However, the background thread will not create a NSAutoreleasePool, so you'd need to create it yourself.
In your code, that would look like:
- (void) startThread:(NSString*)strURL
{
// start new thread to load image
[NSThread detachNewThreadSelector:#selector(loadImageFromURL:) toTarget:self withObject:strURL];
}
- (void) loadImageFromURL:(NSString*)strURL
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSNumber* nn = [NSNumber numberWithInt:self.tag];
NSLog(#"loadURL: Tag number == %i", [nn intValue]);
// other code here actually does the work
[pool drain];
}
Also, as #Carl Norum suggested, you should use drain instead of release when you are done using the autorelelase pool.
Solution for a similar problem but using ARC.
If using ARC, you could get an error "'NSAutoreleasePool' is unavailable: not available in automatic reference counting mode".
Use:
- (void) startThread:(NSString*)strURL
{
// start new thread to load image
[NSThread detachNewThreadSelector:#selector(loadImageFromURL:) toTarget:self withObject:strURL];
}
- (void) loadImageFromURL:(NSString*)strURL
{
#autoreleasepool {
NSNumber* nn = [NSNumber numberWithInt:self.tag];
NSLog(#"loadURL: Tag number == %i", [nn intValue]);
// other code here actually does the work
}
}