SpriteKit 保留循环或内存泄漏
SpriteKit Retain Cycle Or Memory Leak
我的SpriteKit游戏现在有三个场景; Menu.m
、LevelSelect.m
和 Level.m
。当我启动应用程序时,内存使用量为 35MB。从主菜单过渡到关卡 selection 场景后,它几乎保持在 35MB 左右。从关卡 select 场景移动到关卡本身,它会增加约 150MB。诚然,创建关卡涉及更多精灵和 类。但是,当我通过作为 sprite 的覆盖菜单重新加载关卡时,每次重新加载时内存使用量继续增加约 2MB。这可能是一个保留周期问题吗?如果我切换回关卡 select 场景,然后重新进入关卡本身,也是如此,内存继续攀升和攀升。我担心我的状态机的实现。我将在下面 post 一些框架代码:
LevelScene.m
-(void)didMoveToView:(SKView *)view {
...
[self initGameStateMachine];
...
}
...
//other methods for setting up the level present
...
-(void) initGameStateMachine {
LevelSceneActiveState *levelSceneActiveState = [[LevelSceneActiveState alloc] initLevelScene:self];
LevelSceneConfigureState *levelSceneConfigureState = [[LevelSceneConfigureState alloc] initLevelScene:self];
LevelSceneFailState *levelSceneFailState = [[LevelSceneFailState alloc] initLevelScene:self];
LevelSceneSuccessState *levelSceneSuccessState = [[LevelSceneSuccessState alloc] initLevelScene:self];
NSMutableDictionary *states = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
levelSceneActiveState, @"LevelSceneActiveState",
levelSceneConfigureState, @"LevelSceneConfigureState",
levelSceneFailState, @"LevelSceneFailState",
levelSceneSuccessState, @"LevelSceneSuccessState",
nil];
_gameStateMachine = [[StateMachine alloc] initWithStates:states];
[_gameStateMachine enterState:levelSceneActiveState];
}
-(void)update:(CFTimeInterval)currentTime {
//update the on screen keyboard with notes that are being played
[_gameStateMachine updateWithDeltaTime:[NSNumber numberWithDouble:currentTime]];
}
-(void) willMoveFromView:(SKView *)view {
[self removeAllChildren];
}
StateMachine.m
#import "StateMachine.h"
#import "GameState.h"
@interface StateMachine()
@property NSMutableDictionary *states;
@end
@implementation StateMachine
-(instancetype)initWithStates:(NSMutableDictionary*)states {
if (self = [super init]) {
for (id key in [states allValues]) {
if (![key conformsToProtocol:@protocol(GameState)]) {
NSLog(@"%@ does not conform to @protocol(GameState)", key);
return nil;
}
}
_states = states;
}
return self;
}
//this method will be used to start the state machine process
-(bool)enterState:(id)nextState {
if (!_currentState) {
_currentState = [_states objectForKey:[nextState className]];
[[NSNotificationCenter defaultCenter] addObserver:_currentState selector:@selector(keyPressed:) name:@"KeyPressedNotificationKey" object:nil];
return YES;
}
else if ([_currentState isValidNextState:nextState]) {
[_currentState performSelector:@selector(willLeaveState)];
_currentState = [_states objectForKey:[nextState className]];
[[NSNotificationCenter defaultCenter] addObserver:_currentState selector:@selector(keyPressed:) name:@"KeyPressedNotificationKey" object:nil];
return YES;
}
return NO;
}
-(void)updateWithDeltaTime:(NSNumber*)currentTime {
[_currentState performSelector:@selector(updateWithDeltaTime:) withObject:currentTime];
}
@end
LevelSceneActiveState //这是4个状态之一
#import "LevelSceneActiveState.h"
#import "LevelSceneConfigureState.h"
#import "LevelSceneSuccessState.h"
#import "LevelSceneFailState.h"
#import "StateMachine.h"
#import "LevelScene.h"
#import "SSBitmapFontLabelNode.h"
@interface LevelSceneActiveState()
@property LevelScene *levelScene;
@end
@implementation LevelSceneActiveState
-(instancetype)initLevelScene:(LevelScene *)levelScene {
if (self = [super init])
_levelScene = levelScene;
return self;
}
-(void)updateWithDeltaTime:(NSNumber*)currentTime {
//game variables created here
....
//state machine needs to be set here...if set in init, it does not have a value in the LevelScene yet
if (_gameStateMachine == nil)
_gameStateMachine = _levelScene.gameStateMachine;
//game logic performed here
...
//check for timer finishing
if (!_levelScene.timer.isValid) {
//success
if (_levelScene.score >= 7) {
[_gameStateMachine enterState:LevelSceneSuccessState.self];
}
else { //failure
[_gameStateMachine enterState:LevelSceneFailState.self];
}
}
}
//another class is used to trigger notifications of key presses
-(void) keyPressed:(NSNotification*)notification {
NSNumber *keyCodeObject = notification.userInfo[@"keyCode"];
NSInteger keyCode = keyCodeObject.integerValue;
if (keyCode == 53)
[self escapePressed];
}
-(void) escapePressed {
[_gameStateMachine enterState:LevelSceneConfigureState.self];
[_levelScene childNodeWithName:@"timer"].paused = YES;
}
-(bool)isValidNextState:(id)nextState {
if ([[nextState className] isEqualToString:@"LevelSceneConfigureState"])
return YES;
...
return NO;
}
//this makes sure that we're not notifying an object that may not exist
-(void) willLeaveState {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:@"KeyPressedNotificationKey"
object:nil];
}
@end
在状态之间切换时,我不希望 LevelScene
场景消失。我知道这很难诊断,尤其是当您没有看到整个项目时。我该怎么做才能自己进行自我诊断?任何有帮助的 tips/tricks 都会很棒。
[更新] 我尝试了 Product->Profile->Instruments->Allocation 东西,但我不知道如何处理它。内存显示它还在继续上升。
仪器教程文章
对于像我这样在使用 Xcode 的 Instruments 时一窍不通的人,这里有一个来自 Ray Wenderlich 的 amazing article 真的很愚蠢!
我在我的项目中发现了很多问题,我什至认为这些问题都不是因为那篇文章。对于没有人发布关于这个问题的答案,我并不感到惊讶,因为当您遇到这些类型的问题时,它们对于您正在处理的项目来说是非常个人化的,并且很难调试。
我的问题+解决方案
我的问题很普遍。当我的场景重新加载时,我正在加载一组资源,在本例中是一个 .sf2(声音字体)文件,一遍又一遍。老实说,我认为我已经解决了所有 sprite 的问题。
以下是我使用 Instruments 找到它的方式:
- 在 Xcode 中转到
Product
->Profile
然后 select Allocations
这个 window 应该弹出:
- 单击左上角的
red circle
按钮(这将启动您的应用程序)
- 在您的应用中执行似乎会导致问题的操作,然后按
Mark Generation
- 重复导致问题的操作并继续按
Mark Generation
注意:Mark Generation
对当时的应用程序进行快照,以便您可以看到内存中的变化
- 从 运行 停止应用程序,然后深入其中一个世代(我选择了 C 世代,因为那时内存使用的差异变得恒定)
我的采样器(一个 AVAudioUnitSampler
对象)显示为 SamplerState
正在分配一堆内存
我点击了 SamplerState
右边的小箭头,它把我带到了这个视图(显示了大量分配内存的项目)
单击其中一个并单击 extended details
按钮将允许您查看此项的堆栈跟踪
我双击了我认为有问题的方法
双击该方法后,它会显示另一个视图,其中包含您的代码以及分配最多内存的代码行的百分比(非常有帮助!)
注意:在我的例子中,罪魁祸首是在我每次重新加载关卡时分配大约 1.6MB!忽略注释掉的代码,我在修复问题后截取了保存的会话的屏幕截图。
修复我的代码后,不再有任何主要的内存分配..虽然我还有一些东西需要清理!关卡重新加载之间只有 33KB,这要好得多!
我的SpriteKit游戏现在有三个场景; Menu.m
、LevelSelect.m
和 Level.m
。当我启动应用程序时,内存使用量为 35MB。从主菜单过渡到关卡 selection 场景后,它几乎保持在 35MB 左右。从关卡 select 场景移动到关卡本身,它会增加约 150MB。诚然,创建关卡涉及更多精灵和 类。但是,当我通过作为 sprite 的覆盖菜单重新加载关卡时,每次重新加载时内存使用量继续增加约 2MB。这可能是一个保留周期问题吗?如果我切换回关卡 select 场景,然后重新进入关卡本身,也是如此,内存继续攀升和攀升。我担心我的状态机的实现。我将在下面 post 一些框架代码:
LevelScene.m
-(void)didMoveToView:(SKView *)view {
...
[self initGameStateMachine];
...
}
...
//other methods for setting up the level present
...
-(void) initGameStateMachine {
LevelSceneActiveState *levelSceneActiveState = [[LevelSceneActiveState alloc] initLevelScene:self];
LevelSceneConfigureState *levelSceneConfigureState = [[LevelSceneConfigureState alloc] initLevelScene:self];
LevelSceneFailState *levelSceneFailState = [[LevelSceneFailState alloc] initLevelScene:self];
LevelSceneSuccessState *levelSceneSuccessState = [[LevelSceneSuccessState alloc] initLevelScene:self];
NSMutableDictionary *states = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
levelSceneActiveState, @"LevelSceneActiveState",
levelSceneConfigureState, @"LevelSceneConfigureState",
levelSceneFailState, @"LevelSceneFailState",
levelSceneSuccessState, @"LevelSceneSuccessState",
nil];
_gameStateMachine = [[StateMachine alloc] initWithStates:states];
[_gameStateMachine enterState:levelSceneActiveState];
}
-(void)update:(CFTimeInterval)currentTime {
//update the on screen keyboard with notes that are being played
[_gameStateMachine updateWithDeltaTime:[NSNumber numberWithDouble:currentTime]];
}
-(void) willMoveFromView:(SKView *)view {
[self removeAllChildren];
}
StateMachine.m
#import "StateMachine.h"
#import "GameState.h"
@interface StateMachine()
@property NSMutableDictionary *states;
@end
@implementation StateMachine
-(instancetype)initWithStates:(NSMutableDictionary*)states {
if (self = [super init]) {
for (id key in [states allValues]) {
if (![key conformsToProtocol:@protocol(GameState)]) {
NSLog(@"%@ does not conform to @protocol(GameState)", key);
return nil;
}
}
_states = states;
}
return self;
}
//this method will be used to start the state machine process
-(bool)enterState:(id)nextState {
if (!_currentState) {
_currentState = [_states objectForKey:[nextState className]];
[[NSNotificationCenter defaultCenter] addObserver:_currentState selector:@selector(keyPressed:) name:@"KeyPressedNotificationKey" object:nil];
return YES;
}
else if ([_currentState isValidNextState:nextState]) {
[_currentState performSelector:@selector(willLeaveState)];
_currentState = [_states objectForKey:[nextState className]];
[[NSNotificationCenter defaultCenter] addObserver:_currentState selector:@selector(keyPressed:) name:@"KeyPressedNotificationKey" object:nil];
return YES;
}
return NO;
}
-(void)updateWithDeltaTime:(NSNumber*)currentTime {
[_currentState performSelector:@selector(updateWithDeltaTime:) withObject:currentTime];
}
@end
LevelSceneActiveState //这是4个状态之一
#import "LevelSceneActiveState.h"
#import "LevelSceneConfigureState.h"
#import "LevelSceneSuccessState.h"
#import "LevelSceneFailState.h"
#import "StateMachine.h"
#import "LevelScene.h"
#import "SSBitmapFontLabelNode.h"
@interface LevelSceneActiveState()
@property LevelScene *levelScene;
@end
@implementation LevelSceneActiveState
-(instancetype)initLevelScene:(LevelScene *)levelScene {
if (self = [super init])
_levelScene = levelScene;
return self;
}
-(void)updateWithDeltaTime:(NSNumber*)currentTime {
//game variables created here
....
//state machine needs to be set here...if set in init, it does not have a value in the LevelScene yet
if (_gameStateMachine == nil)
_gameStateMachine = _levelScene.gameStateMachine;
//game logic performed here
...
//check for timer finishing
if (!_levelScene.timer.isValid) {
//success
if (_levelScene.score >= 7) {
[_gameStateMachine enterState:LevelSceneSuccessState.self];
}
else { //failure
[_gameStateMachine enterState:LevelSceneFailState.self];
}
}
}
//another class is used to trigger notifications of key presses
-(void) keyPressed:(NSNotification*)notification {
NSNumber *keyCodeObject = notification.userInfo[@"keyCode"];
NSInteger keyCode = keyCodeObject.integerValue;
if (keyCode == 53)
[self escapePressed];
}
-(void) escapePressed {
[_gameStateMachine enterState:LevelSceneConfigureState.self];
[_levelScene childNodeWithName:@"timer"].paused = YES;
}
-(bool)isValidNextState:(id)nextState {
if ([[nextState className] isEqualToString:@"LevelSceneConfigureState"])
return YES;
...
return NO;
}
//this makes sure that we're not notifying an object that may not exist
-(void) willLeaveState {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:@"KeyPressedNotificationKey"
object:nil];
}
@end
在状态之间切换时,我不希望 LevelScene
场景消失。我知道这很难诊断,尤其是当您没有看到整个项目时。我该怎么做才能自己进行自我诊断?任何有帮助的 tips/tricks 都会很棒。
[更新] 我尝试了 Product->Profile->Instruments->Allocation 东西,但我不知道如何处理它。内存显示它还在继续上升。
仪器教程文章
对于像我这样在使用 Xcode 的 Instruments 时一窍不通的人,这里有一个来自 Ray Wenderlich 的 amazing article 真的很愚蠢!
我在我的项目中发现了很多问题,我什至认为这些问题都不是因为那篇文章。对于没有人发布关于这个问题的答案,我并不感到惊讶,因为当您遇到这些类型的问题时,它们对于您正在处理的项目来说是非常个人化的,并且很难调试。
我的问题+解决方案
我的问题很普遍。当我的场景重新加载时,我正在加载一组资源,在本例中是一个 .sf2(声音字体)文件,一遍又一遍。老实说,我认为我已经解决了所有 sprite 的问题。
以下是我使用 Instruments 找到它的方式:
- 在 Xcode 中转到
Product
->Profile
然后 selectAllocations
这个 window 应该弹出:
- 单击左上角的
red circle
按钮(这将启动您的应用程序) - 在您的应用中执行似乎会导致问题的操作,然后按
Mark Generation
- 重复导致问题的操作并继续按
Mark Generation
注意:Mark Generation
对当时的应用程序进行快照,以便您可以看到内存中的变化
- 从 运行 停止应用程序,然后深入其中一个世代(我选择了 C 世代,因为那时内存使用的差异变得恒定)
我的采样器(一个
AVAudioUnitSampler
对象)显示为SamplerState
正在分配一堆内存我点击了
SamplerState
右边的小箭头,它把我带到了这个视图(显示了大量分配内存的项目)单击其中一个并单击
extended details
按钮将允许您查看此项的堆栈跟踪我双击了我认为有问题的方法
双击该方法后,它会显示另一个视图,其中包含您的代码以及分配最多内存的代码行的百分比(非常有帮助!)
注意:在我的例子中,罪魁祸首是在我每次重新加载关卡时分配大约 1.6MB!忽略注释掉的代码,我在修复问题后截取了保存的会话的屏幕截图。修复我的代码后,不再有任何主要的内存分配..虽然我还有一些东西需要清理!关卡重新加载之间只有 33KB,这要好得多!