阅读视图

发现新文章,点击刷新页面。

嵌入主线程消息循环的任务调度器

最近在网友协助下把 soluna port 到包括 wasm 在内的非 windows 平台。其间遇到很多难题,大多是多线程环境的问题。因为 soluna 的根基就是基于 ltask 的多线程调度器,如果用单线程实现它,整个项目的意义就几乎不存在,所以它是把项目维护下去必须解决的问题。

好在 lua 有优秀的 coroutine 支持,它可以把运行流程抽象成数据,而 Lua 本身并未限制数据的具体储存方式,所以完全可以存在于内存堆中,脱离于 C 栈存在,这为各种在 C 环境下的多线程难题开了后门。C 语言依赖栈运行代码逻辑,而栈绑定于线程,线程调度通常由操作系统完成,所以用常规方式无法让代码跨线程运行:即,无法通过常规手法让一段代码的流程前半段在一个线程运行,而用另一个线程运行后半段;但是,在 C 上建立一个 Lua 层,则很容易绕开这个限制,只用标准方法就可以自由控制程序运行流程。

上一次发现利用一些技巧就可以完成一些看似不可能却的确可行的调度方式是 多线程串行运行 Lua 虚拟机

简单复述一下当时的需求:

希望可以在单个 Lua 虚拟机内模拟多线程并发。当一个 Lua 的 coroutine 运行到 C 函数中时,若此刻 C 函数希望阻塞等待一个 IO 请求,常规的方法是 yield 回 Lua 虚拟机,让调度器持有一个 Lua coroutine 的状态,待完成 IO 请求后,再由调度器 resume 这个 coroutine 。这样做的难题是,运行到一半的 C 函数,上下文状态还在 C 所属线程的栈中,一旦 yield 回 Lua 虚拟机,必须放弃 C 栈上的状态,并在下次 resume 时可以重建。这通常难以实现,这也是为何 Lua 的 coroutine C api 难以理解又很难使用的原因。尤其使用第三方 C 库,几乎没可能适配。

另一个折中的方法是让 Lua 虚拟机在 C 函数中阻塞,硬等到 IO 操作完成。但在阻塞过程中,无法使用这个 Lua 虚拟机。若使用者期待 Lua 虚拟机中多个 coroutine 以多线程方式并行工作,恐怕会失望。即使其它 coroutine 的业务和 IO 完全无关,一个 IO 阻塞操作会让它完全无法并行工作。

变通的方式是(在编译时)打开 Lua 的线程锁。在调用 IO 阻塞前解开线程锁,只要 IO 操作本身不涉及对 Lua State 的操作,那么 Lua 解释器在调用 C 函数前的那一刻会解开线程锁,这样就可以允许阻塞操作过程中,Lua 虚拟机可以执行其它操作。

线程锁本身依赖系统线程库的调度器。不适合像 ltask 这样自己实现任务调度(即在有限个系统线程下调度远超系统线程数的任务)。但是,我们可以配合 ltask 实现类似的锁机制。这就是之前这个 patch 实现的东西:Lua 层调用可能阻塞的 C 函数前加锁通知 ltask 调度器,在 C 函数中,用户主动在阻塞操作前解锁。ltask 的调度器在 C 函数返回前就将虚拟机提前放回调度表。当阻塞操作完成后,重新加锁会等待调度器完成(如果有)正在运行的在同一个 Lua 虚拟机上的任务完成。这样,整个 Lua 虚拟机实质上还在串行运行其中的任务。而使用者看起来在一个 coroutine 尚未 yield 之前就开始运行另一个 coroutine ,直到其它 coroutine yield 后再继续未完的工作。同一个 Lua 虚拟机的多个 coroutine 是在多个操作系统线程上完成的,但却保持串行。

这个 patch 最终并未合并进 ltask ,因为我觉得它对使用者有更高的要求。但经此,我开了不少脑洞,明白在必要时牺牲一些复杂度就可以完成一些超乎寻常的任务。


这次我面临的是新的问题:sokol 并未设计成线程安全。api 不能并发。一开始我并不想使用复杂的解决方案,以为只要保证 sokol 不并发就够了。期间遇到的问题是 Windows API 死锁 ,也很容易绕过。

对于图形 API ,我只是简单的将图形 API 调用都塞在同一个 render 服务中。并在主线程的 sokol 回调函数中利用一个信号量和渲染过程同步。虽然 Direct3D ,Matal ,Vulkan 这些为多线程设计的底层图形 API 这么用都没有问题,但 OpenGL (在 Linux 上开启)却将状态放在当前线程上。一开始,我们通过额外调用 MakeCurrent 绕开限制,但在我们向 wasm 移植时却遇到障碍。

最终,我还是希望找到一个方法让所有图形 API 的调用都真正从主线程,也就是 sokol 提供的 callback 函数中发起。而不是用信号量同步,让它们在其它工作线程运行。

难题在于,主线程是通过事件消息循环驱动的,没有全部的控制权。不适合在其上实现任务调度器。一个任务调度器最好有所有时间片的控制权,它才好简单有效的分配时间片,没有任务时可以休眠而不是在事件循环没有新事件时强制休眠。我不想为这种特别的工作方式改造 ltask 的任务调度器,让主线程的事件回调函数伪装成一个功能不完整的特殊工作线程。我实际需要的是:把一个 Lua 虚拟机内的特定任务分配给主线程回调函数运行,在没有这种特定任务时,其它任务还是交给 ltask 做常规调度。

细想之下,解决方法和上一个需求有异曲同工之处:Lua 在启动这种特殊任务(必须在主线程回调函数内运行)前通知调度器。这时把虚拟机暂时移出调度表,而在主线程的回调函数中(通过信号量)发现有新任务到来,就接手处理特殊片段。处理完毕后,再把它归还给调度器。

通过这个方案,我们顺利把 soluna port 到 wasm 环境,同时简化了 Linux/OpenGL 实现。当我了解到 wasm 上有 pthread api 和原生 web worker api 两套多线程 api 后,我又信心满满的想用 worker api 来实现。但最终未能如愿。具体讨论在这个 issue 中 ,倒不是完全做不到,而是我觉得不应该牺牲太多复杂度。比如把 soluna 中所有的 IO 操作都转发到主线程中运行(这是 web worker 的限制所在,也是 wasm pthread 原本要解决的问题)。


昨天发现了上面解决方案实施中的一点纰漏:虽然给 ltask 打了个洞,可以在系统主线程夺过指定任务运行,但在交换控制权回调度器时,忽略了 ltask 的所有工作线程可能因为没有任务而全部休眠的可能性。仅仅把任务推回(线程安全的)任务队列是不够的。还需要重启调度器(如果处于休眠状态)。具体讨论见这个 issue

ps. 自从搬家后,我的 Linux 机器一直没有开机。昨天为了在 Linux 环境下测试,才重新装起来。bug 虽然重现,但视乎在我的机器上更为严重:一旦程序失去响应,整个系统都卡住了,甚至冷启动都没用。直接把机器弄死,而且五分钟内都开不了机(BIOS 进不去,屏幕无信号)。我怀疑是显卡驱动的 bug ,因为太久没升级系统,头一次升级还失败了,pacman 报告出现依赖问题拒绝更新。强删了几个 Electron (这个毒瘤)的几个历史版本后,系统升级才得以继续。最后更新了最新版的 Nvidia 包似乎就一切正常了。

基于 easy_rxdart 的轻量响应式与状态管理架构实践

面向 Flutter/Dart 的响应式与状态管理,easy_rxdart 提供统一的 Stream/Reactive 操作与 Model + InheritedWidget 组合,覆盖防抖/节流/去重、错误恢复、第三方库响应式封装。核心设计旨在减少样板代码、提升组合能力与可读性,让业务逻辑围绕流与状态自然生长,适配中小型到复杂场景的架构演进。

方案选型

  • 响应式核心:以 Stream 为主干,扩展操作符满足事件流需求;用 Reactive<T> 统一链式与组合语义。
  • 状态管理:Model 基于 Listenable,通过 EasyModel<T> 提供上下文,watch/read/listen 精准区分重建与副作用。
  • 第三方整合:通过扩展与工具方法,对 dio、权限、图片选择、存储等提供一致的响应式调用。
  • 取舍与对比:相较 BLoC,减少事件/状态样板,强调“以流为中心”的组合与直观 Model 触发;相较 Riverpod,更贴近 Flutter 机制(InheritedWidget + AnimatedBuilder),简单可控;需要跨层依赖时,用 EasyModelManager 做全局管理。

架构设计

  • 目录分层:
    • 核心:Reactive<T>、流操作扩展、ModelEasyModel<T>EasyStreamControllerSubject 包装。
    • 扩展:面向 Stream/Reactive/Widget/第三方库 的便捷操作。
    • 工具:debounce/throttle/distinct、时间/格式化、定时器组、网络/连接状态。
    • Mixin:应用与路由生命周期、订阅管理。
  • 模块职责:
    • Reactive<T>:包装 Stream<T> 提供 map/flatMap/where/combineLatest/zip/concat/listen,兼容 rxdart。
    • Model + EasyModel<T>:版本与微任务去重策略的最小重建;watch/read/listen 三分法。
    • Stream 扩展:debounceTime/throttleTime/distinctUntilChanged/retryWithDelay/withLatestFrom/buffer/window/sample/audit 等。
    • 管理与集成:EasyModelManager.lazyPut/get/put/delete/reset 全局依赖;第三方能力响应式化。

整体流程图

截屏2025-11-22 12.45.56.png

核心数据流

  • 事件输入:来自控件、网络、定时器、第三方库等。
  • 操作符链:集中完成过滤、限流、错误恢复与组合。
  • 状态触发:Model.notifyListeners() 驱动 UI 最小重建;toReactive 将状态投射为流用于组合。
  • 副作用订阅:无需重建时,用 listen 执行副作用。
flowchart LR
  UI[TextField / Gesture] --> S[Stream<String> / Stream<void>] --> O[debounceTime / throttleTime / distinctUntilChanged] --> M[map / flatMap / combine / zip] --> ST[Model 状态 或 Reactive 输出] --> R[rebuild 或 side-effect]

最小可用示例

定义模型

class CounterModel extends Model {
  int _count = 0;
  int get count => _count;
  void increment() {
    _count++;
    notifyListeners();
  }
}

提供与消费

EasyModel<CounterModel>(
  model: CounterModel(),
  child: Builder(
    builder: (context) {
      final model = EasyModel.watch<CounterModel>(context)!;
      return Column(
        children: [
          Text('${model.count}')
          ,
          ElevatedButton(
            onPressed: () => EasyModel.read<CounterModel>(context)?.increment(),
            child: const Text('Add'),
          ),
        ],
      );
    },
  ),
);

文本输入搜索流

final input = StreamController<String>.broadcast();

final searchStream = input.stream
  .debounceTime(const Duration(milliseconds: 300))
  .distinctUntilChanged()
  .flatMapValue((q) => fetchResult(q))
  .retryWithDelay(count: 3, delay: const Duration(milliseconds: 500));

searchStream.listen((items) {
});

状态到流的桥接

将模型状态投射为 Reactive<T>,用于组合或跨组件订阅。

final counterReactive = model.toReactive(() => model.count);
counterReactive.map((v) => 'Count: $v').listen((text) {
});

第三方集成示例(网络请求)

合理结合错误恢复与重试。

Stream<List<User>> getUsers() =>
  Stream.fromFuture(dio.get('/users'))
    .map((resp) => parseUsers(resp.data))
    .retryWithDelay(count: 2, delay: const Duration(seconds: 1))
    .onErrorReturnItem(<User>[]);

关键设计细节

  • 重建控制:Model 使用版本与微任务去重策略,避免短时间内重复触发。watch 触发构建,read 不触发构建,listen 用于副作用。
  • 订阅生命周期:控制器/Subject 包装统一“谁创建谁销毁”;Mixin 自动清理路由/应用生命周期绑定。
  • 错误治理:timeoutTime/retryWithDelay/onErrorReturn/onErrorResumeNext/defaultIfEmpty/materialize/dematerialize
  • 组合能力:merge/concat/combineLatest/zip/withLatestFrom;窗口与缓冲:windowCount/windowTime/bufferCount/bufferTime

