游戏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