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.
Related
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.
I am retrieving some data from an API resource and I want to store the result inside my City entity using Magical Record and when the process finish, reload a tableView in my ViewController with the results.
All is fine but when I start the app for the first time,dowload process is started and the data is saved in core data.
but the table view in my ViewControllers is empty.
If I launch the app after the first time
the tableView refresh correctly.
I don't know if the problem is in threads... Can anybody help me?
ViewController :
Here I start the request. When block is called, I store cities array and reload tableView
- (void)getCitiesFromDataStore {
[[APIManager sharedManager] getCitiesWithCompletion:^(NSArray *cities) {
_dataSourceArray = cities;
[self.citiesTableView reloadData];
} failure:^(NSError *error) {
NSLog(#"%#",error.localizedDescription);
}];
}
APIMAnager
- (void)getCitiesWithCompletion:(void (^)(NSArray *))succesBlock
failure:(void (^)(NSError *))errorBlock
{
NSArray *cachedCities = [City findAllCities];
if ([cachedCities count] == 0) {
[self GET:#"cities" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *results = responseObject[#"cities"];
[City MR_importFromArray:results];
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
NSArray *cities = [City findAllCities];
succesBlock(cities);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
errorBlock(error);
}];
return;
}
// Si ya hay ciudades almacenadas en CoreData, devuelvo el
// succesblock con las ciudades de CoreData
succesBlock(cachedCities);
}
I have a Category also to manage actions with the City entity
City+DBOperations
+ (NSArray *)findAllCities
{
NSArray *cities = [City MR_findAll];
return cities;
}
I know you said you resolved it, but for others who might be coming here another thing you could try is wrapping the import in a saveWithBlock:completion: and do your find in the completion block.
Also make sure you know which context each method is using. It is often helpful to be explicit about that.
Therefore you could change it to (this is untested, but should give you the concept):
[self GET:#"cities" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *results = responseObject[#"cities"];
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
[City MR_importFromArray:results inContext:localContext];
} completion:^(BOOL contextDidSave, NSError *error) {
NSArray *cities = [User MR_findAllInContext:[NSManagedObjectContext MR_defaultContext]];
succesBlock(cities);
}];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
errorBlock(error);
}];
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.
Is it possible to play a track when the app has exited/gone into a background state?
For example:
- (void)applicationDidEnterBackground:(UIApplication *)application
{
NSURL *trackURL = [NSURL URLWithString:#"spotify:track:489K1qunRVBm2OpS4XGLNd"];
[[SPSession sharedSession] trackForURL:trackURL callback:^(SPTrack *track) {
if (track != nil) {
[SPAsyncLoading waitUntilLoaded:track timeout:kSPAsyncLoadingDefaultTimeout then:^(NSArray *tracks, NSArray *notLoadedTracks) {
[self.playbackManager playTrack:track callback:^(NSError *error) {
if (error) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Cannot Play Track"
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
} else {
self.currentTrack = track;
}
}];
}];
}
}];
}
The above code was taken from the Simple Player app provided with cocoalibspotify.
Copying and pasting code like this won't get you far when backgrounding - you can't use UI elements like UIAlertView, for a start.
You need to declare your application as supporting the audio background mode as documented by Apple. Then, when you go into the background you should make a background task to start playback.
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 :)