由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(一)

news2024/9/20 14:59:33

在这里插入图片描述

概述

从 WWDC 23 开始,苹果推出了全新的数据库框架 SwiftData。它借助于 Swift 语言简洁而富有表现力的特点,抛弃了以往数据库所有的额外配置文件,只靠纯代码描述就可以干脆利索的让数据库的创建和增删改查(CRUD)一气呵成。

在这里插入图片描述

在本系列博文中,我们将从一个简单而“诡异”的运行“事故”开始,有理有据的深入探寻一番 SwiftData 中耐人寻味的“那些事儿”。

在本篇博文中,您将学到如下内容:

  • 概述
  • 1. 崩溃!又见崩溃!
  • 2. 寻根问底
  • 总结

这是本系列第一篇博文。闲言少叙,让我们马上开始 SwiftData 精彩的探究之旅吧!

Let‘s dive in!!!😉


1. 崩溃!又见崩溃!

“事故”的起因很简单,我们在 SwiftData 中创建了两个简单的托管类型 Item 和 Model。

其中,Model 类型里包含了指向 Item 的关系属性 item:

@Model
class Item {
    var name: String
    var timestamp: Date
    
    init(name: String) {
        self.name = name
        timestamp = .now
    }
}

@Model
class Model {
    
    static let UniqID = UUID(uuidString: "3788ABA9-043C-4D34-B119-5D69D486CBBA")!
    var mid: UUID
    
    @Relationship
    var item: Item?
    
    init(mid: UUID, item: Item? = nil) {
        self.mid = mid
        self.item = item
    }
    
    static var shared: Model = {
        let desc = FetchDescriptor<Model>()
        let context = ModelContext(.preview)
        
        if let result = try! context.fetch(desc).first {
            return result
        } else {
            let new = Model(mid: UniqID)
            context.insert(new)
            try! context.save()
            return new
        }
    }()
}

从上面的代码还可以看到,我们为 Model 添加了一个单例静态属性 shared,因为我们不希望创建多个 Model 的实例。

为了更好地在 Xcode 预览中调试代码,我们为 ModelContainer 扩展了一个 preview 静态属性用来获取模型容器中的测试数据:

extension ModelContainer {
    static var preview: ModelContainer = {
        try! ModelContainer(for: .init([Model.self, Item.self]), configurations: .init(isStoredInMemoryOnly: true))
    }()
}

接下来,我们构建 SwiftUI 界面以生成和显示模型容器中的持久数据。

从下面的代码可以看到,当 ContentView 视图显示时我们创建了一个新的 Item 记录,并将它设置到 Model.shared 对象的 Item 关系上,然后将 Item 中随机的值显示在视图中央:

struct ContentView: View {
    @Environment(\.modelContext) var modelContext
        
    var body: some View {
        VStack {
            if let item = Model.shared.item {
                Text(item.name)
            }
        }
        .padding()
        .task {
            let item = Item(name: "\(Int.random(in: 0...10000))")
            modelContext.insert(item)
            
            let model = Model.shared
            modelContext.insert(model)
            model.item = item
            
            try! modelContext.save()
        }
    }
}

然而,就是上面这几十行简单的代码竟然会立即导致运行时的崩溃:

在这里插入图片描述

从上图中可以看到,貌似崩溃直接发生在汇编代码中并没有对应任何源代码,这看起来不妙。

让我们来仔细看看崩溃的具体描述:

