最近,OpenAI 的 Codex 被发现存在一个日志写入问题:程序会持续向本地数据库写入大量日志,导致文件不断膨胀,并产生远超正常水平的磁盘写入。类似的 Bug 过去并不少见。放在几年前,人们大概只是删除文件、重启应用,然后等待下一个版本修复。
但这一次,大家的反应明显不同。比起“还剩多少磁盘空间”,更多人开始关心 SSD 已经累计写入了多少数据,会不会影响闪存寿命,以及未来更换设备时,需要为同样容量的内存和存储多付多少钱。
Codex 的 Bug 未必比过去那些失控的日志程序更加严重。真正发生变化的是:每一次无意义的写入,似乎都重新有了价格。
随着 AI 算力需求迅速增长,存储器厂商越来越倾向于优先保障 HBM、服务器内存和企业级 SSD 等利润更高、需求更旺盛的产品。消费级内存和 SSD 能够分配到的产能因此受到挤压,价格也不断走高。
内存和闪存本来就是典型的周期性行业,价格上涨和下跌并不罕见。但这一轮变化令人不安的地方,在于它似乎不再只是一次简单的库存周期,而是伴随着产业资源向 AI 基础设施长期倾斜。
即使价格最终仍会回落,消费者恐怕也很难迅速回到过去那种“大容量近乎免费”的心理预期。
当存储不再显得取之不尽时,开发者或许也应该转变对资源时的态度。
在我最开始接触电脑时,Apple II 常见的内存配置只有 48 KB。为了使用扩展卡额外提供的 16 KB 空间,开发者需要仔细安排地址、代码和数据,想方设法把每一个字节都利用起来。
今天一个数十 GB 的游戏,必然拥有 FC 时代无法想象的画面、声音、世界规模和交互复杂度。然而,巨大的容量并不能保证它带来的乐趣一定超过一个只有几十 KB 的游戏。
这并不是说现代软件不应该变大。真正值得思考的是,在过去很多年中,开发者已经习惯了存储容量会自然增长、网络会不断变快。日志、缓存和临时数据只要暂时还放得下,就很少有人追问它们为什么存在,又将在什么时候被删除。
昂贵的 SSD 不会自动创造优秀的软件,内存短缺也不会让开发者突然变得更加聪明。但当硬件不再被默认会无限增长时,那些曾经被忽视的工程问题便会重新浮出水面。
我并不怀念那个需要在几十 KB 中反复腾挪的时代。
真正值得重新拾起的,并不是资源匮乏,而是那种曾经十分自然、后来却逐渐消失的工程习惯:
知道每一个字节为什么到来,也知道它将在什么时候离开。
近期推荐
iOS 设备锁定后,Swift Task 能运行多久?(Swift Task lifetime when an iOS device is locked)
这是一篇来自 Apple Developer Forums 的问答。提问者发现,一个会串行执行多个 REST 请求的 Swift Task,在设备锁定后有时能够正常完成,有时会暂停,甚至出现超时。DTS 工程师 Quinn “The Eskimo!” 解释道,Swift Concurrency 完全运行在应用进程中;一旦 iOS 挂起进程,执行 Task 的工作线程也会停止,Task 本身并不会获得额外的后台运行能力。
这种机制在网络请求中尤其容易造成困惑。应用被挂起时,正在进行的 URLSession 请求可能因连接失效而失败;进程恢复后,开发者看到的可能是请求超时或网络错误,而不是 Task 从暂停处继续并顺利完成。Quinn 给出了四种处理方向:设法延缓进程挂起、在应用即将被挂起时停止请求、允许请求失败并做好重试与错误恢复,或者使用 URLSession 的后台会话。
Swift 6.4 中的 anyAppleOS (anyAppleOS in Swift 6.4)
过去,为跨 Apple 平台共享的 API 声明可用性时,开发者需要分别列出各个平台及其对应的系统版本。由于不同平台的版本号长期不一致,这类声明不仅冗长,也容易出现遗漏或版本填写错误。随着 Apple 各平台从 26 开始统一版本号,Swift 6.4 引入了 anyAppleOS,允许开发者在 @available、#available 和 #if os(...) 中,以更简洁的方式描述适用于 Apple 操作系统的 API 与代码。
Artem Mirzabekian 介绍了这一新语法,并梳理了它与平台专属可用性、canImport 以及 Package.swift 平台声明之间的区别,同时说明了如何结合更具体的可用性规则,处理部分平台延后支持或完全不可用的情况。
可复用 SwiftUI 视图的设计剖析 (The Anatomy of a Reusable SwiftUI View)
越来越多的开发者在构建可复用 SwiftUI 视图时,都希望其 API 尽可能贴近 Apple 官方的设计方式,但具体如何落地,往往缺少一套清晰的方法。Alexander Weiß 给出了自己的设计思路:首先判断组件属于描述功能概念的语义型组件,还是具有明确视觉形态的规定型组件,再梳理其语义、合法状态、数据流与交互行为。同时,组件还应遵循 SwiftUI 的状态管理方式,正确使用不可变值、Binding 与 State,并将无障碍支持、环境值和视图身份稳定性纳入设计,而不只是关注 API 的调用形式。
文章通过 Card、Tag 和 Rating 三个示例展示了初始化方法、自定义 View Style 和环境值等具体实现。这套方法的核心并非简单模仿 Apple API 的外形,而是让自定义组件在组合方式、状态流转、交互行为、无障碍支持和环境适配等方面真正融入 SwiftUI 体系。
SwiftUI 环境默认值不稳定的隐性成本 (The hidden cost of unstable SwiftUI environment defaults)
@Entry 宏让自定义环境值的声明更加便捷,但如果直接将引用类型实例作为默认值,例如 @Entry var logger = AppLogger(),就可能带来不必要的视图更新。每当 SwiftUI 没有找到上层注入的值而回退到默认值时,都可能重新创建一个 AppLogger。这些对象虽然功能相同,却不是同一个实例,因此 SwiftUI 会认为环境值发生了变化,进而重新计算读取它的视图。Xcode 27 开始会针对这种写法发出警告,让这个过去不易察觉的问题更早暴露出来。
Natalia Panferova 使用 _printChanges() 直观展示了这一问题,并给出两种解决方式:将默认实例保存在稳定的外部存储中,或者将环境值声明为 Optional,再由应用入口显式注入。
我在《SwiftUI Environment:理念与实践》中也分析过这一问题。其根源在于
@Entry宏生成默认环境值的方式,使默认表达式可能在回退访问时被重复求值。
使用 visualEffect 为 SwiftUI ScrollView 创建纵向视差效果 (Make Vertical Parallax for ScrollView in SwiftUI)
早期的二维游戏通过让不同图层以不同速度移动,用很低的成本便营造出了富有层次感的视觉效果。但在 SwiftUI 中实现滚动视差,真正麻烦的通常不是计算偏移量,而是持续获取每个视图在 ScrollView 中的位置。过去,这类效果往往需要结合 GeometryReader、自定义坐标空间和状态传递来完成,不仅代码量较大,处理不当还可能引发额外的视图更新。
visualEffect 的引入大幅降低了这类效果的实现难度。codelaby 展示了如何直接在 visualEffect 中读取视图的几何信息,并根据滚动位置应用 offset,无须额外维护状态,也不会改变视图原有的布局。
工具
swift-span-algorithms: 给 Span 补上算法能力
在处理连续内存时,Swift 过去主要有两类选择:使用 Array、ContiguousArray 等拥有存储的容器,安全易用,但可能伴随复制、引用计数或额外分配;使用 UnsafeBufferPointer 等指针类型,成本低、控制力强,却需要开发者自行保证内存仍然有效,并避免越界访问。
Swift 6.2 新增的 Span<Element> 填补了两者之间的空白。它是一段连续内存的非拥有视图:不持有底层存储,却能借助编译器追踪生命周期,并通过边界检查提供安全访问。不过,在 Swift 6.2 中,Span 尚未拥有 Collection、Sequence 那般完整的算法能力。
David Retegan 开发的 swift-span-algorithms 正是对这一缺口的补充。它为 Span、RawSpan、MutableSpan 和 InlineArray 提供查找、比较、修剪、分块、滑动窗口、切分及游标(Cursor)等操作。整个库零以无隐藏分配和常数级额外空间为设计目标,在扩展算法能力的同时,仍然遵守 Span 的生命周期约束。