Objective-c singleton object does not working as e

2019-06-10 06:43发布

问题:

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.

回答1:

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.



回答2:

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.



回答3:

When reading an archived shared object, you must retain it when assigning to your singleton variable. NSCoder methods for unarchiving always return autoreleased objects.