处理后台位置更新和Core Data文件保护

rob*_*cer 9 cocoa-touch core-data cllocationmanager

我一直在尝试CLLocationManagerstartMonitoringSignificantLocationChanges,我碰到的一些问题与核心数据.事实证明,自iOS 5.0以来,Core Data默认使用NSFileProtectionCompleteUntilFirstUserAuthentication.这意味着如果设置了密码,则持久存储在设备打开时不可用,直到首次输入密码为止.如果您正在使用位置更新,那么您的应用可能会在此期间启动,并且Core Data将尝试加载持久存储时出错.

显然,转换NSFileProtectionNone是解决这个问题的最简单方法.我不想这样做 - 我没有在数据库中存储任何超级敏感的东西,但这些位置更新也不是超级关键.

我知道我可以[[UIApplication sharedApplication] isProtectedDataAvailable]用来检查数据是否已经解锁,并且我可以applicationProtectedDataWillBecomeUnavailable:在我的应用程序委托中使用它一旦解锁就做出适当的响应.这对我来说似乎很麻烦 - 我必须添加一些额外的检查,以确保如果持久存储不可用则没有出错,一旦它变得可用就重新设置一堆东西,等等.而额外的代码并没有提供太多的好处 - 如果它在这种状态下启动,应用程序仍然无法执行任何操作.

所以我想我只是不确定哪种方式更合适":

  1. 切换到NSFileProtectionNone.
  2. 如果商店不可用,请添加额外的检查以跳过事物,并applicationProtectedDataWillBecomeUnavailable:在用完之后再次设置.
  3. 如果应用程序在后台启动([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground)并且受保护的数据不可用([[UIApplication sharedApplication] isProtectedDataAvailable] == NO))只需调用exit(0)(或类似的东西)退出应用程序.一方面,这似乎是最简单的解决方案,我没有看到任何缺点.但它似乎......"错了"?我想我无法决定它是一个干净的解决方案还是一个懒惰的解决方案.
  4. 还有别的我想不起的东西?

rob*_*cer 5

经过一段时间的思考后,我想出了一个我很满意的解决方案.该exit(0)选项需要考虑的一件事是,如果用户需要一段时间来解锁设备,应用程序可能会不断加载,退出和重新加载.然而,如果你只是阻止应用程序做很多事情,它可能只需要加载一次,并且很可能会更有效率.所以我决定尝试我的选项3,看看它到底有多乱.事实证明这比我想象的要简单.

首先,我BOOL setupComplete向我的应用代表添加了一个属性.这让我可以轻松地检查应用程序是否在各个点完全启动.然后在application:didFinishLaunchingWithOptions:我尝试初始化托管对象上下文,然后执行以下操作:

NSManagedObjectContext *moc = [self managedObjectContext];
if (moc) {
    self.setupComplete = YES;
    [self setupWithManagedObjectContext:moc];
} else {
    UIApplication *app = [UIApplication sharedApplication];
    if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
        [app beginIgnoringInteractionEvents];
    } else [self presentErrorWithTitle:@"There was an error opening the database."];
}
Run Code Online (Sandbox Code Playgroud)

setupWithManagedObjectContext:只是一个完成设置的自定义方法.我不确定beginIgnoringInteractionEvents是否有必要,但我把它添加到了安全的一面.这样当应用程序被带到前面时,我可以确定界面被冻结,直到设置完成.如果急切的用户焦急地窃听,它可能会避免崩溃.

然后applicationProtectedDataDidBecomeAvailable:我打电话给这样的事情:

if (!self.setupComplete) {
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (moc) {
        self.setupComplete = YES;
        [self setupWithManagedObjectContext:moc];
        UIApplication *app = [UIApplication sharedApplication];
        if ([app isIgnoringInteractionEvents]) [app endIgnoringInteractionEvents];
    } else [self presentErrorWithTitle:@"There was an error opening the database."];
}
Run Code Online (Sandbox Code Playgroud)

完成设置并重新启用界面.这是大部分工作,但您还需要检查其他代码,以确保在持久存储可用之前不会调用任何依赖于Core Data的内容.有一点要注意的是,applicationWillEnterForegroundapplicationDidBecomeActive可能会调用之前applicationProtectedDataDidBecomeAvailable,如果用户从这个背景状态启动应用程序.所以我在各个地方添加if (self.setupComplete) { … }了以确保在它准备好之前没有运行.我还有几个地方,我需要在加载数据库后刷新界面.

为了(部分)测试这个没有大量的驱动,我暂时修改application:didFinishLaunchingWithOptions:为不设置数据库:

NSManagedObjectContext *moc = nil; // [self managedObjectContext];
if (moc) {
    self.setupComplete = YES;
    [self setupWithManagedObjectContext:moc];
} else {
    UIApplication *app = [UIApplication sharedApplication];
    // if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
        [app beginIgnoringInteractionEvents];
    // } else [self presentErrorWithTitle:@"There was an error opening the database."];
}
Run Code Online (Sandbox Code Playgroud)

然后我将代码applicationProtectedDataDidBecomeAvailable:移到了applicationWillEnterForeground:.这样我可以启动应用程序,确保没有任何意外情况发生,按主页按钮,再次打开应用程序,并确保一切正常.由于实际代码需要移动一个很长的距离并且每次等待五分钟,这给了我一个很好的方法来估计正在发生的事情.

绊倒我的最后一件事是我的持久商店协调员.典型的实现可能如下所示:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Test.sqlite"];

    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    }    

    return _persistentStoreCoordinator;
}
Run Code Online (Sandbox Code Playgroud)

这基于Apple的示例代码,它在评论中解释说您需要适当地处理错误.我自己的代码比这更多,但我没有考虑的一件事是,如果加载持久存储时出错,这将返回一个非零结果!这允许我的所有其他代码继续进行,就像它正常工作一样.即使再次调用persistentStoreCoordinator,它也只会在没有有效存储的情况下返回相同的协调器,而不是再次尝试加载存储.有多种方法可以解决这个问题,但对我而言,除非能够添加商店,否则最好不要设置_persistentStoreCoordinator:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Test.sqlite"];

    NSError *error = nil;
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if ([coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        _persistentStoreCoordinator = coordinator;
    } else {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    }    

    return _persistentStoreCoordinator;
}
Run Code Online (Sandbox Code Playgroud)