SwiftData/PersistentModel.swift:172: Fatal error: attempting to relate model - PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(url: x-coredata://DCDD7A8E-316D-4281-BD5C-ED76FF2F6E46/Model/p1), implementation: SwiftData.PersistentIdentifierImplementation) with model context - SwiftData.ModelContext to destination model - Optional(SwiftData.PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(url: x-swiftdata://Item/1B72D6AD-F2B6-436D-9817-AA803717A211), implementation: SwiftData.PersistentIdentifierImplementation)) from destination’s model context - SwiftData.ModelContext

那么现在问题来了:头发茂盛的小伙伴们能不能通过上面的源代码和崩溃信息确认崩溃真正的原因呢?大家自己先试一下吧。

2. 寻根问底

稍微“剧透一下”:如果在上述数据模型中不使用 @Relationship 来描述对象之间的关系,那么崩溃就会“烟消云散”。

这似乎意味着,上述错误和 SwiftData 中的 Relationship 连接有着“如胶似漆”的关系,果真如此吗?

再仔细观察一下崩溃信息的内容,它仿佛暗示着错误和模型上下文(ModelContext)息息相关:

… with model context - SwiftData.ModelContext to … model context - SwiftData.ModelContext

回忆一下,在 CoreData 中如果父托管对象包含一个子对象,那么如果它们承载于不同的托管对象上下文(NSManagedObjectContext)在保存时就会发生崩溃

为什么会出现这种情况?一种可能是父对象和子对象不是由同一个 NSManagedObjectContext 创建的,比如:子对象出生于后台线程中的托管对象上下文。


关于 CoreData 中更多后台线程执行的介绍,请小伙伴们移步如下链接观赏进一步内容:

  • Swift进一步优化CoreData后台线程读取数据时间的方法
  • CoreData从后台线程读取数据仍然阻塞UI界面的原因及解决

在 SwiftData 中,情况与此几乎如出一辙。回顾一下 Model.shared 静态属性的代码:

static var shared: Model = {
    let desc = FetchDescriptor<Model>()
    let context = ModelContext(.preview)
    
    if let result = try! context.fetch(desc).first {
        return result
    } else {
        let new = Model(mid: UniqID)
        context.insert(new)
        try! context.save()
        return new
    }
}()

看到了吗?我们根据 ModelContainer.preview 创建了一个新的 ModelContext,但这个模型上下文和 Model#items 关系中对应对象的上下文真的一致吗?

马上确认一下:我们新建 Item 托管对象的模型上下文是如何诞生的

在代码中不难发现,它是通过 modelContainer 修改器方法从 App 的 WindowGroup 中传入的:

@main
struct MyWatch_App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(.preview)
        }
    }
}

然后在 ContentView 中通过 @Environment 引入到视图中:

struct ContentView: View {
    @Environment(\.modelContext) var modelContext
}

注意,貌似它们都对应同一个 ModelContainer.preview 模型容器,但其实它们却有着云泥之别:

  • 用 modelContainer 修改器从 App 的 WindowGroup 传入的上下文实际对应着 ModelContainer 容器中的主上下文
  • 而在 Model.shared 中用 ModelContext 创建的上下文则是容器的一个私有上下文

主上下文必须在主线程或 MainActor 中使用,而私有上下文可以运行在任何其它线程或 Actor 中。

在这里插入图片描述

所以,上面崩溃的前因后果已经很明晰了:**我们的 Model 是从私有上下文中创建的,而它 Item 关系所对应的对象却是从主上下文中创建的。**这在将数据保存到 SwiftData 的持久存储中时必然会引起上下文不一致,从而导致榱崩栋折。

知道了原因,解决起来就很简单了。

一种直观的方法是,同样在 ModelContainer.preview 的主上下文中创建 Model 的共享实例:

@MainActor
static var shared: Model = {
    let desc = FetchDescriptor<Model>()
    
    // 获取 ModelContainer.preview 的主上下文
    let context = ModelContainer.preview.mainContext
    
    if let result = try! context.fetch(desc).first {
        return result
    } else {
        let new = Model(mid: UniqID)
        context.insert(new)
        try! context.save()
        return new
    }
}()

注意:因为 ModelContainer.preview.mainContext 必须在主线程上使用,所以它是被 @MainActor 所修饰着的,因而这一修饰符也必须“传染”到 shared 静态属性自身上。

在这里插入图片描述

运行代码,一切崩溃都变得风吹云散了!我们 Model.shard 关系中 Item 的随机值顺利显示在了视图的中心,棒棒哒!💯

总结

在本篇博文中,我们介绍了一个导致 SwiftData 支持的应用发生轰然崩溃的问题,并随后讨论了它的前因后果以及解决之道。

在下一篇博文里,我们会接着讨论 SwiftData 如何在后台处理数据以及如何将它们同步到界面中;我们还会在后续文章中介绍 SwiftData 2.0 中新祭出的 History Trace 和“墓碑”机制,敬请期待吧。

感谢观赏,再会!😎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2103828.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

