Objective-c singleton object does not working as expected
I have a problem. There is class to store game progress:
struct GameData {
PackData & packDataById(LEVEL_PACK packId);
int gameVersion;
AudioData audio;
PackData sunrise;
PackData monochrome;
PackData nature;
};
//singleton
@interface GameDataObject : NSObject <NSCoding>
{
GameData data_;
}
+(GameDataObject*) sharedObject;
-(id) initForFirstLaunch;
-(GameData*) data;
-(void) save;
@end
and implementation:
@implementation GameDataObject
static GameDataObject *_sharedDataObject = nil;
+ (GameDataObject*) sharedObject
{
if (!_sharedDataObject) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedObject = [defaults objectForKey:Key];
if (!encodedObject) {
_sharedDataObject = [[GameDataObject alloc] initForFirstLaunch];
}
开发者_如何学运维 else {
_sharedDataObject = (GameDataObject*)[NSKeyedUnarchiver unarchiveObjectWithData: encodedObject];
}
}
return _sharedDataObject;
}
-(GameData*) data
{
return &data_;
}
-(id) initForFirstLaunch
{
self = [super init];
if (self) {
data_.audio.reset();
data_.sunrise.reset();
data_.monochrome.reset();
data_.nature.reset();
data_.gameVersion = 1;
data_.sunrise.levelData[0].state = LEVEL_OPENED;
}
return self;
}
-(void) save
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:self] forKey:Key];
[defaults synchronize];
}
-(void) encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeInt:data_.gameVersion forKey:@"game-version"];
[encoder encodeBytes:(uint8_t*)&data_.audio length:sizeof(AudioData) forKey:@"audio-data"];
[encoder encodeBytes:(uint8_t*)&data_.sunrise length:sizeof(PackData) forKey:@"sunrise-pack"];
[encoder encodeBytes:(uint8_t*)&data_.monochrome length:sizeof(PackData) forKey:@"monochrome-pack"];
[encoder encodeBytes:(uint8_t*)&data_.nature length:sizeof(PackData) forKey:@"nature-pack"];
}
-(id) initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self) {
data_.gameVersion = [decoder decodeIntForKey:@"game-version"];
NSUInteger length = 0;
{
const uint8_t *buffer = [decoder decodeBytesForKey:@"audio-data" returnedLength:&length];
assert(length);
memcpy(&data_.audio, buffer, length);
}
{
const uint8_t *buffer = [decoder decodeBytesForKey:@"sunrise-pack" returnedLength:&length];
assert(length);
memcpy(&data_.sunrise, buffer, length);
}
{
const uint8_t *buffer = [decoder decodeBytesForKey:@"monochrome-pack" returnedLength:&length];
assert(length);
memcpy(&data_.monochrome, buffer, length);
}
{
const uint8_t *buffer = [decoder decodeBytesForKey:@"nature-pack" returnedLength:&length];
assert(length);
memcpy(&data_.nature, buffer, length);
}
}
return self;
}
@end
It loads and saves itself correctly when save is called directly after initialization and nothing more is done.
But when I try a simple thing. I write in appDidFinishLaunching
GameDataObject *obj = [GameDataObject sharedObject];
Then everything is done - just one simple menu is loaded, and I minimize the application so
-(void) applicationDidEnterBackground:(UIApplication*)application
{
[[CCDirector sharedDirector] stopAnimation];
[[GameDataObject sharedObject] save];
}
is executed. And in this method obj
is totally corrupted (before saving), sometimes it's even seen with debugger as another class object.
What am I doing wrong?
EDIT
Just launching the app and minimizing it causes the same problem.
As I already mentioned in the comment, you have a memory bug when unarchiving the data using NSKeyedUnarchiver
.
The method +[NSKeyedUnarchiver unarchiveObjectWithData:]
returns an autoreleased object (you can tell from the naming convention: it doesn't contain either new
, alloc
or copy
) so you'd have to take ownership of the object by sending it a retain
message. Now the object won't be released by the autorelease pool at the end of the runloop.
While you have an answer, your overall app architecture could use a bit of refinement.
Notably, it is generally quite fragile to have some massive amount of persistent logic associated with an arbitrary singleton's instantiation. It introduces all kinds of weird ordering dependencies or other mechanisms via which a seemingly minor change can cause your code to break.
A far less fragile pattern is to associate reconstruction of state with known points in an application's lifespan. I.e. if the state is required for the app to work, load the state in applcationDidFinishLaunching:
. If the state is only required by a subsystem, load it when the subsystem is loaded.
Doing so reduces complexity and, by implication, reduces the maintenance costs of your code. Any indeterminism you can eliminate is a future bug removed.
When reading an archived shared object, you must retain it when assigning to your singleton variable. NSCoder methods for unarchiving always return autoreleased objects.
精彩评论