iOS端游戏SDK 常用 数据存储

游戏SDK 常用 数据存储

一、Keychain

可以用来存储一些用户更换设备或者删除游戏也想保存下来的数据,比如防沉迷时长,用户账号等。
keychain(钥匙串)存储在iOS系统中,并且恢复iPhone会使keychain的内容也恢复,删除App是不会影响keychain。

这是我在某个项目里写的keychain单例,用了KeychainItemWrapper
.h文件

@interface QWKeychain : NSObject

+ (QWKeychain * __nonnull)sharedInstance;

/**
 *  只能set基本数据类型,NSNumber,NSString,NSData,NSDate等,不能set继承的Class
 *
 *  @param value
 *  @param type
 */
+ (void)setKeychainValue:(id<NSCopying, NSObject> __nullable)value forType:(id <NSCopying> __nonnull)type;
+ (id __nullable)getKeychainValueForType:(id <NSCopying> __nonnull)type;
+ (void)reset;

- (id __nullable)objectForKeyedSubscript:(id __nonnull)key;
- (void)setObject:(id __nullable)obj forKeyedSubscript:(id <NSCopying> __nonnull)key;

@end

.m文件

#import "QWKeychain.h"

#import "KeychainItemWrapper.h"

#define QW_KEYCHAIN_IDENTITY @"Qingwen"

#define QW_KEYCHAIN_GROUP @"group.iqing"

#define QW_KEYCHAIN_DICT_ENCODE_KEY_VALUE @"QW_KEYCHAIN_DICT_ENCODE_KEY_VALUE"

@interface QWKeychain ()

@property (nonatomic, strong) KeychainItemWrapper *item;

@property (nonatomic, strong) NSArray *commonClasses;

@end

@implementation QWKeychain

DEF_SINGLETON(QWKeychain);

- (instancetype)init
{
    if (self = [super init]) {
        self.commonClasses = @[[NSNumber class],
                               [NSString class],
                               [NSMutableString class],
                               [NSData class],
                               [NSMutableData class],
                               [NSDate class],
                               [NSValue class]];

        [self setup];
    }
    return self;
}

- (void)setup
{
    KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:QW_KEYCHAIN_IDENTITY accessGroup:nil];
    self.item = wrapper;
}

- (id __nullable)objectForKeyedSubscript:(id __nonnull)key
{
    return [self.class getKeychainValueForType:key];
}

- (void)setObject:(id __nullable)obj forKeyedSubscript:(id <NSCopying> __nonnull)key;
{
    [self.class setKeychainValue:obj forType:key];
}

+ (void)setKeychainValue:(id<NSCopying, NSObject> __nullable)value forType:(id <NSCopying> __nonnull)type
{
    QWKeychain *keychain = [QWKeychain sharedInstance];

    __block BOOL find = NO;
    [keychain.commonClasses enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        Class class = obj;
        if ([value isKindOfClass:class]) {
            find = YES;
            *stop = YES;
        }

    }];

    if (!find && value) {
        NSLog(@"error set keychain type [%@], value [%@]",type ,value);
        return ;
    }

    if (!type || !keychain.item) {
        return ;
    }

    id data = [keychain.item objectForKey:(__bridge id)kSecValueData];
    NSMutableDictionary *dict = nil;
    if (data && [data isKindOfClass:[NSMutableData class]]) {
        dict = [keychain decodeDictWithData:data];
    }

    if (!dict) {
        dict = [NSMutableDictionary dictionary];
    }

    if (value) {
        dict[type] = value;
    }
    else {
        [dict removeObjectForKey:type];
    }

    data = [keychain encodeDict:dict];

    if (data && [data isKindOfClass:[NSMutableData class]]) {
        [keychain.item setObject:QW_KEYCHAIN_IDENTITY forKey:(__bridge id)(kSecAttrAccount)];
        [keychain.item setObject:data forKey:(__bridge id)kSecValueData];
    }
}

+ (id __nullable)getKeychainValueForType:(id <NSCopying> __nonnull)type
{
    QWKeychain *keychain = [QWKeychain sharedInstance];
    if (!type || !keychain.item) {
        return nil;
    }

    id data = [keychain.item objectForKey:(__bridge id)kSecValueData];
    NSMutableDictionary *dict = nil;
    if (data && [data isKindOfClass:[NSMutableData class]]) {
        dict = [keychain decodeDictWithData:data];
    }

    return dict[type];
}