10分钟了解OPPO中间件容器化实践

背景 OPPO是一家全球化的科技公司&#xff0c;随着公司的快速发展&#xff0c;业务方向越来越多&#xff0c;对中间件的依赖也越来越紧密&#xff0c;中间件的集群的数量成倍数增长&#xff0c;在中间件的部署&#xff0c;使用&#xff0c;以及运维出现各种问题。 1.中间件与业…

CAD图纸防泄密有哪些方法?五个防泄密方法分享

CAD图纸作为工程设计的重要资料&#xff0c;其防泄密工作至关重要。以下将分享五个有效的CAD图纸防泄密方法&#xff0c;旨在确保图纸的安全性和机密性。 1. 使用CAD软件自带的加密功能 大多数CAD软件&#xff0c;如AutoCAD&#xff0c;都内置了加密功能。在保存CAD图纸时&am…

MySQL主从复制(Linux篇)

1.概述 MySQL的主从复制&#xff08;Master-Slave Replication&#xff09;是一种数据复制解决方案&#xff0c;将主数据库的DDL和DML操作通过二进制日志传到从库服务器中&#xff0c;然后在从库上对这些日志重新执行&#xff08;也叫重做&#xff09;&#xff0c;从而是的从库…

MES系统:现代工厂生产车间的科技与管理创新

一、现代工厂生产车间&#xff1a;高度集成、自动化与智能化 在当今这个日新月异的工业时代&#xff0c;工厂生产车间已经远远超越了传统意义上的简单生产线概念&#xff0c;它们正逐步演变为一个高度集成、自动化与智能化的生态系统。这一变革不仅极大地提升了生产效率&#…

实战项目:俄罗斯方块(七)

文章目录 &#x1f34a;自我介绍&#x1f34a;图形存储及输出设置类型设计图形输出代码设计要实现的结果展示user_global.c(全局变量的C文件)user_print.huser_print.cmain.c 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以&#xff1a;点赞关注评论收藏&#xff08;一…

股指期货与股指期权为什么是熊市中的风险管理利器?

在如今波动剧烈的熊市环境中&#xff0c;期货市场的多空双边机制和T0的交易制度显得尤为灵活和必要。对于投资者而言&#xff0c;利用期货市场对冲股票现货风险成为了一种重要的策略。在期货市场中&#xff0c;股指期货、股指期权以及股票期权是三大得力工具&#xff0c;它们为…

Windows下安装ELK8(elasticsearch、logstash、kibana)及相关工具插件

目录 概述 ELK简介 安装elasticsearch 下载elasticsearch 解压 配置不用登录及关闭SSL 启动elasticsearch 访问elasticsearch 安装elasticsearch-head 提前准备node.js 下载elasticsearch-head 解压 浏览器打开index.html 安装kibana 下载kibana 解压 运行kib…

渠道招商经理岗位职责和任职要求?

渠道经理&#xff0c;作为企业与投资者之间的沟通桥梁&#xff0c;角色是非常重要。 作为一名手工酸奶品牌的创始人&#xff0c;我一直扮演渠道招商经理的角色&#xff0c;我来为大家分享职责和任职要求&#xff01; 一、渠道经理的职责&#xff01; 1、渠道开发 负责公司渠…

Vue3+TypeScript+Vite集成mars3d.layer.WeiVectorTileLayer加载本地shp文件

前景提要&#xff0c;在新建的vite项目中&#xff0c;想要实现mars3d的矢量瓦片方式加载geojson数据。加载本地shp文件。 代码说明&#xff1a; 1.在项目中集成mars3d后&#xff0c;构造map后开始在map上增加读取数据的操作&#xff1a; mars3d.Util.fetchJson({ url: "…

Git学习尚硅谷(003 git分支操作)

尚硅谷Git入门到精通全套教程&#xff08;涵盖GitHub\Gitee码云\GitLab&#xff09; 总时长 4:52:00 共45P 此文章包含第15p-第p18的内容 文章目录 git分支操作分支介绍分支的好处 分支的操作查看分支创建分支切换分支&修改分支合并分支正常合并分支冲突合并 git分支操作…

10,sql约束(2)