典型场景落地

  • 输入框防抖搜索:debounceTime + distinctUntilChanged + flatMapValue + retryWithDelay
  • 滑动或点击行为治理:对交互加 debounce/throttle/distinct
  • 从状态驱动 UI:Model 维护最小状态集,EasyModel<T> 向下传递,构建边界清晰。
  • 复杂流编排:并发/序列/压缩三类组合,对应 merge/concat/zip

流程图:网络请求装配线

flowchart TD
  REQ[请求触发] --> F[Future -> Stream] --> RETRY[retryWithDelay] --> MAP[map / 解析] --> FALLBACK[onErrorReturnItem 或 defaultIfEmpty] --> OUT[输出到 Model / Reactive] --> UI[UI 重建 或 副作用]

性能与工程实践

  • 边界清晰:将“重建”与“副作用”拆分,避免过度重建。
  • 优先扩展操作符:用扩展而非手工逻辑,减少不可预期状态。
  • 错误兜底:所有外部 IO 流建议配置兜底值与重试策略。
  • 资源回收:统一关闭控制器与订阅;跨页面订阅用 Mixin 自动清理。
  • 可测试性:流管线易单测,模型可通过版本与哈希策略验证通知行为。

与主流方案的协作

  • 与 Riverpod 协作:外层管理依赖,内层用 Model + Reactive 做流编排与最小重建。
  • 与 BLoC 协作:保留既有事件/状态结构时,将副作用和组合逻辑沉到 Stream 扩展与 Reactive

适用边界

  • 最佳适配:事件主导交互、网络数据装配、轻到中型状态管理、端上能力整合。
  • 不适配:跨团队大型复杂域模型、严格 CQRS/DDD 的大规模事件场景,建议与更重型框架配合。

总结与落地建议

  • easy_rxdart 将响应式与状态管理统一到可组合的流与轻量模型之上,降低样板与心智负担。
  • 建议从“输入防抖 + 网络装配 + 模型驱动”起步,逐步引入窗口/缓冲与生命周期治理,避免一开始过度工程化。

实践清单

  • 输入框搜索:debounceTime + distinctUntilChanged + flatMapValue + retryWithDelay
  • 列表滚动埋点:throttleTime + mapNotNull + bufferTime
  • 登录态与页面联动:Model.toReactive + combineLatest2 + defaultIfEmpty
  • 网络兜底:timeoutTime + onErrorReturnItem + retryWithDelay

苹果悄悄上线网页版 App Store!官方出品调研竞品更方便~

苹果悄然推出了网页版 App Store(官网地址:apps.apple.com/cn),无需依赖 iOS 或 macOS 设备,只要打开浏览器,无论是安卓手机、Windows 电脑还是其他终端,都能轻松访问 App Store 的丰富内容。不过目前网页版仅支持浏览、搜索应用,屏蔽了下载功能改为了分享。

企业微信20251121-141812.png

核心亮点:多地区快速切换 + 全设备专区适配

网页版 App Store 最让人惊喜的,莫过于无门槛切换全球地区商店。用户只需修改网址中的两位地区代码(遵循《ISO 31666-1》标准,小写格式),就能一键跳转至对应国家 / 地区的 App Store,比如:

无需注册登录,也不用切换账号地区,就能直接查看目标地区的应用榜单、同类型产品分布,以及特定应用的价格、评分、用户评论等核心信息,操作简单到离谱。

同时,网页版几乎 1:1 复刻了移动端 App Store 的视觉设计和功能布局,还新增了设备专区切换功能—— 左侧菜单栏可直接选择 Mac、iPad、Vision、Watch 等设备,无需拥有对应硬件,就能直观查看应用在不同设备上的展示效果,比如 iPad 端的 5 图排版、Watch 端的适配界面等。

2.png

核心实用场景,精准匹配开发者与产品人核心需求

1. 出海竞品调研提速,摆脱第三方工具束缚

过去做海外市场调研,查看不同地区 App Store 的榜单动态、竞品详情,只能依赖点点数据这类第三方平台。不仅要完成登录流程,还面临加载迟缓、数据滞后的问题,部分关键数据甚至需要开通 VIP 才能解锁。网页版 App Store 直接打通全球地区商店通道,无需借助任何额外工具,就能实时获取目标地区的竞品核心信息,从榜单趋势到应用评分、评论、价格等详情全掌握,让出海调研效率大幅提升。

2. 多设备适配核查零门槛,独立开发者福音

独立开发者或小型团队往往难以配齐 Mac、iPad、Vision、Apple Watch 等所有苹果设备,给多设备适配调研带来阻碍。网页版的设备专区切换功能恰好解决了这一难题,左上角下拉菜单即可一键切换至对应设备的专属应用页面。想确认自家应用在 Mac 端的展示效果,或是调研 Watch 端的热门应用类型,只需打开浏览器就能直观查看,零成本完成多设备适配验证。

3. 跨平台无障碍访问,安卓 / Windows 用户不用再借设备

产品经理、运营人员常需调研 App Store 上的应用,但如果手边只有安卓手机或 Windows 电脑,此前只能向同事借用苹果设备才能完成。网页版 App Store 打破了设备系统限制,任何浏览器都能直接访问,跨平台即可轻松浏览应用详情,再也不用为查询一个应用地址而四处借设备。

4. 应用链接分享更高效,告别繁琐查找流程

运营或市场人员需要产品的 App Store 链接时,过去要么翻找存档文档,要么通过第三方平台搜索跳转后复制 URL。现在网页版提供了集中式的应用聚合入口,直接打开网页搜索应用名称,就能快速复制浏览器 URL 一键分享,甚至可让同事自行搜索获取,彻底省去反复沟通查找的麻烦,缩短信息传递路径。

5. 大屏交互体验升级,操作效率再提升

电脑端的大屏优势在网页版 App Store 中得到充分发挥,配合键盘输入搜索,操作比手机端更高效。无论是批量筛选竞品、同时对比多个应用详情,还是沉浸式浏览应用截图与用户评论,体验都更为流畅直观。这种差异就像网页版视频对比手机端,在信息获取和操作便捷性上都有明显提升,让应用调研和探索更省心。

结语

苹果这次低调上线的网页版 App Store,没有大肆宣传,却精准戳中了开发者、产品人、运营等群体的核心需求。它打破了设备和地区的限制,让 App Store 的内容触达更便捷,无论是竞品调研、跨设备适配查看,还是日常应用浏览、分享,都变得更高效、更省心。

对于开发者和产品人来说,这无疑是一份惊喜福利,也让我们看到了苹果在生态开放上的微小但重要的进步。如果你常需要和 App Store 打交道,不妨赶紧收藏网址,体验这份 “无门槛逛店” 的快乐~

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

[WWDC]Why is my app getting killed 学习笔记

WWDC2020-[Video]Why is my app getting killed?

该session重点讲解了iOS App在后台可能被系统终止的原因

同时也介绍了自iOS 14开始,MetricKit推出了新的能力和新的数据用以诊断和通缉App在前后台被系统终止的情况,即MXForegroundExitData和MXBackgroundExitData

iOS App在后台可能被系统终止的原因有:

  1. Crash

  2. Watchdog

  3. CPU resource limit

  4. Memory footprint exceeded

  5. Memory pressure exit(jetsam)

  6. Background task timeout

无论App在前台还是后台被系统终止,MetricKit都提供了诊断和统计数据,

  • 开发者一方面可以在程序中通过订阅MXForegroundExitData和MXBackgroundExitData来查看
  • 同时在Xcode Organizer中也可以查看,详细请参考下文

Crash

  • 在Xcode Organizer中可以查看崩溃信息,同时在代码中也可以通过MXCrashDiagnostic获取崩溃信息

Watchdog

  • 在App中一些关键的状态变化时(如App启动、前后台切换),系统Watchdog会设置超时限制(20s),如果超时时间内一直没有完成(也就是App卡住),App就会被终止
  • 这种问题预示着可能有死锁(如主线程中gcd sync行为)、无限循环代码逻辑(如无限递归调用)
  • 模拟器场景,或者连接debugger调试时不会触发Watchdog的终止行为
  • 在代码中可以通过MXCrashDiagnostic查看是否存在Watchdog终止App的情况

CPU resource limit

  • 当在在后台App持续消耗过高CPU资源时,系统会记录CPU异常信息
  • Xcode Organizer中可以查看,对应着Energy
  • 代码中可以通过MXCPUExceptionDiagnositic获取信息
  • 同时异常此时也会记录到MXBackgroundExitData中

Memory footprint exceeded

  • 如果要适配iPhone 6s以前的设备,要保证App的内存占用不要超过200MB

  • 当App进入后台,为尽可能降低系统因其他应用内存占用而把我们App杀死的可能性,最好让我们App内存占用降低到50MB以下

  • App在一些关键的过渡过程中(如启动、前后台切换),如果耗时过长(超过大概20s)Watchdog会终止App

    • 注意,当App连接debugger时,是不会被Watchdog终止的

Memory pressure exit(jetsam)

  • 当应用在后台时,其他应用占用了太大内存,系统为了给其他在前台的App足够的内存空间,会把在后台的应用杀死,也叫做被系统(丢弃)jetsam了

  • jetsam事件并不意味着App有bug,这是系统的行为,但却预示着在前台的App占用过多的内存

  • 如果我们的App被系统jetsam了该怎么办

    • 在App进入后台时保存好状态信息,如View Controller Stack、Draft input、Media playback position
    • 使用UIKit State Restoration相关的API,App即使被jetsam了,也会很快恢复到原来的样子
    • App在后台时尽量保持内存占用在50MB以下,被终止的概率会下降,但系统并不保证一定不会终止

Background task timeout

  • 对于短暂的且重要的后台任务(通过UIApplication.beginBackgroundTask执行的),如果没有执行endBackgroundTask或者任务时间太长,都会导致超时,超时时间大概30s,时间到达后,任务还未结束(endBackgroundTask),App就会被系统杀死。如果超时时间内结束,则可以正常的进入suspended状态
  • 把每个任务看做只有30s的炸弹的导火线,一旦App到了后台,导火线就被点燃了
  • 如果希望后台任务有更长的时间处理则要用Background Tasks框架
  • 关于iOS App进入后台后会发生什么可以参考--iOS App进入后台时会发生什么根据官方文档(Extending your app’s background exec - 掘金

参考

Swift 多线程读变量安全吗?

前文,我们讲了在 Rust 中多线程读 RefCell 变量不安全的例子(见 Rust RefCell 多线程读为什么也 panic 了?),同样的例子,如果在 Swift 中,多线程读变量安全吗?

先看测试用例:

class Object {
    let value: String
    init(value: String) {
        self.value = value
    }

    deinit {
        print("Object deinit")
    }
}

class Demo {
    var object: Object? = Object(value: String("Hello World"))

    func foo() {
        var tmp = object
        object = nil
        (0..<10000).forEach { index in
            DispatchQueue.global().async {
                do {
                    let v = tmp
                }
                usleep(100)
                print(index, tmp)
            }
        }
    }
}

let demo = Demo()
demo.foo()

多次运行后,没有崩溃

当我们读一个变量时,编译器会自动帮我们插入引用计数的逻辑,类似如下,当对象引用计数为 0 时会释放。

do {
    swift_retain(tmp)
    let v = tmp
    swift_release(tmp)
}

按 Rust 中读 RefCell 变量的思路分析看,Swift 在读变量时也会涉及 retain、release 来写引用计数,为什么 Swift 中不会崩溃呢?

我们来扒一下 Swift 的源码:github.com/swiftlang/s…

1) swift_retain

引用计数 +1,主要代码如下:

在这里插入图片描述

refCounts 表示引用计数,定义如下,可以看出 refCounts 是一个原子变量,这也是保证线程安全的关键。

class RefCounts {
  std::atomic<RefCountBits> refCounts;
  ...
}

masked->refCounts.increment(object, 1)对应函数如下: 在这里插入图片描述

有两处关键代码:

第一个红框表示读取当前引用计数,这是一个原子的读取。

