iOS中的数据库

数据持久化是移动端开发的核心能力之一。iOS提供了从轻量级KV存储到完整关系数据库的多层次方案。本文将从底层存储原理出发,系统性地梳理iOS中各类数据库与持久化技术的设计思想、实现机制和适用场景。 一、iOS数据持久化方案全景 iOS中常见的数据持久化方案,按数据复杂度和使用场景可分为以下几层: graph TD subgraph KV["轻量级KV存储"] A["UserDefaultsplist序列化"] B["Keychain加密存储"] C["MMKVmmap + protobuf"] end subgraph FILE["文件存储"] D["plist / JSON"] E["NSCoding / Codable归档"] end subgraph SQL["关系数据库"] F["SQLiteC语言API"] G["FMDBOC封装"] H["WCDBORM + 加密"] end subgraph ORM["ORM框架"] I["Core DataApple官方"] J["Realm零拷贝"] K["SwiftDataSwift原生"] end KV --> |"数据结构更复杂"| FILE FILE --> |"需要结构化查询"| SQL SQL --> |"需要对象映射"| ORM 方案 数据模型 查询能力 线程安全 加密 适用场景 UserDefaults KV (plist类型) Key查找 线程安全 无 用户偏好、简单配置 Keychain KV (Data) Key查找 线程安全 系统加密 密码、Token、证书 MMKV KV (protobuf) Key查找 线程安全 可选AES 高频读写的KV数据 plist/JSON文件 字典/数组 无 不安全 无 静态配置、缓存 NSCoding/Codable 对象图 无 不安全 无 对象序列化 SQLite 关系表 SQL 需手动处理 可选(SQLCipher) 结构化数据、复杂查询 FMDB 关系表 SQL FMDatabaseQueue 可选 SQLite的OC封装 WCDB 关系表+ORM SQL+ORM 自动管理 内置 高性能结构化存储 Core Data 对象图 NSPredicate NSManagedObjectContext 无 复杂对象关系 Realm 对象 类型安全查询 对象冻结 可选AES 高性能对象存储 SwiftData Swift对象 #Predicate宏 ModelContext 无 Swift原生数据持久化 二、轻量级KV存储 2.1 UserDefaults UserDefaults是iOS最常用的轻量级存储方案,底层将数据以plist格式序列化到磁盘。 ...

May 2, 2026

OpenClaw 源码导读(一):架构总览 — 为"单个主人"而设计的 AI Gateway

2025 年 11 月,Peter Steinberger(PSPDFKit 创始人、前著名 iOS 社区人物)把自己折腾出来的个人 AI 助手 Molty 开源为 OpenClaw。短短几个月,仓库吃到 35 万+ star、93 个 release、360 多个 contributor,核心代码量在 TypeScript/Swift/Kotlin/Go/Python 之间横跨 40 多万行。它和 Claude Code、Codex CLI 乍看都是"跑在本地的 CLI Agent",但设计哲学完全不同——Claude Code 是一个编程助手,OpenClaw 是一个长在你设备上的私人秘书:它挂在 WhatsApp/Telegram/iMessage/微信 的 IM 客户端后面,能自己发消息、自己开浏览器、自己调用 iOS/Android 上的摄像头和麦克风。 本文是 OpenClaw 源码导读系列的第一篇,目标是把整个项目的"地图"摊开。先讲清楚它要解决什么问题、在怎样的信任模型下运行,再把仓库 100 多个顶层模块拎出来分类,最后给出后续系列文章的导航。 一、它到底是什么 1. 一句话定义 OpenClaw 官方给自己的定位是 “Personal AI Assistant. Any OS. Any Platform. The lobster way.” 翻译过来就是:一个在你自己设备上运行、从你自己现有的聊天工具里和你对话的个人 AI 助手。 这个定位里藏着三个关键差异: Personal:它不是多租户 SaaS,也不是团队协作工具,而是单一主人的私人助手。整个信任模型就是为"只有一个 operator"优化的。 Any OS / Any Platform:Gateway 是 Node 进程,可以跑在 macOS/Linux/Windows(WSL2)、甚至 Fly.io/Docker/NAS 上;客户端包含 iOS/Android/macOS 原生 App,还有 Web Dashboard。 The lobster way:作者把它拟人化成一只太空龙虾 Molty,这是一个品牌/吉祥物层面的设计,但也反映了项目的"玩心"。 2. 和 Claude Code 对比:两个相似却完全不同的 CLI Agent 由于作者 Steipete 本身是 Claude Code 的重度用户和 Anthropic 的合作者,社区最常问的问题就是"和 Claude Code 有啥区别"。这里先画一张对比表: ...