MySQL中primary key和unique的区别 在sql、oracle中的constrain有两种约束&#xff0c;都是对列的唯一性限制&#xff1a;unique与primary key&#xff0c;它们的区别如下&#xff1a; 1、unique key要求列唯一&#xff0c;但不包括Null字段&#xff0c;也就是约束的列可以为空…

docker制作达梦数据库驱动的Python镜像记录

docker制作达梦数据库驱动的Python镜像记录 使用Sqlalchemy ORM 操作达梦 提供构建好的docker镜像 docker pull chongjing001/python:3.12 使用虚拟环境(source /venv/bin/activate) 后pip其他库 镜像中安装Python参考上一篇 下载DM8安装包 版本选择 官网 Linux安装包只有 r…

完整指南:CNStream流处理多路并发框架适配到NVIDIA Jetson Orin (二) 源码架构流程梳理、代码编写

目录 1 视频解码代码编写----利用jetson-ffmpeg 1.1 nvstream中视频解码的代码流程框架 1.1.1 类的层次关系 1.1.2 各个类的初始化函数调用层次关系 1.1.3 各个类的process函数调用层次关系 1.2 编写视频解码代码 1.2.1 修改VideoInfo结构体定义 1.2.2 修改解封装代码 …

Kafka【五】Buffer Cache (缓冲区缓存)、Page Cache (页缓存)和零拷贝技术

【1】Buffer Cache (缓冲区缓存) 在Linux操作系统中&#xff0c;Buffer Cache&#xff08;缓冲区缓存&#xff09;是内核用来优化对块设备&#xff08;如磁盘&#xff09;读写操作的一种机制&#xff08;故而有一种说法叫做块缓存&#xff09;。尽管在较新的Linux内核版本中&a…

Spring Cloud全解析:熔断之Hystrix服务监控

Hystrix服务监控 Hystrix除了熔断降级之外&#xff0c;还提供了准实时的调用监控&#xff0c;持续的记录所有通过Hystrix发起的请求的执行信息&#xff0c;并以统计报表的形式展示出来&#xff0c;包括有每秒执行多少请求&#xff0c;多少成功&#xff0c;多少失败等&#xff…

【C++】vector类:模拟实现(适合新手手撕vector)

在实现本文的vector模拟前&#xff0c;建议先了解关于vector的必要知识&#xff1a;【C】容器vector常用接口详解-CSDN博客https://blog.csdn.net/2301_80555259/article/details/141529230?spm1001.2014.3001.5501 目录 一.基本结构 二.构造函数&#xff08;constructor&…

【算法】位运算

【ps】本篇有 10 道 leetcode OJ。 目录 一、算法简介 二、相关例题 1&#xff09;位1的个数 .1- 题目解析 .2- 代码编写 2&#xff09;比特位计数 .1- 题目解析 .2- 代码编写 3&#xff09;汉明距离 .1- 题目解析 .2- 代码编写 4&#xff09;只出现一次的数字 .…

3000字带你了解SD提示词用法,一点就通,小白轻松上手(附提示词生成器)(1.4 SD提示词运用)

提示词是什么 提示词是我们向AI模型发出的指令。正确的提示词能让AI准确反馈所需的输出&#xff0c;而优质的提示词则能使AI生成的内容更优质、更符合你的期望。这与编写程序代码颇为相似&#xff0c;准确的代码逻辑是程序正常运行的前提&#xff0c;而优秀的代码则能减少运行…

知识付费小程序源码轻松实现一站式运营,开启知识变现之旅

技术栈&#xff1a; 以下是一个简单的知识付费小程序的示例代码&#xff1a; app.js&#xff1a;小程序的入口文件 App({onLaunch: function () {// 在小程序启动时执行的代码},globalData: {// 存储全局数据userInfo: null // 用户信息} })pages/index/index.js&#xff1…

【学术会议征稿】第四届智能电网与能源互联网国际会议(SGEI 2024)

第四届智能电网与能源互联网国际会议&#xff08;SGEI 2024&#xff09; 2024 4th International Conference on Smart Grid and Energy Internet 为交流近年来国内外在智能电网和能源互联网领域的理论、技术和应用的最新进展&#xff0c;展示最新成果&#xff0c;由沈阳工业…