第二个红框,表示 CAS(Compare-And-Swap)更新引用计数,这也是一个原子操作,逻辑如下:

  • **比较 (Compare)**:看内存中 refCounts 的当前值,是否还等于刚才读到的 oldbits
  • 如果相等,则交换:相等说明在计算期间,没有其他线程修改过它,则直接将内存中的值更新为 newbits,并返回 true,循环结束
  • 如果不相等,则重置:不相等说明在计算期间,有其他线程抢先修改了内存,此时会将 oldbits 更新为内存中那个最新的、被其他线程改过的值,并返回 false,继续循环,用新的 oldbits 再算一次

可以看出 swift_retain 中对引用计数的读写操作都是原子的。

2) swift_release

引用计数 -1,主要代码如下:

在这里插入图片描述

执行 -1 的代码如下:

在这里插入图片描述

和 swift_retain 很类似,包含两个步骤:

第一个红框是原子的读引用计数。

第二个红框是 CAS 原子的写引用计数。

另外,这里还有另一个点需要注意,swift_release CAS 写引用计数时,传的参数是std::memory_order_release

std::memory_order_release 的作用是避免指令重排,表示在该指令执行完成之前,在代码里写在该指令前面的所有内存操作,必须全部同步到内存中,绝对不允许重排到该指令之后执行。

举个例子:

假设线程 A 在使用对象,然后释放它:

// 线程 A
myObject.someData = 100 // 1. 写数据
// ... 使用完毕 ...
release(myObject)       // 2. 减少引用计数 (可能降为0)

如果没有 std::memory_order_release,CPU 或编译器可能会进行指令重排,把 1 和 2 的顺序颠倒,也就是说,可能先减少了引用计数,再写入数据。

如果发生这种情况,可能导致对一个已释放的对象进行写操作,导致崩溃(Use-After-Free)。

可以对比看下 swift_retain 时传入的参数是**std::memory_order_relaxed**,这是一种性能开销最小、限制最少的内存排序选择,它只保证这个操作本身是原子的,但不保证和其他代码的执行顺序。这是因为 retain 时不会导致对象释放,即使在引用计数写入后执行代码,也不会有影响。

更多内容,欢迎订阅公众号「非专业程序员Ping」!

Swift6 @retroactive:Swift 的重复协议遵循陷阱

欢迎大家给我点个 star!Github: RickeyBoy

背景:一个看似简单的 bug

App 内有一个电话号码输入界面,在使用时用户需要从中选择注册电话对应的国家,以获取正确的电话区号前缀(比如中国是 +86,英国是 +44 等)。

Step 1:入口 Step 2:缺少区号 期望结果
image1.png image2.png image3.png

这是一个看似很简单的 bug,无非就是写 UI 的时候漏掉了区号,那么把对应字段拼上去就行了嘛。不过一番调查之后发现事情没有那么简单。

列表是一个公用组件,我们需要在列表中显示国家及其电话区号,格式像这样:"🇬🇧 United Kingdom (+44)"。所以之前在 User 模块中添加了这个extension:

    extension Country: @retroactive DropdownSelectable {
        public var id: String {
            code
        }
    
        public var displayValue: String {
            emoji + "\t(englishName) ((phoneCode))"
        }
    }

原理一看就明白,displayValue 代表的是展示的内容。但是最终结果展示错误了:明明将电话区号 ((phoneCode)) 拼在了上面,为什么只显示了国家名称:"🇬🇧 United Kingdom"?

代码可以编译。测试通过。没有警告。但功能在生产环境中却是坏的。

顺便说一下,什么是 DropdownSelectable?

DropdownSelectable 是我们 DesignSystem 模块中的一个协议,它使任何类型都能与我们的下拉 UI 组件配合使用:

    protocol DropdownSelectable {
        var id: String { get }           // 唯一标识符
        var displayValue: String { get } // 列表中显示的内容
    }

Part 1: extension 不起作用了

发现问题

经过调试后,我们发现了根本原因:Addresses 模块已经有一个类似的 extension

    // 在 Addresses 模块中
    extension Country: @retroactive DropdownSelectable {
        public var displayValue: String {
            emoji + "\t(englishName)"  // 没有电话区号
        }
    }
Step 1 Step 2
image4.png image5.png

Addresses 模块不需要电话区号,只需要国家名称。这对地址列表来说是合理的。

但关键是:Addresses extension 在运行时覆盖了我们 User extension。我们以为在使用 User 模块的extension(带电话区号),但 Swift 随机选择了 Addresses 的 extension(不带电话区号)。

这就是关键问题。

冲突:同时存在两个拓展协议

代码中发现的两处冲突的拓展协议:

在 User 模块中(我们以为在使用的):

    extension Country: @retroactive DropdownSelectable {
        public var id: String {
            code
        }
        public var displayValue: String {
            emoji + "\t(englishName) ((phoneCode))"  // ✅ 带电话区号
        }
    }

在 Addresses 模块中(实际被使用的):

    extension Country: @retroactive DropdownSelectable {
        public var id: String {
            code
        }
        public var displayValue: String {
            emoji + "\t(englishName)"  // ❌ 不带电话区号
        }
    }

两个模块都有各自合理的实现理由:

  • User 模块:电话号码输入界面需要电话区号
  • Addresses 模块:地址表单不需要电话区号,只需要国家名称

每个开发者都在实现需求时添加了他们需要的内容。代码编译没有警告,新需求测试通过,没人预料到会对旧的需求产生影响。

同时,确实 Swift 也是允许在不同模块中使用相同的 extension。那么到底发生了什么,我们又是如何解决的呢?

Part 2: 为什么会发生这种情况 - Swift 模块系统解析

要理解为什么这是一个问题,我们需要理解 Swift 的模块系统是如何工作的。有趣的是:通常情况下,在不同模块中有相同的 extension 是完全没问题的。但协议遵循是一个特殊情况。

正常情况:extension 在模块间通常工作良好

假设你为一个类型添加了一个辅助方法:

    // 在 UserModule 中
    extension Country {
        var displayValue: String {
            return emoji + "\t(englishName) ((phoneCode))"
        }
    }
    // 在 AddressesModule 中
    extension Country {
        var displayValue: String {
            return emoji + "\t(englishName)"
        }
    }

这完全可以!每个模块看到的是它自己的extension:

  • UserModule 中的代码调用 displayValue 会得到带 phoneCode 的结果
  • AddressesModule 中的代码调用 displayValue 会得到不带 phoneCode 的结果

为什么可以: 常规 extension 方法在编译时根据导入的模块来解析。Swift 根据当前模块的导入准确知道要调用哪个方法。

特殊情况:协议遵循是全局的

但协议遵循的工作方式不同。当你写:

    extension Country: DropdownSelectable {
        var displayValue: String { ... }
    }

你不只是在添加一个方法。你在做一个全局声明:"对于整个应用程序,Country 遵循 DropdownSelectable。"

所以当你创建两个相同的遵循时,会导致重复遵循错误

    // 在 UserModule 中
    extension Country: DropdownSelectable {
        var displayValue: String {
            return emoji + "\t(englishName) ((phoneCode))"
        }
    }
    // 在 AddressesModule 中
    extension Country: DropdownSelectable {
        var displayValue: String {
            return emoji + "\t(englishName)"
        }
    }

当你构建链接两个模块的应用时,Swift 编译器或链接器会报错,类似这样:

'Country' declares conformance to protocol 'DropdownSelectable' multiple times

Part 3: 引入 @retroactive 破坏了编译器检查

剩余问题:这怎么能编译通过?

基本上,如果我们遇到重复遵循错误,编译器会阻止我们。但是为什么这段代码可以正常存在?

一切问题都可以被归咎于 @retroactive

什么是 @retroactive?

在 Swift 6 中,Apple 引入了 @retroactive 关键字来让跨模块遵循变得明确:

    extension Country: @retroactive DropdownSelectable {
        // 让一个外部类型
        // 遵循一个外部协议
    }