May 2, 2026

http2.0 服务端推送

HTTP/2.0 的 服务端推送(Server Push) 是它相较于 HTTP/1.x 引入的一项重要功能,旨在优化网页加载性能,尤其是首次加载时资源依赖的获取效率。 一、什么是服务端推送? HTTP/2 Server Push 允许服务器主动将资源“推送”给客户端,而不是等待客户端明确请求。这在某些场景下可以预加载依赖资源、减少请求延迟,提升页面的首屏加载速度。 例如: 客户端请求了 HTML 页面,服务器可以在返回 HTML 的同时,主动推送页面所需的 CSS/JS 资源。 二、服务端推送的工作原理 流程概览: 浏览器发送主资源请求(如 HTML); 服务器识别该请求需要哪些依赖资源(如 CSS、JS); 服务器将这些资源 打包为 PUSH_PROMISE 帧 发送给客户端,声明它即将发送的资源; 客户端接收到 PUSH_PROMISE 后,会缓存该资源的响应; 当浏览器稍后真正需要这个资源时,不会再发起真实请求,而是从缓存中获取推送的内容; 避免了请求的 RTT 往返延迟。 技术细节: PUSH_PROMISE 是 HTTP/2 中新增的帧类型,用于声明服务器准备推送的资源; 所有推送的资源都绑定到一个主请求流(主页面),不能独立存在; 客户端可选择 拒绝(RST_STREAM) 不想要的推送资源; 推送资源会根据缓存策略存储在浏览器的缓存中,不一定立即使用。 三、实际示例(Nginx + Link Header) 在使用 Nginx 部署 HTTP/2 服务时,可以通过 Link 响应头启用 Server Push: location = /index.html { http2_push /style.css; http2_push /main.js; } 或者使用 Link 响应头方式: ...

July 28, 2025

错误边界

人无完人,代码总会出错,出错并不可怕,关键是怎么处理。 我就想问问大家react的应用的错误怎么捕捉呢? 这个时候: 小白+++:怎么处理? 小白++: ErrorBoundary 小白+: ErrorBoundary, try catch 小黑#: ErrorBoundary, try catch, window.onerror 小黑##: 这个是个严肃的问题,我知道N种处理方式,你有什么更好的方案? ErrorBoundary EerrorBoundary是16版本出来的,有人问那我的15版本呢,我不听我不听,反正我用16,当然15有unstable_handleError。 关于ErrorBoundary官网介绍比较详细,这个不是重点,重点是他能捕捉哪些异常。 子组件的渲染 生命周期函数 构造函数 class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // You can also log the error to an error reporting service logErrorToMyService(error, info); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } } <ErrorBoundary> <MyWidget /> </ErrorBoundary> 开源世界就是好,早有大神封装了react-error-boundary 这种优秀的库。 你只需要关心出现错误后需要关心什么,还以来个 Reset, 完美。 ...

December 17, 2024

地理定位

HTML5 提供了地理定位(Geolocation)API,用于获取用户的当前位置。通过这一功能,网页可以访问用户的地理位置信息,进而实现如地图服务、定位推荐、基于位置的广告等功能。该 API 主要依赖设备的 GPS、Wi-Fi、IP 地址、蓝牙等信息来确定位置。 Geolocation API 的基本用法 主要通过 navigator.geolocation 对象来访问地理位置信息,提供了以下三个方法: getCurrentPosition(success, error, options) 获取用户的当前位置,一次性返回位置信息。 watchPosition(success, error, options) 持续监听用户的位置变化,并且实时返回新的位置信息。 clearWatch(id) 取消 watchPosition 监听的位置变化。 方法的参数说明 success(position):成功获取位置时的回调函数。position 对象包含位置信息,如经度、纬度、速度等。 error(error):可选参数,获取位置失败时的回调函数。error 对象包含错误信息,如用户拒绝提供位置或获取超时等。 options:可选参数,包含配置选项,影响获取位置的精度、超时时间等。 使用示例 if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { const latitude = position.coords.latitude; const longitude = position.coords.longitude; console.log(`Latitude: ${latitude}, Longitude: ${longitude}`); }, (error) => { console.error(`Error Code = ${error.code}: ${error.message}`); }, { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 } ); } else { console.log("Geolocation is not supported by this browser."); } 获取到的 position 对象 position 对象包含用户的位置信息,主要属性包括: ...

October 16, 2024

transition

