后台获取的间歇性数据丢失 - 文档目录中的 NSKeyedUnarchiver return nil 可以吗?
Intermittent data loss with background fetch - could NSKeyedUnarchiver return nil from the documents directory?
我有一个简单的应用程序,它使用应用程序的文档文件夹中的 NSCoding
存储自定义类型的数组(class 的实例称为 Drug
)。
加载和保存代码是对我的主视图控制器的扩展,一旦加载它就一直存在。
数组初始化:
var drugs = [Drug]()
此数组随后会附加下面 loadDrugs()
方法的结果。
func saveDrugs() {
// Save to app container
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
// Save to shared container (for iMessage, Spotlight, widget)
let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
}
这是加载数据的代码。
func loadDrugs() -> [Drug]? {
var appContainerDrugs = NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
return appContainerDrugs
}
数据也使用 CloudKit 存储在 iCloud 中,应用程序可以响应 CK 通知以从另一台设备获取更改。后台提取也会触发相同的方法。
// App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// Code to get reference to my main view controller
// This will have called loadDrugs() to populate local array drugs of type [Drug]
mainVC.getZoneChanges()
}
最后,还有 getZoneChanges()
方法,它使用存储的 CKServerChangeToken
从私有用户数据库中获取更改 CKFetchRecordZoneChangesOperation
。完成块调用 saveDrugs()
.
问题
所有这一切似乎工作正常。但是,有时所有本地数据都会在应用程序的使用之间消失,尤其是在一段时间未使用的情况下。谢天谢地,删除并重新安装应用程序确实会从 iCloud 中提取备份数据。
如果应用有一段时间没有使用(估计被系统终止),似乎会发生这种情况。有些东西必须改变,所以我认为当应用程序终止时调用后台提取可能是问题所在。调试时以及应用程序最近处于前台时一切正常。
可能的原因
我猜问题是我依赖后台获取(或接收 CK 通知)在后台加载我的主视图控制器,然后加载保存的本地数据。
我听说 UserDefaults
在后台无法正常工作,在这种情况下可能存在文件安全保护以防止访问文档目录。如果是这种情况,我可能会加载一个空数组(或者更确切地说是初始化数组而不是将数据附加到它)然后保存它,覆盖现有数据,所有这些都在用户不知情的情况下进行。
如何避免这个问题?有没有办法检查数据是否正确加载?如果出现问题,我尝试使用致命错误进行条件加载,但这会导致应用程序的第一个 运行 出现问题,因为无论如何都没有数据!
编辑
归档 URL 是动态获取的,如下所示。我只是在我的主数据模型 class (Drug
) 中使用静态方法来访问它们:
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("drugs")
问题是您存储的是文件的完整路径,而不仅仅是文件名(或相对路径)。文档目录 URL 可以更改,然后如果您永久存储 URL,您将不会指向文件的正确位置。相反,只需存储文件名并使用 NSFileManager URLsForDirectory
在每次需要时直接获取文档。
此类问题的最常见原因是当设备被锁定且受保护的数据被加密时在后台被唤醒。
作为起点,您可以检查 UIApplication.isProtectedDataAvailable
以验证受保护的数据是否可用。您还可以将所需数据的保护级别降低到 .completeUntilFirstUserAuthentication
(具体操作方法取决于您创建文件的方式)。
通常,您应该尽量减少敏感信息的数据保护级别,因此通常最好在设备锁定时写入其他位置,然后在设备解锁后合并该位置。
我有一个简单的应用程序,它使用应用程序的文档文件夹中的 NSCoding
存储自定义类型的数组(class 的实例称为 Drug
)。
加载和保存代码是对我的主视图控制器的扩展,一旦加载它就一直存在。
数组初始化:
var drugs = [Drug]()
此数组随后会附加下面 loadDrugs()
方法的结果。
func saveDrugs() {
// Save to app container
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
// Save to shared container (for iMessage, Spotlight, widget)
let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
}
这是加载数据的代码。
func loadDrugs() -> [Drug]? {
var appContainerDrugs = NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
return appContainerDrugs
}
数据也使用 CloudKit 存储在 iCloud 中,应用程序可以响应 CK 通知以从另一台设备获取更改。后台提取也会触发相同的方法。
// App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// Code to get reference to my main view controller
// This will have called loadDrugs() to populate local array drugs of type [Drug]
mainVC.getZoneChanges()
}
最后,还有 getZoneChanges()
方法,它使用存储的 CKServerChangeToken
从私有用户数据库中获取更改 CKFetchRecordZoneChangesOperation
。完成块调用 saveDrugs()
.
问题
所有这一切似乎工作正常。但是,有时所有本地数据都会在应用程序的使用之间消失,尤其是在一段时间未使用的情况下。谢天谢地,删除并重新安装应用程序确实会从 iCloud 中提取备份数据。
如果应用有一段时间没有使用(估计被系统终止),似乎会发生这种情况。有些东西必须改变,所以我认为当应用程序终止时调用后台提取可能是问题所在。调试时以及应用程序最近处于前台时一切正常。
可能的原因
我猜问题是我依赖后台获取(或接收 CK 通知)在后台加载我的主视图控制器,然后加载保存的本地数据。
我听说 UserDefaults
在后台无法正常工作,在这种情况下可能存在文件安全保护以防止访问文档目录。如果是这种情况,我可能会加载一个空数组(或者更确切地说是初始化数组而不是将数据附加到它)然后保存它,覆盖现有数据,所有这些都在用户不知情的情况下进行。
如何避免这个问题?有没有办法检查数据是否正确加载?如果出现问题,我尝试使用致命错误进行条件加载,但这会导致应用程序的第一个 运行 出现问题,因为无论如何都没有数据!
编辑
归档 URL 是动态获取的,如下所示。我只是在我的主数据模型 class (Drug
) 中使用静态方法来访问它们:
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("drugs")
问题是您存储的是文件的完整路径,而不仅仅是文件名(或相对路径)。文档目录 URL 可以更改,然后如果您永久存储 URL,您将不会指向文件的正确位置。相反,只需存储文件名并使用 NSFileManager URLsForDirectory
在每次需要时直接获取文档。
此类问题的最常见原因是当设备被锁定且受保护的数据被加密时在后台被唤醒。
作为起点,您可以检查 UIApplication.isProtectedDataAvailable
以验证受保护的数据是否可用。您还可以将所需数据的保护级别降低到 .completeUntilFirstUserAuthentication
(具体操作方法取决于您创建文件的方式)。
通常,您应该尽量减少敏感信息的数据保护级别,因此通常最好在设备锁定时写入其他位置,然后在设备解锁后合并该位置。