Sprite Kit&播放声音会导致应用终止

时间:2013-09-24 08:44:37

标签: ios audio opengl-es multitasking sprite-kit

使用ARC

我遇到的一个问题 - 我有一个SKScene,我使用SKAction类方法播放声音fx

[SKAction playSoundFileNamed:@"sound.wav" waitForCompletion:NO];

现在,当我尝试转到后台时,无论声音结束,显然iOS正在终止我的应用gpus_ReturnNotPermittedKillClient

现在只有当我评论这一行并且没有运行动作时,iOS才能在后台运行它(当然,已暂停,但没有终止)。

我做错了什么?

编辑:如果该线未运行,iOS将不会终止应用 - 例如,如果它位于未运行if statement的{​​{1}}或其他内容中像那样,当布尔是(soundOn == YES)

7 个答案:

答案 0 :(得分:28)

当应用进入后台时,问题是AVAudioSession无法激活。这并不是很明显,因为Sprite Kit没有提到它在内部使用AVAudioSession。

修复非常简单,也适用于ObjectAL =>在应用程序处于后台时将AVAudioSession设置为非活动状态,并在应用程序进入前台时重新激活音频会话。

使用此修复程序的简化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。

答案 1 :(得分:14)

我发现它是关于在AppDelegate applicationDidEnterBackground中停用AVAudioSession的,但通常会失败并显示错误(无效停用):

Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn’t be completed. (OSStatus error 560030580.)

仍会导致此处描述的崩溃:Spritekit crashes when entering background

所以,这对setActive来说还不够:不 - 我们必须有效地停用它(没有那个错误)。我通过向AppDelegate添加专用实例方法做了一个简单的解决方案,只要没有错误就停用AVAudioSession。

简而言之,它看起来像这样:

- (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];
}

NSLog证明:

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不想在几千次尝试后关闭(崩溃也是不可避免的)。因此,在Apple修复它之前,这只能被视为黑客攻击。 顺便说一下,控制启动AVAudioSession也是值得的。

完整解决方案可能如下所示:

- (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很顽固,“播放“声音,即使他们无法播放:”

据我所知,最简单的方法是setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers。我认为,任何其他AVAudioSession类别都需要避免使用playFileNamed:这可以通过制作自己的SKNode runAction:类别来播放声音方法来实现,例如使用AVAudioPlayer。但这是一个单独的主题。

我的AVAudioPlayer实现的完整一体化解决方案在这里:http://iknowsomething.com/ios-sdk-spritekit-sound/

编辑:修复失踪的人。

答案 2 :(得分:1)

我在播放音频方面遇到了类似的问题(虽然我没有在SKAction节点中使用音频)但结果导致背景崩溃。

我尝试通过将我的SKScene的paused属性设置为YES来解决此问题,但是当播放音频时,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中,我调用了一个重建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中有一个错误。

我希望这会有所帮助。

修改

上面提到了很好的答案。不幸的是,它对我不起作用,因为我正在使用音频队列,并且当我的应用程序在后台时需要音乐才能继续播放。

仍在等待Apple的修复。

答案 3 :(得分:1)

我遇到了这个问题,正如其他答案所示,似乎有两种解决方案:当应用变为无效时,(1)停止播放音频,或(2)拆除所有SKView。然后,当应用再次变为活动状态时,您可能需要撤消该响应。

即使当前不在屏幕上的SKView(例如那些与导航控制器堆栈中较早的视图控制器相关联的那些)也会导致崩溃。

所以,我在这里实现了解决方案(2): https://github.com/jawj/GJMLessCrashySKView

它是一个简单的UIView子类,它提供SKView的基本功能,并将它们转发到自己的SKView子视图。但至关重要的是,它会在屏幕关闭或应用程序重新启动时关闭此SKView,并在屏幕上显示应用程序处于活动状态时重建它。如果在屏幕上,它会将已拆除的SKView替换为自身的静态快照,并在重建SKView时再将其淡化,以便当应用程序处于非活动状态但仍然可见时(例如显示电池警告UIAlert,或者我们& #39;双击应用切换器的主页按钮)一切正常。

此解决方案还修复了(可能无关的)奖励头痛,即当SKViews消失并重新出现时(例如因为模态视图控制器被呈现然后被解雇),它们有时会冻结。

答案 4 :(得分:0)

我有同样的问题,所以我用这段代码来播放声音而不是SKAction

SystemSoundID soundID;
NSString *filename = @"soundFileName";
CFBundleRef mainBundle = CFBundleGetMainBundle ();
CFURLRef soundFileUrl = CFBundleCopyResourceURL(mainBundle, (__bridge             CFStringRef)filename, CFSTR("wav"), NULL);
AudioServicesCreateSystemSoundID(soundFileUrl, &soundID);
AudioServicesPlaySystemSound(soundID);

它解决了我的问题,我希望这有帮助

答案 5 :(得分:0)

除了将AVAudioSession设置为活动/非活动状态(如LearnCocos2D的解决方案中所述)之外,我还可以通过在后台/前台应用程序时调用暂停和播放来解决此问题。

- (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];
}

答案 6 :(得分:0)

我遇到了完全相同的问题,我以不同的方式解决了问题(因为其他解决方案无效)。我不知道为什么我的解决方案有效,所以如果有人有一个很棒的解释。

基本上,我为每个需要声音的SKAction创建了新的SKShapeNode个实例。所以我创建了一个单例类,其中包含我需要的每个声音的实例变量。我在每个SKShapeNode中引用这些变量并瞧!

这是单身代码:

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];

干杯!