Набор Sprite и воспроизведение звука ведут к завершению приложения
используя ARC
Просто проблема, с которой я столкнулся - у меня есть сценарий SKS, в котором я играю звуковые эффекты, используя метод класса SKAction.
[SKAction playSoundFileNamed:@"sound.wav" waitForCompletion:NO];
Теперь, когда я пытаюсь перейти в фоновый режим, независимо от того, что звук закончился, по-видимому, iOS закрывает мое приложение из-за gpus_ReturnNotPermittedKillClient
,
Теперь, только когда я комментирую эту строку и не запускаю действие, iOS прекрасно работает в фоновом режиме (конечно, приостановлено, но без завершения).
Что я делаю неправильно?
РЕДАКТИРОВАТЬ: iOS не будет завершать приложение, если строка не была запущена, скажем, если она была в if statement
это не было бежать (soundOn == YES)
или что-то подобное, когда бул false
7 ответов
Проблема в 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.
Я обнаружил, что все дело в деактивации AVAudioSession в AppDelegate applicationDidEnterBackground:, но часто это приводит к ошибке (без деактивации):
Error Domain=NSOSStatusErrorDomain Code=560030580 "The operation couldn’t be completed. (OSStatus error 560030580.)
что все еще приводит к сбою, описанному здесь: Spritekit падает при входе в фоновый режим.
Таким образом, для 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)
Это действительно коротко, потому что это не заботится о переполнении стека:), если 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;
}
}
Эта проблема, однако, является несложной задачей по сравнению с прерываниями AVAudioSession в приложении SpriteKit. Если мы не справимся с этим, рано или поздно у нас начнутся большие проблемы с утечками памяти и ЦП 99% (56% от [SKSoundSource queuedBufferCount] и 34% от [SKSoundSource isPlaying] - см. Инструменты), потому что SpriteKit упрям и " играет "звуки, даже они не могут быть воспроизведены:)
Насколько я обнаружил, самый простой способ - установить Category:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers. Я думаю, что любые другие категории AVAudioSession должны избегать playFileNamed: вообще. Это можно сделать, создав собственный метод SKNode runAction: категория для воспроизведения звука, например, с помощью AVAudioPlayer. Но это отдельная тема.
Мое полное решение "все в одном" с реализацией AVAudioPlayer находится здесь: http://iknowsomething.com/ios-sdk-spritekit-sound/
Редактировать: Исправлено отсутствие парен.
У меня была похожая проблема с воспроизведением аудио (хотя я не использую аудио в узле SKAction) с тем же фоновым сбоем в результате.
Я попытался решить эту проблему, установив paused
собственность моей SKScene к YES
, но при воспроизведении звука в SpriteKit возникает ошибка. В этой ситуации метод обновления фактически вызывается после того, как для 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.
У меня была эта проблема, и, как видно из других ответов, кажется, что есть два решения: когда приложение становится неактивным, либо (1) прекращает воспроизведение звука, либо (2) разрушает все ваши SKView. Затем вам может понадобиться отменить этот ответ, когда приложение снова станет активным.
Даже SKViews, которые в данный момент не отображаются на экране (например, те, которые связаны с контроллерами представления ранее в стеке контроллера навигации), вызовут сбой.
Итак, я реализовал решение (2) здесь: https://github.com/jawj/GJMLessCrashySKView
Это простой подкласс UIView, который предлагает основные функции SKView и перенаправляет их в свой собственный подвид SKView. Важно отметить, что он разрушает этот SKView всякий раз, когда он выходит из экрана или приложение уходит в отставку, и восстанавливает его, если и когда он возвращается на экран и приложение активно. Если на экране, он заменяет снятый SKView на еще один моментальный снимок, и затем снова исчезает, когда SKView перестраивается, так что, когда приложение неактивно, но все еще видно (например, отображается предупреждение о батарее UIAlert, или мы дважды щелкнул кнопку "Домой" для переключателя приложений) все выглядит нормально.
Это решение также устраняет (возможно, не связанную) бонусную головную боль, которая заключается в том, что когда SKView исчезают и появляются снова (например, из-за того, что модальный контроллер представления представлен, а затем отклонен), они иногда зависают.
Мне удалось решить эту проблему, вызвав режим паузы и воспроизведения, когда фон / приоритет приложения, в дополнение к установке активного / неактивного 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];
}
У меня была точно такая же проблема, и я решил ее по-другому (так как другие решения не работали). Я понятия не имею, почему мое решение работало так, если у кого-то есть объяснение, которое было бы удивительным.
В основном я создавал новые 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];
Ура!
У меня была такая же проблема, поэтому я использовал этот код для воспроизведения звуков вместо SKAction
SystemSoundID soundID;
NSString *filename = @"soundFileName";
CFBundleRef mainBundle = CFBundleGetMainBundle ();
CFURLRef soundFileUrl = CFBundleCopyResourceURL(mainBundle, (__bridge CFStringRef)filename, CFSTR("wav"), NULL);
AudioServicesCreateSystemSoundID(soundFileUrl, &soundID);
AudioServicesPlaySystemSound(soundID);
и это решило мои проблемы, надеюсь, это поможет