CSS transition 提供了一种在更改 CSS 属性时控制动画速度的方法。其可以让属性变化成为一个持续一段时间的,而不是立即生效的过程。 比如,将一个元素的颜色从白色改为黑色,通常这个改变是立即生效的,使用 CSS 过渡后该元素的颜色将按照一定的曲线速率从白色变化为黑色。这个过程可以自定义。 通常将两个状态之间的过渡称为隐式过渡,因为开始与结束之间的状态由浏览器决定。 CSS 过渡可以决定哪些属性发生动画效果(通过明确地列出这些属性),何时开始(通过设置延时),持续多久(通过设置时长)以及如何动画(通过定义缓动函数,如线性或先快后慢)。比如在不同的伪元素之间切换,像是 :hover,:active 或者通过 JavaScript 实现的状态变化。 开发者可以定义哪一属性需以何种方式用于动画,由此允许创造复杂的过渡。然而因为为某些属性赋予动画无意义,所以这些属性无动画性。 备注: auto 值常常较复杂,规范指出不要在它上动画。一些用户代理,比如基于 Gecko 的,实现了这个需求;然而另外一些用户代理,比如基于 WebKit 的,没有这么严格限制。在 auto 上使用动画,取决于浏览器及其版本,可能会导致非预期结果,应当避免使用。 属性 以下 transition 属性,若有多个属性值,则以,(逗号)衔接。 transition-property 指定哪个或哪些 CSS 属性名称用于过渡。只有指定的属性才会在过渡中发生动画,其他属性仍如通常那样瞬间变化。如果指定简写属性(比如 background),那么其完整版中所有可以动画的属性都会被应用过渡。 取值如下: none:没有过渡动画。 all(初始值):所有可被动画的属性都表现出过渡动画。 <custom-ident>:属性名称。由小写字母 a 到 z,数字 0 到 9,下划线(_)和破折号(-)。第一个不能为非破折号字符或数字。同时,不能以两个破折号开头。通俗来说就是可以是自定义名称也可以是css规范中的属性名。 transition-duration 表示过渡属性从旧的值转变到新的值所需要的时间。你可以为所有属性指定一个值,或者指定多个值,或者为每个属性指定不同的时长。 该属性值以秒(s)或毫秒(ms)为单位指定过渡动画所需的时间。默认值为 0s,表示不会呈现过渡动画,属性会瞬间完成转变。不接受负值。 可以指定多个时长,每个时长会被应用到由 transition-property 指定的对应属性上。 如果某个属性的值列表小于 transition-property 的属性值数量,那么时长列表会重复,重复规则是按照现有值进行重复。例如: 如3s,则重复多个3s div { transition-property: opacity, left, top, height; transition-duration: 3s; } 将视为: div { transition-property: opacity, left, top, height; transition-duration: 3s, 3s, 3s, 3s; } 如果是3s,5s,则会重复多个3s,5s。 div { transition-property: opacity, left, top, height; transition-duration: 3s, 5s; } 将视为: ...

October 8, 2024

异步组件

当我们的项目达到一定的规模时,对于某些组件来说,我们并不希望一开始全部加载,而是需要的时候进行加载;这样的做得目的可以很好的提高用户体验。 为了实现这个功能,Vue3中为我们提供了一个方法,即defineAsyncComponent,这个方法可以传递两种类型的参数,分别是函数类型和对象类型,接下来我们分别学习。 传递工厂函数作为参数 defineAsyncComponent方法接收一个工厂函数是它的基本用法,这个工厂函数必须返回一个Promise,Promise的resolve应该返回一个组件。 我们这里以Vue Cli创建的项目为例,这里我稍微做了一下修改,将头部的图片拆分为一个组件,代码如下: <template> <logo-img /> <hello-world msg="Welcome to Your Vue.js App" /> </template> <script setup> import LogoImg from './components/LogoImg.vue' import HelloWorld from './components/HelloWorld.vue' </script> 现在我们就将<hello-world>组件修改为异步组件,示例代码如下: <template> <logo-img /> <hello-world msg="Welcome to Your Vue.js App" /> </template> <script setup> import { defineAsyncComponent } from 'vue' import LogoImg from './components/LogoImg.vue' // 简单用法 const HelloWorld = defineAsyncComponent(() => import('./components/HelloWorld.vue'), ) </script> 我们这里为了看到效果,将import延迟执行,示例代码如下: <script setup> import { defineAsyncComponent } from 'vue' import LogoImg from './components/LogoImg.vue' // 定义一个耗时执行的函数,t 表示延迟的时间, callback 表示需要执行的函数,可选 const time = (t, callback = () => {}) => { return new Promise(resolve => { setTimeout(() => { callback() resolve() }, t) }) } // 定义异步组件,这里这样写是为了查看效果 const HelloWorld = defineAsyncComponent(() => { return new Promise((resolve, reject) => { ;(async function () { try { await time(2000) const res = await import('./components/HelloWorld.vue') resolve(res) } catch (error) { reject(error) } })() }) }) </script> 代码运行结果如下所示: ...