你需要使用 @retroactive 当:

  • 类型定义在不同的模块中(例如,来自模块 A 的 Country
  • 协议定义在不同的模块中(例如,来自模块 B 的 DropdownSelectable
  • 你在第三个模块中添加遵循(例如,在 UserModuleAddressesModule 中)

为什么 @retroactive 会破坏编译器检查重复编译问题?

没有 @retroactive 的情况下,重复遵循已经是编译时错误。但有了 @retroactive,问题变得更加棘手 —— 因为现在你明确声明了影响整个应用运行时的东西,而不仅仅是你的模块。

当你写 @retroactive 时,你在说:

"我要为一个我不拥有的现有类型添加遵循,作用于整个 App。"

这意味着编译器允许你 追溯地/逆向地(retroactively) 为在其他地方定义的类型添加遵循。这很强大,但也改变了 Swift 检查重复的方式。

关键点:

Swift 在每个模块内强制执行重复遵循规则,但不跨模块。换句话说,编译器只检查它当前正在构建的代码。

  • 每个生产者模块(UserModule、AddressesModule)单独编译时是正常的(它只"看到"自己的遵循)。到目前为止是正常的。
  • 导入两者的消费者(至少你有一个,就是你的 app target!),会构建失败,因为它看到了两个相同的协议遵循

添加 @retroactive 之后:

使用 @retroactive,Swift 将一些检查推迟到链接时,所以两个模块都能成功编译,即使它们都在声明相同的全局遵循。

重复只有在链接之后才会变得可见,当两个模块都被加载到同一个运行时镜像中时 —— 而那时,编译器已经太晚无法阻止它了。

这就是为什么这些重复可以"逃过"编译器的安全检查,导致令人困惑的运行时级别的 bug。

运行时发生了什么

当链接器发现 (Country, DropdownSelectable) 有两个实现时:

  • 选项 A:UserModule 的实现(带电话区号)
  • 选项 B:AddressesModule 的实现(不带电话区号)

它只能注册一个。所以它根据链接顺序选择一个 —— 基本上是链接器首先处理的那个模块。另一个遵循会被静默忽略。

这解释了为什么 UserModule 的实现被忽略了。

Part 4: 解决方案 - 包装结构体来拯救

幸运的是我们有一个非常简单的修复方法:使用包装类型

解决方案模式

不要让 Country 本身遵循协议,而是包装它:

    // UserModule 示例
    struct CountryWithPhoneDropdown: DropdownSelectable {
        let country: Country
        var id: String { country.code }
        var displayValue: String {
            country.emoji + "\t(country.englishName) ((country.phoneCode))"
        }
    }
    // AddressModule 示例
    struct CountryAddressDropdown: DropdownSelectable {
        let country: Country

        var id: String { country.code }
        var displayValue: String {
            country.emoji + "\t(country.englishName)"
        }
    }
    // 使用方式
    countries.map { CountryWithPhoneDropdown(country: $0) }
    countries.map { CountryAddressDropdown(country: $0) }

Part 5: 预防 — 如何防止它再次发生

当然,如果想要不仅是修复这个问题,而是预防这个问题,那么可以通过在工作流程中添加静态分析CI 检查来轻松避免重复的 @retroactive 遵循。

这确保任何重复的 @retroactive 遵循在到达生产环境之前被发现,避免类似的运行时错误。

结语

这个 bug 根本不是简单的 UI 问题,想要彻底解决就需要深度理解 Swift 的运行机制。协议拓展可以跨模块重复,但协议遵循是全局的,@retroactive 叠加 Swift 的这种能力造成了这次的 bug。

一旦我们理解了这一点,修复就很简单了。

让弹幕飞一会儿!一个轻量级iOS弹幕库的实现与使用

🚀 让弹幕飞一会儿!一个轻量级iOS弹幕库的实现与使用

本文完整源码地址:github.com/chengshixin…

🎯 前言

"前方高能!"、"233333"、"awsl"... 这些熟悉的弹幕是不是让你想起了在B站追番的快乐时光?弹幕已经成为现代视频应用的标配功能,它不仅能够增强用户互动,还能创造独特的社区氛围。

今天,我要为大家介绍一个我自己开发的轻量级iOS弹幕库——BarrageView!这个库不仅功能丰富,而且代码优雅,绝对是你在iOS应用中集成弹幕功能的不二之选!

✨ 功能特色

🎮 四大弹幕方向

  • 从右到左:经典模式,B站同款
  • 从左到右:反向思维,别具一格
  • 从上到下:竖屏专属,瀑布流效果
  • 从下到上:逆流而上,视觉冲击

🔄 两种播放模式

  • 单次播放:适合展示重要信息
  • 循环播放:营造持续的热闹氛围

🎨 全面自定义

  • 字体大小、颜色随心配
  • 背景透明度自由调节
  • 移动速度精准控制
  • 弹幕间距智能避让

🛠 快速上手

基本使用

// 创建弹幕视图
let barrageView = BarrageView(frame: CGRect(x: 0, y: 100, width: view.bounds.width, height: 200))

// 设置弹幕数据
barrageView.setBarrageData([
    "前方高能预警!",
    "这个功能太棒了!",
    "iOS开发者福音",
    "已star,感谢作者"
])

// 开始播放
barrageView.startBarrage()

高级配置

// 自定义样式和效果
barrageView.setDirection(.rightToLeft)
barrageView.setPlayMode(.loop)
barrageView.setSpeed(80.0)
barrageView.setFontSize(18.0)
barrageView.setTextColor(.red)
barrageView.setTextBackgroundColor(UIColor.black.withAlphaComponent(0.8))

🔧 核心实现原理

🎪 弹幕调度机制

BarrageView采用定时器+动画的双重调度机制:

// 定时器负责生成新弹幕
timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
    self?.createBarrageLabel()
}

// UIView动画负责移动效果
UIView.animate(withDuration: duration, delay: 0, options: .curveLinear) {
    label.frame.origin = endPoint
}

🚦 智能避让算法

为了防止弹幕重叠,我们实现了智能的位置检测:

private func isYPositionOccupied(y: CGFloat, labelHeight: CGFloat) -> Bool {
    for label in activeLabels {
        let labelMinY = label.frame.minY - 10  // 预留安全间距
        let labelMaxY = label.frame.maxY + 10
        
        if y > labelMinY && y < labelMaxY {
            return true  // 位置被占用
        }
    }
    return false  // 位置可用
}

⏸️ 流畅的暂停恢复

通过CALayer扩展实现精准的动画控制:

extension CALayer {
    func pauseAnimation() {
        let pausedTime = convertTime(CACurrentMediaTime(), from: nil)
        speed = 0.0
        timeOffset = pausedTime
    }
    
    func resumeAnimation() {
        let pausedTime = timeOffset
        speed = 1.0
        timeOffset = 0.0
        beginTime = 0.0
        let timeSincePause = convertTime(CACurrentMediaTime(), from: nil) - pausedTime
        beginTime = timeSincePause
    }
}

📱 实际应用场景

🎥 视频播放器

// 在视频播放时启动弹幕
func videoDidStartPlaying() {
    barrageView.startBarrage()
}

func videoDidPause() {
    barrageView.pauseBarrage()
}

func videoDidResume() {
    barrageView.resumeBarrage()
}

🎮 直播互动

// 收到新消息时实时添加弹幕
func didReceiveNewMessage(_ message: String) {
    var currentData = barrageView.barrageTexts
    currentData.append(message)
    barrageView.setBarrageData(currentData)
}

🎉 活动庆典

// 节日特效弹幕
func setupFestivalBarrage() {
    barrageView.setTextColor(.red)
    barrageView.setFontSize(20)
    barrageView.setBarrageData(festivalWishes)
}

🚀 性能优化技巧

内存管理

  • 使用weak self避免循环引用
  • 及时移除完成动画的标签
  • 合理控制同时显示的弹幕数量

动画优化

  • 使用curveLinear保证匀速运动
  • 避免频繁创建销毁对象
  • 复用UILabel减少内存分配

🔮 未来规划

  • 支持富文本弹幕
  • 添加弹幕点击事件
  • 实现3D弹幕效果
  • 支持弹幕轨道管理
  • 添加弹幕过滤机制

💫 结语

BarrageView不仅仅是一个工具库,更是我对iOS动画和用户体验的一次深度探索。通过这个项目,我学习到了:

  • 🎯 精准的动画控制:如何让弹幕平滑移动又不失性能
  • 🧠 智能的布局算法:如何避免弹幕间的"交通事故"
  • 🎨 优雅的代码设计:如何构建可扩展、易维护的架构

如果你对这个项目感兴趣,欢迎:

Stargithub.com/chengshixin…

🐛 提交Issue → 反馈bug或提出新功能建议

🔀 Pull Request → 一起让这个项目变得更好


让我们的应用也拥有B站一样的弹幕文化吧! 🎊

"弹幕虽小,却能承载万千情感;代码虽简,却能创造无限可能。"

本文由BarrageView作者撰写,转载请注明出处。

Apple更新App审核条款,严打擅自与第三方 AI 共享个人数据的应用

App审核条款变更

最近iOS开发者该都收到了Apple发来了更新审核条款的邮件,原文内容如下:

  • 1.2.1(a): This new guideline specifies that creator apps must provide a way for users to identify content that exceeds the app’s age rating, and use an age restriction mechanism based on verified or declared age to limit access by underage users.
  • 2.5.10: This language has been deleted ("Apps should not be submitted with empty ad banners or test advertisements.”).
  • 3.2.2(ix): Clarified that loan apps may not charge a maximum APR higher than 36%, including costs and fees, and may not require repayment in full in 60 days or less.
  • 4.1(c): This new guideline specifies that you cannot use another developer's icon, brand, or product name in your app's icon or name, without approval from the developer.
  • 4.7: Clarifies that HTML5 and JavaScript mini apps and mini games are in scope of the guideline.
  • 4.7.2: Clarifies that apps offering software not embedded in the binary may not extend or expose native platform APIs or technologies to the software without prior permission from Apple.
  • 4.7.5: Clarifies that apps offering software not embedded in the binary must provide a way for users to identify content that exceeds the app’s age rating, and use an age restriction mechanism based on verified or declared age to limit access by underage users.
  • 5.1.1(ix): Adds crypto exchanges to the list of apps that provide services in highly regulated fields.
  • 5.1.2(i): Clarifies that you must clearly disclose where personal data will be shared with third parties, including with third-party AI, and obtain explicit permission before doing so.

翻译过来就是:

  • 1.2.1(a): 这项新的指南明确规定,创作者应用(creator apps)必须提供一种方式,让用户能够识别超出应用年龄分级的内容,并使用基于验证或声明年龄的年龄限制机制来限制未成年用户访问这些内容。

  • 2.5.10: 此措辞已被删除(原措辞为:“应用不应提交带有空白广告横幅或测试广告。”)。

  • 3.2.2(ix): 澄清了贷款应用收取的最高年利率 (Maximum APR) 不得高于 36% (包括所有成本和费用),并且不得要求在 60 天或更短时间内全额偿还贷款。

  • 4.1(c): 这项新的指南明确规定,未经该开发者批准,您不得在您的应用图标或名称中使用其他开发者的图标、品牌或产品名称

  • 4.7: 澄清了 HTML5 和 JavaScript 小程序(mini apps)和迷你游戏(mini games) 属于该指南的管辖范围。

  • 4.7.2: 澄清了提供未嵌入二进制文件的软件的应用,未经 Apple 事先许可,不得向该软件扩展或暴露原生平台 API 或技术。

  • 4.7.5: 澄清了提供未嵌入二进制文件的软件的应用,必须提供一种方式,让用户能够识别超出应用年龄分级的内容,并使用基于验证或声明年龄的年龄限制机制来限制未成年用户访问这些内容。

  • 5.1.1(ix):加密货币交易所 (crypto exchanges) 加入到提供高度监管服务的应用列表。

  • 5.1.2(i): 澄清了您必须清楚地披露个人数据将与第三方(包括第三方 AI)共享的位置,并在共享之前获得明确的许可

值得一提的是,Apple新规首次明确要求,各类应用若要把用户个人数据提供给第三方 AI,必须事先公开说明并获得用户授权

苹果公司目前正着手调整其 App Store 政策,此举被视为对即将到来的 AI 时代,尤其是对 2026 年即将发布的全新、更智能的 Siri 的战略性准备。

据传闻,下一代 Siri 将具备更强大的跨应用语音操作能力,其部分核心技术据悉将由 Google 的 Gemini 模型提供支持。

政策调整背后的考量

苹果在此时更新开发者指南,主要目标之一是加强对用户隐私的保护,特别是要防止应用程序在用户不知情或未经同意的情况下,将个人数据传输给 AI 服务提供商或其他相关公司。

这次政策修改的关键意义不在于引入了全新的数据保护概念,而在于苹果首次将 AI 相关的企业和技术明确纳入了既有的监管框架

具体变化和影响

原有的审核规则 5.1.2 (i) 已经要求开发者在分享用户数据前必须透明披露并获得用户许可,并禁止未经允许地“使用、传输或分享”个人信息。这一规定是苹果为遵守如欧盟 GDPR、加州 CCPA 等全球隐私法规的重要举措,旨在确保用户对个人数据拥有控制权。违规应用将面临被下架的风险。

新版本在这一要求的基础上加入了更具针对性的明确措辞:开发者必须清楚说明个人数据会被提供给哪些第三方——包括第三方 AI,并且在数据共享操作发生前,必须获取用户的明确授权

这一变化预计将对那些依赖 AI 技术来收集、处理用户数据以提供个性化服务或特定功能的应用程序产生影响。然而,由于“AI”是一个广阔的范畴,既包含大型语言模型(LLM),也涵盖各种机器学习技术,目前尚不清楚苹果将以何种程度和力度去执行这一新要求。

深入 iMessage 底层:一个 Agent 是如何诞生的

iMessage 深度集成在 Apple 生态中,却从未提供官方 API。本文邀请 imessage-kit 作者 LingJueYa 分享如何突破这一限制,让 AI Agent 进入 iMessage。文章详细介绍了从解析 SQLite 数据库、处理 Core Data 时间戳、绕过 macOS 沙盒限制,到用 AppleScript 实现消息发送的完整技术方案,以及在构建过程中踩过的坑与解决之道。

Homebrew 5.0:并行加速、MCP 加持,与 Intel 的最后倒计时 -- 肘子的 Swift 周报 #0111

issue111.webp

🚀 《肘子的 Swift 周报》

每周为你精选最值得关注的 Swift、SwiftUI 技术动态

Homebrew 5.0:并行加速、MCP 加持,与 Intel 的最后倒计时

几天前,我像往常一样在输入 brew update 后顺手执行了 brew upgrade。出乎意料的是,终端里突然出现了从未见过的画面——大量组件与工具并行下载、整齐排列、同时推进。短暂的惊讶之后,我才从新闻中得知:Homebrew 已经发布了 5.0 版本

此次更新内容相当丰富。除了默认启用并行下载外,还正式将 Raspberry Pi、ARM 迷你 PC、Windows ARM 上的 WSL2 等 ARM64/AArch64 设备纳入 Tier 1 支持,并新增多项指令与能力。其中,官方提供的本地 MCP 服务尤为引人注目。通过 brew mcp-server,开发者可以让 AI Agent 自动操作 Homebrew,意味着 brew 也顺利接入了正在兴起的 AI 工作流。这是一项颇具时代感的更新。

不过,并非所有消息都同样令人愉快。随着 macOS Tahoe 26 大概率成为最后一个支持 Intel x86_64 的版本,Homebrew 也相应调整了自身的支持策略:从 2026 年 9 月起,Intel Mac 将被降级为 Tier 3;到 2027 年 9 月(或更晚),对 Intel 的支持则可能完全终止。

不可否认,在过去十余年里,Intel 架构为苹果带来了庞大的“潜在用户”:它能原生运行 Windows,让许多本不属于 Mac 生态的用户因兼容性而选择苹果设备,为苹果的市场份额提供了关键支撑。如今,随着 Apple Silicon 的成熟,Intel Mac 注定会与 MOS 6502、PowerPC 等一同成为苹果硬件发展史上的重要篇章,在不久的将来缓缓落幕。

回顾历史,每次 CPU 架构转换都激发了苹果在产品设计上的创新灵感,催生出许多具有时代印记的经典产品——从 68k 时代的 Lisa 开创图形界面先河,到 PowerPC 时代的 iMac G3 半透明美学和彩色贝壳本,再到 Intel 时代的小白/小黑 MacBook 以及重新定义轻薄的 MacBook Air。在 M 系列芯片时代,Apple 在性能、能效与系统集成上实现了跨越式提升,但在硬件外观与工业设计语言上,尚未出现能够留下强烈时代烙印的革新之作。

期待尽早看到可以计入史册的新设计,别让我们等太久。

本期内容 | 前一期内容 | 全部周报列表

近期推荐

Grow on iOS 26:UIKit + SwiftUI 混合架构下的 Liquid Glass 适配实战

Grow 是一款在 173 个国家和地区获得 App Store 编辑推荐、拥有超过 18 万五星评价的健康管理应用。在适配 iOS 26 的 Liquid Glass 设计语言时,团队遇到了不少挑战:如何在 UIKit + SwiftUI 混合架构下实现原生的 morph 效果?如何精确控制 Scroll Edge Effect?如何处理自定义导航栏元素的动态尺寸?Grow 的开发者之一 Shuhari,分享了团队在这次适配过程中的实战经验。文章涵盖 Sheet、Navigation、Popover 等场景的改造方案,深入探讨 UIBarButtonItem 尺寸计算、CABackdropLayer 副作用处理等底层细节,还展示了如何利用 Core Text 创造“玻璃文字”效果。所有核心概念都配有完整的 Demo 工程


警惕参数化测试中的陷阱 (Pitfalls of Parameterized Tests)

参数化测试 (Parameterized Tests)是 Swift Testing 中颇具代表性的新特性,它让开发者能够在最小化重复代码的同时扩大测试覆盖范围,为同一逻辑轻松验证多组输入。然而 Alex Ozun 在大规模迁移实践中发现,这项功能虽然便捷,却也暗藏不少容易忽略的陷阱,甚至可能悄悄降低测试的有效性。文章结合多个示例展示了一些常见陷阱,并提出了如避免在 #expect 两侧重复使用测试参数、明确区分示例测试与属性测试等多项实践建议。


为任务显式指定“身份” (Task Identity)

在 SwiftUI 中,task / onAppear 会在视图“出现”时执行一次,但它们并不会像视图那样自动跟踪依赖——如果任务闭包依赖了某个状态,该状态变化后任务本身不会自动重新触发。Chris Eidhof 以加载远程图片为例,展示了这一容易被忽略的问题,并建议为任务显式指定“身份”(identity),例如使用 .task(id: url),让相关依赖(如 URL 或由多个值组合而成的复合标识)参与任务的重新执行条件,使 SwiftUI 能在依赖更新时取消旧任务并启动新任务。作者提醒,凡是在视图中使用 task / onAppear 时,都应确保相关的依赖已经体现在任务的身份(identity)中。


Objective-C API 引发的 Unicode 错误 (One Swift mistake everyone should stop making today)

Swift 已经诞生十年了,但在日常开发中开发者使用的很多 Swift API 仍只是对 Objective-C API 的简单包装,这可能会引发一些容易忽视的严重问题。Paul Hudson 在本文中就通过 replacingOccurrences(of:with:) 展示了这种情况:在处理由多个 Unicode 标量组成的字符(如国旗表情)时,该方法可能会“误拆”字符、匹配不存在的序列,从而生成完全错误的结果。Paul 的建议非常简单:在 Swift 中应优先使用原生的 replacing(_:with:),它能够正确地按字符语义处理 Unicode,避免这些诡异且难以排查的字符串错误。

随着 Foundation 在 Swift 社区重构完成,在 macOS 等平台上,对于具备类似功能的 API,通常应优先选择新 Foundation 中提供的 Swift 原生版本。这样不仅可以避免上述问题,而且也提前为跨平台做好准备。


和 Christian 一起学习 Swift 并发 (Learning About Swift Concurrency (from Matt Massicotte’s Blog) with a Zettelkasten)

Swift 的并发演进并非一帆风顺,引入 Approachable Concurrency 概念后,不同编译选项组合甚至可能得到完全不同的编译结果,理解成本也随之水涨船高。Christian Tietze 原本只打算做一个简短演示:展示如何使用卡片盒笔记法(Zettelkasten)来消化 Matt Massicotte 关于 Swift 并发的博客文章,结果在实作过程中不断撞见更深层的复杂性——例如:actor 无法直接满足带有 nonisolated 要求的 Sendable 协议,除非显式将成员标记为 nonisolatednonisolated(unsafe)。等他回过神来,视频已经录到了 80 分钟。

视频很好地呈现了“深入学技术”的真实面貌:不是线性的知识堆叠,而是充满困惑、假设以及有待日后用代码与文档验证的开放问题。同时也侧面证明,卡片盒笔记法非常适合应对 Swift 并发这类复杂且持续演进的主题,通过构建可搜索、可链接的笔记网络,承载理解在时间维度上的逐步收敛。


Claude Code Skills 功能介绍以及使用经验

Ein Verne 在本文中介绍了 Claude 新推出的 Skills 机制 —— 一种用于扩展 Claude 能力的模块化体系。相比 MCP、Slash Commands 和传统插件,Skills 更强调可组合性、可移植性以及对上下文窗口的友好使用方式。每个 Skill 都以独立文件夹的形式存在,包含名称、描述、操作指令(SKILL.md)、可执行脚本、参考文档与资源文件等。Claude 会在执行任务时自动扫描并匹配合适的技能,并通过“渐进式披露(Progressive Disclosure)”按需加载细节,从而显著降低上下文消耗。作者认为,Skills 本质上将“提示词工程”演进为“工作流工程”,让 Claude 从通用智能助手进一步迈向可维护的智能基础设施形态。


在 iOS 中集成 Rust:基于 UniFFI 的多平台工作流 (Multiplatform with Rust on iOS)

就像许多 Swift 开发者希望把代码带出苹果生态一样,iOS 本身也对其他开发语言保持着相当开放的态度。Tjeerd in 't Veen 在这篇文章中分享了一份详实的 Rust + iOS 集成指南,展示如何通过 Mozilla 的 UniFFI 将 Rust 代码优雅地接入到 iOS 项目中。UniFFI 能将 Rust 的 enum 自动映射为 Swift enum,并把函数名从 snake_case 转为 camelCase,让 Rust 模块在 Swift 侧看起来就像原生 API。

文章给出了一整套可落地的工作流:从创建 Rust 库、为多种 iOS 架构构建静态库、打包 XCFramework,到最终封装成 Swift Package,每一步都有详细说明与常见陷阱提示。这套方案不仅让 iOS 工程可以像使用普通 Swift 包一样消费 Rust 逻辑,也为后续在 Android 等平台复用同一份 Rust 代码打下了良好基础。

工具

VisualDiffer 2:从 Objective-C 到 Swift 的重生

Davide Ficano 将其经营多年的 macOS 文件对比工具 VisualDiffer 完全开源,并从 Objective-C 彻底重写为 Swift。这不是简单的语言迁移或 AI 辅助转换,而是一次从零开始的手工重构。

核心功能保持不变:

  • 🟩 直观对比 - 并排展示目录差异,用颜色标识新增、修改或缺失的文件
  • 🧩 深入分析 - 支持文件级别的逐行对比(基于 UNIX diff)
  • 🧹 智能过滤 - 自动排除版本控制文件(.git、.svn)和系统文件(.DS_Store)
  • 性能优化 - 支持多种对比策略,从快速的日期/大小对比到精确的逐字节对比

Reddit 上,作者坦言自己依旧非常欣赏 Objective-C,但 Swift 的潜力让他愿意承受迁移的巨大成本。UI 层(特别是 NSTableView 与 delegate 模式)的重写过程尤为艰难,早期充满了并发属性标注,但随着理解加深,Swift 的优势逐渐显现。


FSWatcher:高性能的 Swift 原生文件系统监控库

十里 在开发图片压缩工具 Zipic 时,需要实时感知图片文件变化以便进行及时处理,为此开发了 FSWatcher。这是一个基于 macOS/iOS 底层 kqueue 机制的文件系统监控库,采用事件驱动而非轮询方式,资源消耗极低。

核心特性:

  • 🎯 智能过滤:支持按文件类型、大小、修改时间等多维度过滤,并可链式组合
  • 🔍 预测性忽略:自动识别并跳过自身生成的输出文件(如 *_compressed.jpg),避免循环触发
  • 📁 递归监控:可监控整棵目录树,支持深度限制与排除规则
  • 现代 API:完整支持 Combine、async/await 以及传统闭包回调模式

该库非常适合作为图片处理流程的监听器、开发工具的热重载组件,或构建轻量化自动备份系统等需要实时文件变动感知的场景。


SFSymbolKit:零维护的类型安全 SF Symbols 库

市面上已有不少用于改进 SF Symbols 使用体验的库,但 LiYanan 的 SFSymbolKit 仍然颇具特色:所有符号与可用性信息都由工具直接从系统框架自动生成,一键即可完成更新,真正做到无需人工维护。

核心优势:

  • 数据源可靠:直接读取 /System/Library/PrivateFrameworks/SFSymbols.framework/,与系统 100% 同步
  • 完全自动化:运行 ./update_symbols.sh 即可更新,无需手动添加新符号
  • 版本感知:自动生成 @available 属性,编译时检查符号兼容性
  • 用户自助:任何人都可以在本地更新,不依赖作者发版

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

Swift 6 迁移常见 crash: _dispatch_assert_queue_fail

我的 Github:github.com/RickeyBoy/R…

大量 iOS 内容欢迎大家关注~

最近在将公司项目迁移到 Swift 6 的过程中,解决了好几个相似的 crash。关键字如下

    _dispatch_assert_queue_fail
    
    "%sBlock was %sexpected to execute on queue [%s (%p)]
    
    Task 208: EXC_BREAKPOINT (code=1, subcode=0x103546f18)

在这里记录和分享,希望遇到相似的问题之后能够更快的解决。

Crash 1 信息

image1.png

image2.png

原因与解决 1

首先根据 crash 的提示,可以清楚地知道:Block 预期在一个线程上执行,而实际在另一个线程上执行。第一反应:大概率是主线程 vs 后台线程之间的不一致导致的。

如果经常熟练处理 Swift 6 升级的小伙伴就知道,一定是有地方标记了 @MainActor,也就意味着相对应的方法一定是要在主线程上执行的;而实际阴差阳错,在后台线程执行了。

所以接下来可能需要找,到底哪里线程不一致呢?我们根据代码来寻找即可。

image3.png

不难找到,根据 Combine 调用链,可以发现其中一处对于 userPublisher 的监听时,选择了在 global 上去执行后面的操作,所以这里需要将这一个逻辑去掉。

于此同时,对于 userPublisher 的发布,我们也最好将其默认放在主线程上,因为他是和 UI 相关的,所以需要做这样的改动:

image4.png

坑点

目前是不是觉得好像这个类型的 crash 不算很难解决?没错,这个 crash 的提示相对清楚,知道大概原因后去找就相对容易了。

不过需要注意的是,当使用 Combine 框架遇上这类型的 crash 时,crash 断点只会发生在 Publisher 而不是 Observer 处,所以我们需要自己去寻找调用方,看下在哪里出现了线程使用不一致的问题。

Crash 2 信息

好的,那么同类型的一个 crash 再来看看:

image5.png

报错信息就不贴了,和上一个 crash 是一样的,都是:"%sBlock was %sexpected to execute on queue [%s (%p)]

这里可以看到,crash 断点处在子线程,也是在 AsyncStream 发布处断点。那么根据经验推断,可以大概知道原因:

  1. 此处发布的时候,处在子线程
  2. 下游调用方,一定有某个地方要求在主线程
  3. 实际线程与要求线程不一致,所以导致 crash

原因与解决 2

这里寻找过程就不赘述了。原来的发布者是处在子线程,而后面的监听者处在主线程,因此需要改在主线程发布。

image6.png

Swift 一个小型游戏对象模型渐进式设计(五)——Swift 并发世界:把 Attackable 搬进 actor

为什么“并发”突然成了刚需

真实场景里:

  • 游戏服务器:32 条网络线程并发处理玩家技能;
  • 客户端:主线程发动画,后台线程算伤害,Timer 触发 dot;
  • 单机多核:SceneKit 物理回调、Vision 识别、Swift Concurrency Task 同时读写同一 BOSS 的血量。

如果还用传统锁:

objc_sync_enter(self)
hp -= damage
objc_sync_exit(self)

轻则性能抖动,重则死锁;而 Swift 5.5 起的 Actor 模型 把“互斥”升级为消息队列,编译期即可检查“跨 actor 引用是否安全”,让“数据竞争”成为编译错误。

Actor 101:30 秒速览

  1. 定义
actor Boss {
    var hp: Double = 100
    func takeDamage(_ amount: Double) {
        hp = max(0, hp - amount)
    }
}
  1. 调用规则
  • 内部:同步函数,直接访问 hp
  • 外部:必须通过 await 异步消息,编译器自动加队列。
let boss = Boss()
await boss.takeDamage(10)   // 编译通过
boss.hp                     // ❌ 编译错误:actor-isolated
  1. 关键保证

Actor 隔离域(isolation domain):同一时间只有一条消息在执行,天然“可线性化”(Serializability)。

把协议能力搬进 actor

目标:

  • 不破坏前两篇的泛型协议架构;
  • 让任何实体既能以“值语义”跑在单线程,也能以“ actor 引用”跑在多线程;
  • 客户端/服务器共用同一套算法。
  1. 定义并发版协议
/// 可并发受伤
protocol ConcurrentWoundable: AnyObject {
    associatedtype Value: NumericValue
    func takeDamage(_ amount: Value) async
    var currentHp: Value { get }
}

注意:

  • AnyObject 限制只让 class/actor 符合,因为需要共享引用;
  • 方法标记 async,调用方必须 await
  1. 让 actor 直接符合
actor ConcurrentBoss<Value: NumericValue>: ConcurrentWoundable {
    private(set) var hp: Value
    let maxHp: Value
    
    init(hp: Value, maxHp: Value) {
        self.hp = hp; self.maxHp = maxHp
    }
    
    func takeDamage(_ amount: Value) async {
        hp = max(Value(0), hp - amount)
    }
    
    nonisolated var currentHp: Value { hp }   // 只读快照,无需 await
}

nonisolated 关键字:编译器允许外部同步读取,但不能写。

  1. 并发安全暴击算法

把上篇的 DamageCalculator 泛型算法保持值语义,计算过程无锁;只有最后 takeDamage 进 actor 才排队。

let calc = AnyDamageCalculator(Double.self) { base in base * 1.5 }
let damage = calc.calculate(base: 50)          // 无锁计算
await boss.takeDamage(damage)                  // 一次消息

分离“计算”与“状态变更”:计算无锁、变更串行,兼顾性能与安全。

分布式 Actor:跨进程也能 “await boss.takeDamage”

Swift 5.9 起引入 distributed actor,同一语法即可跨进程/跨机器:

distributed actor RemoteBoss: ConcurrentWoundable {
    distributed func takeDamage(_ amount: Value) async {
        hp = max(Value(0), hp - amount)
    }
}

调用方:

let boss = try await RemoteBoss.resolve(id: bossID, using: .init())
await boss.takeDamage(30)

底层由 Swift gRPC 传输消息,开发者零成本获得分布式对象模型。

实战:并发 Boss 战模拟器

场景:

  • 4 个玩家并发放技能,伤害随机;
  • 1 个后台线程每 0.5 s 触发 dot;
  • 1 个渲染线程每帧读血量更新 UI;

代码:

protocol NumericValue: Comparable & Sendable {
    static func + (lhs: Self, rhs: Self) -> Self
    static func - (lhs: Self, rhs: Self) -> Self
    static func * (lhs: Self, rhs: Self) -> Self
    static func / (lhs: Self, rhs: Self) -> Self
    static func > (lhs: Self, rhs: Self) -> Bool   // 与标量乘
    init(_ value: Int)                               // 能从整数字面量初始化
}
extension Double: NumericValue {}

/// 可并发受伤
protocol ConcurrentWoundable: AnyObject {
    associatedtype Value: NumericValue
    func takeDamage(_ amount: Value) async
    var currentHp: Value { get }
}

// 1. 并发 BOSS
actor BossBattle: @preconcurrency ConcurrentWoundable {
    private(set) var hp: Double
    let maxHp: Double
    init(hp: Double) {
        self.hp = hp;
        self.maxHp = hp
    }
    
    func takeDamage(_ amount: Double) async {
        hp = max(0, hp - amount)
        if hp == 0 { print("BOSS 被击败!") }
    }
    
    var currentHp: Double { hp }
}

// 2. 玩家技能
func playerTask(id: Int, boss: BossBattle) async {
    for _ in 0..<5 {
        let damage = Double.random(in: 5...15)
        await boss.takeDamage(damage)
        print("Player\(id) 造成 \(damage)")
        try? await Task.sleep(for: .milliseconds(.random(in: 100...300)))
    }
}

// 3. dot 后台
func dotTask(boss: BossBattle) async {
    while await boss.currentHp > 0 {
        await boss.takeDamage(3)
        print("dot 3 点")
        try? await Task.sleep(for: .milliseconds(500))
    }
}

// 4. 渲染线程(只读)
func renderTask(boss: BossBattle) async {
    while await boss.currentHp > 0 {
        let hp = await boss.currentHp
        print("UI 血量:\(Int(hp))")
        try? await Task.sleep(for: .seconds(1/60))
    }
}

// 5. 启动

Task {
    let boss = BossBattle(hp: 100)
    let _ = await withDiscardingTaskGroup { group in
        for i in 1...4 {
            group.addTask {
                await playerTask(id: i, boss: boss)
            }
        }
        
        group.addTask {
            await dotTask(boss: boss)
        }
        
        group.addTask {
            await renderTask(boss: boss)
        }
    }
}

运行结果(节选):

Player3 造成 11.0
Player1 造成 8.0
dot 3 点
UI 血量:78
...
BOSS 被击败!

全程无需手动加锁,编译器保证任何时刻只有一条消息在修改 hp

与 SwiftUI 无缝衔接

@MainActor
final class BossModel: ObservableObject {
    private let boss = BossBattle(hp: 100)
    
    @Published private(set) var hpText = ""
    
    func start() async {
        await renderLoop()
    }
    
    @MainActor
    private func renderLoop() async {
        while await boss.currentHp > 0 {
            hpText = "血量 \(Int(await boss.currentHp))"
            try? await Task.sleep(for: .seconds(1))
        }
        hpText = "BOSS 被击败"
    }
    
    func attack() async {
        await boss.takeDamage(Double.random(in: 10...20))
    }
}

@MainActor 保证所有 SwiftUI 状态更新跑在主线程;业务逻辑在后台 actor 串行执行,零数据竞争。

常见坑与最佳实践

  1. 在 actor 里访问全局可变状态

    同样要 await,否则编译报错。

  2. nonisolated 只能读,不能写;写必须走消息。

  3. 不要把长时间阻塞代码(sleep、sync I/O)直接放进 actor,会卡住消息队列;应拆到 Task.detachedAsyncSequence

  4. 跨 actor 调用时,值类型会被拷贝,不要传递大型数组;可改用 AsyncSequence 流式输出。

  5. 分布式 actor 的方法参数/返回值必须遵循 Codable,否则无法序列化

Swift 一个小型游戏对象模型渐进式设计(四)——类型擦除与 Existential:当泛型遇见动态派发

为什么“泛型”还不够

上一篇我们写出了这样的代码:

let calc: any DamageCalculator<Double> = CritCalculator(rate: 1.5)

它编译得快、跑得也快,但当你想把它存进数组、或者作为属性逃逸到运行时,就会遇到三个灵魂问题:

  1. 编译器不知道具体类型有多大,如何分配内存?
  2. 协议里有 associatedtype,为什么不能用 DamageCalculator 直接当做类型?
  3. 同样一句 calculate(base:),为什么有时走内联、有时走虚表?

答案都指向同一个机制:Existential Container(存在性容器),社区俗称“类型擦除盒”。

Existential 是什么

Swift 把“符合某个协议的值”打包成一种统一大小的盒子,这个盒子就叫 existential。

语法层面:

  • any Protocol // Swift 5.6+ 显式 existential
  • 老代码里的 Protocol // 隐式 existential,即将被逐步废弃

盒子内部到底长什么样?继续看。

Existential Container 的内存布局

以 64 bit 为例,标准布局 5 个字(40 byte):

+-------- 0:  value buffer (3 ptr = 24 byte)  
+--------24:  value witness table (VWT)  
+--------32:  protocol witness table (PWT)  
  1. value buffer

    • 小值(Int、Double、CGPoint…)直接内联;
    • 大值(String、Array、自定义 class)堆分配,buffer 存指针;
  2. VWT

    管理“值语义”生命周期:拷贝、销毁、搬移。

  3. PWT

    管理“协议方法”派发地址,相当于 C++ 的 vtable。

结论:哪怕只是一个 Double,装进 any NumericValue 后也会膨胀到 40 字节;如果频繁在数组里拷贝,就会带来隐式堆分配和缓存抖动。

关联类型协议的“额外”盒子

当协议带 associatedtype 时,existential 还需要一份通用签名(generic environment),用于在运行时保存类型元数据。

因此:

let x: any Attackable        // ❌ 编译错误:associatedtype Value 未定
let y: any Attackable<Int>   // ✅ Swift 5.9 新语法:parameterized existential

后者内部比“无关联类型”再多 8 byte,总计 48 byte。

苹果在 WWDC23 给出的性能警告:< 3 个 witness 方法且 value ≤ 24 byte 时,existential 才基本无额外开销;否则请考虑“手写类型擦除”或“泛型特化”。

实战:手写 AnyDamageCalculator

目标:

  • 对外暴露固定大小(无动态盒子);
  • 对内保存任意具体计算器;
  • 仍保持 Value 泛型参数。
  1. 定义抽象基类(引用语义)
class AnyDamageCalculatorBox<Value: NumericValue>: DamageCalculator {
    func calculate(base: Value) -> Value { fatalError("abstract") }
}
  1. 定义具体盒子(泛型类)
final class ConcreteBox<T: DamageCalculator>: AnyDamageCalculatorBox<T.Value> {
    private let concrete: T
    init(_ concrete: T) { self.concrete = concrete }
    override func calculate(base: Value) -> Value {
        concrete.calculate(base: base)
    }
}
  1. 定义值包装(对外类型)
struct AnyDamageCalculator<Value: NumericValue>: DamageCalculator {
    private let box: AnyDamageCalculatorBox<Value>
    
    init<C: DamageCalculator>(_ concrete: C) where C.Value == Value {
        self.box = ConcreteBox(concrete)
    }
    
    func calculate(base: Value) -> Value {
        box.calculate(base: base)
    }
}
  1. 使用:
let crit = CritCalculator(rate: 1.5)
let erased: AnyDamageCalculator<Double> = AnyDamageCalculator(crit)
array.append(erased)   // 数组元素大小 = 1 ptr,无 existential 盒子
  • 内存大小:8 byte(一个 class 指针);
  • 拷贝成本:一次 ARC retain;
  • 方法派发:虚表一次,但不再额外带 VWT/PWT。

Swift 5.9 新武器:Parameterized Existential

let list: [any DamageCalculator<Double>] = [
    CritCalculator(rate: 1.5),
    MultiplierCalculator(upstream: CritCalculator(rate: 2), multiplier: 1.2)
]

编译器会自动生成“隐藏盒子”,但仍带 48 byte 拷贝成本。

适合场景:

  • 原型阶段、快速迭代;
  • 对性能不敏感的工具代码;

高性能路径(渲染、音频、网络解析)继续用手写擦除或泛型特化。

类型擦除的通用套路(模板)

任何带 associatedtype 的协议,都可以套下面 4 步:

  1. 创建 AnyProtocolBase<AssociatedType> 抽象类;
  2. 创建 ConcreteBox<T: Protocol> 具体类,持有 T
  3. 创建 AnyProtocol<AssociatedType> 值类型,内部存 AnyProtocolBase 指针;
  4. 对外 API 全部 override / forward 到抽象类。

什么时候用哪种形态?

需求 \ 方案        泛型特化   any Protocol   手写擦除
------------------------------------------------------------
编译期已知类型       ✅          ❌             ❌
需要进数组/逃逸      ❌          ✅             ✅
对性能极度敏感       ✅          ❌             ✅
不想写样板代码       ✅          ✅             ❌(可用宏)

一句话:编译期能定类型就用泛型;运行时再决定就用擦除;原型阶段先 any 再说。

Swift 一个小型游戏对象模型渐进式设计(三)——把能力再抽象一层,写一套“伤害计算器”框架

为什么要“再抽象一层”

上两篇我们已经用协议把“攻击”拆成了能力插件,但遗留了一个硬核问题:

  • 游戏前期用 Int 足够,后期为了避免除法误差想换成 Double,甚至金融级精度要用 Decimal
  • 如果给每种数值类型都复制一份协议,就会出现 AttackableIntAttackableDouble…爆炸式增长。

Swift 的泛型(Generic)+ 关联类型(associatedtype)可以“一次性”写出算法,然后让编译器在调用点自动生成对应版本的代码,既保证类型安全,又保持运行时零成本。

把 Attackable 升级成泛型协议

  1. 定义“数值”契约

先约定一个“可运算、可比较”的基本协议,把 +*/> 等运算符包进去:

protocol NumericValue: Comparable {
    static func + (lhs: Self, rhs: Self) -> Self
    static func * (lhs: Self, rhs: Self) -> Self
    static func / (lhs: Self, rhs: Self) -> Self
    static func > (lhs: Self, rhs: Self) -> Bool   // 与标量乘
    init(_ value: Int)                               // 能从整数字面量初始化
}
  1. 让标准库类型自动符合

Swift 5.7 之后可以用 extension 给标准库类型“批量”实现:

extension Int: NumericValue {}
extension Double: NumericValue {}
extension Decimal: NumericValue {
    static func *(lhs: Decimal, rhs: Double) -> Decimal {
        lhs * Decimal(rhs)
    }
}

FloatCGFloat 同理)

  1. 泛型版 Attackable
protocol Attackable {
    associatedtype Value: NumericValue   // ① 关联类型
    func attack() -> Value
}

注意:

① 这里不能再给 attack() 提供默认实现,因为返回类型是泛型,不同数值的“默认伤害”语义不同;

② 如果确实想提供默认,可以再包一层泛型扩展

给“默认伤害”一个泛型实现

利用协议扩展的“where 子句”只对特定数值生效:

extension Attackable where Value == Double {
    func attack() -> Value { 10.0 }
}
extension Attackable where Value == Int {
    func attack() -> Value { 10 }
}
extension Attackable where Value == Decimal {
    func attack() -> Value { Decimal(10) }
}

这样任何符合者只要 Value 是上述三种之一,不实现 attack() 也能编译通过;想定制就再写一遍覆盖即可。

把“伤害计算器”也做成泛型组件

需求:

  • 支持“暴击”、“易伤”、“免伤”多层修正;
  • 算法写一次,对 Int / Double / Decimal 全部生效;
  • 编译期决定类型,无运行时派发。
  1. 定义计算器协议
protocol DamageCalculator<Value> {
    associatedtype Value: NumericValue
    /// 传入基础伤害,返回最终伤害
    func calculate(base: Value) -> Value
}
  1. 默认实现:暴击 * 1.5
struct CritCalculator<Value: NumericValue>: DamageCalculator {
    let rate: Value   // 暴击倍率
    
    func calculate(base: Value) -> Value {
        base * rate
    }
}
  1. 链式组合:装饰器模式
struct MultiplierCalculator<Value: NumericValue>: DamageCalculator {
    let upstream: any DamageCalculator<Value>  // 上游计算器
    let multiplier: Double
    
    func calculate(base: Value) -> Value {
        let upstreamDamage = upstream.calculate(base: base)
        return upstreamDamage * multiplier
    }
}

使用:

let crit: any DamageCalculator<Double> = CritCalculator(rate: 1.5)
let vulnerable = MultiplierCalculator(upstream: crit, multiplier: 1.2)  // 易伤 +20%
let final = vulnerable.calculate(base: 100)   // 100 * 1.5 * 1.2 = 180.0

把计算器塞进实体——“能力注入”

我们不再让实体“继承”伤害逻辑,而是把计算器当成属性注入:

struct Warrior<Value: NumericValue>: Attackable {
    let calculator: any DamageCalculator<Value>
    
    func attack() -> Value {
        let base: Value = Value(50)        // 自己定基础值
        return calculator.calculate(base: base)
    }
}

使用:

let warriorD = Warrior<Double>(calculator: vulnerable)
print(warriorD.attack())   // 90.0

一个文件里同时玩三种精度

let wInt    = Warrior<Int>(calculator: CritCalculator(rate: 2))
let wDouble = Warrior<Double>(calculator: CritCalculator(rate: 2))
let wDec    = Warrior<Decimal>(calculator: CritCalculator(rate: 2))

print(wInt.attack())     // 100
print(wDouble.attack())  // 100.0
print(wDec.attack())     // 100

同一套算法,编译器自动生成三份特化(specialization)代码,运行时无盒子、无动态派发。

性能实测:零开销承诺是否兑现?

测试环境:M1 Mac / Swift 5.9 / -O 优化

let p = Warrior<Double>(calculator: CritCalculator(rate: 1.8))
measure {
    for _ in 0..<1_000_000 { _ = p.attack() }
}

结果:

  • 泛型特化版本:0.047 s
  • 手写 Double 专用版本:0.046 s

差距在 2% 以内,属于测量误差;汇编层面已无线程堆分配、无 protocol witness 调用。

什么时候回到引用语义?

  1. 计算器需要状态缓存(如随机种子、CD 计时)且要共享;
  2. 需要继承 NSObjec 以兼容 KVO / Core Data;
  3. 需要互斥锁、原子引用计数。

其余场景继续 struct + 泛型协议

最终决策清单(速查表)

需求场景 首选方案 备选方案
只是多态 protocol 默认实现 class + override
多精度算法 泛型 protocol + associatedtype 宏/模板代码生成
共享可变状态 class actor
值语义 + 组合 struct + protocol
运行时动态替换 class + objc SwiftUI 的 AnyView 类型擦除

Swift 一个小型游戏对象模型渐进式设计(二)——协议与默认实现:如何写出不用继承的多态

用 protocol + extension 把上一篇的 BOSS 战例彻底重构,让代码轻量、可测试、易扩展

为什么“不用继承”

上一篇我们用 class Entity → Monster / Boss 的经典继承树完成了需求,但留下几个隐痛:

  1. 值类型无法参与:Swift 的 struct 不能继承 class。
  2. 多继承死路:一个 BOSS 既要“可攻击”又要“可飞行”还要“可分裂”,Swift 不支持多类继承。
  3. 隐式共享状态:父类新增的存储属性,所有子类被迫买单,造成“胖基类”。
  4. 单元测试困难:想单独测“狂暴逻辑”必须把整个 Boss 实例 new 出来,还要喂血量。

协议(protocol)是什么

一句话:协议只定义“契约”,不关心“怎么存”。

protocol Attackable {
    func attack() -> Double
}

任何类型(class / struct / enum / actor)只要实现 attack(),就自动“符合” Attackable,从而获得多态能力。

协议本身不能存属性,但可以通过“关联属性”或“协议扩展”给出默认实现,达到“代码复用”而“不强制继承”。

协议扩展:给协议加“默认实现”

语法:

extension Attackable {
    func attack() -> Double { 10.0 }   // 默认伤害
}

现在任何符合者如果不自己写 attack(),就自动拿到 10 点伤害。

想定制?在自己的类型里重新实现即可,不需要 override 关键字——因为协议不涉继承链。

拆成“能力插件”

  1. 可攻击
protocol Attackable {
    func attack() -> Double
}
extension Attackable {
    func attack() -> Double { 10.0 }
}
  1. 可定位
protocol Locatable {
    var x: Double { get set }
    var y: Double { get set }
}
  1. 可受伤
protocol Woundable {
    var hp: Double { get set }
    var maxHp: Double { get }
}
extension Woundable {
    var isRage: Bool { hp < maxHp * 0.2 }   // 狂暴判断
}
  1. 可随机伤害
protocol RandomDamage {
    func randomDamage(base: Int, range: Int) -> Double
}
extension RandomDamage {
    func randomDamage(base: Int, range: Int) -> Double {
        Double.random(in: 0.0..<Double(range)) + Double(base)
    }
}

用 struct 组装各种实体

Swift 的 struct 可以符合多个协议,享受所有默认实现,零继承。

  1. 普通小怪
struct Monster: Attackable, Locatable, Woundable, RandomDamage {
    var hp: Double
    let maxHp: Double
    var x: Double
    var y: Double
    
    // 自己定制伤害
    func attack() -> Double {
        randomDamage(base: 5, range: 6)
    }
}
  1. BOSS
struct Boss: Attackable, Locatable, Woundable {
    var hp: Double
    let maxHp: Double
    var x: Double
    var y: Double
    
    // 狂暴机制
    func attack() -> Double {
        let base: Double = 10
        return isRage ? base * 2 : base
    }
}
  1. 飞行小怪(新增能力,无需改旧代码)
protocol Flyable {
    var altitude: Double { get set }
}
struct FlyingMonster: Attackable, Locatable, Woundable, Flyable, RandomDamage {
    var hp: Double
    let maxHp: Double
    var x: Double
    var y: Double
    var altitude: Double
    
    func attack() -> Double {
        randomDamage(base: 4, range: 5) + 2   // 空对地加 2
    }
}

多态依旧可用:协议作为类型

let army: [any Attackable & Woundable] = [
    Monster(hp: 30, maxHp: 30, x: 0, y: 0),
    Boss(hp: 100, maxHp: 100, x: 1, y: 1),
    FlyingMonster(hp: 20, maxHp: 20, x: 2, y: 2, altitude: 10)
]

for unit in army {
    print("伤害=\(unit.attack()), 狂暴=\(unit.isRage)")
}

打印示例:

伤害=8.857546603881572, 狂暴=false
伤害=10.0, 狂暴=false
伤害=9.333377580674401, 狂暴=false

把 Boss 的血量打到 19 再跑一次,就能看到伤害翻倍,逻辑与继承版完全一致。

单元测试变得多简单?

想测“狂暴判断”只要 new 一个符合 Woundable 的伪对象即可,完全不用构造整个 Boss:

struct Mock: Woundable {
    var hp: Double
    let maxHp: Double = 100
}

let mock = Mock(hp: 19)
XCTAssertTrue(mock.isRage)

协议组合(Protocol Composition)的语法糖

typealias GameUnit = Attackable & Woundable & Locatable
func move(_ unit: inout GameUnit, toX x: Double, y: Double) {
    unit.x = x
    unit.y = y
}

一个类型别名即可把“能力包”当成一个整体使用,比继承树清爽得多。

什么时候仍需要 class 继承

  1. 需要 Objective-C 运行时动态替换(KVO、Swizzle)。
  2. 需要析构器 deinit 做资源清理。
  3. 需要共享引用语义(多个指针指向同一对象)。
  4. 需要互斥锁、原子操作等“引用计数”场景。

其余场景,优先 struct + 协议。

小结:一条决策流程图

image.png

Swift 一个小型游戏对象模型渐进式设计(一)——继承机制解读:从基础类到防止重写

为什么必须有“继承”

在真实世界里,我们习惯把事物归类:车 → 自行车 → 双人自行车。

Swift 的 class 类型允许我们用同样的层级方式建模,把公共的代码放在“上层”,把差异化的代码放在“下层”,这就是继承(Inheritance)。

它带来的三大价值:

  1. 代码复用:公共逻辑写一次。
  2. 统一接口:上层可用“父类指针”操作一切子类。
  3. 多态:运行时才决定到底执行哪段代码。

基础概念速览

  1. 基类(Base Class):不继承任何类的类。
  2. 子类(Subclass):写在冒号后面的类,它自动拥有父类所有成员。
  3. 重写(Override):子类对继承来的成员重新实现,需加关键字 override。
  4. super:在子类内部访问“父类实现”的前缀。
  5. final:阻止后面的人再继续重写或继承。

基类长什么样

/// 基类:最普通的“车”
class Vehicle {
    var currentSpeed = 0.0          // 存储属性,默认 0
    
    /// 计算属性:只读,返回人类可读的描述
    var description: String {
        return "traveling at \(currentSpeed) mph"
    }
    
    /// 实例方法:基类里什么都不做,留给子类去“填坑”
    func makeNoise() {
        // 空实现
    }
}

// 使用
let someVehicle = Vehicle()
print("Vehicle: \(someVehicle.description)")
// 打印:Vehicle: traveling at 0.0 mph

子类化(Subclassing)

语法:

class 子类: 父类 { /* 新增或覆盖 */ }

示例 1:自行车

class Bicycle: Vehicle {
    var hasBasket = false   // 新增属性
}

let bike = Bicycle()
bike.hasBasket = true
bike.currentSpeed = 15
print("Bicycle: \(bike.description)")
// 打印:Bicycle: traveling at 15.0 mph

示例 2:双人自行车(子类还能再被继承)

class Tandem: Bicycle {
    var currentNumberOfPassengers = 0
}

let tandem = Tandem()
tandem.hasBasket = true
tandem.currentNumberOfPassengers = 2
tandem.currentSpeed = 22
print("Tandem: \(tandem.description)")
// 打印:Tandem: traveling at 22.0 mph

重写(Override)全规则

  1. 方法重写:必须写 override;否则编译器报错。
class Train: Vehicle {
    override func makeNoise() {
        print("Choo Choo")
    }
}
Train().makeNoise()   // Choo Choo
  1. 属性重写

    子类“看不到”父类属性到底是存储型还是计算型,只能按“名字 + 类型”去匹配。

    a) 只读变读写:可以补充 setter。

    b) 读写变只读:❌ 不允许。

