26

使用弧

只是我遇到的一个问题-我有一个 SKScene,我在其中使用 SKAction 类方法播放声音效果

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

现在,当我尝试进入后台时,无论声音结束,显然 iOS 正在终止我的应用程序,原因是gpus_ReturnNotPermittedKillClient.

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

我究竟做错了什么?

编辑:如果该行未运行,iOS 将不会终止应用程序 - 比如说,如果它在一个if statement未运行(soundOn == YES)或类似的情况下,当 bool 是false

4

7 回答 7

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 中。

于 2013-10-09T22:11:03.070 回答
14

我发现这都是关于在 AppDelegate applicationDidEnterBackground: 中停用 AVAudioSession 的,但通常它会因错误而失败(没有停用效果):

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

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

因此,仅 setActive:NO 是不够的 - 我们必须有效地停用它(没有那个错误)。我通过向 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 修复它之前,这只能被视为一种 hack。顺便说一句,控制启动 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 app 中的 AVAudioSession 中断相比,这个问题是小菜一碟。如果我们不处理它,迟早我们会遇到内存泄漏和 CPU 99% 的大麻烦(56% 来自 [SKSoundSource queuedBufferCount] 和 34% 来自 [SKSoundSource isPlaying] - 请参阅 Instruments),因为 SpriteKit 很顽固并且“播放”的声音,即使它们无法播放 :)

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

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

编辑:修复了缺少的括号。

于 2014-01-25T11:01:23.533 回答
1

我在播放音频时遇到了类似的问题(尽管我没有在 SKAction 节点中使用音频),结果导致相同的背景崩溃。

我试图通过将paused我的 SKScene 的属性设置为 来解决这个问题YES,但是当播放音频时,SpriteKit 中似乎存在一个错误。在这种情况下,update 方法实际上在 paused 设置为 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的修复。

于 2013-09-30T05:08:28.233 回答
1

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

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

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

它是一个简单的 UIView 子类,提供 SKView 的基本功能并将它们转发到它自己的 SKView 子视图。但是,至关重要的是,每当它离开屏幕或应用程序退出活动时,它都会拆除这个 SKView,并在它重新出现在屏幕上并且应用程序处于活动状态时重建它。如果在屏幕上,它会用自己的静止快照替换被拆除的 SKView,并在重建 SKView 时再次淡出,这样当应用程序处于非活动状态但仍然可见时(例如,显示电池警告 UIAlert,或者我们已经双击应用程序切换器的主页按钮)一切看起来都很正常。

这个解决方案还解决了一个(可能不相关的)额外头痛问题,即当 SKView 消失并重新出现时(例如,因为模态视图控制器出现然后又被关闭),它们有时会冻结。

于 2014-04-16T11:19:19.287 回答
1

除了设置 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];
}
于 2014-05-23T00:46:14.797 回答
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);

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

于 2014-05-10T11:44:15.397 回答
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];

干杯!

于 2015-07-31T14:20:11.137 回答