什么是核心数据故障?

故障是核心数据的重要组成部分。 即使该术语听起来不祥,但故障是Core Data记录的生命周期固有的。 在本教程中,您将学习什么是故障,如何处理故障以及如何调试与故障相关的问题。

先决条件

核心数据是一个高级主题,因此我假设您已经熟悉Xcode和iOS开发。 即使本教程将使用Swift编程语言,本教程中的所有内容也适用于Objective-C。

瓦特是错吗?

得益于Apple核心数据团队的辛勤工作,Core Data擅长于其工作。 核心数据经过高度优化,可以在不牺牲性能的情况下保持较低的内存占用。 错误是Core Data消耗尽可能少的内存的技术之一。

故障并非核心数据所独有。 许多其他框架(例如EmberRuby on Rails)也使用了类似的技术。 这个想法很简单,仅在需要时才加载数据。 为了使故障起作用,Core Data通过在编译时创建代表故障的自定义子类来做一些魔术。 让我们来看一个例子。

我创建了一个简单的示例应用程序来使用。 从GitHub下载Xcode项目,然后在Xcode中打开它。 该项目使用Swift 2.1,这意味着您需要Xcode 7.1或更高版本才能满足编译器的要求。

数据模型包含两个实体, ListItem 。 列表可以包含零个或多个项目,并且一个项目始终链接到列表。 这是一对多关系的经典示例。

资料模型

运行应用程序,并通过创建一些列表和项目,用一些数据填充持久存储SQLite数据库。 完成后退出应用程序。

触发故障

您现在应该拥有一个包含一些数据的应用程序。 让我们通过添加一些打印语句来查看故障在实际中是如何工作的。 打开ListsViewController.swift并查找prepareForSegue(_:sender:) 。 在这种方法中,我们获取用户在表视图中选择的列表。 取消注释prepareForSegue(_:sender:)的打印语句,如下面的实现所示。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == SegueListViewController {
        guard let indexPath = tableView.indexPathForSelectedRow else { return }
        
        // Fetch List
        let list = self.fetchedResultsController.objectAtIndexPath(indexPath) as! List
        
        print("1: \(list)")
        
        if let items = list.items {
            print("2: \(items)")
            print("3: \(items.count)")
            print("4: \(items)")
            
            if let item = items.anyObject() {
                print("5: \(item)")
                print("6: \(item.name)")
                print("7: \(item)")
            }
        }
        
        print("8: \(list)")
        
        // Fetch Destination View Controller
        let listViewController = segue.destinationViewController as! ListViewController
        
        // Configure View Controller
        listViewController.list = list
    }
}

运行该应用程序,然后在列表视图控制器中点击列表之一。 仅当您单击一个列表具有一个或多个项目时,此示例才有意义。 让我们逐步检查打印语句的输出。

1: <Faulting.List: 0x154e5d0b0> (entity: List; id: 0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7> ; data: {
    items = "<relationship fault: 0x154e3d5b0 'items'>";
    name = "List 6";
})

首先要注意的是类名称Faulting.List 。 这是我们期望的,因为模块名为Faulting ,而类名为List 。 在Swift中,类的完整类名称由模块的名称和类的名称组成。

控制台中的输出还显示实体的名称, List和数据字典。 name属性设置为列表6,items属性标记为关系故障 。 这是核心数据中的第一类故障。 Core Data知道不需要加载关系。 相反,它将关系标记为错误。 如您在下面看到的,第二份印刷声明确认了这一点。

2: Relationship 'items' fault on managed object (0x154e5d0b0) <Faulting.List: 0x154e5d0b0> (entity: List; id: 0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7> ; data: {
    items = "<relationship fault: 0x154e3d5b0 'items'>";
    name = "List 6";
})

但是,第三个打印语句更有趣。 它打印与列表关联的项目数。

3: 2

核心数据只能通过触发关系故障来做到这一点。 它向持久性存储请求与列表关联的项目。 但是,这只是故事的一部分,如第四份印刷品声明所示。