示例:给车加“档位”描述

class Car: Vehicle {
    var gear = 1
    
    override var description: String {
        // 先拿父类描述,再拼接自己的
        return super.description + " in gear \(gear)"
    }
}

let car = Car()
car.currentSpeed = 25
car.gear = 3
print("Car: \(car.description)")
// 打印:Car: traveling at 25.0 mph in gear 3
  1. 属性观察器重写

    可以为任何继承来的属性(无论存储/计算)添加 willSet/didSet。

    典型场景:自动档根据速度换挡。

class AutomaticCar: Car {
    override var currentSpeed: Double {
        didSet {
            gear = Int(currentSpeed / 10) + 1   // 自己算档位
        }
    }
}

let auto = AutomaticCar()
auto.currentSpeed = 35
print("AutomaticCar: \(auto.description)")
// 打印:AutomaticCar: traveling at 35.0 mph in gear 4

防止继承与重写——final

  1. 防重写
class Parent {
    final var id = 1          // 子类不能 override
    final func foo() {}       // 子类不能 override
}
  1. 防继承
final class Tool {}         // 任何人写 class MyTool: Tool {} 都会编译失败

super 的使用场景小结

  1. 在 override 方法里:super.someMethod()
  2. 在 override 属性 getter/setter 里:super.someProperty
  3. 在 override 下标里:super[index]

