Sprite Kit leaks don't make sense - memory-leaks

I have been running Instruments to see why my SKScene won't deallocate and getting "leaks" that don't make any sense. One of the "leaks" is on a scene that properly deallocates and points to this:
border.path = path;
As the line causing the leak but the very next line is:
CGPathRelease(path);
border.lineWidth = 1.0f;
border.strokeColor = [SKColor yellowColor];
[border setAlpha:0.0f];
[border runAction:[SKAction fadeAlphaTo:1.0f duration:0.2f]];
[self addChild:border];
So you can clearly see it's released.I am also getting "leaks" for methods like:
-(void)explosionShake{
//[self testTargets];
NSArray *objectArray = [self children];
for (SKNode *node in objectArray) {
[node runAction:[SKAction moveBy:CGVectorMake(0.0f, 10.0f) duration:.05] completion:^{
[node runAction:[SKAction moveBy:CGVectorMake(0.0f, -15.0f) duration:.05] completion:^{
[node runAction:[SKAction moveBy:CGVectorMake(0.0f, 5.0f) duration:.05]];
}];
}];
}
}
After the scene ends, and also for another object which I remove from it's parent.
Could these "leaks" be caused by trying to run an action on a node that has been removed from it's parent? Because as "explosionshake" is running, one of the nodes may be removed from it's parent in another method. What about this method:
SKEmitterNode *testForExplosion = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:#"explosionTest" ofType:#"sks"]];
//testForExplosion.position = location;
//[testForExplosion setParticleSpeed:self.frame.size.width/self.gameSpeed];
SKNode *node = [SKNode node];
[self addChild:node];
[node setPosition:location];
[node runAction:[SKAction moveBy:CGVectorMake(-self.frame.size.width, 0) duration:self.gameSpeed]];
[node addChild:testForExplosion];
SKAction *wait = [SKAction waitForDuration:4.0f];
SKAction *remove = [SKAction removeFromParent];
NSArray *array = [NSArray arrayWithObjects:wait,remove, nil];
SKAction *sequence = [SKAction sequence:array];
The first line in the method is highlighted in red and the last one in green (that's just a chunk of the method). I am pretty confused about why these are "leaks", but it's probably contributing to why my scene isn't getting deallocated.
If anybody could give me pointers as to why these methods could possibly be causing memory leaks, that would be very, very helpful. I have plenty more methods which are supposedly leaking SKCSprites, when my code clearly calls removeFromParent

1)
SKShapeNode are not the most solid class to use in the game. I mean, better to use a SKSpriteNode with a shape image. If still use SKShapeNode, try code below. May not work, but give it a try.
- (void)dealloc
{
if(self.shapeNode){
[self.shapeNode setPath:NULL];
[self.shapeNode removeFromParent];
self.shapeNode = nil;
}
}
2)
It's clear that there is a strong reference to the node. May rewrite like:
-(void)explosionShake
{
NSArray *objectArray = [self children];
for (SKNode *node in objectArray) {
// create a weak reference of the node
__weak typeof(node) weaknode = node;
// run action
[node runAction:[SKAction moveBy:CGVectorMake(0.0f, 10.0f) duration:.05] completion:^{
[weaknode runAction:[SKAction moveBy:CGVectorMake(0.0f, -15.0f) duration:.05] completion:^{
[weaknode runAction:[SKAction moveBy:CGVectorMake(0.0f, 5.0f) duration:.05]];
}];
}];
}
}
3)
The third section seem to have some missing code because I don't see a leak.
Check this other thread for an example of how to run an action with a completion block.
SKAction Perform Selector syntax error
Hope this helps.

Related

Avoiding leaks with __weak