October 5, 2024

尾部优化

ES6 引入了尾调用优化(Tail Call Optimization,TCO)的规范,但它的实现依赖于 JavaScript 引擎的支持(比如 V8 并未开启 TCO),所以这是一个理论上重要、实践中需要谨慎对待的考点。 一、什么是尾调用(Tail Call)? 尾调用是指: 函数的最后一步是调用另一个函数,并直接返回这个函数的执行结果。 例如: function foo() { return bar(); // bar 是 foo 的尾调用 } 尾调用意味着: 当前函数的调用栈帧不需要再做任何事。 调用栈可以复用,不需要为当前函数保留栈帧,从而节省内存。 二、什么是尾调用优化(TCO)? 尾调用优化的目标是: 当一个函数的最后一步是调用另一个函数,JS 引擎可以不再保留当前函数的调用记录,直接跳转执行被调用函数,复用当前栈帧。 也就是说: 不会形成函数调用嵌套的“栈帧链”; 可以避免函数递归导致的栈溢出(stack overflow)。 三、尾调用成立的条件 必须同时满足以下三个条件: 条件 说明 1. 在严格模式下 TCO 只在 strict mode 下才可能被引擎开启 2. 是函数的最后一步调用 return 后没有任何操作 3. 返回调用结果 return bar(x) 形式,不能是 bar(x); return y ✅ 尾调用示例: 'use strict'; function f(x) { return g(x); // 最后一步是返回 g(x),符合尾调用优化条件 } ❌ 非尾调用: function f(x) { const y = g(x); return y; // 不是直接返回函数调用 } 四、尾调用优化应用场景:尾递归 递归调用时,若能做到尾调用,就能避免栈溢出。 ...

October 2, 2024

DOM操作

1、DOM基本概念 1.1、DOM、DOM节点与DOM树 DOM:(Document Object Model, 文档对象模型)是文档内容(HTML或XML)在编程语言上的抽象模型,它建模了文档的内容和结构,并提供给编程语言一套完整的操纵文档的API。 本文面向HTML,不讨论XML等文档,下文中“文档===HTML”。 DOM节点:简称节点(Node),是DOM模型的组成单元。HTML的基本单元是标签,节点常常与标签对应,但连续的文本内容也是一个文本标签。 DOM树:DOM树是DOM结构的表示形式,DOM把文档的每个节点根据父子关系连接,形成DOM树。 1.2、节点、节点类型和节点类 节点:前面提过,节点是DOM树的组成单元。在JS看来,一个节点就是JS对象。下面用node表示任意的节点。 节点类型:并非所有的节点都是一样的,DOM规定文档中有12种节点类型,分别用常量1 ~ 12(有与之对应的常量名称Node.XXX_NODE)表示,可以通过node.nodeType属性获取节点的类型常量。 现在,有些类型的节点已经弃用了,常见到的只有几种类型的节点,包括: 元素节点: 类型常量为Node.ELEMENT_NODE或1。最常见的一类节点,对应文档中的元素。大部分DOM操作都是在元素节点层次的。 文本节点:类型常量为Node.TEXT_NODE或2。对应文档中的文本,任何文档内容都有对应的文本节点,即使空格和换行符。 空格和换行不会对页面内容产生影响,但它们确实以文本节点的形式存在于DOM树中。 Document节点:类型常量为Node.DOCUMENT_NODE或9。它不对应文档的内容,而是作为文档的入口节点,每个文档都有且仅有一个入口,因为这种独特性,赋予一个特殊的变量名称document。 注释节点:类型常量为Node.COMMENT_NODE或8。它对应文档中的注释标签,文档的注释内容也是可读取和修改的。 节点类:DOM内置许多节点类,类之间存在继承关系,形成一套节点类框架。每个节点对象都属于节点类,拥有该类和其父类的方法与属性,这使得操作节点十分简单。节点类框架的一部分大概如图: 这些节点上有丰富的属性和方法,是继承的结果,可以看到,一个HTML标签元素至少有四层的继承关系。 以<a>标签为例,它属于HTMLAnchorElement类,获得了a.target,a.download等属性,接着继承了HTMLElement类上的title, hidden等属性和click()等方法,又从Element类继承了tagName, className等属性和getAttribute(), setAttribute()等方法,再从Node类继承了nodeType(前面说过的节点类型), appenChild(), removeChild()等方法,最后从EventTarget类中继承了事件相关的属性和方法。 不要混淆节点类型和节点类这两个概念。前者是一个生活中的类别,后者是编程意义上的类。节点对象的nodeType属性表示了它的类型,而节点类是该节点的从属的类。因为Dode是一个抽象类,所以,如果知道了某个节点从属的类,我们就知道它的节点类型。 区分节点与元素节点。我们经常关心元素节点(简称元素),因为这是一类最常使用的节点,但是并非所有节点都是元素。 1.3、探索DOM结构 前面的说法太抽象了,让我们用实际的例子看看文档、DOM树与节点的关系。 Live DOM Viewer是一个可以根据HTML文档实时查看DOM树的网站。你把下面的例子复制过去,或者自己去探索。 一个简单的HTML文档: <!DOCTYPE HTML> <html> <body> A simple text. <ol title="this is a title"> <li>czpcalm</li> <!-- comment --> </ol> </body> </html> 它对应的DOM图(颜色区分了节点类型): 留意这个图,你要注意几点: 总共有4种类型的节点,分别是标签节点(红色),文本节点(灰色),注释节点(黄色)和DOCTYPE节点(紫色)。 文档没有<head>标签却出现了HEAD节点。这是因为HTML必然存在<html>,<head>,<body>标签,不存在时会自动补上。顺便一提,当出现<table>标签时,也一定会有<tbody>标签。 文档中的文本都会形成文本节点的内容,包括空格␣和换行↵。第一,单独的空格和换行都会形成对应的文本节点;第二,有内容的文本节点的值包含前导和后继的空白。 不是说HTML中的空白字符都被忽略吗?怎么这里又说全都是有效的字符? 在从文档解析生成DOM树的过程中,HTML中的任何字符都是有效的;不过,在接下去的页面渲染的过程中,空白内容被忽略。所以从文档到页面的整过过程中,空白确实被忽略了。 理解DOM树中的父子关系对应文档中的包含(嵌套)关系。一个极佳的类比是文件树,把元素看做文件夹,文本看做文件,文件夹中可以存放文件和新的文件夹,然后一层层深入下去,DOM树也是如此。 几个原则可以帮我们快速理解这个DOM树的构建: ...

October 1, 2024

iOS反射

反射(Reflection)是指程序在运行时检查、访问和修改自身结构(类型、属性、方法等)的能力。在 iOS 开发中,Objective-C 和 Swift 分别提供了不同层次的反射支持:OC 依赖 Runtime 提供完整的动态反射能力,Swift 则通过 Mirror 提供只读的内省能力。 Objective-C 的反射能力 Objective-C 的反射能力完全建立在 Runtime 之上。Runtime 在运行时维护了完整的类型元数据(类对象、元类对象、方法列表、属性列表、成员变量列表、协议列表等),并通过一系列 C 函数 API 暴露给开发者,使得程序可以在运行时动态地查询和修改几乎所有类型信息。 关于 Runtime 的消息发送、消息转发、Method Swizzling、关联对象等核心能力,请参考 runtime。本节聚焦于 Runtime 中与"反射"直接相关的能力——即运行时的类型内省和动态操作。 类型内省 类型内省(Introspection)是反射中最基础的能力——在运行时查询一个对象的类型信息。 NSObject 提供的内省方法 NSObject 定义了一组内省方法,这些方法底层都依赖 Runtime 的 isa 指针和类型元数据来实现: id obj = [[NSMutableArray alloc] init]; // 类型判断 [obj isKindOfClass:[NSArray class]]; // YES — 判断是否是某个类或其子类的实例 [obj isMemberOfClass:[NSMutableArray class]]; // YES — 判断是否是某个类的直接实例 [obj conformsToProtocol:@protocol(NSCoding)]; // YES — 判断是否遵循某个协议 // 方法响应检查 [obj respondsToSelector:@selector(addObject:)]; // YES — 判断是否能响应某个消息 // 获取类型信息 NSStringFromClass([obj class]); // @"__NSArrayM"(真实的私有子类名) NSStringFromSelector(@selector(count)); // @"count" 需要注意 isKindOfClass: 和 isMemberOfClass: 的区别:前者沿继承链向上查找,后者只比较当前类。另外,[obj class] 返回的可能是私有子类(如类簇 NSArray 的实际类是 __NSArrayI 或 __NSArrayM),而 object_getClass(obj) 能获取真正的 isa 指向的类(KVO 场景下会是 NSKVONotifying_ 前缀的子类)。 ...

May 2, 2026