4: Relationship 'items' on managed object (0x154e5d0b0) <Faulting.List: 0x154e5d0b0> (entity: List; id: 0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7> ; data: {
    items =     (
        "0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21>",
        "0xd000000000580002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p22>"
    );
    name = "List 6";
}) with objects {(
    <Faulting.Item: 0x154e83d60> (entity: Item; id: 0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21> ; data: <fault>),
    <Faulting.Item: 0x154e55e50> (entity: Item; id: 0xd000000000580002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p22> ; data: <fault>)
)}

我们不再看到关系错误,但仍然看到错误。 那是什么意思 核心数据只能通过触发或解决关系错误来为我们提供列表项的数量。 但是,这并不意味着Core Data可以解决关系项。 控制台中的输出确认了这一点。

我们可以看到项目的记录在那里,包括Core Data在内部使用的标识符。 但是,数据字典被标记为故障。 同样,核心数据仅能满足我们的要求。 幸运的是,实质性细节由Core Data处理。

让我们深入一点,从列表中获取其中一项。 我们通过在items对象上调用anyObject()来实现。 我们在第五个打印语句中打印结果项目。

5: <Faulting.Item: 0x144d7f100> (entity: Item; id: 0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21> ; data: <fault>)

输出不应让您感到惊讶。 输出确认我们正在处理Item实体。 毫不奇怪,数据字典仍然被标记为故障。 在第六个打印语句中,我们打印项目的名称属性。

6: Item 0

因为我们要求记录的属性值,所以Core Data会触发错误以提供该值。 它获取该项目的数据并填充数据字典。 第七份印刷声明证实了这些发现。

7: <Faulting.Item: 0x144d7f100> (entity: Item; id: 0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21> ; data: {
    list = "0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7>";
    name = "Item 0";
})

数据字典包含名称属性以及列表关系。 第八条也是最后一条打印语句表明list对象的关系故障已解决。

8: <Faulting.List: 0x144e55da0> (entity: List; id: 0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7> ; data: {
    items =     (
        "0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21>",
        "0xd000000000580002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p22>"
    );
    name = "List 6";
})

无法完成故障

我决定写这篇文章来解释一个问题,许多使用Core Data的开发人员会在某个时间点遇到另一个问题,从而引发无法实现的错误。 问题很简单。 假设您有一个包含许多项目的列表,并且用户有时会删除该列表。 该列表中的项目将如何处理? 如果Core Data尝试触发属于该列表的项目之一的列表关系,会发生什么? 让我们找出答案。

让我们重新访问从GitHub下载的项目。 打开Faulting.xcdatamodeld ,项目的数据模型。 选择“ 列表”实体的项目关系,然后打开右侧的“ 数据模型检查器 ”。 我们感兴趣的是删除规则 ,该规则当前设置为Cascade 。 这意味着删除列表时,列表中的每个项目都会被删除。 这很有意义,因为我们不想放弃与列表无关的项目。

删除规则级联

选择Item实体的列表关系,然后打开Data Model Inspector 。 此关系的Delete Rule设置为Nullify 。 这意味着当删除目标记录(项目)时,关系的目标(列表对象)设置为null。 这是关系的默认删除规则。

删除规则无效

运行应用程序,创建一些列表和项目,然后点击左上方的“ 项目”按钮以显示您创建的每个项目。 点按左上角的“ 取消 ”,删除列表之一,然后再次点按“ 项目”按钮以查看更改。 与预期的一样,与已删除列表关联的项目也已被Core Data删除。 这不是魔术。 核心数据仅执行我们在数据模型中定义的删除规则。

我们可以得出结论,为此类应用程序正确建立了关系。 但是,如果我们没有正确配置删除规则会发生什么。 打开数据模型,并将关系, 项目列表的删除规则设置为No Action 。 再次启动该应用程序,并创建一些列表和项目。 如果删除列表,然后单击左上角的“ 项目”按钮以查看永久性存储中的每个项目,则与已删除列表关联的项目仍应存在。 即使删除了它们所属的列表,也没有删除它们。

点击列表之一,看看会发生什么。 如果您在iOS 8上运行该应用程序,则该应用程序将由于未捕获的异常而崩溃。 如果您在iOS 9上运行该应用程序,那么您只会在控制台中看到错误。 别再挠头了,让我们一起解决这个问题。