In sprite kit, I've been trying to work out why my SKScene won't deallocate and I believe I have finally taken a step closer to the answer: I use a lot of SKActions with completion blocks. I have just learned that I must use weak references.
How do I properly do this? Does everything inside the block have to be weak? For example, I just recently changed one of my blocks to look like this:
__weak typeof(self.heli) weakheli = self.heli;
[weakheli runAction:[SKAction fadeAlphaTo:1.0f duration:1.0f] completion:^{
ghostMode = NO;
}];
Do I also have to make a __weak typeof(ghoseMode) weakGhostMode = ghostMode statement and only change weakGhostMode inside the block?
As another example, should:
[weakSelf runAction:[SKAction waitForDuration:.1f] completion:^{
if (mgFiring) {
[self fireMG];
}
}];
Be changed to:
[weakSelf runAction:[SKAction waitForDuration:.1f] completion:^{
if (mgFiring) {
[weakSelf fireMG];
}
}];
Thanks for the help! When I was learning Sprite Kit through tutorials, we were never warned about using completion blocks with strong references.
This does not require __weak because the heli object is used to run the action, it is not referenced inside the block.
__weak typeof(self.heli) weakheli = self.heli;
[weakheli runAction:[SKAction fadeAlphaTo:1.0f duration:1.0f] completion:^{
ghostMode = NO;
}];
So this will be fine:
[self.heli runAction:[SKAction fadeAlphaTo:1.0f duration:1.0f] completion:^{
ghostMode = NO;
}];
Same goes for this:
[weakSelf runAction:[SKAction waitForDuration:.1f] completion:^{
if (mgFiring) {
[weakSelf fireMG];
}
}];
It should be:
[self runAction:[SKAction waitForDuration:.1f] completion:^{
if (mgFiring) {
[weakSelf fireMG];
}
}];
The weakSelf is only needed (if at all) inside the block.

Initialising NSManagedObjectContext on a serial NSOperationQueue