容易踩的坑

  1. 忘记写 override → 编译期报错。
  2. 重写时把只读属性改成只写 setter, Swift 不允许。
  3. 在 init 里访问 super 属性前,必须保证本类存储属性已初始化(初始化器规则)。
  4. 把 struct 拿去继承 → Swift 只有 class 支持继承,struct/enum 不行。

继承的边界与替代方案

继承是“白盒复用”,子类会依赖父类实现细节,容易造成“脆弱基类”问题。Swift 提供了更轻量的组合手段:

  1. 协议(protocol)+ 默认实现 → 无需继承即可获得多态。
  2. 值类型(struct)+ 组合 → 把“能力”做成属性,而非父类。
  3. 面向协议编程(POP)→ 把“is-a”转成“can-do”,降低耦合。

实战扩展:一个小型游戏对象模型

需求:

  • 所有游戏实体都有血量 hp 与坐标 (x,y)。
  • 玩家可以攻击,造成随机伤害。
  • BOSS 血量低于 20% 时进入狂暴模式,攻击力翻倍。

代码:

// 1. 基类
class Entity {
    var hp: Double
    var x: Double, y: Double
    
    init(hp: Double, x: Double, y: Double) {
        self.hp = hp; self.x = x; self.y = y
    }
    
    func attack() -> Double {
        return 10.0   // 默认伤害
    }
}

