Upon launch of my iOS SpriteKit project, I preload some sprites in the background. Every 10th approach or so fails with an NSGenericException
within main
.
I am suspecting [SKTexture loadImageData]
to be the directly linked with the problem, since it is enumerating a collection by looking for textures.
I am aware of this problem, proposed solutions didn't really help though. Here is my stack traces, thanks for any comments.
2015-02-23 12:03:11.299 Leaving Earth[568:141596] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <NSConcreteMapTable: 0x17e0b840> was mutated while being enumerated.'
*** First throw call stack:
(0x294a449f 0x36c5ec8b 0x294a3f21 0x2a0f3a95 0x293cbd93 0x2c8035d1 0x2c7f7a9b 0x2c85aacd 0x2c7f73bf 0x2c7fca5f 0x2c7fc149 0x2c86a1df 0x2c870a2f 0x2c808127 0x4e19c7 0x4e91d9 0x2c807cab 0x2c804a35 0x2c836041 0x444183 0x2c3d8803 0x2c3d866b 0x30cc082b 0x2a3ca4e1 0x2945a0a5 0x2946a573 0x2946a50f 0x29468b11 0x293b63c1 0x293b61d3 0x307b40a9 0x2c9c47b1 0x12b251 0x371deaaf)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb) bt
* thread #1: tid = 0x2291c, 0x372a4dfc libsystem_kernel.dylib`__pthread_kill + 8, queue = 'com.apple.spritekit.renderQueue', stop reason = signal SIGABRT
frame #0: 0x372a4dfc libsystem_kernel.dylib`__pthread_kill + 8
frame #1: 0x37322d36 libsystem_pthread.dylib`pthread_kill + 62
frame #2: 0x37244908 libsystem_c.dylib`abort + 76
frame #3: 0x365809c8 libc++abi.dylib`abort_message + 88
frame #4: 0x3659a670 libc++abi.dylib`default_terminate_handler() + 268
frame #5: 0x36c5ef24 libobjc.A.dylib`_objc_terminate() + 192
frame #6: 0x36597de2 libc++abi.dylib`std::__terminate(void (*)()) + 78
frame #7: 0x365975a8 libc++abi.dylib`__cxa_throw + 112
frame #8: 0x36c5ed5e libobjc.A.dylib`objc_exception_throw + 250
frame #9: 0x294a3f20 CoreFoundation`__NSFastEnumerationMutationHandler + 128
frame #10: 0x2a0f3a94 Foundation`-[NSConcreteMapTable countByEnumeratingWithState:objects:count:] + 56
frame #11: 0x293cbd92 CoreFoundation`-[__NSFastEnumerationEnumerator nextObject] + 110
frame #12: 0x2c8035d0 SpriteKit`+[SKTextureAtlas(Internal) findTextureNamed:] + 284
frame #13: 0x2c7f7a9a SpriteKit`__26-[SKTexture loadImageData]_block_invoke + 1654
frame #14: 0x2c85aacc SpriteKit`SKSpinLockSync(int*, void () block_pointer) + 104
frame #15: 0x2c7f73be SpriteKit`-[SKTexture loadImageData] + 302
frame #16: 0x2c7fca5e SpriteKit`-[SKTexture(Private) load] + 174
frame #17: 0x2c7fc148 SpriteKit`+[SKTexture preloadTextures] + 320
frame #18: 0x2c86a1de SpriteKit`SKCRenderer::preRender() + 418
frame #19: 0x2c870a2e SpriteKit`SKCRenderer::renderScene(SKScene*, bool) + 94
frame #20: 0x2c808126 SpriteKit`-[SKView _renderContent] + 1102
frame #21: 0x004e19c6 libdispatch.dylib`_dispatch_client_callout + 22
frame #22: 0x004e91d8 libdispatch.dylib`_dispatch_barrier_sync_f_invoke + 96
frame #23: 0x2c807caa SpriteKit`-[SKView renderContent] + 82
frame #24: 0x2c804a34 SpriteKit`__29-[SKView setUpRenderCallback]_block_invoke + 120
frame #25: 0x2c836040 SpriteKit`-[SKDisplayLink _callbackForNextFrame:] + 248
frame #26: 0x00444182 libglInterpose.dylib`-[DYDisplayLinkInterposer forwardDisplayLinkCallback:] + 270
frame #27: 0x2c3d8802 QuartzCore`CA::Display::DisplayLinkItem::dispatch() + 98
frame #28: 0x2c3d866a QuartzCore`CA::Display::DisplayLink::dispatch_items(unsigned long long, unsigned long long, unsigned long long) + 366
frame #29: 0x30cc082a IOMobileFramebuffer`IOMobileFramebufferVsyncNotifyFunc + 90
frame #30: 0x2a3ca4e0 IOKit`IODispatchCalloutFromCFMessage + 256
frame #31: 0x2945a0a4 CoreFoundation`__CFMachPortPerform + 132
frame #32: 0x2946a572 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 34
frame #33: 0x2946a50e CoreFoundation`__CFRunLoopDoSource1 + 346
frame #34: 0x29468b10 CoreFoundation`__CFRunLoopRun + 1608
frame #35: 0x293b63c0 CoreFoundation`CFRunLoopRunSpecific + 476
frame #36: 0x293b61d2 CoreFoundation`CFRunLoopRunInMode + 106
frame #37: 0x307b40a8 GraphicsServices`GSEventRunModal + 136
frame #38: 0x2c9c47b0 UIKit`UIApplicationMain + 1440
* frame #39: 0x0012b250 Leaving Earth`main(argc=1, argv=0x00276a68) + 116 at main.m:16
(lldb)
Update
After explicitly re-using the same texture for copies of subclassed sprites, the exception only occurs in the main (but still with the same frequency). This is how I build my node tree:
The
SKSpriteView
preloads the last played level while displaying the menu.
@implementation LESpriteView //... [self.gamePlayScene loadLevel:[LEModel getLastPlayedLevel] completionHandler:^{ if (self.scene == self.loadingScene) { SKTransition* transition = [SKTransition crossFadeWithDuration: 0.2]; [self presentScene:self.gamePlayScene transition: transition]; }}]; //... @end
Within the
LEGameplayScene
, theLEWorldBuilder
gets fed with data from a csv file containing node positions.
@implementation LEGameplayScene
//...
-(void) loadLevel: (LELevel) level completionHandler:(LELoadCompletionHandler) handler {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self loadLevel:level];
if (!handler) return;
dispatch_async(dispatch_get_main_queue(), ^{
handler();
});
});
}
-(void) loadLevel: (LELevel) level {
self.gameState = LEGameStateLoading;
self.scene.paused = YES;
self.view.paused = YES;
[LESpriteFactory setupSharedSprites];
//...
//Load the world
NSDictionary* infoDict = [LECommonUtilities getInfoForLevelNumber:level];
[self.world removeFromParent];
self.world = [[LEWorld alloc] init];
[self addChild:self.world];
[LEWorldBuilder buildWorld:self.world basedOnInfo:infoDict];
//...
}
@end
- The
LEWorldBuilder
parses the CSV and creates sprites by calling methods ofLESpriteFactory
, which it then adds to different layers of the "root" world node and/or groups them in arrays to be added dynamically (object culling).
The LESpriteFactory
handles the creation of new objects like so:
@implementation LESpriteFactory
//Cached sprites
LEItemNode* star;
LEItemNode* cloud;
LEItemNode* asteroid;
LEItemNode* satellite;
LECountdownLabel* label;
LETimeBreakerNode* timeBreaker;
LERepairMarkerNode* repairMarker;
//...
static BOOL alreadyLoaded = NO;
/** Loads all shared sprites to copy them on demand */
+ (void) setupSharedSprites {
if (!alreadyLoaded) {
star = [[LEStarNode alloc] init];
asteroid = [[LEAsteroidNode alloc] init];
timeBreaker = [[LETimeBreakerNode alloc] init];
label = [[LECountdownLabel alloc] initWithFontSize:10];
repairMarker = [[LERepairMarkerNode alloc] init];
cloud = [[LECloudNode alloc] init];
satellite = [[LESatelliteNode alloc] init];
//Start idle action after initialisation (during wouldn't work)
[star runIdleAction];
[repairMarker runIdleAction];
[timeBreaker runIdleAction];
[satellite runIdleAction];
}
alreadyLoaded = YES;
}
+ (LEItemNode*) star {
return [star copy];
}
//...
@end
While each sprite caches and reuses its SKTexture
upon copy:
@implementation LEStarNode
static SKTexture* sharedTexture;
-(id) init {
if (!sharedTexture) {
sharedTexture = [SKTexture textureWithImageNamed:@"LE_Collectable_Star.png"];
}
self = [super initWithTexture:sharedTexture];
//...
}
//...
@end