I have an NSOperationQueue that has some NSBlockOperations added to it, among which are blockOperations A and B. The NSOperationQueue has a maxConcurrencyOperationCount of 1.
blockOperation B, is dependant on A being finished. In each of the blockOperations I am calling a method, which in turn calls another method, that initialises a new NSManagedObjectContext (with the persistentStoreCoordinator from a singleton), that I use to create and add objects to a Core Data database. The code invoked by the aforementioned second method call in A and B looks like this (it varies slightly for for each of them):
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
managedObjectContext.persistentStoreCoordinator = [[CoreDataController sharedCoreDataController] persistantStoreCoordinator];
for (NSDictionary *articleDictionary in array) {
if (![Article articleExistsWithIDInDictionary:articleDictionary inContext:managedObjectContext]) {
[Article articleFromDictionary:articleDictionary inContext:managedObjectContext];
}
}
[[CoreDataController sharedCoreDataController] saveContext:managedObjectContext];
// method ends.
The saveContext: code looks like this:
NSError *error = nil;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
Having spent a lot of time reading Apples Docs about Core Data Concurrency, NSOperation, etc., I'm still unsure if what I'm doing with NSManagedObjectContext is thread-safe, and generally considered to be OK? Some clarification and/or indication of that I should be doing differently would be much appreciated. If you need to see any more code, please ask.
Thanks in advance.
What you are doing is NOT thread safe.
If you decide to create a context per operation, you better use the confinement concurrency type:
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
this way you don't need to change anything in your current code.
if you want to use the context with NSPrivateQueueConcurrencyType you must access objects within that context by the performBlock: or performBlockAndWait::
[managedObjectContext performBlockAndWait:^{//wait is used as to not end the operation before this code is executed
for (NSDictionary *articleDictionary in array) {
if (![Article articleExistsWithIDInDictionary:articleDictionary
inContext:managedObjectContext])
{
[Article articleFromDictionary:articleDictionary
inContext:managedObjectContext];
}
}
}];
I would probably go with my first solution in your case.
all that said, you could simply use a "private queue" context as a serial queue (as long as you add block operation to it in the order you need them to execute).
A context performBlock: method will queue the block and execute it serially in regard to other blocks added that context for execution in the background:
//add this to your CoreDataController
context = [[CoreDataController sharedCoreDataController] serialExecutionBGContext];
[context performBlock:^{ //your block operation code1}];
[context performBlock:^{ //your block operation code2}];
this will perform code1 and code2 in the background serially.
In this manner, you save the overhead of allocating a new context, and might benefit from caching done by this context.
You might want to reset this context every now and then so it will not get bloated with fetched objects.
The concern with the context is that it be accessed only within a single thread. Setting the MaxConcurrencyOperationCount does not guarantee that. Another approach is to make the context a "thread" variable, storing a context in each thread dictionary where it is used.
Ex:
+ (NSManagedObjectContext*)managedObjectContext
{
NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
NSManagedObjectContext *context = [threadDictionary valueForKey:#"QpyManagedObjectContext"];
if (context == nil) {
#autoreleasepool {
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[context setStalenessInterval: 10.0];
[context setMergePolicy:[[NSMergePolicy alloc]initWithMergeType:NSOverwriteMergePolicyType]];
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[Qpyd managedObjectModel]];
[context setPersistentStoreCoordinator:coordinator];
NSString *STORE_TYPE = NSSQLiteStoreType;
NSString *path = [[NSProcessInfo processInfo] arguments][0];
path = [path stringByDeletingPathExtension];
NSURL *url = [NSURL fileURLWithPath:[path stringByAppendingPathExtension:#"sqlite"]];
NSError *error;
NSPersistentStore *newStore = [coordinator addPersistentStoreWithType:STORE_TYPE configuration:nil URL:url options:nil error:&error];
if (newStore == nil) {
NSLog(#"Store Configuration Failure %#", ([error localizedDescription] != nil) ? [error localizedDescription] : #"Unknown Error");
}
[threadDictionary setObject:context forKey:#"QpyManagedObjectContext"];
}
}
return context;
}

Massive Memory Leak - CocoaLibSpotify

I'm using the CocoaLibSpotify library to load album art for Spotify search results.
Instruments reports no leaks, and static analysis isn't helping out either, and I've manually reviewed all of my code that deals with keeping track of loading the album art, yet, after loading a few hundred results, the app consumes over 100mb of memory and crashes.
I believe that CocoaLibSpotify is keeping a cache of the images in memory, but there is no way that I have found of disabling the cache. There is a "flushCaches" method, which I've been calling each time I get a memory warning, but, it is ineffective.
Here's what I'm using to load the album art, I keep a reference to all of the SPImage objects in an array, so that I can use them when serving up table view rows.
[self sendRequestToURL: #"http://ws.spotify.com/search/1/track.json" withParams: #{#"q": spotifySearchBar.text} usingMethod: #"GET" completionHandler: ^(id result, NSError *error) {
//after the search completes, re-enable the search button, replace the searchResults, and
// request the result table to reload the data
spotifySearchBar.userInteractionEnabled = YES;
[searchBar endEditing: YES];
[searchResults release];
int resultLength = [[result objectForKey: #"tracks"] count] < 100 ? [[result objectForKey: #"tracks"] count] : 100;
searchResults = [[[result objectForKey: #"tracks"] subarrayWithRange: NSMakeRange(0, resultLength)] retain];
for(int i = 0; i < 100; i++) {
[albumArtCache replaceObjectAtIndex: i withObject: [NSNull null]];
}
for(NSDictionary *trackDict in searchResults) {
NSString *trackURI = [trackDict objectForKey: #"href"];
[SPTrack trackForTrackURL: [NSURL URLWithString: trackURI] inSession: session callback: ^(SPTrack *track) {
[SPAsyncLoading waitUntilLoaded: track timeout: kSPAsyncLoadingDefaultTimeout then:^(NSArray *loadedItems, NSArray *notLoadedItems) {
if(track == nil) return;
[SPAsyncLoading waitUntilLoaded: track.album timeout: kSPAsyncLoadingDefaultTimeout then:^(NSArray *loadedItems, NSArray *notLoadedItems) {
if(track.album == nil) return;
[SPAsyncLoading waitUntilLoaded: track.album.largeCover timeout: kSPAsyncLoadingDefaultTimeout then:^(NSArray *loadedItems, NSArray *notLoadedItems) {
if(track.album.largeCover == nil) return;
if(![searchResults containsObject: trackDict]) {
NSLog(#"new search was performed, discarding loaded result");
return;
} else{
[albumArtCache replaceObjectAtIndex: [searchResults indexOfObject: trackDict] withObject: track.album.largeCover];
[resultTableView reloadRowsAtIndexPaths: #[[NSIndexPath indexPathForRow: [searchResults indexOfObject: trackDict] inSection: 0]] withRowAnimation: UITableViewRowAnimationAutomatic];
}
}];
}];
}];
}];
}
[resultTableView reloadData];
}];
And here is the code that deals with loading table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: #"artistCell"];
if(cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleSubtitle reuseIdentifier: #"artistCell"] autorelease];
}
cell.textLabel.text = [[searchResults objectAtIndex: indexPath.row] objectForKey: #"name"];
cell.detailTextLabel.text = [[[[searchResults objectAtIndex: indexPath.row] objectForKey: #"artists"] objectAtIndex: 0] objectForKey: #"name"];
if([albumArtCache objectAtIndex: indexPath.row] != [NSNull null]) {
cell.imageView.image = ((SPImage *)[albumArtCache objectAtIndex: indexPath.row]).image;
} else{
cell.imageView.image = nil;
}
return cell;
}
I really have no idea what's going wrong. Any assistance would be greatly appreciated.
First off, you should use SPSearch rather than the web API for searching.
The reason that Instruments isn't showing a memory leak is because there isn't one - CocoaLibSpotify caches albums and images internally for performance reasons. As a result, loaded album covers will also stick around.
Now, loading hundreds of 1024x1024 images into memory is obviously going to end badly. An easy way to mitigate the problem would be to not load the largest size image - it's not normally required for a table view at 1024x1024 pixels.
Otherwise, you can modify CocoaLibSpotify to be able to unload images. The easiest way to do this is to probably add a method to SPImage that basically does the opposite of -startLoading - namely, setting the image property to nil, the hasStartedLoading and loaded properties to NO and calling sp_image_release on the spImage property before setting that to NULL.

Core Data read only managed objects on thread not returning result to delegate

I need to use some core data managed objects in an NSOperation. The problem is that core data is not thread safe and apparently the object can't be loaded from the new thread. Does anybody know a good tutorial for this? I need the object read only... so the thread will not modify them in any way. Some other, unrelated entities may be added on the main thread while these objects are used in the background, but the background entities don't need to be modified at all..
Hmm seemed I fixed the background running issue, but now the problem is nothing is returned to the delegate... Why? In the thred if I nslog the results are all shown but that call to the delegate never happens
This is the code:
-(void)evaluateFormula:(Formula *)frm runNo:(NSUInteger)runCount{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:2];
NSManagedObjectID *formulaId = frm.objectID;
for (int i = 0; i < runCount; i++) {
NSInvocationOperation * op = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(runFormula:) object:formulaId];
[queue addOperation:op];
}
}
-(void)runFormula:(NSManagedObjectID *)fId {
NSManagedObjectContext *thredContext =[[NSManagedObjectContext alloc] init];
NSPersistentStoreCoordinator *coord = (NSPersistentStoreCoordinator *)[(PSAppDelegate *)[[UIApplication sharedApplication] delegate] persistentStoreCoordinator];
[thredContext setPersistentStoreCoordinator:coord];
Formula *f = (Formula *)[thredContext objectWithID:fId];
NSDictionary *vars = [self evaluateVariables:[f.hasVariables allObjects]];
NSMutableString *formula = [NSMutableString stringWithString:f.formula];
for (NSString *var in [vars allKeys]) {
NSNumber *value =[vars objectForKey:var];
[formula replaceOccurrencesOfString:var withString:[value stringValue] options:NSCaseInsensitiveSearch range:NSMakeRange(0, [formula length])];
}
//parse formula
NSNumber *result = [formula numberByEvaluatingString];
// NSLog(#" formula %# result : %d",formula,[result intValue]);
//aggregate results
[self performSelectorOnMainThread:#selector(aggregate:) withObject:result waitUntilDone:YES]; // the delegate doesn't get called ...
}
-(void)aggregate:(NSNumber *)res {
[self.delegate didReceiveResult:res];
}

Using Core Data in IOS6 (delete/edit)

I am saving the latest internet request of my tableviewdata in an (core data) entity, but have problems with error exceptions about "faults".
I have two methods 'loadData' which gets the latest 'ordersitems' that will be loaded in my tableview AND 'loadThumbnails' which will try to cache the thumbnail into the core data entity.
The problem occurs when the managedobject gets deleted and the thumbnail method still tries to access it. Though i made a variable stopThumbnails to stop the loadThumbnails method, the problem keeps occurring.
What is the proper iOS 6 way to lazyload the images and save them to coredata but check if the object has not been deleted? i found this Core Data multi thread application which was useful but my newbie understanding of core data is still limited and i have problems writing code. I read the apple docs about http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/coredata/Articles/cdConcurrency.html but it was hard to understand completely.
I want at least my http request to load asychronous (but preferably as much as possible) i came up with the following:
-(void)viewdidload
{
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:#"OrderItems"];
fetchReq.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.data = [self.managedObjectContext executeFetchRequest:fetchReq error:nil];
MYFILTER = #"filter=companyX";
[self loadData];
}
-(void)loadData
{
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//json request from url
NSDictionary *reqData = myOrderJSONRequest(MYFILTER);
dispatch_async( dispatch_get_main_queue(), ^{
if(reqData!=NULL && reqData!=nil)
{
//request successful so delete all items from entity before inserting new ones
stopThumbnails = YES;
for(int i=self.data.count-1;i>=0;i--)
{
[self.managedObjectContext deleteObject:[self.data objectAtIndex:i]];
}
[self.managedObjectContext save:nil];
if(reqData.count>0)
{
//insert latest updates
for (NSDictionary *row in reqData){
OrderItem *item = [NSEntityDescription insertNewObjectForEntityForName:#"OrderItem" inManagedObjectContext:self.managedObjectContext];
item.order_id = [NSNumber numberWithInt:[[row objectForKey:#"order_id"] intValue]];
item.description = [row objectForKey:#"description"];
item.thumbnail_url = [row objectForKey:#"thumbnail_url"];
}
[self.managedObjectContext save:nil];
}
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:#"OrderItems"];
fetchReq.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.data = [self.managedObjectContext executeFetchRequest:fetchReq error:nil];
[TableView reloadData];
//LOAD THUMBNAILS ASYNCHRONOUS
stopThumbnails = NO;
[self loadThumbnails];
}
else{
//NO INTERNET
}
});
});
}
-(void)loadThumbnails
{
if(!loadingThumbnails)
{
loadingThumbnails = YES;
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i=0;i<self.data.count; i++) {
if(!stopThumbnails)
{
OrderItem *item = [self.data objectAtIndex:i];
if(item.thumbnail==NULL)
{
//ASYNCHRONOUS IMAGE REQUEST
NSURL *image_url = [NSURL URLWithString:item.thumbnail_url];
NSData *image_data = [NSData dataWithContentsOfURL:image_url];
dispatch_async( dispatch_get_main_queue(), ^{
if(image_data!=nil && image_data!=NULL && !stopThumbnails)
{
//IMAGE REQUEST SUCCESSFUL
item.thumbnail = image_data;
[self.managedObjectContext save:nil];
//RELOAD AFFECTED TABLEVIEWCELL
NSIndexPath* rowToReload = [NSIndexPath indexPathForRow:i inSection:0];
NSArray* rowsToReload = [NSArray arrayWithObjects:rowToReload, nil];
[TableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationFade];
}
else
{
loadingThumbnails = NO;
return;
}
});
}
if(stopThumbnails)
{
dispatch_async( dispatch_get_main_queue(), ^{
loadingThumbnails = NO;
return;
});
}
}
else{
dispatch_async( dispatch_get_main_queue(), ^{
loadingThumbnails = NO;
return;
});
}
}
dispatch_async( dispatch_get_main_queue(), ^{
loadingThumbnails = NO;
return;
});
});
}
}
Any help is of course greatly appreciated :)
Well i dont know if this is the right approach but it works, so i'll mark this as an answer.
To do everything on the background i used a second nsmanagedobjectcontext (MOC) and then merge the changes to the main MOC. the dispatch queue works great although i had to use the NSManagedObjectContextDidSaveNotification in order to merge the changes of the two contexts.
since IOS 5 its possible to use blocks instead that do the merging for you. So i decided to use this instead of the dispatch way (this way i didnt have to use notofications).
Also using blocks i got the same problem (faults) when an object got selected on a background queue while is was deleted on a different queue. So i decided instead of deleting it right away, insert a NSDate 'deleted' property for the OrderItem. then have a timer with a delete check to see if there are objects that have been deleted longer than 10 minutes ago. This way i am sure no thumbnail was still downloading. It works. Though i would still like to know id this is the right approach :)

Resources