+ (void)reset
{
    QWKeychain *keychain = [QWKeychain sharedInstance];
    if (!keychain.item) {
        return ;
    }

    id data = [keychain encodeDict:[NSMutableDictionary dictionary]];

    if (data && [data isKindOfClass:[NSMutableData class]]) {
        [keychain.item setObject:QW_KEYCHAIN_IDENTITY forKey:(__bridge id)(kSecAttrAccount)];
        [keychain.item setObject:data forKey:(__bridge id)kSecValueData];
    }
}

- (NSMutableData *)encodeDict:(NSMutableDictionary *)dict
{
    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    [archiver encodeObject:dict forKey:QW_KEYCHAIN_DICT_ENCODE_KEY_VALUE];
    [archiver finishEncoding];
    return data;
}

- (NSMutableDictionary *)decodeDictWithData:(NSMutableData *)data
{
    NSMutableDictionary *dict = nil;
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    if ([unarchiver containsValueForKey:QW_KEYCHAIN_DICT_ENCODE_KEY_VALUE]) {
        @try {
            dict = [unarchiver decodeObjectForKey:QW_KEYCHAIN_DICT_ENCODE_KEY_VALUE];
        }
        @catch (NSException *exception) {
            NSLog(@"keychain 解析错误");
            [QWKeychain reset];
        }
    }
    [unarchiver finishDecoding];

    return dict;
}

@end

二、沙盒存储

每一个 App 只能在自己的创建的文件系统(存储区域)中进行文件的操作,不能访问其他 App 的文件系统(存储区域),该文件系统(存储区域)被成为沙盒。所有的非代码文件都要保存在此,例如图像,图标,声音,plist,文本文件等。

沙盒机制保证了 App 的安全性,因为只能访问自己沙盒文件下的文件。

Home目录:
沙盒的主目录,可以通过它查看沙盒目录的整体结构。

// 获取程序的Home目录
NSString *homePaht = NSHomeDirectory();

Documents目录
保存应用程序运行时生成的持久化数据。可被iTunes备份,可备份到 iCloud。

NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;

Library/Caches目录

存储程序的默认设置和其他信息,其下有两个重要目录:

Library/Preferences 目录:包含应用程序的偏好设置文件。不应该直接创建偏好设置文件,而是应该使用UserDefaults类来取得和设置应用程序的偏好。
Library/Caches 目录:主要存放缓存文件,此目录下文件不会在应用退出时删除。

NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;

tmp目录
存储临时文件,当在退出程序或设备重启时,文件会被清除。

NSString *tmpPath = NSTemporaryDirectory();

以上就是我在开发游戏SDK常用到的两个数据存储方式,iOS的存储方式还有很多,比如plist文件写入读取、SQLite写入读取、Bundle、归档、FMDB等。

PS:再贴一下KeychainItemWrapper代码:
.h:

#import <UIKit/UIKit.h>

/*
    The KeychainItemWrapper class is an abstraction layer for the iPhone Keychain communication. It is merely a 
    simple wrapper to provide a distinct barrier between all the idiosyncracies involved with the Keychain
    CF/NS container objects.
*/
@interface KeychainItemWrapper : NSObject
{
    NSMutableDictionary *keychainItemData;		// The actual keychain item data backing store.
    NSMutableDictionary *genericPasswordQuery;	// A placeholder for the generic keychain item query used to locate the item.
}

@property (nonatomic, retain) NSMutableDictionary *keychainItemData;
@property (nonatomic, retain) NSMutableDictionary *genericPasswordQuery;

// Designated initializer.
- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;
- (void)setObject:(id)inObject forKey:(id)key;
- (id)objectForKey:(id)key;

// Initializes and resets the default generic keychain item data.
- (void)resetKeychainItem;

@end

.m:

#import "KeychainItemWrapper.h"
#import <Security/Security.h>