// 2. 普通小怪
class Monster: Entity {
    override func attack() -> Double {
        let damage = Double.random(in: 0..<6) + 5
        return damage
    }
}

// 3. BOSS
class Boss: Entity {
    override func attack() -> Double {
        let base = super.attack()
        // 狂暴判断
        let rage = hp < 20   // 假设 maxHp = 100
        return rage ? base * 2 : base
    }
}

// 4. 使用
let boss = Boss(hp: 15, x: 0, y: 0)
print("BOSS 伤害:\(boss.attack())")  // 血量<20,伤害翻倍

总结与思考

  1. 继承是 Swift 面向对象体系的基石,但“能用”不等于“该用”。
  2. 优先把“共性”做成协议或工具函数,再考虑是否抽象出基类。
  3. 重写时始终加 override,既安全又自文档化。
  4. 用 final 明确“设计边界”,让后来者少踩坑。
  5. 与值类型、协议、组合搭配,才能发挥 Swift 真正的威力。

Swift 中的迭代机制:Sequence、Collection 与 Iterator 完全拆解

前言

日常开发里,我们写 for item in list 像呼吸一样自然。

但 Swift 编译器在背后悄悄做了三件事:

  1. 调用 list.makeIterator() 拿到一个迭代器
  2. 反复调用 iterator.next()
  3. 把返回的可选值解包后赋给 item

