Sprite Kit和播放声音导致应用程序终止
使用ARC
只是我碰到一个问题 – 我有一个SKScene,在其中使用SKAction类方法播放声音
[SKAction playSoundFileNamed:@"sound.wav" waitForCompletion:NO];
现在,当我尝试去背景,无论声音结束,显然iOS终止我的应用程序,由于gpus_ReturnNotPermittedKillClient
。
现在,只有当我评论这行,而不是运行iOS的后台运行(当然,暂停,但没有终止)的行动。
我究竟做错了什么?
编辑 :iOS不会终止应用程序,如果行不运行 – 说,如果它是在一个if statement
没有运行的if statement
(soundOn == YES)
或类似的东西,当布尔是false
问题是AVAudioSession
无法在应用程序进入后台时处于活动状态。 这并不是很明显,因为Sprite Kit没有提到它在内部使用AVAudioSession。
该修复非常简单,也适用于ObjectAL =>在应用程序处于后台时将AVAudioSession
设置为非活动状态,并在应用程序进入前台时重新激活audio会话。
使用此修复程序的简化AppDelegate如下所示:
#import <AVFoundation/AVFoundation.h> ... - (void)applicationWillResignActive:(UIApplication *)application { // prevent audio crash [[AVAudioSession sharedInstance] setActive:NO error:nil]; } - (void)applicationDidEnterBackground:(UIApplication *)application { // prevent audio crash [[AVAudioSession sharedInstance] setActive:NO error:nil]; } - (void)applicationWillEnterForeground:(UIApplication *)application { // resume audio [[AVAudioSession sharedInstance] setActive:YES error:nil]; }
PS:这个修复将会包含在Kobold Kit v7.0.3中。
我发现这是关于在AppDelegate applicationDidEnterBackground:中停用AVAudioSession,但经常失败,错误(没有停用有效):
Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn't be completed. (OSStatus error 560030580.)
这仍然导致这里描述的崩溃: Spritekit在进入后台时崩溃 。
所以, 设置主动性是不够的:否 – 我们必须有效地停用它(没有这个错误) 。 我做了一个简单的解决scheme,在AppDelegate中添加专门的实例方法,只要没有错误,就可以closuresAVAudioSession。
总之它看起来像这样:
- (void)applicationDidEnterBackground:(UIApplication *)application { NSLog(@"%s", __FUNCTION__); [self stopAudio]; } - (void)stopAudio { NSError *error = nil; [[AVAudioSession sharedInstance] setActive:NO error:&error]; NSLog(@"%s AudioSession Error: %@", __FUNCTION__, error); if (error) [self stopAudio]; }
NSLogcertificate:
2014-01-25 11:41:48.426 MyApp[1957:60b] -[ATWAppDelegate applicationDidEnterBackground:] 2014-01-25 11:41:48.431 MyApp[1957:60b] -[ATWAppDelegate stopAudio] AudioSession Error: Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn't be completed. (OSStatus error 560030580.)" 2014-01-25 11:41:48.434 MyApp[1957:60b] -[ATWAppDelegate stopAudio] AudioSession Error: Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn't be completed. (OSStatus error 560030580.)" 2014-01-25 11:41:48.454 MyApp[1957:60b] -[ATWAppDelegate stopAudio] AudioSession Error: Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn't be completed. (OSStatus error 560030580.)" 2014-01-25 11:41:49.751 MyApp[1957:60b] -[ATWAppDelegate stopAudio] AudioSession Error: (null)
这真的很简短,因为它不关心stackoverflow :)如果AVAudioSession不想在几千次尝试后closures(崩溃是不可避免的,那么也是)。 所以,这只能被视为黑客直到苹果修复它。 顺便说一下,控制启动AVAudioSession也是值得的。
完整的解决scheme可以像这样:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSLog(@"%s", __FUNCTION__); [self startAudio]; return YES; } - (void)applicationDidEnterBackground:(UIApplication *)application { NSLog(@"%s", __FUNCTION__); // SpriteKit uses AVAudioSession for [SKAction playSoundFileNamed:] // AVAudioSession cannot be active while the application is in the background, // so we have to stop it when going in to background // and reactivate it when entering foreground. // This prevents audio crash. [self stopAudio]; } - (void)applicationWillEnterForeground:(UIApplication *)application { NSLog(@"%s", __FUNCTION__); [self startAudio]; } - (void)applicationWillTerminate:(UIApplication *)application { NSLog(@"%s", __FUNCTION__); [self stopAudio]; } static BOOL isAudioSessionActive = NO; - (void)startAudio { AVAudioSession *audioSession = [AVAudioSession sharedInstance]; NSError *error = nil; if (audioSession.otherAudioPlaying) { [audioSession setCategory: AVAudioSessionCategoryAmbient error:&error]; } else { [audioSession setCategory: AVAudioSessionCategorySoloAmbient error:&error]; } if (!error) { [audioSession setActive:YES error:&error]; isAudioSessionActive = YES; } NSLog(@"%s AVAudioSession Category: %@ Error: %@", __FUNCTION__, [audioSession category], error); } - (void)stopAudio { // Prevent background apps from duplicate entering if terminating an app. if (!isAudioSessionActive) return; AVAudioSession *audioSession = [AVAudioSession sharedInstance]; NSError *error = nil; [audioSession setActive:NO error:&error]; NSLog(@"%s AVAudioSession Error: %@", __FUNCTION__, error); if (error) { // It's not enough to setActive:NO // We have to deactivate it effectively (without that error), // so try again (and again... until success). [self stopAudio]; } else { isAudioSessionActive = NO; } }
然而,这个问题与SpriteKit应用程序中的AVAudioSession中断相比是小菜一碟 。 如果我们不处理,迟早会出现内存泄漏和CPU 99%的严重问题(来自[SKSoundSource queuedBufferCount]的56%和[SKSoundSource isPlaying]的34%),因为SpriteKit固执而“播放“的声音,即使他们不能播放:)
据我发现,最简单的方法是设置类别:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers。 我认为,任何其他AVAudioSession类别都需要避免playFileNamed:。 这可以通过使用您自己的SKNode runAction:category来播放声音方法,例如使用AVAudioPlayer。 但这是另一个话题。
我用AVAudioPlayer实现完整的一体化解决scheme在这里: http : //iknowsomething.com/ios-sdk-spritekit-sound/
编辑:修复缺lessparen。
我在播放audio方面遇到了类似的问题(虽然我没有在SKAction节点中使用audio),结果也是如此。
我试图通过将我的SKScene的paused
属性设置为YES
来解决这个问题,但是当播放audio时,SpriteKit中似乎存在一个错误。 在这种情况下,更新方法实际上是在暂停设置为YES之后调用的。 这是我的更新代码:
- (void)update:(CFTimeInterval)currentTime { /* Called before each frame is rendered */ if (self.paused) { // Apple bug? NSLog(@"update: called while SKView.paused == YES!"); return; } // update! [_activeLayer update]; }
当NSLog被追查出来,应用程序将会随GL错误一起崩溃。
我发现解决这个问题的唯一方法是相当重的。 进入后台时,我必须删除和取消分配我的整个SKView。
在applicationDidEnterBackground
我在我的ViewController中调用了一个这样的函数:
[self.playView removeFromSuperview];
确保你没有任何强大的SKView引用,因为它必须被释放才能工作。
在applicationWillEnterForeground
我调用一个函数来重build我的SKView,如下所示:
CGRect rect = self.view.frame; if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) { CGFloat temp = rect.size.width; rect.size.width = rect.size.height; rect.size.height = temp; } SKView *skView = [[SKView alloc] initWithFrame:rect]; skView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; [self.view insertSubview:skView atIndex:0]; self.playView = skView; // Create and configure the scene. self.myScene = [CustomScene sceneWithSize:skView.bounds.size]; self.myScene.scaleMode = SKSceneScaleModeResizeFill; // Present the scene. [skView presentScene:self.myScene];
是的,这感觉就像一个彻头彻尾的黑客,但我认为在SpriteKit有一个错误。
我希望这有帮助。
编辑
上面的大接受的答案。 不幸的是,它不适合我,因为我使用的audio队列,需要音乐,当我的应用程序在后台时继续播放。
还在等待苹果的修复。
我有这个问题,并在其他答案看到有两种解决scheme:当应用程序变为不活动时,(1)停止播放audio,或(2)拆除所有SKViews。 当应用程序再次激活时,您可能需要撤消该响应。
即使当前不在屏幕上的SKViews(例如那些在导航控制器堆栈中与视图控制器相关的视图控制器)也会导致崩溃。
所以,我在这里实现了解决scheme(2): https : //github.com/jawj/GJMLessCrashySKView
这是一个简单的UIView子类,它提供了SKView的基本function,并将它们转发到它自己的SKView子视图。 最重要的是,无论什么时候它离开屏幕或者应用程序退出活动状态,它都会将这个SKViewclosures,并且在屏幕上显示并且该应用程序处于活动状态时重build该SKView。 如果在屏幕上,它用自己的静态快照取代了被拆除的SKView,并在重buildSKView时重新淡出,这样,当应用程序处于非活动状态但仍然可见时(例如显示电池警告UIAlert,或者我们已经双击了应用程序切换器的主页button)一切看起来很正常。
这个解决scheme也解决了一个(可能无关的)头痛的问题,那就是当SKViews消失并重新出现时(例如,因为模态视图控制器被呈现然后被解散),它们有时会冻结。
我有同样的问题,所以我用这个代码播放声音,而不是SKAction
SystemSoundID soundID; NSString *filename = @"soundFileName"; CFBundleRef mainBundle = CFBundleGetMainBundle (); CFURLRef soundFileUrl = CFBundleCopyResourceURL(mainBundle, (__bridge CFStringRef)filename, CFSTR("wav"), NULL); AudioServicesCreateSystemSoundID(soundFileUrl, &soundID); AudioServicesPlaySystemSound(soundID);
它解决了我的问题,我希望这有助于
除了设置AVAudioSession活动/不活动(如LearnCocos2D的解决scheme中提到的)之外,我还可以通过调用暂停和播放背景/前景应用程序来解决此问题。
- (void)applicationWillResignActive:(UIApplication *)application { [self.audioPlayer pause]; [[AVAudioSession sharedInstance] setActive:NO error:nil]; } - (void)applicationDidEnterBackground:(UIApplication *)application { [self.audioPlayer pause]; [[AVAudioSession sharedInstance] setActive:NO error:nil]; } - (void)applicationWillEnterForeground:(UIApplication *)application { [[AVAudioSession sharedInstance] setActive:YES error:nil]; [self.audioPlayer play]; }
我有完全相同的问题,我解决了不同的方式(因为其他解决scheme不起作用)。 我不知道为什么我的解决scheme工作,所以如果有人有一个可怕的解释。
基本上,我为每个需要声音的SKShapeNode
创build了新的SKAction
实例。 所以我创build了一个带有实例variables的单例类,用于我需要的每个声音。 我在每个SKShapeNode
引用这些variables,瞧!
这里是单例代码:
CAAudio.h
#import <Foundation/Foundation.h> #import <SpriteKit/SpriteKit.h> @interface CAAudio : NSObject @property (nonatomic) SKAction *correctSound; @property (nonatomic) SKAction *incorrectSound; + (id)sharedAudio; @end
CAAudio.m
+ (id)sharedAudio { static CAAudio *sharedAudio = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedAudio = [self new]; sharedAudio.correctSound = [SKAction playSoundFileNamed:@"correct.wav" waitForCompletion:YES]; sharedAudio.incorrectSound = [SKAction playSoundFileNamed:@"incorrect.wav" waitForCompletion:YES]; }); return sharedAudio; }
声音然后像这样访问:
SKAction *sound = [[CAAudio sharedAudio] correctSound];
干杯!