无法访问对象

即使应用程序在iOS 8上崩溃,我们在控制台中看到的输出仍然清晰明了。

Faulting[7189:2427906] *** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x175b2ba0 <x-coredata://3920E0C9-6613-47E6-AA19-66AF6555140A/List/p1>''

首先要注意的是,由于未捕获的异常,应用程序崩溃了。 但是,对于我们的讨论而言,更重要的是引发异常的原因。 核心数据告诉我们,无法解决一段关系上的错误。 核心数据所指的关系是我们在表格视图中点击的项目的列表关系。

如果您查看tableView(tableView:didSelectRowAtIndexPath:) ,那么您将了解为何Core Data引发异常。 在这种方法中,我们获取用户点击的项目,并将列表的名称打印到控制台。

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
    
    if let item = self.fetchedResultsController.objectAtIndexPath(indexPath) as? Item {
        print(item.list?.name)
    }
    
}

由于列表关系是故障,因此Core Data需要触发该故障来解决。 这意味着Core Data向持久性存储请求列表记录。 问题在于记录不再位于持久性存储中,这就是引发异常的原因。

删除无法访问的故障

直到iOS 9为止,这始终是默认行为。 从iOS 9开始,Core Data不再引发异常。 而是将一条隐秘消息记录到控制台。 尽管消息不清楚,但仍然包含问题的原因。

Faulting[2806:1306995] CoreData: warning: An NSManagedObjectContext delegate overrode fault handling behavior to silently delete the object with ID '0xd000000000140000 <x-coredata://396CF04E-B8B4-4108-8719-3C651F880C33/List/p5>' and substitute nil/0 for all property values instead of throwing.

要点是,当无法执行故障时,Core Data不再引发异常。 好消息是,如果您没有捕获到异常,您的应用程序将不再崩溃。

在iOS 9上不引发异常的原因是由于在NSManagedObjectContext类上引入了新属性shouldDeleteInaccessibleFaults和新方法shouldHandleInaccessibleFault(_:forObjectID:triggeredByProperty:) 。 在本教程中,我不会介绍这些新增内容。

您需要记住的是,默认情况下,iOS 9 shouldDeleteInaccessibleFaults设置为true 。 这意味着无法访问的管理对象被标记为已删除,并且其属性被清零。 如果shouldDeleteInaccessibleFaults设置为false ,则Core Data将通过引发异常来恢复为旧行为。

当然, shouldDeleteInaccessibleFaults属性应与shouldHandleInaccessibleFault(_:forObjectID:triggeredByProperty:) 。 如果重写此方法,则可以更优雅地处理无法访问的对象。

处理故障

当由于Core Data无法执行故障而遇到异常时,您可能会认为Core Data有点攻击性。 对于刚接触该框架的人员来说,这种反应非常普遍。 事实是,开发商有过错。 设计核心数据时会考虑一组特定的目标,而错误是实现这些目标的重要组成部分。

尽管有它们的名字,但错误应该总是可以实现的。 如果无法解决故障,则意味着数据模型设置不正确,或者应用程序未遵循Core Data框架规定的规则。

在《 核心数据编程指南》中 ,Apple列出了许多可能导致无法解决的故障。 最常见的情况是配置错误的关系(删除规则)和删除托管对象,而应用程序仍然对该托管对象有很强的引用。

请注意,还有其他可能的情况。 根据我的经验,由于核心数据堆栈的问题,开发人员经常会遇到类似的问题。 例如,如果应用程序对托管对象保持强烈引用,并在某个时候取消分配该托管对象的托管对象上下文,则Core Data将不再能够对该托管对象执行任何故障。 我希望您了解如果您遵守Core Data的规则,就不会发生这种情况。

结论

核心数据故障非常有用,并且是Apple持久性框架的关键组成部分。 我希望本文能教您如何处理错误,以及遇到无法实现的错误时应在哪里寻找。 如果您有任何疑问,请随时将其留在下面的评论中,或通过Twitter与我联系。

翻译自: https://code.tutsplus.com/tutorials/what-is-a-core-data-fault--cms-25157

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值