一旦理解这三步,你就能

  • 自己写“能 for-in 的数据结构”
  • 避免“遍历同时修改”导致的崩溃
  • 把回调式 API 优雅地转成 AsyncSequence

Sequence:最小迭代单元

协议定义(核心部分,Swift 5.9 仍不变)

public protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    
    func makeIterator() -> Iterator
}

public protocol IteratorProtocol {
    associatedtype Element
    mutating func next() -> Element?
}

关键知识点

  1. Sequence 只承诺“能生成迭代器”,不保证能反复遍历,也不保证有 count
  2. 迭代器几乎总是 struct:值语义保证“复制一份就从头开始”,不会意外共享状态。
  3. 单趟序列(例如网络流)完全合法;第二次 makeIterator() 可以返回空迭代器。

代码示例:自定义一个“从 n 倒数到 0”的序列

struct Countdown: Sequence {
    let start: Int
    
    // 每次 for-in 都会调用一次,生成新的迭代器
    func makeIterator() -> Iterator {
        Iterator(current: start)
    }
    
    struct Iterator: IteratorProtocol {
        var current: Int
        
        // 返回 nil 时代表迭代结束
        mutating func next() -> Int? {
            guard current >= 0 else { return nil }
            defer { current -= 1 }          // 先返回,再减
            return current
        }
    }
}

// 使用
for number in Countdown(start: 3) {
    print(number)   // 3 2 1 0
}

Collection:在 Sequence 上加了三把锁

Collection 额外保证

  • 可多次遍历且顺序稳定(除非自己把文档写错)
  • 提供 countendIndex、下标访问
  • 支持切片、前缀、后缀等默认实现

协议片段

public protocol Collection: Sequence {
    associatedtype Index: Comparable
    var startIndex: Index { get }
    var endIndex: Index { get }
    subscript(position: Index) -> Element { get }
    func index(after i: Index) -> Index
}

因为多趟安全,mapfilter 可以提前分配内存;

因为下标存在,ArrayDictionarySet 都直接 conform。

for-in 的糖衣剥开长这样

编译器把

for element in container {
    print(element)
}

翻译成

var iterator = container.makeIterator()
while let element = iterator.next() {
    print(element)
}

理解这段模板代码,你就能:

  • 在 Playground 里手动模拟 for 循环
  • 把“遍历同时修改”的崩溃场景复现出来

遍历同时修改:崩溃现场与三种安全写法

现场:遍历数组时删除元素

var todoItems = ["A", "B", "C"]

// 目前倒是没有崩溃,但是也不是很符合逻辑
for (index, item) in todoItems.enumerated() {
    if item == "B" {
        todoItems.remove(at: index)   // ❌ Fatal error: Collection modified while enumerating
    }
}

原因:数组缓冲区搬迁,迭代器指针失效。

三种安全写法:

  1. 官方一次性 API
todoItems.removeAll { $0 == "B" }
  1. 先记下索引,后删除
let indexesToRemove = todoItems.indices.filter { todoItems[$0] == "B" }
for i in indexesToRemove.reversed() {
    todoItems.remove(at: i)
}
  1. 过滤后整体替换
todoItems = todoItems.filter { $0 != "B" }

AsyncSequence:把“迭代”搬到异步世界

协议定义

public protocol AsyncSequence {
    associatedtype Element
    associatedtype AsyncIterator: AsyncIteratorProtocol where AsyncIterator.Element == Element
    func makeAsyncIterator() -> AsyncIterator
}

public protocol AsyncIteratorProtocol {
    associatedtype Element
    mutating func next() async throws -> Element?
}

消费方式

for await element in stream {
    print(element)          // 会在每次 next() 挂起时让出线程
}

桥接回调式 API 的模板:进度条场景

func makeProgressStream() -> AsyncStream<Double> {
    AsyncStream { continuation in
        let token = ProgressCenter.onUpdate { value in
            continuation.yield(value)
            if value >= 1.0 { continuation.finish() }
        }
        continuation.onTermination = { _ in
            ProgressCenter.removeObserver(token)
        }
    }
}

// 使用
Task {
    for await p in makeProgressStream() {
        progressView.progress = Float(p)
    }
}

自己动手:一个固定容量的 RingBuffer

需求:保持最新 N 条日志,支持 for-in 打印。

struct RingBuffer<Element>: Collection {
    private var storage: [Element?]   // 用 Optional 占位
    private var head = 0
    private var tail = 0
    private(set) var count = 0
    private let capacity: Int
    
    init(capacity: Int) {
        self.capacity = capacity
        storage = Array(repeating: nil, count: capacity)
    }
    
    // 写入新元素,覆盖最旧数据
    mutating func append(_ newElement: Element) {
        storage[tail] = newElement
        tail = (tail + 1) % capacity
        if count == capacity {
            head = (head + 1) % capacity   // 丢弃最旧
        } else {
            count += 1
        }
    }
    
    // MARK: Collection 必备
    typealias Index = Int
    
    var startIndex: Int { 0 }
    var endIndex: Int { count }
    
    func index(after i: Int) -> Int {
        precondition(i < endIndex, "Index out of bounds")
        return i + 1
    }
    
    subscript(position: Int) -> Element {
        precondition((0..<count).contains(position), "Index out of bounds")
        let offset = (head + position) % capacity
        return storage[offset]!
    }
}

// 使用
var buffer = RingBuffer<Int>(capacity: 3)
for i in 1...5 {
    buffer.append(i)   // 1,2,3 → 2,3,4 → 3,4,5
}

for value in buffer {
    print(value)   // 3 4 5
}

总结与扩展场景

  1. 协议层次

IteratorProtocolSequenceCollectionBidirectionalCollectionRandomAccessCollection

每一层只加必要约束,绝不多要一颗糖。

  1. 值语义是 Swift 迭代的灵魂

结构体迭代器复制即“新游标”,避免共享状态,这点与 Objective-C 的 NSEnumerator 形成鲜明对比。

  1. 遍历同时修改的崩溃本质是“迭代器失效”

所有带指针/索引的集合都存在,掌握“先记录后改”或“一次性 API”即可。

  1. AsyncSequence 让“事件流”变成普通 for-in

网络下载、蓝牙数据、用户点击序列都能用同一套思维建模;配合 AsyncStream 几乎零成本桥接老代码。

  1. 自定义 Collection 是架构试金石

RingBuffer 这类小容器写一遍,你会深刻理解“下标换算”、“容量与 count 区别”、“前置条件断言”这些日常被标准库隐藏的细节。

学习资料

  1. www.donnywals.com/a-deep-dive…
❌