/*

These are the default constants and their respective types,
available for the kSecClassGenericPassword Keychain Item class:

kSecAttrAccessGroup			-		CFStringRef
kSecAttrCreationDate		-		CFDateRef
kSecAttrModificationDate    -		CFDateRef
kSecAttrDescription			-		CFStringRef
kSecAttrComment				-		CFStringRef
kSecAttrCreator				-		CFNumberRef
kSecAttrType                -		CFNumberRef
kSecAttrLabel				-		CFStringRef
kSecAttrIsInvisible			-		CFBooleanRef
kSecAttrIsNegative			-		CFBooleanRef
kSecAttrAccount				-		CFStringRef
kSecAttrService				-		CFStringRef
kSecAttrGeneric				-		CFDataRef
 
See the header file Security/SecItem.h for more details.

*/

@interface KeychainItemWrapper (PrivateMethods)
/*
The decision behind the following two methods (secItemFormatToDictionary and dictionaryToSecItemFormat) was
to encapsulate the transition between what the detail view controller was expecting (NSString *) and what the
Keychain API expects as a validly constructed container class.
*/
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert;
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert;

// Updates the item in the keychain, or adds it if it doesn't exist.
- (void)writeToKeychain;

@end

@implementation KeychainItemWrapper

@synthesize keychainItemData, genericPasswordQuery;

- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;
{
    if (self = [super init])
    {
        // Begin Keychain search setup. The genericPasswordQuery leverages the special user
        // defined attribute kSecAttrGeneric to distinguish itself between other generic Keychain
        // items which may be included by the same application.
        genericPasswordQuery = [[NSMutableDictionary alloc] init];
        
		[genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
        [genericPasswordQuery setObject:identifier forKey:(id)kSecAttrGeneric];
		
		// The keychain access group attribute determines if this item can be shared
		// amongst multiple apps whose code signing entitlements contain the same keychain access group.
		if (accessGroup != nil)
		{
#if TARGET_IPHONE_SIMULATOR
			// Ignore the access group if running on the iPhone simulator.
			// 
			// Apps that are built for the simulator aren't signed, so there's no keychain access group
			// for the simulator to check. This means that all apps can see all keychain items when run
			// on the simulator.
			//
			// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
			// simulator will return -25243 (errSecNoAccessForItem).
#else			
			[genericPasswordQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif
		}
		
		// Use the proper search constants, return only the attributes of the first match.
        [genericPasswordQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
        [genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
        
        NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];
        
        NSMutableDictionary *outDictionary = nil;
        
        if (! (SecItemCopyMatching((__bridge CFDictionaryRef)tempQuery, (void *)&outDictionary) == noErr))
        {
            // Stick these default values into keychain item if nothing found.
            [self resetKeychainItem];
			
			// Add the generic attribute and the keychain access group.
			[keychainItemData setObject:identifier forKey:(id)kSecAttrGeneric];
			if (accessGroup != nil)
			{
#if TARGET_IPHONE_SIMULATOR
				// Ignore the access group if running on the iPhone simulator.
				// 
				// Apps that are built for the simulator aren't signed, so there's no keychain access group
				// for the simulator to check. This means that all apps can see all keychain items when run
				// on the simulator.
				//
				// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
				// simulator will return -25243 (errSecNoAccessForItem).
#else			
				[keychainItemData setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif
			}
		}
        else
        {
            // load the saved data from Keychain.
            self.keychainItemData = [self secItemFormatToDictionary:outDictionary];
        }
    }
    
	return self;
}

- (void)setObject:(id)inObject forKey:(id)key 
{
    if (inObject == nil) return;
    id currentObject = [keychainItemData objectForKey:key];
    if (![currentObject isEqual:inObject])
    {
        [keychainItemData setObject:inObject forKey:key];
        [self writeToKeychain];
    }
}

- (id)objectForKey:(id)key
{
    return [keychainItemData objectForKey:key];
}

- (void)resetKeychainItem
{
    if (!keychainItemData) 
    {
        if (self.keychainItemData) {
            self.keychainItemData = nil;
        }
        self.keychainItemData = [[NSMutableDictionary alloc] init];
    }
    else if (keychainItemData)
    {
        NSMutableDictionary *tempDictionary = [self dictionaryToSecItemFormat:keychainItemData];
		SecItemDelete((CFDictionaryRef)tempDictionary);
//        NSAssert( junk == noErr || junk == errSecItemNotFound, @"Problem deleting current dictionary." );
    }
    
    // Default attributes for keychain item.
    [keychainItemData setObject:@"" forKey:(id)kSecAttrAccount];
    [keychainItemData setObject:@"" forKey:(id)kSecAttrLabel];
    [keychainItemData setObject:@"" forKey:(id)kSecAttrDescription];
    
	// Default data for keychain item.
    [keychainItemData setObject:@"" forKey:(id)kSecValueData];
}

- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert
{
    // The assumption is that this method will be called with a properly populated dictionary
    // containing all the right key/value pairs for a SecItem.
    
    // Create a dictionary to return populated with the attributes and data.
    NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
    
    // Add the Generic Password keychain item class attribute.
    [returnDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
    
    // Convert the NSString to NSData to meet the requirements for the value type kSecValueData.
	// This is where to store sensitive data that should be encrypted.
    if ([[dictionaryToConvert objectForKey:(id)kSecValueData] isKindOfClass:[NSString class]]) {
        NSString *passwordString = [dictionaryToConvert objectForKey:(id)kSecValueData];
        [returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding] forKey:(id)kSecValueData];
    }
    else {
        [returnDictionary setObject:[dictionaryToConvert objectForKey:(id)kSecValueData] forKey:(id)kSecValueData];
    }
    
    return returnDictionary;
}

- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert
{
    // The assumption is that this method will be called with a properly populated dictionary
    // containing all the right key/value pairs for the UI element.
    
    // Create a dictionary to return populated with the attributes and data.
    NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
    
    // Add the proper search key and class attribute.
    [returnDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    [returnDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
    
    // Acquire the password data from the attributes.
    NSData *passwordData = NULL;
    if (SecItemCopyMatching((__bridge CFDictionaryRef)returnDictionary, (void *)&passwordData) == noErr)
    {
        // Remove the search, class, and identifier key/value, we don't need them anymore.
        [returnDictionary removeObjectForKey:(id)kSecReturnData];
        
        // Add the password to the dictionary, converting from NSData to NSString.
        NSString *password = [[NSString alloc] initWithBytes:[passwordData bytes] length:[passwordData length]
                                                     encoding:NSUTF8StringEncoding];
        if (password) {
            [returnDictionary setObject:password forKey:(id)kSecValueData];
        }
        else {
            [returnDictionary setObject:passwordData forKey:(id)kSecValueData];
        }
    }
    else
    {
        // Don't do anything if nothing is found.
//        NSAssert(NO, @"Serious error, no matching item found in the keychain.\n");
    }
   
	return returnDictionary;
}

- (void)writeToKeychain
{
    NSDictionary *attributes = NULL;
    NSMutableDictionary *updateItem = NULL;

    if (SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery, (void *)&attributes) == noErr)
    {
        // First we need the attributes from the Keychain.
        updateItem = [NSMutableDictionary dictionaryWithDictionary:attributes];
        // Second we need to add the appropriate search key/values.
        [updateItem setObject:[genericPasswordQuery objectForKey:(id)kSecClass] forKey:(id)kSecClass];
        
        // Lastly, we need to set up the updated attribute list being careful to remove the class.
        NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainItemData];
        [tempCheck removeObjectForKey:(id)kSecClass];
		
#if TARGET_IPHONE_SIMULATOR
		// Remove the access group if running on the iPhone simulator.
		// 
		// Apps that are built for the simulator aren't signed, so there's no keychain access group
		// for the simulator to check. This means that all apps can see all keychain items when run
		// on the simulator.
		//
		// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
		// simulator will return -25243 (errSecNoAccessForItem).
		//
		// The access group attribute will be included in items returned by SecItemCopyMatching,
		// which is why we need to remove it before updating the item.
		[tempCheck removeObjectForKey:(id)kSecAttrAccessGroup];
#endif
        
        // An implicit assumption is that you can only update a single item at a time.
		
        SecItemUpdate((CFDictionaryRef)updateItem, (CFDictionaryRef)tempCheck);
//		NSAssert( result == noErr, @"Couldn't update the Keychain Item." );
    }
    else
    {
        // No previous item found; add the new one.
        SecItemAdd((CFDictionaryRef)[self dictionaryToSecItemFormat:keychainItemData], NULL);
//		NSAssert( result == noErr, @"Couldn't add the Keychain Item." );
    }
}

@end
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值