AutorÃa | Ultima modificación | Ver Log |
// Copyright 2019 Google//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.#import "Crashlytics/Crashlytics/FIRCLSUserDefaults/FIRCLSUserDefaults.h"#import "Crashlytics/Crashlytics/Components/FIRCLSApplication.h"#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"#define CLS_USER_DEFAULTS_SERIAL_DISPATCH_QUEUE "com.crashlytics.CLSUserDefaults.access"#define CLS_USER_DEFAULTS_SYNC_QUEUE "com.crashlytics.CLSUserDefaults.io"#define CLS_TARGET_CAN_WRITE_TO_DISK !TARGET_OS_TV// These values are required to stay the same between versions of the SDK so// that when end users upgrade, their crashlytics data is still saved on disk.#if !CLS_TARGET_CAN_WRITE_TO_DISKstatic NSString *const FIRCLSNSUserDefaultsDataDictionaryKey =@"com.crashlytics.CLSUserDefaults.user-default-key.data-dictionary";#endifNSString *const FIRCLSUserDefaultsPathComponent = @"CLSUserDefaults";/*** This class is an isolated re-implementation of UserDefaults which isolates our storage* from that of our customers. This solves a number of issues we have seen in production, firstly* that customers often delete or clear UserDefaults, unintentionally deleting our data.* Further, we have seen thread safety issues in production with UserDefaults, as well as a number* of bugs related to accessing UserDefaults before the device has been unlocked due to the* FileProtection of UserDefaults.*/@interface FIRCLSUserDefaults ()@property(nonatomic, readwrite) BOOL synchronizeWroteToDisk;#if CLS_TARGET_CAN_WRITE_TO_DISK@property(nonatomic, copy, readonly) NSURL *directoryURL;@property(nonatomic, copy, readonly) NSURL *fileURL;#endif@property(nonatomic, copy, readonly)NSDictionary *persistedDataDictionary; // May only be safely accessed on the DictionaryQueue@property(nonatomic, copy, readonly)NSMutableDictionary *dataDictionary; // May only be safely accessed on the DictionaryQueue@property(nonatomic, readonly) dispatch_queue_tserialDictionaryQueue; // The queue on which all access to the dataDictionary occurs.@property(nonatomic, readonly)dispatch_queue_t synchronizationQueue; // The queue on which all disk access occurs.@end@implementation FIRCLSUserDefaults#pragma mark - singleton+ (instancetype)standardUserDefaults {static FIRCLSUserDefaults *standardUserDefaults = nil;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{standardUserDefaults = [[super allocWithZone:NULL] init];});return standardUserDefaults;}- (id)copyWithZone:(NSZone *)zone {return self;}- (id)init {if (self = [super init]) {_serialDictionaryQueue =dispatch_queue_create(CLS_USER_DEFAULTS_SERIAL_DISPATCH_QUEUE, DISPATCH_QUEUE_SERIAL);_synchronizationQueue =dispatch_queue_create(CLS_USER_DEFAULTS_SYNC_QUEUE, DISPATCH_QUEUE_SERIAL);dispatch_sync(self.serialDictionaryQueue, ^{#if CLS_TARGET_CAN_WRITE_TO_DISKself->_directoryURL = [self generateDirectoryURL];self->_fileURL = [[self->_directoryURLURLByAppendingPathComponent:FIRCLSUserDefaultsPathComponentisDirectory:NO] URLByAppendingPathExtension:@"plist"];#endifself->_persistedDataDictionary = [self loadDefaults];if (!self->_persistedDataDictionary) {self->_persistedDataDictionary = [NSDictionary dictionary];}self->_dataDictionary = [self->_persistedDataDictionary mutableCopy];});}return self;}- (NSURL *)generateDirectoryURL {NSURL *directoryBaseURL =[[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectoryinDomains:NSUserDomainMask] lastObject];NSString *hostAppBundleIdentifier = [self getEscapedAppBundleIdentifier];return [self generateDirectoryURLForBaseURL:directoryBaseURLhostAppBundleIdentifier:hostAppBundleIdentifier];}- (NSURL *)generateDirectoryURLForBaseURL:(NSURL *)directoryBaseURLhostAppBundleIdentifier:(NSString *)hostAppBundleIdentifier {NSURL *directoryURL = directoryBaseURL;// On iOS NSApplicationSupportDirectory is contained in the app's bundle. On OSX, it is not (it is// ~/Library/Application Support/). On OSX we create a directory// ~/Library/Application Support/<app-identifier>/com.crashlytics/ for storing files.// Mac App Store review process requires files to be written to// ~/Library/Application Support/<app-identifier>/,// so ~/Library/Application Support/com.crashlytics/<app-identifier>/ cannot be used.#if !TARGET_OS_SIMULATOR && !TARGET_OS_EMBEDDEDif (hostAppBundleIdentifier) {directoryURL = [directoryURL URLByAppendingPathComponent:hostAppBundleIdentifier];}#endifdirectoryURL = [directoryURL URLByAppendingPathComponent:@"com.crashlytics"];return directoryURL;}- (NSString *)getEscapedAppBundleIdentifier {return FIRCLSApplicationGetBundleIdentifier();}#pragma mark - fetch object- (id)objectForKey:(NSString *)key {__block id result;dispatch_sync(self.serialDictionaryQueue, ^{result = [self->_dataDictionary objectForKey:key];});return result;}- (NSString *)stringForKey:(NSString *)key {id result = [self objectForKey:key];if (result != nil && [result isKindOfClass:[NSString class]]) {return (NSString *)result;} else {return nil;}}- (BOOL)boolForKey:(NSString *)key {id result = [self objectForKey:key];if (result != nil && [result isKindOfClass:[NSNumber class]]) {return [(NSNumber *)result boolValue];} else {return NO;}}// Defaults to 0- (NSInteger)integerForKey:(NSString *)key {id result = [self objectForKey:key];if (result && [result isKindOfClass:[NSNumber class]]) {return [(NSNumber *)result integerValue];} else {return 0;}}#pragma mark - set object- (void)setObject:(id)object forKey:(NSString *)key {dispatch_sync(self.serialDictionaryQueue, ^{[self->_dataDictionary setValue:object forKey:key];});}- (void)setString:(NSString *)string forKey:(NSString *)key {[self setObject:string forKey:key];}- (void)setBool:(BOOL)boolean forKey:(NSString *)key {[self setObject:[NSNumber numberWithBool:boolean] forKey:key];}- (void)setInteger:(NSInteger)integer forKey:(NSString *)key {[self setObject:[NSNumber numberWithInteger:integer] forKey:key];}#pragma mark - removing objects- (void)removeObjectForKey:(NSString *)key {dispatch_sync(self.serialDictionaryQueue, ^{[self->_dataDictionary removeObjectForKey:key];});}- (void)removeAllObjects {dispatch_sync(self.serialDictionaryQueue, ^{[self->_dataDictionary removeAllObjects];});}#pragma mark - dictionary representation- (NSDictionary *)dictionaryRepresentation {__block NSDictionary *result;dispatch_sync(self.serialDictionaryQueue, ^{result = [self->_dataDictionary copy];});return result;}#pragma mark - synchronization- (void)synchronize {__block BOOL dirty = NO;// only write to the disk if the dictionaries have changeddispatch_sync(self.serialDictionaryQueue, ^{dirty = ![self->_persistedDataDictionary isEqualToDictionary:self->_dataDictionary];});_synchronizeWroteToDisk = dirty;if (!dirty) {return;}NSDictionary *state = [self dictionaryRepresentation];dispatch_sync(self.synchronizationQueue, ^{#if CLS_TARGET_CAN_WRITE_TO_DISKBOOL isDirectory = NO;BOOL pathExists = [[NSFileManager defaultManager] fileExistsAtPath:[self->_directoryURL path]isDirectory:&isDirectory];if (!pathExists) {NSError *error;if (![[NSFileManager defaultManager] createDirectoryAtURL:self->_directoryURLwithIntermediateDirectories:YESattributes:nilerror:&error]) {FIRCLSErrorLog(@"Failed to create directory with error: %@", error);}}if (![state writeToURL:self->_fileURL atomically:YES]) {FIRCLSErrorLog(@"Unable to open file for writing at path %@", [self->_fileURL path]);} else {#if TARGET_OS_IOS// We disable NSFileProtection on our file in order to allow us to access it even if the// device is locked.NSError *error;if (![[NSFileManager defaultManager]setAttributes:@{NSFileProtectionKey : NSFileProtectionNone}ofItemAtPath:[self->_fileURL path]error:&error]) {FIRCLSErrorLog(@"Error setting NSFileProtection: %@", error);}#endif}#elseNSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];[defaults setObject:state forKey:FIRCLSNSUserDefaultsDataDictionaryKey];[defaults synchronize];#endif});dispatch_sync(self.serialDictionaryQueue, ^{self->_persistedDataDictionary = [self->_dataDictionary copy];});}- (NSDictionary *)loadDefaults {__block NSDictionary *state = nil;dispatch_sync(self.synchronizationQueue, ^{#if CLS_TARGET_CAN_WRITE_TO_DISKBOOL isDirectory = NO;BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:[self->_fileURL path]isDirectory:&isDirectory];if (fileExists && !isDirectory) {state = [NSDictionary dictionaryWithContentsOfURL:self->_fileURL];if (nil == state) {FIRCLSErrorLog(@"Failed to read existing UserDefaults file");}} else if (!fileExists) {// No file found. This is expected on first launch.} else if (fileExists && isDirectory) {FIRCLSErrorLog(@"Found directory where file expected. Removing conflicting directory");NSError *error;if (![[NSFileManager defaultManager] removeItemAtURL:self->_fileURL error:&error]) {FIRCLSErrorLog(@"Error removing conflicting directory: %@", error);}}#elsestate = [[NSUserDefaults standardUserDefaults] dictionaryForKey:FIRCLSNSUserDefaultsDataDictionaryKey];#endif});return state;}#pragma mark - migration// This method migrates all keys specified from UserDefaults to FIRCLSUserDefaults// To do so, we copy all known key-value pairs into FIRCLSUserDefaults, synchronize it, then// remove the keys from UserDefaults and synchronize it.- (void)migrateFromNSUserDefaults:(NSArray *)keysToMigrate {BOOL didFindKeys = NO;// First, copy all of the keysToMigrate which are stored UserDefaultsfor (NSString *key in keysToMigrate) {id oldValue = [[NSUserDefaults standardUserDefaults] objectForKey:(NSString *)key];if (nil != oldValue) {didFindKeys = YES;[self setObject:oldValue forKey:key];}}if (didFindKeys) {// First synchronize FIRCLSUserDefaults such that all keysToMigrate in UserDefaults are stored// in FIRCLSUserDefaults. At this point, data is duplicated.[[FIRCLSUserDefaults standardUserDefaults] synchronize];for (NSString *key in keysToMigrate) {[[NSUserDefaults standardUserDefaults] removeObjectForKey:(NSString *)key];}// This should be our last interaction with UserDefaults. All data is migrated into// FIRCLSUserDefaults[[NSUserDefaults standardUserDefaults] synchronize];}}// This method first queries FIRCLSUserDefaults to see if the key exist, and upon failure,// searches for the key in UserDefaults, and migrates it if found.- (id)objectForKeyByMigratingFromNSUserDefaults:(NSString *)keyToMigrateOrNil {if (!keyToMigrateOrNil) {return nil;}id clsUserDefaultsValue = [self objectForKey:keyToMigrateOrNil];if (clsUserDefaultsValue != nil) {return clsUserDefaultsValue; // if the value exists in FIRCLSUserDefaults, return it.}id oldNSUserDefaultsValue =[[NSUserDefaults standardUserDefaults] objectForKey:keyToMigrateOrNil];if (!oldNSUserDefaultsValue) {return nil; // if the value also does not exist in UserDefaults, return nil.}// Otherwise, the key exists in UserDefaults. Migrate it to FIRCLSUserDefaults// and then return the associated value.// First store it in FIRCLSUserDefaults so in the event of a crash, data is not lost.[self setObject:oldNSUserDefaultsValue forKey:keyToMigrateOrNil];[[FIRCLSUserDefaults standardUserDefaults] synchronize];[[NSUserDefaults standardUserDefaults] removeObjectForKey:keyToMigrateOrNil];[[NSUserDefaults standardUserDefaults] synchronize];return oldNSUserDefaultsValue;}@end