普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月25日iOS

老司机 iOS 周报 #363 | 2026-01-26

作者 ChengzhiHuang
2026年1月25日 20:23

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

文章

🐕 精通 UITableViewDiffableDataSource ——从入门到重构的现代 iOS 列表开发指南

@阿权:本文围绕 iOS 现代列表 UITableViewDiffableDataSource 展开,核心是替代传统数据源代理模式,解决列表开发中的崩溃、状态不一致等痛点,并在最后提供一个轻量工具集 DiffableDataSourceKit 来简化系统 API 的调用。文章核心内容如下:

  1. 使用 UITableViewDiffableDataSource API,通过声明式的“快照”来管理数据状态,系统自动计算并执行 UI 更新动画,从而一劳永逸地解决了传统模式中数据与 U 状态不同步导致的崩溃问题。
  2. 全文通过构建一个音乐播放列表 App 来贯穿始终,从移除 Storyboard、定义遵循 Hashable 协议的数据模型开始,一步步教你初始化数据源和填充数据。
  3. 文章还详细讲解了:
    • 自定义多样化的单元格:包括使用传统的 Auto Layout 布局,以及利用 iOS 14+ 的 UIContentConfiguration 进行现代化配置。
    • 实现核心交互:具体涉及了拖拽重排、滑动删除,以及如何通过 Cell 的代理事件处理等交互。
    • 处理复杂逻辑:特别讲解了如何利用模型的 Hashable 来实现“原地刷新”而非替换的刷新机制。

除了文章提到的 UITableViewDiffableDataSource,用好这些技术,不妨可以再看看以下几个 WWDC:

  1. WWDC19 - 215 - Advances in Collection View Layout
  2. WWDC19 - 220 - Advances in UI Data Sources
  3. WWDC20 - 10026 - Lists in UICollectionView
  4. WWDC20 - 10027 - Modern cell configuration

另外,与其用 UITableView 承载数据,其实 Apple 更推荐使用 UICollectionView 来实现列表,甚至还提供了增强版的 Cell。

恰逢 App Store 要求用 Xcode 26 带来的强制升级,不少 App 也终于抛弃了 iOS 12、iOS 13,也是用新技术(也不新了)升级项目架构的最好时机。

除了 API 本身,我们也应关注到一些架构或设计模式上的变化与趋势:

  1. 声明式。新的 API 更多使用构造时定义逻辑的声明式语法,在一开始就把相关的布局、样式等逻辑给定义好,后续不再通过各种存储属性配置逻辑。极大地减少了开发者对状态的维护。例如 UICollectionViewCompositionalLayout 通过 Item、可嵌套的 Group、Section 一层一层地在初始化配置布局。
  2. 数据驱动。声明式常常是为了数据驱动,因为声明式定义的不是最终对象,而是一些前置的配置数据结构。例如 Cell 提供了 Configuration 结构体,通过配置和重新赋值结构体来实现 UI 更新,而不是直接操作 View。类似的,UIButton 也提供了类型的 Configuration 结构体用于配置 UI。更深一层的意义,驱动 UI 的配置数据、视图甚至可以复用的和无痛迁移的。例如 UITableViewCellUICollectionViewCell 的配置类及其关联子视图是通用的,自定义 Cell 可以把重心放在自定义 Configuration 配置上,这样就可以把相同的视图样式套用在各种容器中。
  3. 数据绑定。将索引替换为 id 甚至是具体业务类型。以往 UITableViewUICollectionView 的 API 都是围绕索引(IndexPath)展开的,所有的数据(DataSource)、布局(CollectionViewLayout)和视图(Presentation: Cell、ReuseableView)即使有分离,但都需要通过索引来交换。虽然这样简化了不同模块的耦合和通信逻辑,但因为大多数业务场景数据是动态的,这让索引只是个临时态,一不小心就会用错,轻则展示错误,重则引入崩溃。DiffableDataSource 最具里程碑的一点是剔除了索引,直接让具体业务模型跟 Cell 直接绑定,不经过索引。
  4. 类型安全。不再是 Any/AnyObject,而是直接绑定一个具体的类型。常见做法是通过泛型机制将类型传入。
  5. 用轮子,而不是造轮子。系统 API 足够自由,直接使用实例,而不是子类化自定义逻辑。以往的开发经验,都是一言不合就重写实现,重新造控件、布局,UIButtonUICollectionViewLayout 就是两个典型的 case。近年来系统 API 都在丰富使用的自由度和易用程度,例如 UIButton 提供了许多拿来就能用的灵活样式,开发者只需要微调个 Configuration 就是能实现业务效果。UICollectionViewCompositionalLayout 则是用 Item、Group、Section 构造足够复杂的布局场景。另外一点验证了这个趋势的是,iOS 26 中,只有官方提供的控件、导航框架才有完整的液态玻璃交互。

架构的演进一般是为了提高研效、减少出错。一个合理、高效的代码架构,在当业务需求变得复杂的时候,业务调用代码不会随业务的复杂而线性增长,而是逐渐减少。

🐎 Dart 官方再解释为什么放弃了宏编程,并转向优化 build_runner ? 和 Kotlin 的区别又是什么?

@Crazy:本文主要介绍了 Dart 官方放弃宏编程改为优化 build_runner 的原因,在读本文之前,要先明白什么是宏编程。文章中介绍了 Dart 在实现宏编程的过程中试用的方案与思考,放弃的原因总结起来有三个 :

  1. 编译会卡在一个“先有鸡还是先有蛋”的死结
  2. 工具链双前端导致宏支持会引发“工作量爆炸 + 性能灾难”
  3. 即使做成了,也“高不成低不就”:替代不了 build_runner,不如直接扩展 build_runner 能力

文章最后还对比了 Kotlin 的 Compiler Plugins、KSP 与 Swift 的 Swift Macros 的差距,总的来说 build_runner 还有很长的一段路要走。

🐕 @_exported import VS public import

@AidenRao:Swift 6 带来了全新的 import 访问级别控制:@_exported import。它和我们熟悉的 public import 有什么不同?简单来说,public import 只是将一个模块声明为公开 API 的一部分,但使用者仍需手动导入它;而 @_exported import 则是将依赖的符号完全“吸收”,调用方无需关心底层依赖。文章深入对比了两者的意图和应用场景,并给出了明确建议:日常开发中应优先选择官方支持的 public import,仅在封装 SDK 或构建聚合模块(Umbrella Module)这类希望为用户简化导入操作的场景下,才考虑使用 @_exported

🐕 MVVM + Reducer Pattern

@含笑饮砒霜:这篇文章主要讲述如何将 MVVM 架构与 Reducer 模式结合来提升 iOS 应用中状态管理的可控性和可维护性。作者指出:传统的 MVVM 模式在复杂状态下易出现分散的状态变更和难以追踪的问题,这会导致难调试、隐式状态转换、竞态条件等不良后果;而 Reducer 模式(受 Redux/TCA 启发)通过 “单一状态源 + 明确 action + 纯函数 reduce ” 的方式,使状态变更更可预测、更易测试。文章建议在 ViewModel 内部局部引入 reducer,把所有状态通过单一 reduce(state, action) 处理,并把副作用(如异步任务)当作 effects 处理,从而达到更明确、可追踪且易单元测试的效果,同时保留 MVVM 和领域层的清晰分层,不强依赖某个框架。

🐢 用第一性原理拆解 Agentic Coding:从理论到实操

@Cooper Chen:文章从第一性原理出发,系统拆解了 Agentic Coding 背后的底层逻辑与工程现实,澄清了一个常见误区:效率瓶颈不在于上下文窗口不够大,而在于我们如何与 AI 协作。作者以 LLM 的自回归生成与 Attention 机制为起点,深入分析了 Coding Agent 在长任务中常见的“走偏”“失忆”“局部最优”等问题,并指出这些并非工具缺陷,而是模型工作方式的必然结果。

文章最有价值之处,在于将理论约束转化为可执行的工程实践:通过“短对话、单任务”的工作方式控制上下文质量;用结构化配置文件和工具设计引导 Agent 行为;通过 Prompt Caching、Agent Loop、上下文压缩等机制提升系统稳定性。更进一步,作者提出“复利工程(Compounding Engineering)”这一关键理念——不把 AI 当一次性工具,而是通过文档、规范、测试和审查,将每一次经验沉淀为系统的长期记忆。

最终,文章给出的启示非常清晰:AI 编程不是魔法,而是一门需要刻意练习的协作技能。当你真正理解模型的边界,并用工程化方法加以约束和放大,AI 才能从“能写代码”进化为“可靠的编程合作者”。

🐎 Universal Links At Scale: The Challenges Nobody Talks About

@Damien:文章揭示了 Universal Links 在大规模应用中的隐藏复杂性:AASA 文件缺乏 JSON 模式验证导致静默失效,Apple CDN 缓存延迟使问题修复滞后,苹果特有通配符语法和 substitutionVariables 变量无现成工具支持。作者提出通过 CI 集成模式验证、CDN 同步检查、自定义正则解析和 staging 环境测试的完整方案,并开源了 Swift CLI 工具实现全链路自动化验证。

🐕 How I use Codex GPT 5.2 with Xcode (My Complete Workflow)

@JonyFang: 本视频深入介绍了如何让 AI 代理(如 Codex GPT 5.2)真正提升 iOS/macOS 开发效率的三个核心策略:

  1. 构建脚本自动化(Build Scripts):通过标准化的构建流程,让 AI 能够理解和复现你的构建环境
  2. 让构建失败显而易见(Make Build Failures Obvious):优化错误信息的呈现方式,使 AI 能够快速定位问题根源
  3. 给你的代理装上"眼睛"(Give Your Agent Eyes):这是最核心的部分 - 让 AI 能够"看到"应用运行时的状态,而不仅仅是读取代码

最有价值之处:作者强调了一个常被忽视的问题 - AI 代码助手不仅需要理解代码逻辑,更需要理解应用的运行时状态。通过工具如 Peekaboo 等,让 AI 能够获取视觉反馈(截图、UI 层级等),从而提供更精准的问题诊断和代码建议。这种"可观测性优先"的思路,与传统的代码审查工作流形成了有趣的对比,值得所有尝试将 AI 工具深度集成到开发流程中的团队参考。

视频时长约 49 分钟,适合希望系统性提升 AI 辅助开发效率的 iOS/macOS 开发者观看。

工具

🐎 Skip Is Now Free and Open Source

@Crazy:Skip 框架正式免费并且开源,该库从 2023 年开始开发,已有三年的开发历程。该库的目的是让开发者能够仅用一套 Swift 与 SwiftUI 代码库,同时打造 iOS 与 Android 上的高品质移动应用——而且不必接受那些自“跨平台工具诞生以来就一直存在”的妥协。因为 Skip 是采用编译为 Kotlin 与 Compose 的方式,所以相应的执行效率是非常高的。相较于其他的跨平台开发,效率高,并且使用的是 Swift 语言。既然已经免费并开源,移动端开发的时候又多了一个可供选择的跨端技术。

内推

重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考

具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)

关注我们

我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参

同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom

说明

🚧 表示需某工具,🌟 表示编辑推荐

预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)

ARC 原理与 weak 底层实现(Side Table 深度解析)

作者 汉秋
2026年1月25日 17:22

ARC 原理与 weak 底层实现(Side Table 深度解析)

面向:有一定 iOS / Runtime 基础的开发者

目标:真正搞清楚 weak 到底 weak 了谁、SideTable 里存的是什么、为什么能自动置 nil


一、先给结论(非常重要)

weak 不是修饰对象,而是修饰“指针变量”

SideTable 记录的是:某个对象,被哪些 weak 指针地址指向

换句话说:

  • ❌ 不是「A weak 引用 B」
  • ❌ 不是「对象记住了谁 weak 它」
  • ✅ 是「Runtime 记住:哪些内存地址(weak 指针)指向了这个对象」

二、从一行代码开始

@property (nonatomic, weak) Person *person;

编译后本质是:

Person *__weak _person;

说明三点:

  1. _person 是一个普通指针变量
  2. weak 修饰的是这个指针变量的行为
  3. 并不是 Person 对象“变成了 weak”

三、明确三个核心角色

Person *p = [[Person alloc] init];
self.person = p; // weak

此时内存中存在三样东西:

角色 含义
Person 对象 真正的 OC 实例
strong 指针 拥有对象(如 p)
weak 指针 不拥有对象(如 self->_person)

weak 的对象不是 Person,而是 _person 这个指针变量。


四、objc_storeWeak 到底做了什么?

self.person = p;

编译后:

objc_storeWeak(&self->_person, p);

注意这里传入的两个参数:

  • &self->_person 👉 weak 指针的地址

  • p 👉 对象地址

Runtime 的真实意图:

登记:对象 p,被这个 weak 指针地址弱引用了


五、SideTable / weak_table 的真实逻辑结构

1️⃣ SideTable(简化)

struct SideTable {
    spinlock_t lock;
    RefcountMap refcnts;     // 强引用计数
    weak_table_t weak_table; // 弱引用表
};

2️⃣ weak_table_t

struct weak_table_t {
    weak_entry_t *weak_entries;
};

3️⃣ weak_entry_t(重点)

struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被 weak 的对象
    weak_referrer_t *referrers;         // weak 指针地址数组
};

六、SideTable 中真正存的是什么?

用一张逻辑图表示:

SideTable
└── weak_table
    └── weak_entry
        ├── referent = Person 对象
        └── referrers = [
              &self->_person,
              &vc->_delegate,
              &cell->_model
          ]

关键点(一定要记住):

  • weak_table 的 key 是对象
  • value 是 所有指向它的 weak 指针地址

七、谁被 weak?谁被记录?

以:

self.person = p;

为例:

问题 答案
谁被 weak? _person 这个指针变量
谁被引用? Person 对象
SideTable 记录什么? Person → weak 指针地址列表

八、对象释放时为什么能自动置 nil?

当 Person 的引用计数降为 0:

objc_destroyWeak(obj);

Runtime 的逻辑流程:

1. 找到 obj 对应的 weak_entry
2. 遍历 referrers(weak 指针地址)
3. 对每个地址执行:
      *(Person **)referrer = nil
4. 移除 weak_entry

⚠️ Runtime 完全不知道变量名,只操作内存地址。


九、用内存视角完整走一遍

1️⃣ 内存布局

0x1000  Person 对象

0x2000  p (strong)        → 0x1000
0x3000  self->_person     → 0x1000

2️⃣ weak_table

key: 0x1000
value: [0x3000]

3️⃣ Person 释放

free(0x1000)
*(0x3000) = nil

最终:

self.person == nil

十、为什么 weak 不会产生野指针?

修饰符 行为
assign 不 retain、不置 nil → 野指针
weak Runtime 扫表并置 nil

weak 的安全性来自 Runtime 的集中清理机制


十一、为什么 weak_table 以“对象”为中心?

因为:

对象释放是一个确定事件

以对象为 key:

  • 对象销毁 → 一次性清理所有 weak 指针
  • 性能可控
  • 逻辑集中

十二、常见误解澄清

❌ A weak 引用 B

✅ A 的某个指针 weak 指向 B

❌ 对象知道谁 weak 它

✅ Runtime 知道,对象本身不知道

❌ weak 是对象属性

✅ weak 是指针语义

❌ weak 只是“不 retain”

✅ weak = 不 retain + 注册 weak_table + 自动置 nil


十三、一句话总结

weak 的本质不是弱引用对象,

而是 Runtime 记录“哪些指针弱指向了这个对象”,

并在对象销毁时统一把这些指针置为 nil。


十四、下一步可继续深入

  • block 捕获下 weak_table 的变化过程

  • __unsafe_unretained 与 weak 的实现对比

  • objc-runtime 源码中 weak_entry 的真实实现

Flutter 底层原理xyz题全面梳理

作者 忆江南
2026年1月25日 16:25

一、Flutter 架构原理

1. Flutter 的整体架构是怎样的?

Flutter 采用分层架构设计,从上到下分为三层:

Framework 层(Dart)

  • Material/Cupertino:UI 组件库
  • Widgets:组合式 UI 构建
  • Rendering:布局、绘制、命中测试
  • Animation/Painting/Gestures:动画、绘图、手势
  • Foundation:基础工具类

Engine 层(C++)

  • Skia:2D 渲染引擎,负责光栅化
  • Dart Runtime:Dart 虚拟机,JIT/AOT 编译
  • Text:文本排版引擎(libtxt + HarfBuzz + ICU)
  • Shell:平台适配层

Embedder 层(平台相关)

  • 负责与原生平台交互
  • 提供 Surface 供渲染
  • 处理输入事件、生命周期
┌─────────────────────────────────────────┐
│              Framework (Dart)            │
│  ┌────────────────────────────────────┐ │
│  │      Material / Cupertino          │ │
│  ├────────────────────────────────────┤ │
│  │            Widgets                  │ │
│  ├────────────────────────────────────┤ │
│  │           Rendering                 │ │
│  ├────────────────────────────────────┤ │
│  │   Animation / Painting / Gestures  │ │
│  ├────────────────────────────────────┤ │
│  │          Foundation                 │ │
│  └────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│              Engine (C++)                │
│  Skia │ Dart VM │ Text │ Platform Shell │
├─────────────────────────────────────────┤
│        Embedder (Platform Specific)      │
│     Android │ iOS │ Windows │ macOS     │
└─────────────────────────────────────────┘

二、三棵树原理(核心重点)

2. Widget、Element、RenderObject 三棵树的关系?

Widget Tree(配置树)

  • Widget 是不可变的(immutable)配置描述
  • 轻量级,可频繁创建销毁
  • 只是 "蓝图",不直接参与渲染

Element Tree(元素树)

  • Widget 的实例化对象,连接 Widget 和 RenderObject
  • 可变的,有生命周期
  • 负责管理 Widget 的复用和更新
  • 持有 BuildContext(Element 就是 BuildContext)

RenderObject Tree(渲染树)

  • 真正负责布局(layout)和绘制(paint)
  • 包含尺寸、位置信息
  • 计算成本高,尽量复用
Widget Tree          Element Tree         RenderObject Tree
    │                     │                      │
Container ─────────► ComponentElement           │
    │                     │                      │
    └──► Padding ────► SingleChildRender ───► RenderPadding
              │           Element                    │
              └──► Text ────────────────────► RenderParagraph

3. 为什么要设计三棵树?

分离关注点

  • Widget:描述 UI 配置,开发者友好
  • Element:管理生命周期,高效 diff
  • RenderObject:专注渲染性能

性能优化

  • Widget 可频繁重建(O(n)创建成本低)
  • Element 复用机制减少重建
  • RenderObject 只在必要时更新

复用策略

// Element 的 updateChild 核心逻辑
if (widget == newWidget) return child;  // 同一实例,无需更新
if (widget.runtimeType == newWidget.runtimeType &&
    widget.key == newWidget.key) {
  child.update(newWidget);  // 复用 Element,更新配置
  return child;
}
// 类型或 key 不同,销毁重建

三、渲染管线原理

4. Flutter 的渲染流程是怎样的?

一帧的渲染流程(约 16.67ms @60fps):

┌─────────────────────────────────────────────────────────┐
│                    一帧渲染流程                          │
├──────────┬──────────┬──────────┬──────────┬────────────┤
│  Build   │  Layout  │  Paint   │ Composite│   Raster   │
│  构建     │   布局    │   绘制    │   合成    │   光栅化   │
├──────────┴──────────┴──────────┴──────────┴────────────┤
│ setState │ 计算大小  │ 生成Layer │ 构建场景  │  GPU渲染   │
│ 触发重建  │ 确定位置  │   Tree    │   树     │   上屏    │
└─────────────────────────────────────────────────────────┘
         UI Thread                          GPU Thread

详细步骤

  1. Build Phase

    • setState() 标记 Element 为 dirty
    • WidgetsBinding.drawFrame() 触发
    • 调用 build() 方法重建 Widget 树
    • Element 进行 diff,决定复用或重建
  2. Layout Phase

    • 从根节点开始,父传约束给子
    • 子返回尺寸给父
    • 确定每个节点的 Size 和 Position
    • Constraints go down, Sizes go up
  3. Paint Phase

    • 遍历 RenderObject 树
    • 调用 paint() 方法生成绘制指令
    • 构建 Layer Tree(分层合成)
  4. Composite Phase

    • 合成 Layer Tree 为 Scene
    • 提交给 Engine
  5. Raster Phase

    • Skia 将 Scene 光栅化为像素
    • GPU 渲染上屏

5. Layout 布局的约束传递机制?

Flutter 使用 盒约束模型(Box Constraints)

class BoxConstraints {
  final double minWidth;
  final double maxWidth;
  final double minHeight;
  final double maxHeight;
}

约束传递过程

父 Widget
    │
    ├──► 传递 Constraints(约束)给子
    │
子 Widget
    │
    ├──► 在约束范围内确定自己的 Size
    │
    └──► 返回 Size 给父

父 Widget
    │
    └──► 确定子的 Position(偏移)

核心原则

  • Tight Constraints:min == max,强制固定大小
  • Loose Constraints:min = 0,可任意大小
  • Unbounded:max = infinity,需要子自行决定大小

6. RepaintBoundary 的作用原理?

RepaintBoundary 创建独立的 Layer,隔离重绘区域。

原理

// 普通情况:共享 Layer,一处改变全部重绘
Parent Layer
    ├── Child A (changed)
    ├── Child B (也被重绘)
    └── Child C (也被重绘)

// 使用 RepaintBoundary:独立 Layer
Parent Layer
    ├── Child A Layer (changed,只重绘这个)
    ├── Child B Layer (保持不变)
    └── Child C Layer (保持不变)

使用场景

  • 频繁动画的局部区域
  • 复杂列表项
  • 独立更新的组件(如 CustomPaint)

四、Element 生命周期与更新机制

7. Element 的生命周期?

abstract class Element {
  // 1. 创建:Widget.createElement() 调用
  Element(Widget widget);

  // 2. 挂载:插入 Element 树
  void mount(Element? parent, Object? newSlot) {
    _parent = parent;
    _slot = newSlot;
    _lifecycleState = _ElementLifecycle.active;
    // 创建 RenderObject(如果是 RenderObjectElement)
  }

  // 3. 更新:Widget 配置变化时
  void update(Widget newWidget) {
    _widget = newWidget;
  }

  // 4. 重建:标记 dirty 后
  void rebuild() {
    performRebuild();  // 调用 build()
  }

  // 5. 卸载:从树中移除
  void unmount() {
    _lifecycleState = _ElementLifecycle.defunct;
  }
}

8. setState() 的工作原理?

void setState(VoidCallback fn) {
  // 1. 执行回调修改状态
  fn();

  // 2. 标记 Element 为 dirty
  _element!.markNeedsBuild();
}

void markNeedsBuild() {
  if (dirty) return;  // 避免重复标记
  _dirty = true;

  // 3. 将 Element 加入 dirty 列表
  owner!.scheduleBuildFor(this);
}

// 4. 下一帧统一重建所有 dirty Elements
void buildScope(Element context, [VoidCallback? callback]) {
  while (_dirtyElements.isNotEmpty) {
    final element = _dirtyElements.removeLast();
    element.rebuild();
  }
}

重要特性

  • setState 是异步的(下一帧才生效)
  • 多次 setState 会合并(同一 Element 只重建一次)
  • build() 中调用 setState 会报错

五、Key 的原理与作用

9. Key 的作用是什么?底层原理?

作用:标识 Element,控制 Element 的复用策略。

没有 Key 的匹配规则

// 只比较 runtimeType
canUpdate(oldWidget, newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType;
}

有 Key 的匹配规则

// 比较 runtimeType + key
canUpdate(oldWidget, newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

10. LocalKey vs GlobalKey 的区别?

LocalKey(ValueKey、ObjectKey、UniqueKey):

  • 仅在兄弟节点间唯一
  • 用于 ListView 中元素重排序
  • 轻量级,不跨树访问

GlobalKey

  • 全局唯一,可跨树访问
  • 持有 Element 引用,可获取 State 和 RenderObject
  • 成本较高,慎用
// GlobalKey 的底层实现
final Map<GlobalKey, Element> _globalKeyRegistry = {};

class GlobalKey<T extends State> extends Key {
  Element? get _currentElement => _globalKeyRegistry[this];

  BuildContext? get currentContext => _currentElement;

  Widget? get currentWidget => _currentElement?.widget;

  T? get currentState => (_currentElement as StatefulElement?)?.state as T?;
}

六、InheritedWidget 原理

11. InheritedWidget 的实现原理?

InheritedWidget 实现了高效的跨层级数据传递。

核心机制

abstract class InheritedWidget extends ProxyWidget {
  // 1. 创建 InheritedElement
  @override
  InheritedElement createElement() => InheritedElement(this);

  // 2. 判断是否需要通知依赖者
  bool updateShouldNotify(InheritedWidget oldWidget);
}

class InheritedElement extends ProxyElement {
  // 3. 存储依赖关系
  final Map<Element, Object?> _dependents = {};

  // 4. 注册依赖(dependOnInheritedWidgetOfExactType 时调用)
  void setDependencies(Element dependent, Object? value) {
    _dependents[dependent] = value;
  }

  // 5. 更新时通知所有依赖者
  void notifyClients(InheritedWidget oldWidget) {
    for (final dependent in _dependents.keys) {
      dependent.didChangeDependencies();
    }
  }
}

查找过程(O(1) 复杂度)

T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>() {
  // 每个 Element 持有祖先 InheritedElement 的 Map
  final ancestor = _inheritedWidgets?[T];

  if (ancestor != null) {
    // 注册依赖关系
    ancestor.setDependencies(this, null);
  }

  return ancestor?.widget as T?;
}

为什么是 O(1)?

  • Element 在 mount 时会继承父节点的 _inheritedWidgets Map
  • 查找直接通过 Type 作为 key 获取

七、BuildContext 的本质

12. BuildContext 是什么?

本质:BuildContext 就是 Element

abstract class Element implements BuildContext {
  @override
  Widget get widget => _widget!;

  @override
  bool get mounted => _lifecycleState == _ElementLifecycle.active;

  @override
  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>() {...}

  @override
  T? findAncestorWidgetOfExactType<T extends Widget>() {...}

  @override
  T? findAncestorStateOfType<T extends State>() {...}
}

为什么要抽象成 BuildContext?

  • 限制开发者只能访问必要的 API
  • 隐藏 Element 的内部实现细节
  • 提供更清晰的语义("构建上下文")

八、Dart 异步原理

13. Event Loop 事件循环机制?

Dart 是单线程模型,通过事件循环处理异步:

┌─────────────────────────────────────────┐
│              Event Loop                  │
│                                          │
│  ┌─────────────────────────────────┐    │
│  │      Microtask Queue            │    │ ← 优先级高
│  │  scheduleMicrotask / then       │    │
│  └─────────────────────────────────┘    │
│                  ↓                       │
│  ┌─────────────────────────────────┐    │
│  │        Event Queue              │    │ ← 优先级低
│  │  Future / Timer / I/O / 手势    │    │
│  └─────────────────────────────────┘    │
│                                          │
│  while (true) {                         │
│    while (microtaskQueue.isNotEmpty) {  │
│      microtaskQueue.removeFirst()();    │
│    }                                     │
│    if (eventQueue.isNotEmpty) {         │
│      eventQueue.removeFirst()();        │
│    }                                     │
│  }                                       │
└─────────────────────────────────────────┘

执行顺序

  1. 执行同步代码
  2. 执行所有 Microtask(直到队列空)
  3. 执行一个 Event
  4. 回到步骤 2
void main() {
  print('1');  // 同步

  Future(() => print('2'));  // Event Queue

  scheduleMicrotask(() => print('3'));  // Microtask Queue

  Future.microtask(() => print('4'));  // Microtask Queue

  print('5');  // 同步
}
// 输出:1, 5, 3, 4, 2

14. Future 和 async/await 的原理?

Future 本质

class Future<T> {
  // 状态:pending / completed / error
  _FutureState _state;

  // 结果值或错误
  T? _value;
  Object? _error;

  // 回调链表
  _FutureListener? _listeners;

  // 完成时触发所有回调
  void _complete(T value) {
    _value = value;
    _state = _FutureState.completed;
    _propagateToListeners();
  }
}

async/await 编译转换

// 源代码
Future<int> fetchData() async {
  final response = await http.get(url);
  return response.statusCode;
}

// 编译后(伪代码)
Future<int> fetchData() {
  return Future.sync(() {
    return http.get(url).then((response) {
      return response.statusCode;
    });
  });
}

15. Isolate 的通信原理?

Isolate 是独立的内存空间,通过 Port 通信:

┌─────────────────┐         ┌─────────────────┐
│    Main Isolate │         │   New Isolate   │
│                 │         │                 │
│  ┌───────────┐  │  消息   │  ┌───────────┐  │
│  │ SendPort  │──┼────────►│  │ReceivePort│  │
│  └───────────┘  │         │  └───────────┘  │
│                 │         │                 │
│  ┌───────────┐  │  消息   │  ┌───────────┐  │
│  │ReceivePort│◄─┼─────────│  │ SendPort  │  │
│  └───────────┘  │         │  └───────────┘  │
│                 │         │                 │
│  独立 Heap      │         │   独立 Heap     │
└─────────────────┘         └─────────────────┘

消息传递机制

  • 消息通过深拷贝传递(不共享内存)
  • 支持基本类型、List、Map、SendPort
  • 不支持闭包、函数引用
// 使用 compute 简化
final result = await compute(expensiveOperation, data);

// 底层实现
Future<R> compute<Q, R>(ComputeCallback<Q, R> callback, Q message) {
  final receivePort = ReceivePort();
  await Isolate.spawn(_isolateEntry, [receivePort.sendPort, callback, message]);
  return await receivePort.first as R;
}

九、Platform Channel 原理

16. Flutter 如何与原生通信?

三种 Channel 类型

Flutter (Dart)          Platform (Native)
     │                        │
     │    MethodChannel       │
     │ ◄─────────────────────►│  请求/响应模式
     │                        │
     │   EventChannel         │
     │ ◄──────────────────────│  数据流/事件模式
     │                        │
     │ BasicMessageChannel    │
     │ ◄─────────────────────►│  基础消息模式
     │                        │

MethodChannel 调用流程

// Dart 端
final channel = MethodChannel('com.example/battery');
final batteryLevel = await channel.invokeMethod('getBatteryLevel');

// iOS 端 (Swift)
let channel = FlutterMethodChannel(name: "com.example/battery",
                                    binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { (call, result) in
  if call.method == "getBatteryLevel" {
    result(UIDevice.current.batteryLevel * 100)
  }
}

底层通信机制

  1. Dart 调用 invokeMethod,序列化参数
  2. 通过 JNI(Android)/ C API(iOS)传递二进制数据
  3. 原生端解码,执行方法,编码结果
  4. 返回给 Dart,反序列化

消息编解码器

  • StandardMessageCodec:支持基本类型
  • JSONMessageCodec:JSON 格式
  • BinaryCodec:原始字节

十、热重载原理

17. Hot Reload 的实现原理?

开发机                              设备/模拟器
   │                                    │
   │  1. 检测文件变化                    │
   ├──────────────────────────────────► │
   │                                    │
   │  2. 增量编译,生成 kernel 差异文件   │
   ├──────────────────────────────────► │
   │                                    │
   │  3. 通过 VM Service 发送到设备      │
   ├──────────────────────────────────► │
   │                                    │
   │  4. Dart VM 加载新代码              │
   │  5. 重建 Widget 树                  │
   │  6. 保留 State 状态                 │
   │                                    │

关键点

  • JIT 编译模式支持(Debug only)
  • 增量编译(只编译变化的代码)
  • Widget 重建,State 保留
  • 不会触发 initState()

Hot Reload vs Hot Restart

特性 Hot Reload Hot Restart
State 状态 保留 丢失
速度 快(~1s) 较慢(几秒)
全局变量 保留 重置
静态字段 保留 重置

十一、手势识别原理

18. Flutter 的手势系统是如何工作的?

事件分发流程

原生触摸事件
     │
     ▼
GestureBinding.handlePointerEvent()
     │
     ▼
命中测试 (Hit Test) ──► 确定触摸点下的所有 Widget
     │
     ▼
收集 HitTestEntry 列表
     │
     ▼
事件分发到所有命中的 Widget
     │
     ▼
手势竞技场 (Gesture Arena) 决定胜者

手势竞技场(Gesture Arena)

// 当多个手势识别器竞争时
GestureRecognizer A (Tap)     ──┐
GestureRecognizer B (DoubleTap) │ ──► Arena ──► 胜者执行
GestureRecognizer C (LongPress) ┘

竞争规则

  1. 识别器调用 addPointer 加入竞技场
  2. 确定手势后调用 accept()reject()
  3. 只有一个胜者时,胜者获胜
  4. 所有竞争者 reject,第一个 hold 的获胜

十二、动画原理

19. Flutter 动画的底层实现?

核心组件

AnimationController
     │
     ├── Ticker ──► 接收 vsync 信号(每帧一次)
     │
     ├── AnimationStatus ──► forward / reverse / completed / dismissed
     │
     └── Value (0.0 ~ 1.0) ──► 当前进度值

Ticker 与 SchedulerBinding

class Ticker {
  TickerCallback _onTick;

  void scheduleTick() {
    // 注册到下一帧回调
    SchedulerBinding.instance.scheduleFrameCallback(_tick);
  }

  void _tick(Duration elapsed) {
    _onTick(elapsed);  // 更新动画值
    if (isActive) scheduleTick();  // 继续下一帧
  }
}

动画流程

VSync 信号 (60fps)
     │
     ▼
SchedulerBinding.handleBeginFrame()
     │
     ▼
Ticker._tick() ──► AnimationController 更新 value
     │
     ▼
notifyListeners() ──► AnimatedBuilder 重建
     │
     ▼
下一帧...

十三、内存管理与垃圾回收

20. Dart 的垃圾回收机制?

分代式垃圾回收

┌─────────────────────────────────────┐
│            Dart Heap                │
│                                      │
│  ┌──────────────┐  ┌──────────────┐ │
│  │  New Space   │  │  Old Space   │ │
│  │  (年轻代)     │  │   (老年代)    │ │
│  │              │  │              │ │
│  │ 小对象/短命   │  │ 大对象/长寿   │ │
│  │ 频繁 GC      │  │ 较少 GC      │ │
│  │ Copy GC     │  │ Mark-Sweep   │ │
│  └──────────────┘  └──────────────┘ │
└─────────────────────────────────────┘

New Space(年轻代)

  • 采用 半空间复制算法
  • 分为 From 和 To 两个区域
  • GC 时存活对象复制到 To,From 清空
  • 速度快,但需要双倍空间

Old Space(老年代)

  • 采用 标记-清除-整理 算法
  • GC 过程:标记存活对象 → 清除死对象 → 整理内存碎片
  • 并发执行,减少停顿

对象晋升

  • 在 New Space 存活多次 GC 后晋升到 Old Space
  • 大对象直接分配到 Old Space

十四、图片加载原理

21. Flutter 图片加载与缓存机制?

Image.network(url)
     │
     ▼
ImageProvider ──► 创建 ImageStreamCompleter
     │
     ▼
ImageCache ──► 检查缓存(内存)
     │
     ├── 命中 ──► 直接返回 ImageInfo
     │
     └── 未命中 ──► 加载图片
                      │
                      ▼
               NetworkImage._loadAsync()
                      │
                      ▼
               HttpClient.get() 下载
                      │
                      ▼
               instantiateImageCodec() 解码
                      │
                      ▼
               缓存到 ImageCache
                      │
                      ▼
               返回 ImageInfo ──► RenderImage 绘制

ImageCache 策略

class ImageCache {
  // 限制缓存大小
  int _maximumSize = 1000;  // 最大数量
  int _maximumSizeBytes = 100 << 20;  // 最大字节数 (100MB)

  // LRU 缓存
  final Map<Object, _PendingImage> _pendingImages;
  final Map<Object, _CachedImage> _cache;
}

十五、Sliver 与列表原理

22. ListView 的懒加载原理?

Sliver 协议

// 约束(从 Viewport 传递给 Sliver)
class SliverConstraints {
  double scrollOffset;      // 已滚动的距离
  double remainingPaintExtent;  // 剩余可绘制区域
  double viewportMainAxisExtent;  // 视口大小
  // ...
}

// 几何信息(Sliver 返回给 Viewport)
class SliverGeometry {
  double scrollExtent;   // 总滚动范围
  double paintExtent;    // 绘制范围
  double layoutExtent;   // 布局范围
  // ...
}

懒加载实现

┌──────────────────────────────────────┐
│              Viewport                │
│  ┌────────────────────────────────┐  │
│  │    ┌─────────┐                 │  │
│  │    │ Item 0  │ ← 已回收        │  │
│  │    └─────────┘                 │  │
│  │    ┌─────────┐                 │  │
│  │    │ Item 1  │ ← 缓存区        │  │
│  │    └─────────┘                 │  │
│  ├──────────────── 可视区域 ────────┤  │
│  │    ┌─────────┐                 │  │
│  │    │ Item 2  │ ← 渲染          │  │
│  │    ├─────────┤                 │  │
│  │    │ Item 3  │ ← 渲染          │  │
│  │    ├─────────┤                 │  │
│  │    │ Item 4  │ ← 渲染          │  │
│  │    └─────────┘                 │  │
│  ├──────────────────────────────────┤  │
│  │    ┌─────────┐                 │  │
│  │    │ Item 5  │ ← 缓存区        │  │
│  │    └─────────┘                 │  │
│  │    ┌─────────┐                 │  │
│  │    │ Item 6  │ ← 未构建        │  │
│  │    └─────────┘                 │  │
│  └────────────────────────────────┘  │
└──────────────────────────────────────┘

SliverList 核心逻辑

// 只构建可视区域 + 缓存区的 children
void performLayout() {
  // 计算首个可见 child 的 index
  final int firstIndex = getMinChildIndexForScrollOffset(scrollOffset);

  // 布局可见的 children
  while (endScrollOffset < targetEndScrollOffset) {
    final child = obtainChild(index);  // 按需创建
    layoutChild(child);
    index++;
  }

  // 回收不可见的 children
  collectGarbage(leadingGarbage, trailingGarbage);
}

十六、Skia 与 Impeller

23. Skia 渲染引擎的工作原理?

Skia 渲染流程

Layer Tree (Dart)
     │
     ▼
Scene (C++)
     │
     ▼
Skia ──► 绘制命令转换为 GPU 指令
     │
     ▼
OpenGL / Metal / Vulkan
     │
     ▼
GPU 渲染
     │
     ▼
帧缓冲区 ──► 屏幕显示

Impeller(新渲染引擎)

  • 预编译 Shader,避免运行时编译卡顿
  • 更好的利用 Metal(iOS)和 Vulkan(Android)
  • 减少 Jank(掉帧)

十七、状态管理原理

24. Provider 的实现原理?

核心基于 InheritedWidget

class InheritedProvider<T> extends InheritedWidget {
  final T value;

  // 订阅依赖
  static T of<T>(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>()!.value;
  }

  @override
  bool updateShouldNotify(InheritedProvider<T> oldWidget) {
    return value != oldWidget.value;
  }
}

ChangeNotifier + Selector 优化

// ChangeNotifier 提供细粒度更新
class Counter extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();  // 通知监听者
  }
}

// Selector 只监听特定属性
Selector<Counter, int>(
  selector: (_, counter) => counter.count,
  builder: (_, count, __) => Text('$count'),
)

十八、编译与运行模式

25. JIT vs AOT 编译的区别?

特性 JIT (Just-In-Time) AOT (Ahead-Of-Time)
编译时机 运行时 构建时
启动速度 慢(需编译) 快(直接运行)
运行性能 可动态优化 固定优化
包体积 小(携带源码) 大(包含机器码)
调试支持 支持热重载 不支持
使用场景 Debug 模式 Release 模式

编译产物

Debug (JIT):
  └── app.dill (Kernel 字节码)

Release (AOT):
  └── app.so / App.framework (机器码)

十九、常见性能优化原理

26. const 构造函数的优化原理?

// 没有 const:每次 build 创建新实例
Widget build() {
  return Container(
    child: Text('Hello'),  // 新实例
  );
}

// 使用 const:编译期创建单例,复用
Widget build() {
  return const Container(
    child: Text('Hello'),  // 同一实例
  );
}

优化效果

  • 减少对象创建,降低 GC 压力
  • Widget 相同实例,Element 直接跳过更新
  • canUpdate 返回 true 且 widget == newWidget,无需 rebuild

27. shouldRebuild 与 shouldRepaint 的作用?

// Delegate 的 shouldRebuild
class MyDelegate extends SliverChildBuilderDelegate {
  @override
  bool shouldRebuild(MyDelegate oldDelegate) {
    return data != oldDelegate.data;  // 只在数据变化时重建
  }
}

// CustomPainter 的 shouldRepaint
class MyPainter extends CustomPainter {
  @override
  bool shouldRepaint(MyPainter oldDelegate) {
    return color != oldDelegate.color;  // 只在属性变化时重绘
  }
}

二十、Flutter Web 原理

28. Flutter Web 的两种渲染模式?

HTML Renderer

Widget ──► HTML Elements + CSS
         └► Canvas (部分组件)
  • 优点:包体积小,SEO 友好
  • 缺点:与原生 Flutter 渲染有差异

CanvasKit Renderer

Widget ──► Skia (WebAssembly) ──► WebGL Canvas
  • 优点:渲染一致性高,与移动端相同
  • 缺点:包体积大(~2MB),首次加载慢

总结

以上涵盖了 Flutter 底层原理的核心x'y'z题,重点包括:

  1. 三棵树架构:Widget/Element/RenderObject 的职责分离
  2. 渲染管线:Build → Layout → Paint → Composite → Raster
  3. Element 复用:基于 runtimeType 和 Key 的 diff 算法
  4. 事件循环:Microtask 优先于 Event 执行
  5. 平台通道:通过二进制消息实现 Dart 与原生通信
  6. 热重载:JIT 编译 + 增量代码注入 + Widget 重建
  7. 列表优化:Sliver 协议实现按需加载和回收
  8. 内存管理:分代 GC,年轻代复制算法 + 老年代标记清除

掌握这些底层原理,不仅能应对xyz,更能帮助你在实际开发中写出高性能的 Flutter 应用。

KVC / KVO 与 ivar / property 的底层关系

作者 汉秋
2026年1月25日 16:20

KVC / KVO 与 ivar / property 的底层关系

关键词:KVC、KVO、ivar、property、Runtime、isa-swizzling


一、为什么 KVC / KVO 一定要和 ivar / property 一起理解

在 Objective-C 中:

  • ivar 是数据的真实存储

  • property 是访问 ivar 的规则

  • KVC / KVO 本质上都是“访问规则之上的机制”

如果不理解 ivar 和 property,就一定理解不清 KVC / KVO


二、KVC(Key-Value Coding)底层原理

1️⃣ 什么是 KVC

KVC 是一种:

通过字符串 key 间接访问对象属性的机制

[person setValue:@"Hanqiu" forKey:@"name"];
NSString *name = [person valueForKey:@"name"];

2️⃣ KVC 的本质

  • 本质是 一套查找规则

  • 最终结果:

    • 要么调用方法

    • 要么直接访问 ivar

📌 KVC 并不依赖 property 是否存在


3️⃣ KVC 的 setValue:forKey: 查找顺序(重点)

当执行:

[person setValue:value forKey:@"name"];

查找顺序如下:

1. setName:
2. _setName:
3. +accessInstanceVariablesDirectly == YES ?
   3.1 _name
   3.2 _isName
   3.3 name
   3.4 isName
4. 调用 setValue:forUndefinedKey:

⚠️ 关键点

  • 默认 +accessInstanceVariablesDirectly 返回 YES
  • KVC 可以绕过 setter,直接改 ivar

4️⃣ KVC 的 valueForKey: 查找顺序

1. getName
2. name
3. isName
4. _name
5. _isName
6. 调用 valueForUndefinedKey:

5️⃣ KVC 与 ivar / property 的关系总结

场景 是否需要 property 是否访问 ivar
存在 setter
无 setter
无 ivar ❌(崩溃)

KVC 是“方法优先,ivar 兜底”的机制****


三、KVO(Key-Value Observing)底层原理

1️⃣ 什么是 KVO

KVO 是一种:

监听属性变化的观察机制

[person addObserver:self
         forKeyPath:@"name"
            options:NSKeyValueObservingOptionNew
            context:nil];

2️⃣ KVO 的本质(一句话)

KVO 监听的是 setter 的调用,而不是 ivar 的变化


3️⃣ KVO 的底层实现机制(核心)

当第一次添加观察者时,系统会:

  1. 动态生成一个子类(NSKVONotifying_XXX)
  2. 修改对象的 isa 指针(isa-swizzling)
  3. 在子类中重写 setter
Person
  ↑ isa
NSKVONotifying_Person

4️⃣ 重写的 setter 做了什么

伪代码如下:

- (void)setName:(id)value {
    [self willChangeValueForKey:@"name"];
    [super setName:value];
    [self didChangeValueForKey:@"name"];
}

👉 通知发生在 setter 内部


5️⃣ 为什么直接修改 ivar 不触发 KVO

_name = @"A";      // ❌ 不触发 KVO
self.name = @"B"; // ✅ 触发 KVO

原因:

  • ivar 赋值不走 setter
  • KVO 根本无法感知

四、KVO 与 property / ivar 的强关联关系

1️⃣ KVO 是否依赖 property?

情况 是否支持 KVO
有 setter
只有 ivar
Category property + Associated Object ⚠️(可行但危险)

📌 KVO 实际依赖的是 setter,而不是 property 关键字


2️⃣ 手动触发 KVO

如果你必须直接改 ivar:

[self willChangeValueForKey:@"name"];
_name = @"C";
[self didChangeValueForKey:@"name"];

五、KVC + KVO 联合场景分析(高频面试)

场景:用 KVC 修改属性,是否触发 KVO?

[person setValue:@"D" forKey:@"name"];

结论:

  • 如果最终调用 setter → ✅ 触发 KVO

  • 如果直接命中 ivar → ❌ 不触发

是否触发,取决于 KVC 查找路径


六、Runtime 视角看 KVC / KVO

1️⃣ KVC 使用的 Runtime 能力

  • objc_msgSend
  • class_getInstanceVariable
  • object_setIvar

2️⃣ KVO 使用的 Runtime 能力

  • objc_allocateClassPair
  • object_setClass
  • 动态方法重写

七、常见面试陷阱总结

❌ 误区 1:KVO 监听的是 ivar

❌ 错

✔ 监听的是 setter 的调用


❌ 误区 2:没有 property 就不能 KVO

❌ 错

✔ 只要有 setter 方法即可


❌ 误区 3:KVC 一定会触发 KVO

❌ 错

✔ 是否触发取决于是否调用 setter


八、一张关系总图(文字版)

           ┌──────────────┐
           │   property   │
           │ getter/setter│
           └──────┬───────┘
                  │
        KVO 监听   │ setter
                  ▼
               ivar(真实数据)
                  ▲
                  │
            KVC 兜底访问

九、终极总结

KVC 是“方法优先、ivar 兜底”的键值访问机制;KVO 是通过 isa-swizzling 重写 setter 来监听属性变化的机制,本质与 ivar 无关,只与 setter 是否被调用有关。


Objective-C 类结构全景解析

作者 汉秋
2026年1月25日 15:16

从 isa 到 cache,从方法列表到属性列表

一次把「一个 Class 里到底装了什么」讲清楚

在 Runtime 视角下,Objective-C 的 Class 并不是一个抽象概念

而是一块结构严谨、职责清晰的内存结构

本文将围绕 Class 的真实组成,系统讲解:

  • isa 指针到底指向哪
  • cache 为什么决定性能
  • 方法列表、属性列表、协议列表各自干什么
  • 一个类里,除了方法,还存了哪些东西

一、先给结论:一个 Class 里有什么?

从 Runtime 角度,一个类(Class)至少包含以下几大部分:

Class
 ├─ isa
 ├─ superclass
 ├─ cache
 ├─ method list
 ├─ property list
 ├─ protocol list
 ├─ ivar list
 ├─ class_rw_t / class_ro_t
 └─ 元类(Meta Class

下面我们逐一展开。


二、isa —— 类的“身份指针”

1. isa 是什么

  • isa 是一个指针
  • 对象的 isa → Class
  • 类的 isa → Meta Class
instance ──isa──▶ Class ──isa──▶ Meta Class

在 arm64 以后:

  • isa 是 非纯指针(non-pointer isa)

  • 高位存储了:

    • 引用计数信息

    • weak 标志

    • 是否有关联对象

逻辑语义没有变化


三、cache —— 方法调用的性能核心

1. cache 是什么

  • cache 是一个 SEL → IMP 的映射表
  • 存在于 Class 中
  • 用于加速方法查找
cache
 ├─ bucket[SEL → IMP]
 └─ mask / occupied

2. cache 在方法查找中的位置

objc_msgSend 查找顺序:

1️⃣ cache
2️⃣ method list
3️⃣ superclass → 重复 12

cache 永远是第一站。


3. cache 的填充时机

  • cache 是 懒加载

  • 第一次方法调用:

    • cache 未命中

    • method list 找到 IMP

    • 写入 cache

之后同一个 SEL:

直接命中 cache


4. cache 为什么不区分类?

cache 的 key 是:

SEL

但 cache 属于 某一个 Class

因此:

A.fooA 的 cache
B.fooB 的 cache

即使 SEL 相同,也互不干扰。


四、method list —— 方法的“原始数据源”

1. method list 是什么

  • method list 是一个数组
  • 每一项是一个 method_t
method_t
 ├─ SEL name
 ├─ IMP imp
 └─ const char *types

也就是我们熟悉的三要素:

SEL + IMP + Type Encoding


2. method list 的来源

method list 由以下部分合并而来:

  • 类本身实现的方法

  • Category 中的方法

⚠️ Category 的方法:

  • 后加载、前插入
  • 因此可以覆盖原方法

五、property list —— 属性的声明信息

1. property list 是什么

  • 属性列表存的是 声明信息
  • 不是 ivar
  • 不是 getter / setter 的实现
objc_property_t
 ├─ name
 └─ attributes (copy, nonatomic, strong ...)

2. property list 干什么用

  • Runtime 反射

  • KVC / KVO

  • 自动序列化 / ORM

但注意:

方法调用完全不依赖 property list


六、ivar list —— 实例变量的真实布局

1. ivar list 是什么

  • ivar list 描述的是:

    • 成员变量
    • 内存偏移
    • 类型
ivar_t
 ├─ name
 ├─ type
 └─ offset

2. ivar list 与对象内存

instance memory
 ├─ isa
 ├─ ivar1
 ├─ ivar2
  • ivar list 决定对象内存布局
  • 子类 ivar 会追加在父类之后

七、protocol list —— 协议信息

1. protocol list 是什么

  • 存储类遵循的协议

  • 包含:

    • 必选方法

    • 可选方法

主要用于:

  • conformsToProtocol:
  • Runtime 查询

八、class_rw_t / class_ro_t —— 可变与只读区

1. class_ro_t(只读)

  • 编译期确定

  • 存储:

    • 原始方法列表
    • ivar list
    • property list

2. class_rw_t(可写)

  • 运行时动态生成

  • 存储:

    • Category 方法

    • 动态添加的方法

这也是 Category 能“修改类行为”的根本原因。


九、Meta Class —— 类方法的归宿

1. Meta Class 是什么

  • 类方法不是存在 Class 里
  • 而是存在 Meta Class 的 method list 中
[Class foo]
 → 查找 Meta Class 的 cache / method list

十、一张完整 Runtime 结构图(逻辑)

instance
  └─ isa → Class
              ├─ isa → Meta Class
              ├─ superclass
              ├─ cache
              ├─ method list
              ├─ property list
              ├─ ivar list
              ├─ protocol list
              └─ class_rw_t / class_ro_t

十一、终极理解(非常重要)

  • 方法调用性能 = cache 决定

  • 行为修改能力 = method list + rw 区

  • 内存布局 = ivar list 决定

  • 反射能力 = property / protocol 提供

它们各司其职,互不混乱。


十二、一句话总结

Class 是 Runtime 的作战单元:

cache 决定快慢,method list 决定行为,

ivar 决定内存,property 决定语义,

isa 决定你是谁。

理解这一层结构,

你就真正理解了 Objective-C Runtime 的“骨架”。

iOS 常用调试工具大全-打造你的调试武器库

作者 sweet丶
2026年1月25日 00:55

还记得你第一次使用NSLog(@"Hello, World!")的时刻吗?那是调试的起点。但随着应用复杂度呈指数级增长,我们需要的工具也经历了革命性进化:

  • 第一代:基础输出(NSLogprint
  • 第二代:图形化界面(Xcode调试器、Instruments)
  • 第三代:运行时动态调试(FLEX、Lookin)
  • 第四代:智能化监控(性能追踪、自动化检测)

今天,一个成熟的iOS开发者工具箱中,至少需要掌握3-5种核心调试工具,它们就像外科医生的手术刀——精准、高效、各有所长。

一、运行时调试工具

1. FLEX (Flipboard Explorer)

功能最全的运行时调试套件,集成后可以测试期间随时开启\关闭工具条,比如设置摇一摇后启动。

优点: 功能全面,无需连接电脑
缺点: 内存占用稍大
场景: 日常开发调试UI问题排查
GitHub: https://github.com/FLEXTool/FLEX?tab=readme-ov-file

主要功能:

  • 手机上检查和修改层次结构中的视图。
  • 查看对象内存分配,查看任何对象的属性和ivar,动态修改许多属性和ivar,动态调用实例和类方法。
  • 查看详细的网络请求历史记录,包括时间、标头和完整响应。
  • 查看系统日志消息(例如,来自NSLog)。
  • 查看沙盒中的文件,查看所有的bundle和资源文件,浏览文件系统中的SQLite/Rerm数据库。
  • 动态查看和修改NSUserDefaults值。

2. Lookin - 腾讯出品

3D视图层级工具,类Xcode Inspector和Reveal。相比Xcode中查看图层的优势有两个:

  • 独立于Xcode运行,不会被Xcode阻断,能显示view的被引用的属性名。
  • 集成了'LookinServer'库的APP启动后,在Mac上启动Lookin后即可刷新显示当前图层。(真机需连接电脑后才展示)
// 集成步骤一:官网下载lookin;
-  官网: https://lookin.work
-  GitHub: https://github.com/QMUI/LookinServer

// 集成步骤二:
CocoaPods安装:
// 1.如果是OC工程
pod 'LookinServer', :configurations => ['Debug']
// 2.如果是OC工程
// 在 iOS 项目的 Podfile 中 添加 “Swift” 这个 Subspec
pod 'LookinServer', :subspecs => ['Swift'], :configurations => ['Debug']
// 或者添加 “SwiftAndNoHook”这个 Subspec 也行
pod 'LookinServer', :subspecs => ['SwiftAndNoHook'], :configurations => ['Debug']

二、网络调试工具

1. Proxyman - 现代网络调试神器

// 官网: https://proxyman.io
// 特点:
 现代UI,操作流畅
 HTTPS解密(无需安装证书到系统)
 重放修改拦截请求
 Map Local/Map Remote功能
 脚本支持(JavaScript)
 支持Apple Silicon

使用场景:
 API接口调试
 图片/资源请求优化
 模拟慢速网络
 修改响应数据测试

2. Charles - 老牌网络代理

// 官网: https://www.charlesproxy.com
// 特点:
 功能极其全面
 跨平台支持
 脚本功能强大(Charles Proxy Script)
 带宽限制断点调试
 支持HTTP/2HTTP/3

设置步骤:
1. 安装Charles
2. 在iOS设备设置代理
3. 安装Charles根证书
4. 信任证书(设置通用关于证书信任设置)

// 常用功能:
 Breakpoints(请求拦截修改)
 Rewrite(规则重写)
 Map Local(本地文件映射)
 Throttle(网络限速)

3. mitmproxy - 开源命令行工具

# 官网: https://mitmproxy.org
# 特点:
✅ 完全开源免费
✅ 命令行操作,适合自动化
✅ 脚本扩展(Python)
✅ 支持透明代理

# 安装:
brew install mitmproxy

# 使用:
# 启动代理
mitmproxy --mode transparent --showhost

# iOS设置:
# 1. 安装证书: mitm.it
# 2. 配置Wi-Fi代理

三、UI/布局调试工具

1. Reveal - 专业的UI调试工具

// 官网: https://revealapp.com
// 特点:
 实时3D视图层级
 详细的AutoLayout约束查看
 内存图查看器
 支持SwiftUI预览
 强大的筛选和搜索

// 集成:
// 方式1: 通过Reveal Server框架
pod 'Reveal-SDK', :configurations => ['Debug']

// 方式2: LLDB加载(无需集成代码)
(lldb) expr (void)[[NSClassFromString(@"IBARevealLoader") class] revealApplication];

// 价格: 付费(提供免费试用)

2. InjectionIII - 热重载神器

// GitHub: https://github.com/johnno1962/InjectionIII
// 特点:
 代码修改后实时生效
 无需重新编译运行
 支持Swift和Objective-C
 保留应用状态

// 安装:
# App Store搜索 "InjectionIII"

// 配置:
1. 下载安装InjectionIII
2. 在AppDelegate中配置:
#if DEBUG
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
#endif

3. 项目添加文件监视:
// 在InjectionIII App中添加项目路径

四、性能调试工具

1. Xcode Instruments - 官方性能分析套件

// 核心工具集:
┌─────────────────────────────────────┐
          Xcode Instruments          
├─────────────────────────────────────┤
 Time Profiler    # CPU使用分析       
 Allocations      # 内存分配分析      
 Leaks           # 内存泄漏检测      
 Network         # 网络活动分析      
 Energy Log      # 电量消耗分析      
 Metal System    # GPU性能分析       
 SwiftUI         # SwiftUI性能分析   
└─────────────────────────────────────┘

// 使用技巧:
1. 录制时过滤系统调用:
   Call Tree:  Hide System Libraries
               Invert Call Tree
               Flattern Recursion

2. 内存图调试:
   Debug Memory Graph按钮
   查看循环引用内存泄漏

3. 使用Markers:
   import os
   let log = OSLog(subsystem: "com.app", category: "performance")
   os_signpost(.begin, log: log, name: "Network Request")
   // ... 操作
   os_signpost(.end, log: log, name: "Network Request")

2. MetricKit - 线上性能监控框架

// Apple官方性能数据收集框架
import MetricKit

class MetricKitManager: MXMetricManagerSubscriber {
    static let shared = MetricKitManager()
    
    private init() {
        let manager = MXMetricManager.shared
        manager.add(self)
    }
    
    func didReceive(_ payloads: [MXMetricPayload]) {
        // 接收性能数据
        for payload in payloads {
            print("CPU: \(payload.cpuMetrics)")
            print("内存: \(payload.memoryMetrics)")
            print("启动时间: \(payload.launchMetrics)")
            print("磁盘IO: \(payload.diskIOMetrics)")
        }
    }
    
    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        // 接收诊断数据(崩溃、卡顿等)
    }
}

// 需要用户授权,适合生产环境监控

3. Tracy - 腾讯开源的性能监控

// GitHub: https://github.com/Tencent/tracy
// 特点:
 卡顿监控(主线程阻塞检测)
 内存泄漏检测
 大对象分配监控
 网络性能监控
 崩溃收集

// 集成:
pod 'Tracy', :configurations => ['Debug']

// 使用:
Tracy.start()
// 自动监控各种性能指标

五、内存/崩溃调试工具

1. MLeaksFinder - 腾讯出品的内存泄漏检测

// GitHub: https://github.com/Tencent/MLeaksFinder
// 特点:
 自动检测视图控制器内存泄漏
 无需编写任何代码
 支持自定义白名单
 精准定位泄漏对象

// 原理:
// 监听UIViewController的pop/dismiss
// 延迟检查是否仍然存在

// 集成:
pod 'MLeaksFinder'

// 自定义配置:
// 1. 添加白名单
[NSClassFromString(@"WhiteListClass") class]

// 2. 忽略特定泄漏
[MLeaksFinder addIgnoreClass:[IgnoreClass class]]

2. FBRetainCycleDetector - Facebook循环引用检测

// GitHub: https://github.com/facebook/FBRetainCycleDetector
// 特点:
 检测Objective-C对象的循环引用
 支持检测NSTimer的强引用
 可集成到单元测试中
 Facebook内部广泛使用

// 使用:
let detector = FBRetainCycleDetector()
detector.addCandidate(myObject)
let cycles = detector.findRetainCycles()

// 输出格式化的循环引用链
for cycle in cycles {
    print(FBRetainCycleDetectorFormatter.format(cycle))
}

3. KSCrash - 强大的崩溃收集框架

// GitHub: https://github.com/kstenerud/KSCrash
// 特点:
 捕获所有类型崩溃(OC异常C++异常Mach异常等)
 生成完整的崩溃报告
 支持符号化
 可自定义上报服务器

// 集成:
pod 'KSCrash'

// 配置:
import KSCrash

let installation = makeEmailInstallation("crash@company.com")
installation.addConditionalAlert(withTitle: "Crash Detected",
                                message: "The app crashed last time")
KSCrash.shared().install()

// 高级功能:
// 1. 用户数据记录
KSCrash.shared().userInfo = ["user_id": "123"]

// 2. 自定义日志
KSCrash.shared().log.error("Something went wrong")

// 3. 监控卡顿
KSCrash.shared().monitorDeadlock = true

六、日志调试工具

1. CocoaLumberjack - 专业日志框架

// GitHub: https://github.com/CocoaLumberjack/CocoaLumberjack
// 特点:
 高性能日志记录
 多日志级别(Error, Warn, Info, Debug, Verbose)
 多种输出目标(Console, File, Database)
 日志轮转和清理
 支持Swift和Objective-C

// 集成:
pod 'CocoaLumberjack/Swift'

// 配置:
import CocoaLumberjackSwift

// 控制台日志
DDLog.add(DDOSLogger.sharedInstance)

// 文件日志
let fileLogger = DDFileLogger()
fileLogger.rollingFrequency = 60 * 60 * 24 // 24小时
fileLogger.logFileManager.maximumNumberOfLogFiles = 7
DDLog.add(fileLogger)

// 使用:
DDLogError("错误信息")
DDLogWarn("警告信息")
DDLogInfo("普通信息")
DDLogDebug("调试信息")
DDLogVerbose("详细信息")

// 上下文过滤:
let context = 123
DDLogDebug("带上下文的消息", context: context)

2. SwiftyBeaver - Swift专用日志框架

// GitHub: https://github.com/SwiftyBeaver/SwiftyBeaver
// 特点:
Swift实现
 彩色控制台输出
 多种目的地(Console, File, Cloud)
 平台同步(macOS App)
 支持emoji和格式化

// 使用:
import SwiftyBeaver
let log = SwiftyBeaver.self

// 添加控制台目的地
let console = ConsoleDestination()
console.format = "$DHH:mm:ss$d $L $M"
log.addDestination(console)

// 添加文件目的地
let file = FileDestination()
file.logFileURL = URL(fileURLWithPath: "/path/to/file.log")
log.addDestination(file)

// 日志级别:
log.verbose("详细")    // 灰色
log.debug("调试")      // 绿色
log.info("信息")       // 蓝色
log.warning("警告")    // 黄色
log.error("错误")      // 红色

3. XCGLogger - 功能丰富的日志框架

// GitHub: https://github.com/DaveWoodCom/XCGLogger
// 特点:
 高度可配置
 支持日志过滤
 自定义日志目的地
 自动日志轮转
 详细的文档

// 使用:
import XCGLogger

let log = XCGLogger.default

// 配置
log.setup(level: .debug,
          showLogIdentifier: false,
          showFunctionName: true,
          showThreadName: true,
          showLevel: true,
          showFileNames: true,
          showLineNumbers: true,
          showDate: true)

// 自定义过滤器
log.filters = [
    Filter.Level(from: .debug),  // 只显示.debug及以上
    Filter.Path(include: ["ViewController"], exclude: ["ThirdParty"])
]

log.debug("调试信息")
log.error("错误信息")

七、自动化调试工具

1. Fastlane - 自动化工具集

# 官网: https://fastlane.tools
# 特点:
✅ 自动化构建、测试、部署
✅ 丰富的插件生态
✅ 与CI/CD深度集成
✅ 跨平台支持

# 常用命令:
fastlane screenshots    # 自动截图
fastlane beta          # 发布测试版
fastlane release       # 发布正式版
fastlane match         # 证书管理

# 集成调试功能:
lane :debug_build do
  # 1. 设置调试配置
  update_app_identifier(
    app_identifier: "com.company.debug"
  )
  
  # 2. 启用调试功能
  update_info_plist(
    plist_path: "Info.plist",
    block: proc do |plist|
      plist["FLEXEnabled"] = true
      plist["NSAllowsArbitraryLoads"] = true
    end
  )
  
  # 3. 构建
  gym(
    scheme: "Debug",
    export_method: "development"
  )
end

2. slather - 代码覆盖率工具

# GitHub: https://github.com/SlatherOrg/slather
# 特点:
✅ 生成代码覆盖率报告
✅ 支持多种输出格式(html, cobertura, json)
✅ 与CI集成
✅ 过滤第三方库代码

# 安装:
gem install slather

# 使用:
# 1. 运行测试并收集覆盖率
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 14' -enableCodeCoverage YES

# 2. 生成报告
slather coverage --html --show --scheme MyApp MyApp.xcodeproj

# 3. 在Jenkins中集成
slather coverage --input-format profdata --cobertura-xml --output-directory build/reports MyApp.xcodeproj

八、特殊场景调试工具

1. SparkInspector - 实时对象监控

// 官网: https://sparkinspector.com
// 特点:
 实时监控所有对象实例
 查看对象属性变化
 方法调用追踪
 内存泄漏检测

// 集成:
// 1. 下载Spark Inspector应用
// 2. 集成框架到项目
// 3. 通过Spark Inspector连接调试

// 适用场景:
 复杂的对象关系调试
 观察模式数据流
 内存泄漏定位

3. LLDB - 底层调试神器

# Xcode内置,但功能极其强大
# 常用命令:

# 1. 查看变量
(lldb) po variable
(lldb) p variable
(lldb) v variable

# 2. 修改变量
(lldb) expr variable = newValue

# 3. 调用方法
(lldb) expr [self doSomething]
(lldb) expr self.doSomething()

# 4. 断点命令
(lldb) breakpoint set -n "[ClassName methodName]"
(lldb) breakpoint command add 1  # 为断点1添加命令
> po $arg1
> continue
> DONE

# 5. 内存查看
(lldb) memory read 0x12345678
(lldb) memory write 0x12345678 0x42

# 6. 自定义LLDB命令
(lldb) command regex rlook 's/(.+)/image lookup -rn %1/'
(lldb) rlook methodName

# 7. Swift特定命令
(lldb) frame variable -L  # 显示局部变量
(lldb) type lookup String # 查看类型信息

九、工具矩阵

需求场景 推荐工具 理由
日常开发调试 FLEX + Proxyman 功能全面,无需额外环境
UI/布局问题 Lookin + Reveal 3D视图,实时修改
性能优化 Xcode Instruments + Tracy 官方工具+线上监控
内存泄漏 MLeaksFinder + FBRetainCycleDetector 自动检测+深度分析
网络调试 Proxyman/Charles 功能专业,操作友好
日志管理 CocoaLumberjack + SwiftyBeaver 功能强大+美观输出
自动化 Fastlane + slather 流程自动化+质量监控
底层调试 LLDB + InjectionIII 深度控制+热重载

团队规范建议

# iOS团队调试工具规范

## 必装工具(所有开发者)
1. Proxyman/Charles - 网络调试
2. Lookin - UI调试  
3. InjectionIII - 热重载

## 项目集成(Podfile)
```ruby
target 'MyApp' do
  # 调试工具(仅Debug)
  pod 'FLEX', :configurations => ['Debug']
  pod 'CocoaLumberjack', :configurations => ['Debug']
  pod 'MLeaksFinder', :configurations => ['Debug']
end

总结

核心建议:

  1. 不要过度依赖单一工具 - 不同工具有不同适用场景
  2. 掌握核心原理 - 理解工具背后的工作原理比单纯使用更重要
  3. 建立个人调试工具箱 - 根据习惯组合适合自己的工具集
  4. 关注新工具发展 - iOS开发工具生态在持续进化
  5. 重视自动化 - 将重复调试工作自动化,提高效率

终极目标: 快速定位问题 → 深入分析原因 → 有效解决问题

这些工具大多数都有免费版本或开源版本,建议从最常用的几个开始,逐步建立自己的调试能力体系。

掌握这些工具,不是为了炫耀技术,而是为了让你的代码更健壮,让你的用户更满意,让你自己在深夜加班时少掉几根头发。

昨天以前iOS

介绍几款单人桌游

作者 云风
2026年1月24日 21:08

上个月我花了不少时间在 dotAge 这个游戏中。我很喜欢这种通过精算规划应对确定风险的感觉。由于 dotAge 有很强的欧式桌游的设计感,所以我在桌游中尝试了一些有类似设计元素的单人游戏。

我感觉体验比较接近的有 Voidfall (2023) 和 Spirit Island (2017) 。因为灵魂岛(spirit island )更早一些,而且 steam 上有官方的电子版,bgg 上总体排名也更高,所以我在上面花的时间最多。

这两个游戏的特点都是确定性战斗机制,即在战斗时完全没有投骰这类随机元素介入。在开战之前,玩家就能完全确定战斗结果。战斗只是规划的一环,考虑的是该支付多少成本或许多大的收益。而且灵魂岛作为一款卡牌驱动的游戏,完全排除了抽牌的随机性,只在从市场上加入新牌(新能力)时有一点随机性。一旦进入玩家牌组,什么时候什么卡牌可以使用,完全是在玩家规划之内的。这非常接近 dotAge 中规划应对危机时的体验。

灵魂岛的背景像极了电影 Avatar :岛的灵魂通过原住民发挥神力赶走了外来殖民者。每个回合,把神力的成长、发威(玩家行动)和殖民者(系统危机)的入侵、成长和破坏以固定次序循环。其中,殖民者的入侵在版图上的地点有轻微的随机性,但随后的两个回合就在固定规则下,在同一地点地成长和破坏(玩家需要处理的危机)。扮演岛之灵魂的玩家可以选择到破坏之刻去那个地块消除危机,在此之前玩家有两个回合可以准备;也可以提前在殖民者成长之前将其消灭在萌芽之中,但这给玩家的准备时间更少,却往往意味着更小的消耗;还可以暂时承受损失,集中力量于它处或更快的发展神力。游戏提供给玩家的策略选择着实丰富。

法术卡并不多,每个神灵只有几张专属的固定初始能力卡,其它所有的能力都是所有神灵共用,让玩家自由组合的。每当玩家选择成长时,可以随机 4 选 1 。不像卡牌构筑类游戏会有很多卡片,这个游戏总体卡片不多,每张都有决定性作用。每个回合通常也只能打出一两张 张,待到可以一回合可以打出三张甚至四张(很少见)时,已经进入游戏后期在贯彻通关计划了。法力点数用来支付每张卡的打出费用这个设计粗看和卡牌构筑游戏类似,但实际玩下来感觉有挺大的不同。灵魂岛每个回合未用完的法力点并不会清零,而会留置到下回合使用且没有上限。从玩家规划角度看,更像是需要玩家去规划整局游戏的法力点分配。精确的打出每个回合的很少的几张卡片。因为抽回打过的法术卡并不随机,玩家便要在法力成长和法术重置上做明确选择。挑选法术序列变成了精密规划的一环。

在 dotAge 中,版图是需要规划的,玩家需要取舍每个格子上到底放什么建筑以达到连锁功效最大化。而在灵魂岛中,每张法术会提供一些元素,同一回合激活的元素组合可以给法术本身效果加成。我觉得这两个设定有异曲同工之秒。我在思考游戏设计时,受 dotAge 和 Dawnmaker 的影响,总觉得需要在版图的位置上做文章才好体现出建筑的组合,玩过灵魂岛才发现,其实单靠卡牌不考虑版图布局其实也能实现类似的体验:几张特定的法术卡组合在同一回合打出会对单一法术有额外加成,而这种组合可以非常丰富。去掉随机抽卡机制,让玩家可以 100% 控制自己牌库中的组合选择;而且总牌量很少,每个回合出牌数及其有限(受单回合出牌数及法力点双重限制),让发牌组合必须有所取舍。这像极了我在 dotAge 的狭小地图空间中布局建筑的体验,这个格子放了这个,那个建筑就得不到加成。

但受限于桌游,灵魂岛的游戏体验和 dotAge 差别还是很大的。我玩了(并击败了)多级难度的灵魂岛,难度越高差异越明显。桌游必须要求短回合快节奏,这让游戏规划的容错性大大降低。dotAge 一局游戏可以玩一整天,即使是超高难度,也允许玩家犯点小错误。由于电子游戏可以把元素做得更多,让机器负责运转规则,单点的数值关系就可以更简单直白。而灵魂岛这种需要在很少的行动中体现复杂计划的多样性,那些法术的真正功效就显得过于晦涩:虽然法术字面上的解释并不负责,但理解每个法术背后的设计逻辑,在游戏中做出准确的决策要难得多。

我在标准难度下,玩了十几盘才真正胜利过一次灵魂岛。之后每增加一点难度,感觉挑战就大了不少;反观 dotAge 我在第二盘就领会了游戏得玩法而通关,困难难度也并未带来太大的挫折感。但现在往上加难度玩灵魂岛,我还是心有余悸,不太把握得住。而且直到现在我都没敢尝试 2 个神灵以上的组合玩法,那真是太烧脑了。难怪实体版桌游都是多人合作,而不是 1 控 2 去玩。


Voidfall 从游戏结构上更接近 dotAge 一点。它完全没有战斗,就是纯跑分。只要你跑分速度超过了系统规则,就胜利了。dotAge 几乎就是这个框架:玩家需要在疾病、恐惧、温度和自然四个领域积累积分抵抗系统产生的四类危机。在每次危机来领前做好准备,也就是积累产生对应领域积分的能力。

但无论是 spirit island 还是 voidfall 都没有 dotAge 中最重要的工人分配机制。从游戏机制角度看,dotAge 更像是电子化的 Agricola (2007) 农场主。因为农场主在桌游玩家中太经典,几乎所有桌游玩家都玩过,这里就不多作介绍了。虚空陨落(voidfall)则是一个比较新的游戏,值得简单讲一下。它没有官方电子版,但在 Tabletop Simulator 中有 mod 可以玩。

和 dotAge 的四个领域有点类似,voidfall 中玩家有军事、经济、科技、政治四个方向的议程可以选择。获得对应的议程卡后,就可以大致确定一个得分路线。不同的路线同时影响着玩家当局游戏的游戏过程。

桌游的流程不会设计的太长,在 voidfall 中只设计了三个阶段,每个阶段有一张事件卡,引导玩家的得分手段。这些事件的效果是可预测的,这和 dotAge 的预言很像。三个阶段也和 dotAge 的季节交替末日来临类似:用规则控制游戏节奏,明确的区分游戏不同阶段要作的事情。一开始生产建设、然后扩张战斗、最后将得分最大化。

我没有特别仔细的玩这个游戏,但从粗浅的游戏体验看,还是颇为喜欢的。过几天会多试试。


我对“确定性战斗机制”这点其实没有特别的偏爱。基于骰子的风险管理机制也很喜欢。

前两年就特别关注过 ISS Vanguard (2022) 这个游戏。最近又(在 Tabletop Simulator 上)玩了一下 Robinson Crusoe: Adventures on the Cursed Island (2012) 和 Civolution (2024) 。这几个游戏都特别重,几句话比较难说清楚,而且我游戏时长也不多,这里就不展开了。

顺便说一句,同样是鲁宾逊的荒岛求生题材的单人桌游 Friday (2011) 是一个非常不错的轻量游戏。如果不想花太多时间在重度游戏上,它非常值得一玩。这是一款及其特别的卡牌构筑类游戏,整个游戏机制不多见的把重点放在卡组瘦身上:即玩家更多考虑的是如何有效的把初始卡组中效率低效的卡精简掉。

游戏上手容易,大约花 5 分钟就能读完规则;设置成本极低,只使用一组卡片;但却颇有难度,我差不多在玩了 20 盘之后才找到胜利的诀窍。淘宝上就可以买到中文版(中文名:星期五),推荐一试。

iOS客户端开发基础知识——写文件避“坑”指南(二)

作者 zhangjiezhi_
2026年1月24日 18:37

更多精彩文章,欢迎关注作者微信公众号:码工笔记

一、背景 & 问题

上一篇文章讲过,在iOS、macOS平台上,要保证新写入的文件内容成功落盘,需要调用fcntl(fd, FULL_SYNC)(注:开源chromium里也是这么做的[1]):

FULL_SYNC

Does the same thing as fsync(2) then asks the drive to flush all buffered data to the permanent storage device (arg is ignored). As this drains the entire queue of the device and acts as a barrier, data that had been fsync'd on the same device before is guaranteed to be persisted when this call returns. This is currently implemented on HFS, MS-DOS (FAT), Universal Disk Format (UDF) and APFS file systems. The operation may take quite a while to complete. Certain FireWire drives have also been known to ignore the request to flush their buffered data.

从上面man page的描述可以看出,FULL_SYNC是将设备unified buffer里的数据全部强制落盘,因为buffer中的数据可能不只包含刚刚写入的,可能还包含了之前写入的数据,虽然达到了持久化的目的,但时间不可控,可能会耗时很长,严重影响应用性能。

有没有什么优化方式呢?

二、F_BARRIERFSYNC

从应用开发者的角度,很多场景下并不需要这么强的落盘保证,大多数场景下,如果能保证写入顺序,也即先写入数据A,后写入数据B,如果后续读数据时读到了数据B,则A也一定存在,应用侧就可以自己做数据完整性检查了,从而可以做兜底逻辑。这样一来既能减少强制落盘对性能的影响,又能保证数据的完整性。

fcntlF_BARRIERFSYNC这个选项就是为了解决这个问题的。先看一下man page说明:

F_BARRIERFSYNC

Does the same thing as fsync(2) then issues a barrier command to the drive (arg is ignored). The barrier applies to I/O that have been flushed with fsync(2) on the same device before. These operations are guaranteed to be persisted before any other I/O that would follow the barrier, although no assumption should be made on what has been persisted or not when this call returns. After the barrier has been issued, operations on other FDs that have been fsync'd before can still be re-ordered by the device, but not after the barrier. This is typically useful to guarantee valid state on disk when ordering is a concern but durability is not. A barrier can be used to order two phases of operations on a set of file descriptors and ensure that no file can possibly get persisted with the effect of the second phase without the effect of the first one. To do so, execute operations of phase one, then fsync(2) each FD and issue a single barrier. Finally execute operations of phase two. This is currently implemented on HFS and APFS. It requires hardware support, which Apple SSDs are guaranteed to provide.

调用此方法后,系统虽不能保证数据是否真正落盘成功,但能保证写入的顺序,也即如果后写入的数据成功落盘,则先写入的数据一定已经落盘。

注:Apple的SSD都支持。

Apple的官方建议[2]是:如果有强落盘需求,可以用FULL_SYNC,但这会导致性能下降及设备损耗,如果只需要保证写入顺序,则建议用F_BARRIERFSYNC。

Minimize explicit storage synchronization

Writing data on iOS adds the data to a unified buffer cache that the system then writes to file storage. Forcing iOS to flush pending filesystem changes from the unified buffer can result in unnecessary writes to the disk, degrading performance and increasing wear on the device. When possible, avoid calling fsync(_:), or using the fcntl(_:_:) F_FULLFSYNC operation to force a flush.

Some apps require a write barrier to ensure data persistence before subsequent operations can proceed. Most apps can use the fcntl(_:_:) F_BARRIERFSYNC for this.

Only use F_FULLFSYNC when your app requires a strong expectation of data persistence. Note that F_FULLFSYNC represents a best-effort guarantee that iOS writes data to the disk, but data can still be lost in the case of sudden power loss.

三、例子:SQLite主线的问题和苹果的优化

SQLite是移动端最常用的文件数据库,读写文件是其功能的基石。SQLite是如何实现落盘的呢?看看SQLite仓库主线逻辑[3]:

#elif HAVE_FULLFSYNC
  if( fullSync ){
    rc = osFcntl(fd, F_FULLFSYNC, 0);
  }else{
    rc = 1;
  }
  /* If the FULLFSYNC failed, fall back to attempting an fsync().
  ** It shouldn't be possible for fullfsync to fail on the local
  ** file system (on OSX), so failure indicates that FULLFSYNC
  ** isn't supported for this file system. So, attempt an fsync
  ** and (for now) ignore the overhead of a superfluous fcntl call.
  ** It'd be better to detect fullfsync support once and avoid
  ** the fcntl call every time sync is called.
  */
  if( rc ) rc = fsync(fd);

#elif defined(__APPLE__)
  /* fdatasync() on HFS+ doesn't yet flush the file size if it changed correctly
  ** so currently we default to the macro that redefines fdatasync to fsync
  */
  rc = fsync(fd);

如果开了PRAGMA fullsync = ON,也是使用了F_FULLSYNC来保证写入成功。没开的话是使用fsync,这里应该是有问题的。

那iOS的libsqlite是怎么做的呢?这个库苹果没有开源,只能逆向看一下,搜搜相关的几个方法,应该在这一段汇编这里:

                                    loc_1b0d62f40:
00000001b0d62f40 682240F9               ldr        x8, [x19, #0x40]             ; CODE XREF=sub_1b0d62d34+444
00000001b0d62f44 880000B4               cbz        x8, loc_1b0d62f54

00000001b0d62f48 080140F9               ldr        x8, [x8]
00000001b0d62f4c 08A940B9               ldr        w8, [x8, #0xa8]
00000001b0d62f50 28FDFF35               cbnz       w8, loc_1b0d62ef4

                                    loc_1b0d62f54:
00000001b0d62f54 280C0012               and        w8, w1, #0xf                 ; CODE XREF=sub_1b0d62d34+528
00000001b0d62f58 1F0D0071               cmp        w8, #0x3
00000001b0d62f5c A80A8052               mov        w8, #0x55
00000001b0d62f60 08019F1A               csel       w8, w8, wzr, eq
00000001b0d62f64 69024239               ldrb       w9, [x19, #0x80]
00000001b0d62f68 3F011F72               tst        w9, #0x2
00000001b0d62f6c 69068052               mov        w9, #0x33
00000001b0d62f70 0101891A               csel       w1, w8, w9, eq
00000001b0d62f74 741A40B9               ldr        w20, [x19, #0x18]
00000001b0d62f78 A1000034               cbz        w1, loc_1b0d62f8c

00000001b0d62f7c FF0300F9               str        xzr, [sp, #0x170 + var_170]
00000001b0d62f80 E00314AA               mov        x0, x20
00000001b0d62f84 7B93C794               bl         0x1b3f47d70
00000001b0d62f88 60040034               cbz        w0, loc_1b0d63014

                                    loc_1b0d62f8c:
00000001b0d62f8c E00314AA               mov        x0, x20                      ; argument "fildes" for method imp___auth_stubs__fsync, CODE XREF=sub_1b0d62d34+580
00000001b0d62f90 9C7A0494               bl         imp___auth_stubs__fsync      ; fsync
00000001b0d62f94 00040034               cbz        w0, loc_1b0d63014

翻译成C语言伪代码:

// x19 is the context pointer (self/this)
// w1 is an input argument (flags)

// 1. Pre-check
struct SubObject* obj = self->ptr_40;
if (obj) {
    if (obj->ptr_0->status_a8 != 0) {
        goto loc_1b0d62ef4; // Busy/Error path
    }
}

// 2. Determine Sync Command
int fd = self->file_descriptor; // offset 0x18
int command = 0;

// Check config flag at offset 0x80
if (self->flags_80 & 0x02) {
    command = 0x33; // F_FULLFSYNC (51)
} 
else if ((w1 & 0x0F) == 3) {
    command = 0x55; // F_BARRIERFSYNC (85)
}

// 3. Try Specialized Sync
int result = -1;
if (command != 0) {
    // Likely fcntl(fd, command, 0)
    result = unknown_func_1b3f47d70(fd, command, 0); 
    
    if (result == 0) {
        goto success; // loc_1b0d63014
    }
}

// 4. Fallback to standard fsync
// Reached if command was 0 OR if specialized sync failed
result = fsync(fd);

if (result == 0) {
    goto success;
}

// ... handle error ...

可以看出这个逻辑中既有F_FULLSYNC又有F_BARRIERFSYNC。写了个简单demo验证了一下,PRAGMA fullsync = ON会用F_FULLSYNCPRAGMA fullsync = OFF用的是F_BARRIERFSYNC

所以,如果在苹果系统上使用自己编译的sqlite库时,需要注意把这个逻辑加上。

*总之,在iOS/macOS平台写文件的场景,需要考虑好对性能、稳定性的需求,选用合适的系统机制。

参考资料

__CFRunLoopServiceMachPort函数详解

作者 iOS在入门
2026年1月23日 18:45

借助AI辅助。

__CFRunLoopServiceMachPort 函数逐行注释

这是 RunLoop 在 macOS 上休眠和唤醒的核心函数,通过 mach_msg() 系统调用实现线程阻塞。


完整注释代码

static Boolean __CFRunLoopServiceMachPort(
    mach_port_name_t port,              // 要等待的端口(或端口集合)
    mach_msg_header_t **buffer,         // 消息缓冲区指针的地址
    size_t buffer_size,                 // 缓冲区大小
    mach_port_t *livePort,              // [输出] 被唤醒的端口
    mach_msg_timeout_t timeout,         // 超时时间(毫秒,TIMEOUT_INFINITY=无限)
    voucher_mach_msg_state_t *_Nonnull voucherState,  // voucher 状态(追踪用)
    voucher_t *voucherCopy,             // voucher 副本
    CFRunLoopRef rl,                    // RunLoop(用于追踪)
    CFRunLoopModeRef rlm                // Mode(用于追踪)
) {
    // ========================================
    // 函数返回值说明:
    // • true: 收到消息,livePort 指向唤醒的端口
    // • false: 超时或错误
    // ========================================
    
    Boolean originalBuffer = true;
    // 标记是否使用原始缓冲区
    // true: 使用调用者提供的栈上缓冲区
    // false: 使用动态分配的堆缓冲区(消息太大时)
    
    kern_return_t ret = KERN_SUCCESS;
    // Mach 内核调用的返回值
    // 初始化为成功状态
    
    for (;;) {
        // 无限循环,直到:
        // 1. 成功接收消息(return true)
        // 2. 超时(return false)
        // 3. 致命错误(break 后 HALT)
        
        /* In that sleep of death what nightmares may come ... */
        // 莎士比亚《哈姆雷特》引用:"在死亡的睡眠中会有什么噩梦来临..."
        // 暗示线程即将进入"休眠"状态
        
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        // 获取消息指针
        // *buffer 是指向缓冲区的指针
        
        // ========================================
        // 步骤 1: 初始化消息头
        // ========================================
        
        msg->msgh_bits = 0;
        // 消息标志位,初始化为 0
        // mach_msg 会设置适当的接收标志
        
        msg->msgh_local_port = port;
        // 设置本地端口(接收端口)
        // 这是我们要等待的端口(或端口集合)
        
        msg->msgh_remote_port = MACH_PORT_NULL;
        // 远程端口(发送目标)设为空
        // 因为我们只接收,不发送
        
        msg->msgh_size = buffer_size;
        // 设置缓冲区大小
        // 告诉内核我们能接收多大的消息
        
        msg->msgh_id = 0;
        // 消息 ID,初始化为 0
        // 接收后会包含实际的消息 ID
        
        // ========================================
        // 步骤 2: 记录追踪事件(调试用)
        // ========================================
        
        if (TIMEOUT_INFINITY == timeout) {
            // 如果是无限等待
            CFRUNLOOP_SLEEP();
            // 探针宏:记录休眠事件(DTrace)
            cf_trace(KDEBUG_EVENT_CFRL_SLEEP, port, 0, 0, 0);
            // 内核追踪:记录 RunLoop 即将休眠
        } else {
            // 如果有超时时间(轮询模式)
            CFRUNLOOP_POLL();
            // 探针宏:记录轮询事件
            cf_trace(KDEBUG_EVENT_CFRL_POLL, port, 0, 0, 0);
            // 内核追踪:记录 RunLoop 轮询
        }
        
        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_END, rl, rlm, port, timeout);
        // 追踪:RunLoop 运行阶段结束
        
        cf_trace(KDEBUG_EVENT_CFRL_IS_WAITING | DBG_FUNC_START, rl, rlm, port, timeout);
        // 追踪:开始等待阶段
        
        // ========================================
        // 步骤 3: 调用 mach_msg 等待 ⭐⭐⭐
        // 【这是整个 RunLoop 最核心的一行代码!】
        // ========================================
        
        ret = mach_msg(
            msg,                    // 消息缓冲区
            // 选项组合:
            MACH_RCV_MSG |          // 接收消息模式
            MACH_RCV_VOUCHER |      // 接收 voucher(追踪信息)
            MACH_RCV_LARGE |        // 支持大消息(自动重新分配缓冲区)
            ((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0) | 
            // 如果有超时,添加 MACH_RCV_TIMEOUT 标志
            MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) |
            // 接收 trailer(消息尾部附加信息)
            MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
            // trailer 包含 audit token 和 voucher
            0,                      // 发送大小(不发送,所以为 0)
            msg->msgh_size,         // 接收缓冲区大小
            port,                   // 接收端口(或端口集合)
            timeout,                // 超时时间(毫秒)
            MACH_PORT_NULL          // 通知端口(不使用)
        );
        // 【线程在这里阻塞】
        // 等待以下情况之一:
        // 1. port 收到消息 → 返回 MACH_MSG_SUCCESS
        // 2. 超时 → 返回 MACH_RCV_TIMED_OUT
        // 3. 消息太大 → 返回 MACH_RCV_TOO_LARGE
        // 4. 其他错误 → 返回错误码
        
        cf_trace(KDEBUG_EVENT_CFRL_IS_WAITING | DBG_FUNC_END, rl, rlm, port, timeout);
        // 追踪:等待阶段结束(被唤醒)
        
        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_START, rl, rlm, port, timeout);
        // 追踪:RunLoop 运行阶段开始
        
        // ========================================
        // 步骤 4: 处理 voucher(性能追踪)
        // ========================================
        
        // Take care of all voucher-related work right after mach_msg.
        // 在 mach_msg 之后立即处理所有 voucher 相关工作
        // If we don't release the previous voucher we're going to leak it.
        // 如果不释放之前的 voucher,会造成内存泄漏
        
        voucher_mach_msg_revert(*voucherState);
        // 恢复之前的 voucher 状态
        // 释放上次收到的 voucher(如果有)
        
        // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
        // 调用者负责调用 voucher_mach_msg_revert
        // 这个调用让接收到的 voucher 成为当前的
        
        *voucherState = voucher_mach_msg_adopt(msg);
        // 采用(adopt)消息中的 voucher
        // 返回新的 voucher 状态
        // voucher 用于追踪消息的来源和上下文
        
        if (voucherCopy) {
            // 如果调用者需要 voucher 副本
            *voucherCopy = NULL;
            // 重置为 NULL
            // 调用者可以在需要时拷贝
        }

        CFRUNLOOP_WAKEUP(ret);
        // 探针宏:记录唤醒事件,传入返回值
        
        cf_trace(KDEBUG_EVENT_CFRL_DID_WAKEUP, port, 0, 0, 0);
        // 内核追踪:记录 RunLoop 被唤醒
        
        // ========================================
        // 步骤 5: 处理返回结果
        // ========================================
        
        if (MACH_MSG_SUCCESS == ret) {
            // 情况 1: 成功接收到消息
            
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            // 返回被唤醒的端口
            // 调用者通过这个值判断唤醒源:
            // • _wakeUpPort → 手动唤醒
            // • _timerPort → 定时器
            // • dispatchPort → GCD 主队列
            // • 其他 → Source1
            
            return true;
            // 返回成功,结束函数
        }
        
        if (MACH_RCV_TIMED_OUT == ret) {
            // 情况 2: 接收超时(正常情况)
            
            if (!originalBuffer) free(msg);
            // 如果使用了动态分配的缓冲区,释放它
            
            *buffer = NULL;
            // 将缓冲区指针设为 NULL
            
            *livePort = MACH_PORT_NULL;
            // 没有唤醒端口(超时)
            
            return false;
            // 返回失败(超时)
        }
        
        if (MACH_RCV_TOO_LARGE != ret) {
            // 情况 3: 其他错误(非 "消息太大")
            // 这些是致命错误,需要崩溃
            
            if (((MACH_RCV_HEADER_ERROR & ret) == MACH_RCV_HEADER_ERROR) || 
                (MACH_RCV_BODY_ERROR & ret) == MACH_RCV_BODY_ERROR) {
                // 如果是消息头错误或消息体错误
                
                kern_return_t specialBits = MACH_MSG_MASK & ret;
                // 提取特殊错误位
                
                if (MACH_MSG_IPC_SPACE == specialBits) {
                    // IPC 空间不足
                    CRSetCrashLogMessage("Out of IPC space");
                    // 设置崩溃日志消息
                    // 可能原因:Mach 端口泄漏
                    
                } else if (MACH_MSG_VM_SPACE == specialBits) {
                    // 虚拟内存空间不足
                    CRSetCrashLogMessage("Out of VM address space");
                    // 内存耗尽
                    
                } else if (MACH_MSG_IPC_KERNEL == specialBits) {
                    // 内核 IPC 资源短缺
                    CRSetCrashLogMessage("Kernel resource shortage handling IPC");
                    // 内核资源不足
                    
                } else if (MACH_MSG_VM_KERNEL == specialBits) {
                    // 内核 VM 资源短缺
                    CRSetCrashLogMessage("Kernel resource shortage handling out-of-line memory");
                    // 内核内存不足
                }
            } else {
                // 其他类型的错误
                CRSetCrashLogMessage(mach_error_string(ret));
                // 设置错误字符串为崩溃日志
            }
            break;
            // 跳出循环,准备崩溃
        }
        
        // ========================================
        // 步骤 6: 处理 MACH_RCV_TOO_LARGE(消息太大)
        // ========================================
        // 如果执行到这里,说明 ret == MACH_RCV_TOO_LARGE
        
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        // 计算需要的缓冲区大小
        // round_msg: 向上取整到合适的大小
        // msg->msgh_size: 实际消息大小(已在 msg 中设置)
        // MAX_TRAILER_SIZE: trailer 的最大大小
        
        if (originalBuffer) *buffer = NULL;
        // 如果之前使用的是原始缓冲区(栈上的)
        // 将指针设为 NULL,下面会分配新的
        
        originalBuffer = false;
        // 标记不再使用原始缓冲区
        
        *buffer = __CFSafelyReallocate(*buffer, buffer_size, NULL);
        // 重新分配更大的缓冲区
        // 如果 *buffer 是 NULL,相当于 malloc
        // 否则相当于 realloc
        // 下次循环会使用新缓冲区重新接收

        if (voucherCopy != NULL && *voucherCopy != NULL) {
            // 如果有 voucher 副本
            os_release(*voucherCopy);
            // 释放 voucher(引用计数 -1)
        }
    }
    // 继续循环,使用新缓冲区重新调用 mach_msg
    
    HALT;
    // 如果跳出循环(因为致命错误),停止程序
    // HALT 宏会触发断点或终止进程
    
    return false;
    // 这行代码实际不会执行(HALT 不会返回)
    // 但保留以满足编译器要求
}

函数执行流程图

┌─────────────────────────────────────────────────────────────┐
│  开始 __CFRunLoopServiceMachPort                            │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  初始化消息头                                                 │
│  • msgh_local_port = port (等待的端口)                        │
│  • msgh_size = buffer_size                                  │
│  • 其他字段置 0                                               │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  记录追踪事件                                                 │
│  • SLEEP (无限等待) 或 POLL (有超时)                           │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  ⭐ 调用 mach_msg() - 线程在此阻塞 ⭐                          │
│                                                             │
│  等待事件:                                                  │
│  • Timer 端口有消息                                          │
│  • Source1 端口有消息                                        │
│  • dispatch 端口有消息                                       │
│  • _wakeUpPort 有消息                                        │
│  • 超时                                                      │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  被唤醒,检查返回值                                            │
└─────────────────────────────────────────────────────────────┘
          │               │                │
          ▼               ▼                ▼
    ┌─────────┐    ┌──────────┐    ┌──────────────┐
    │ SUCCESS │    │ TIMED_OUT│    │  TOO_LARGE   │
    └─────────┘    └──────────┘    └──────────────┘
          │               │                │
          ▼               ▼                ▼
    ┌─────────┐    ┌──────────┐    ┌──────────────┐
    │处理voucher│   │ 清理缓冲  │    │ 扩大缓冲区    │
    │返回true  │   │返回false  │     │ 重新接收     │
    └─────────┘    └──────────┘    └──────────────┘
                                          │
                                          ▼
                                    ┌──────────┐
                                    │返回循环开始│
                                    └──────────┘

关键点说明

1. mach_msg 的两种模式

模式 timeout 值 行为
休眠模式 TIMEOUT_INFINITY 永久阻塞,直到收到消息
轮询模式 0 或小值 立即返回或短暂等待

2. 可能的返回值

返回值 说明 处理方式
MACH_MSG_SUCCESS 成功收到消息 返回 true
MACH_RCV_TIMED_OUT 超时 返回 false
MACH_RCV_TOO_LARGE 消息太大 扩大缓冲区重试
其他错误 致命错误 崩溃(HALT)

3. voucher 的作用

voucher 是 macOS 的性能追踪机制:
├── 追踪消息来源
├── 记录 QoS(服务质量)
├── 性能分析(Instruments)
└── 调试辅助

4. 缓冲区管理

初始: 使用栈缓冲区(3KB)
  ↓
mach_msg 返回 TOO_LARGE
  ↓
计算实际大小: msg->msgh_size + MAX_TRAILER_SIZE
  ↓
动态分配堆缓冲区
  ↓
重新调用 mach_msg
  ↓
成功接收大消息

总结

__CFRunLoopServiceMachPort 是 RunLoop 休眠的核心

  1. 准备消息头:设置接收端口和缓冲区
  2. 调用 mach_msg:线程阻塞等待 ⭐
  3. 被唤醒:检查返回值和 livePort
  4. 处理特殊情况:超时、消息过大、错误
  5. 返回结果:告诉调用者是哪个端口唤醒的

这就是 RunLoop "无事件时不消耗 CPU" 的秘密!

理财学习笔记(一):每个人必须自己懂理财

作者 唐巧
2026年1月24日 08:20

序言

我打算系统性整理一下这几年投学习投资理财的心得。因为一方面通过总结,可以让自己进一步加深对投资的理解。另一方面我也想分享给同样想学习理财的读者们。

我的女儿虽然还在读小学,但我也给她报了一个针对小学生的财商课。她对理财非常有兴趣,我也想通过这一系列的文章,给她分享她爸爸的理财成长经历。

这是这是本系列的第一篇,主题是每个人必须自己懂理财。

我身边的案例

我是 80 年代出生的,不得不说,我所处的是那个年代是缺乏理财和财商教育的。因此,我发现我身边的人大多不具备优秀的理财能力。

下面我举几个身边朋友的真实例子。

朋友 A:

他都把挣到的钱存银行定期或者余额宝。但是在现在这个年代,收益率是非常低的,只有一点几。但是他非常胆小,怕买其他的产品会导致亏损,所以说不敢碰。

朋友 B:

朋友 B 买了很多基金。但是他胆子很小,每个只买 1000 - 5000 块钱。然后账户里面有着几十只基金。既看不过来,也不知道应该如何操作。

唯一好的一面是:不管任何行业有行情,他都有一只基金命中。这让他的错失恐惧症(FOMO)小了很多。

朋友 C:

我这个朋友之前在快手上班,在 P2P 盛行的年代,把自己的所有积蓄都投在 P2P 上,最后爆雷,损失惨重。

朋友 D:

这个朋友通过另外一个朋友了解到有一个股票正在做庄阶段,未来会大涨,于是就听信买入,最后损失了 90%。

朋友 E:

朋友 E 的大学同学有一个在香港卖保险,于是听朋友的推荐在香港买了很多保险。但是过了 5 年,他发现收益率和最初承诺的相差非常大。这个时候看合同才发现,合同上写的收益测算并不保证。但是现在赎回的话,只能拿到非常少的本金,所以他只能继续硬着头皮每年交钱。

只有理解才能有效持有

听完上面几个朋友的故事,你身边有类似的朋友吗?

我跟一些朋友交流,我问他们,你们为什么不自己先学习投资理财的知识,之后再去做相关的操作呢?他们很多回答说,这个事情太专业了,专业的事情交给专业的人做就可以了。

当我反问他们:假如你买了一个专业人士管理的基金,那你对他的信仰来自于哪呢?你其实对他每个月发的报告并没有完全的判断能力,你只能选择相信他。

大多数时候,你其实相信的是他过去的业绩。如果它连续三年、连续五年一直都盈利,或者有超额收益,你就会持续持有它,甚至买入更多。

如果它连续几年亏损或者某一年大额亏损,你就会质疑它,甚至赎回它。

你的信心其实就是来源于过去的业绩表现。那这和散户的追涨杀跌有什么本质区别呢?

在你持仓持续下跌的那些时间,你能睡好觉吗?如果你不能理解它,那显然不能。

所以我说,每个人必须懂投资理财。

只有你深刻理解了你买入的是什么,才能在它下跌的时候有信心继续持有它,甚至抄底,才能睡得着觉。

小结

每个人都必须懂理财。因为银行的定期存款利率太低,而其他理财产品都需要深刻理解,才可能做到长期持有。

另外,社会上充斥着像 P2P 一类的产品,以及宣传这类产品的巧舌如簧的销售。他们不断地诱惑着我们,如果我们没有辨识能力,也可能将自己辛苦一辈子挣到的钱损失掉。

以上。

mach_msg_header_t详解

作者 iOS在入门
2026年1月23日 18:01

借助AI能力分析。

mach_msg_header_t - Mach 消息头

作用

这是 Mach 消息的头部结构,用于在 macOS/iOS 的进程间(或线程间)传递消息。

6个字段详解

typedef struct {
    mach_msg_bits_t      msgh_bits;         // 消息标志位
    mach_msg_size_t      msgh_size;         // 消息总大小(字节)
    mach_port_t          msgh_remote_port;  // 目标端口(收信人)
    mach_port_t          msgh_local_port;   // 本地端口(回信地址)
    mach_port_name_t     msgh_voucher_port; // 追踪端口(调试用)
    mach_msg_id_t        msgh_id;           // 消息ID(自定义)
} mach_msg_header_t;

形象比喻(信封):

字段 对应信封上的 说明
msgh_remote_port 收件人地址 消息发往哪个端口
msgh_local_port 回信地址 如果需要回复,发到这里
msgh_size 信件大小 包括信封和内容
msgh_bits 邮寄方式 挂号信、平信等
msgh_id 信件编号 用于区分不同类型的信
msgh_voucher_port 追踪单号 用于追踪和调试

在 RunLoop 中的使用

1. 发送唤醒消息(CFRunLoopWakeUp)

// 构造消息头
mach_msg_header_t header;
header.msgh_remote_port = rl->_wakeUpPort;  // 发往唤醒端口
header.msgh_local_port = MACH_PORT_NULL;    // 不需要回复
header.msgh_size = sizeof(mach_msg_header_t); // 只有头,无内容
header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
header.msgh_id = 0;

// 发送(唤醒 RunLoop)
mach_msg(&header, MACH_SEND_MSG, ...);

2. 接收消息(RunLoop 休眠)

// 准备缓冲区
uint8_t buffer[3 * 1024];
mach_msg_header_t *msg = (mach_msg_header_t *)buffer;

msg->msgh_local_port = waitSet;  // 在哪个端口等待
msg->msgh_size = sizeof(buffer);  // 缓冲区大小

// 阻塞等待(线程休眠)
mach_msg(msg, MACH_RCV_MSG, ...);

// 被唤醒后,检查消息来源
if (msg->msgh_local_port == _wakeUpPort) {
    // 手动唤醒
} else if (msg->msgh_local_port == _timerPort) {
    // 定时器到期
}

关键理解

mach_msg_header_t 是 Mach IPC 的核心

  1. 通信基础:所有 Mach 消息都以这个头开始
  2. 路由信息:指明消息的来源和去向
  3. RunLoop 休眠/唤醒:通过接收/发送消息实现

完整消息结构

┌──────────────────────┐
│ mach_msg_header_t    │ ← 消息头(必需)
├──────────────────────┤
│ 消息体(可选)        │ ← 实际数据
├──────────────────────┤
│ trailer(可选)       │ ← 附加信息
└──────────────────────┘

RunLoop 的简化消息:只有头部,无消息体(称为 "trivial message"),足以唤醒线程。

objc_msgSend(obj, @selector(foo)); 到底发生了什么?

作者 汉秋
2026年1月23日 17:53

objc_msgSend(obj, @selector(foo)); 到底发生了什么?

在 Objective-C 的世界里,有一句话几乎是底层原教旨主义

Objective-C 是一门基于消息发送(Message Sending)的语言,而不是函数调用。

而这一切,都浓缩在一行看似普通、却极其核心的代码中:

objc_msgSend(obj, @selector(foo));

本文将从语法糖 → 运行时 → 完整调用链,一步一步拆解:

  • 这行代码到底在“发什么”
  • 消息是如何被找到并执行的
  • 如果找不到方法,Runtime 又做了什么

一、从表面看:它等价于什么?

这行代码:

objc_msgSend(obj, @selector(foo));

在语义上 等价于

[obj foo];

也就是说:

给对象 obj 发送一条名为 foo 的消息

[obj foo] 只是编译器提供的语法糖,真正执行的永远是 objc_msgSend。


二、谁是发送者?谁是接收者?

很多初学者会卡在这个问题上:

到底是谁“调用”了谁?

正确理解方式

  • 接收者(receiver) :obj

  • 消息(selector) :foo

  • 发送动作的发起者:当前代码位置(不重要)

Objective-C 不关心调用栈的“谁” ,只关心:

👉 这条消息发给谁

所以永远用这句话来理解:

给 obj 发送 foo 消息


三、Runtime 真正发生的 6 个步骤(核心)

下面是你在 Xcode 里写下一行 [obj foo] 后,Runtime 在背后真实发生的完整流程


步骤 1️⃣:取得接收者obj

id obj = ...;
objc_msgSend(obj, @selector(foo));
  • obj 是一个对象指针
  • 本质上指向一块内存
  • 内存布局的第一个成员,就是 isa 指针
obj
 ├─ isa → Class
 ├─ ivar1
 ├─ ivar2

如果 obj == nil:

  • 整个流程直接结束
  • 返回 0 / nil
  • 不会崩溃(OC 的著名特性)

步骤 2️⃣:通过isa找到 Class

Class cls = obj->isa;
  • isa 指向对象所属的类

  • 这是 所有方法查找的起点

示例:

@interface Person : NSObject
- (void)foo;
@end
obj (Person 实例)
  └─ isa → Person

步骤 3️⃣:在方法缓存(cache)中查找

Runtime 首先查 cache,而不是方法列表

Class Person
 ├─ cache      ← ① 先查这里
 ├─ methodList ← ② 再查这里
 └─ superclass
  • cache 是一个哈希表:SEL → IMP

  • 命中 cache = 极快(接近 C 函数调用)

如果在 cache 中找到了 foo:

IMP imp = cache[foo];
imp(obj, @selector(foo));

流程结束

步骤 4️⃣:在方法列表 & 父类中查找

如果 cache 未命中:

  1. 查 Person 的方法列表
  2. 找不到 → superclass
  3. 一直向上查,直到 NSObject
Person
  ↓
NSObject

如果在某个类中找到:

  • 将 SEL → IMP 放入 cache(下次更快)
  • 立即执行 IMP

步骤 5️⃣:动态方法解析(resolve)

如果 整个继承链都没找到 foo

Runtime 会给你一次**“临时补救”的机会**。

+ (BOOL)resolveInstanceMethod:(SEL)sel;

示例:动态添加方法

@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo)) {
        class_addMethod(self, sel, (IMP)fooIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooIMP(id self, SEL _cmd) {
    NSLog(@"动态添加的 foo 被调用了");
}

@end

如果返回 YES:

  • Runtime 重新从步骤 3 开始查找

步骤 6️⃣:消息转发(Message Forwarding)

如果你没有动态添加方法,Runtime 进入 消息转发三连

6.1 快速转发

- (id)forwardingTargetForSelector:(SEL)aSelector;
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return otherObj;
    }
    return [super forwardingTargetForSelector:aSelector];
}

等价于:

[otherObj foo];

6.2 完整转发(NSInvocation)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:otherObj];
}

6.3 最终失败 → 崩溃

如果以上都没处理:

-[Person foo]: unrecognized selector sent to instance

应用直接崩溃 💥


四、完整流程总览(记住这个顺序)

objc_msgSend
  ↓
isa
  ↓
cache
  ↓
method list
  ↓
superclass
  ↓
resolveInstanceMethod
  ↓
forwardingTargetForSelector
  ↓
forwardInvocation
  ↓
crash

五、为什么 objc_msgSend 这么重要?

  • KVC / KVO

  • 方法交换(Method Swizzling)

  • AOP / Hook

  • 崩溃防护

  • 热修复(早期方案)

全都建立在它之上。

理解 objc_msgSend,

才算真正“入门” Objective-C Runtime。


六、结语

当你再看到这行代码时:

objc_msgSend(obj, @selector(foo));

请在脑中自动展开:

cache → superclass → resolve → forwarding → crash

那一刻,你已经不是在“写 OC”,而是在和 Runtime 对话

Flutter 最新xyz

作者 忆江南
2026年1月23日 16:19

包含 55+ 道xyz,覆盖基础、原理、性能优化、复杂场景和高难度题目


一、Dart 语言基础xyz(15题)

1. Dart 是值传递还是引用传递?

答案

类型 传递方式 示例
基本类型(int、double、bool、String) 值传递 修改不影响原值
对象和集合(List、Map、Set、自定义类) 引用传递 修改会影响原对象
void modifyInt(int value) {
  value = 100; // 不影响原值
}

void modifyList(List<int> list) {
  list.add(4); // 会影响原列表
}

2. constfinal 的区别?

答案

特性 const final
赋值时机 编译时确定 运行时确定
是否可用于类成员 需要 static const 可以
对象创建 共享同一对象 每次创建新对象
嵌套要求 所有成员必须是 const 无要求
const int a = 10;                    // ✓ 编译期常量
final int b = DateTime.now().year;   // ✓ 运行时常量
const DateTime c = DateTime.now();   // ✗ 报错,编译时无法确定

3. vardynamicObject 的区别?

答案

关键字 类型检查时机 类型能否改变 使用场景
var 编译时 一旦确定不可改变 类型推断
dynamic 运行时 可随时改变 动态类型、JSON解析
Object 编译时 只能调用 Object 方法 需要类型安全的通用类型
var x = 'hello';    // x 被推断为 String
x = 123;            // ✗ 报错

dynamic y = 'hello';
y = 123;            // ✓ 可以

Object z = 'hello';
z.length;           // ✗ 报错,Object 没有 length

4. .. 级联操作符与 . 的区别?

答案

操作符 返回值 用途
. 方法的返回值 普通方法调用
.. this(当前对象) 链式调用配置
var paint = Paint()
  ..color = Colors.red
  ..strokeWidth = 5.0
  ..style = PaintingStyle.stroke;

5. Dart 的空安全(Null Safety)是什么?

答案

Dart 2.12+ 引入空安全,区分可空类型非空类型

String name = 'Flutter';      // 非空,不能赋值 null
String? nickname = null;       // 可空,可以赋值 null

// 空安全操作符
String? text = null;
int length = text?.length ?? 0;  // 安全访问 + 默认值
String value = text!;            // 断言非空(危险!)

6. late 关键字的作用?

答案

late 用于延迟初始化非空变量:

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late AnimationController controller; // 延迟初始化

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: Duration(seconds: 1), vsync: this);
  }
}

使用场景

  • 需要在构造函数之后初始化的非空变量
  • 惰性计算的变量

7. Mixin 是什么?与继承的区别?

答案

Mixin 用于代码复用,不同于继承:

特性 继承(extends) 混入(with)
数量 单继承 可多个
构造函数 可以有 不能有
代码复用
类型关系 is-a has-ability
mixin Flyable {
  void fly() => print('Flying!');
}

mixin Swimmable {
  void swim() => print('Swimming!');
}

class Duck extends Animal with Flyable, Swimmable {
  // Duck 同时拥有 fly() 和 swim()
}

8. extendswithimplements 的执行顺序?

答案

顺序为:extends → with → implements

class Child extends Parent with Mixin1, Mixin2 implements Interface {
  // 1. 首先继承 Parent
  // 2. 然后混入 Mixin1, Mixin2(后者覆盖前者的同名方法)
  // 3. 最后实现 Interface
}

方法查找顺序(从右到左): Child → Mixin2 → Mixin1 → Parent → Object


9. Dart 中的泛型是什么?

答案

泛型用于类型安全代码复用

// 泛型类
class Box<T> {
  T value;
  Box(this.value);
}

// 泛型方法
T first<T>(List<T> items) {
  return items[0];
}

// 泛型约束
class NumberBox<T extends num> {
  T value;
  NumberBox(this.value);

  double toDouble() => value.toDouble();
}

10. Dart 的 typedef 是什么?

答案

typedef 用于定义函数类型别名

// 定义函数类型
typedef Compare<T> = int Function(T a, T b);

// 使用
int sort(int a, int b) => a - b;
Compare<int> comparator = sort;

// 新语法(Dart 2.13+)
typedef IntList = List<int>;
typedef StringCallback = void Function(String);

11. Dart 的 extension 扩展方法是什么?

答案

extension 用于给现有类添加方法,无需继承:

extension StringExtension on String {
  String capitalize() {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1)}';
  }

  bool get isEmail => contains('@');
}

// 使用
'hello'.capitalize();  // 'Hello'
'a@b.com'.isEmail;     // true

12. Dart 的 factory 构造函数是什么?

答案

factory 构造函数可以返回已有实例子类实例

class Logger {
  static final Logger _instance = Logger._internal();

  // 工厂构造函数
  factory Logger() {
    return _instance; // 返回单例
  }

  Logger._internal();
}

// 使用
var l1 = Logger();
var l2 = Logger();
print(l1 == l2); // true,同一个实例

13. Dart 3 的 Records(记录类型)是什么?

答案

Records 是 Dart 3 引入的匿名复合类型

// 位置记录
(int, String) getUserInfo() => (1, 'John');

var info = getUserInfo();
print(info.$1); // 1
print(info.$2); // 'John'

// 命名记录
({int id, String name}) getUser() => (id: 1, name: 'John');

var user = getUser();
print(user.id);   // 1
print(user.name); // 'John'

14. Dart 3 的 Pattern Matching(模式匹配)是什么?

答案

模式匹配用于解构和条件匹配

// switch 表达式
String describe(Object obj) => switch (obj) {
  int n when n > 0 => 'Positive number: $n',
  int n when n < 0 => 'Negative number: $n',
  String s => 'String: $s',
  _ => 'Unknown type',
};

// 解构
var (x, y) = (1, 2);
var {'name': name, 'age': age} = {'name': 'John', 'age': 30};

// if-case
if (json case {'name': String name, 'age': int age}) {
  print('Name: $name, Age: $age');
}

15. Dart 3 的 Sealed Class 是什么?

答案

sealed 类用于限制子类,实现穷尽式 switch:

sealed class Shape {}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);
}

class Rectangle extends Shape {
  final double width, height;
  Rectangle(this.width, this.height);
}

// 编译器会检查是否穷尽所有子类
double area(Shape shape) => switch (shape) {
  Circle(radius: var r) => 3.14 * r * r,
  Rectangle(width: var w, height: var h) => w * h,
};

二、Flutter 核心原理xyz(15题)

16. Flutter 的三棵树是什么?各自职责是什么?

答案

类型 职责 特点
Widget Tree 配置层 描述 UI 结构 不可变、轻量、频繁重建
Element Tree 连接层 管理生命周期、持有 State 可变、持久化
RenderObject Tree 渲染层 布局、绘制、事件处理 重量级、存储几何信息

创建流程

Widget.createElement() → Element
Element.createRenderObject() → RenderObject

为什么需要三棵树?

  • Widget 频繁重建成本低
  • Element 复用避免重复创建
  • RenderObject 只在必要时更新

17. Flutter 完整渲染流程是什么?

答案

┌─────────────────────────────────────────────┐
│                   UI 线程                    │
├─────────────────────────────────────────────┤
│ 1. Build(构建)                             │
│    - 从脏 Element 开始重建                   │
│    - 调用 build() 方法                       │
│    - 生成新的 Widget 树                      │
├─────────────────────────────────────────────┤
│ 2. Layout(布局)                            │
│    - 约束从父向子传递                        │
│    - 几何信息从子向父返回                    │
│    - 计算大小和位置                          │
├─────────────────────────────────────────────┤
│ 3. Paint(绘制)                             │
│    - 生成绘制指令                            │
│    - 构建 Layer Tree                         │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│               光栅线程(Raster)              │
├─────────────────────────────────────────────┤
│ 4. Composite(合成)                         │
│    - 图层合成                                │
│    - Skia/Impeller 栅格化                    │
│    - 提交给 GPU                              │
└─────────────────────────────────────────────┘
                    ↓
                显示到屏幕

性能标准

  • 60fps:每帧 ≤ 16ms
  • 120fps:每帧 ≤ 8.3ms

18. setState() 的底层原理是什么?

答案

void setState(VoidCallback fn) {
  // 1. 执行回调函数,修改状态
  fn();

  // 2. 标记当前 Element 为脏
  _element!.markNeedsBuild();
}

// markNeedsBuild() 的实现
void markNeedsBuild() {
  // 标记为脏
  _dirty = true;

  // 加入脏 Element 列表
  owner!.scheduleBuildFor(this);
}

流程

  1. 执行回调更新状态
  2. 标记 Element 为脏
  3. 注册到 BuildOwner 的脏列表
  4. 下一帧触发重建
  5. 只重建脏 Element 及其子树

19. Flutter 的约束(Constraints)系统是什么?

答案

约束是父节点向子节点传递的布局信息

class BoxConstraints {
  final double minWidth;   // 最小宽度
  final double maxWidth;   // 最大宽度
  final double minHeight;  // 最小高度
  final double maxHeight;  // 最大高度
}

布局算法

1. 父节点传递约束给子节点
2. 子节点选择约束范围内的大小
3. 子节点返回实际大小给父节点
4. 父节点确定子节点位置

严格约束(Tight Constraints):

  • minWidth == maxWidthminHeight == maxHeight
  • 子节点无法改变大小
  • 父节点可直接定位而无需重新布局子节点

20. Key 的作用是什么?有哪些类型?

答案

作用:帮助 Flutter 在 Widget 树重建时正确匹配和复用 Element

Key 类型 作用域 使用场景
GlobalKey 整个应用唯一 跨组件访问 State、保持状态
LocalKey 局部唯一 列表项复用
ValueKey 基于值 数据驱动列表
ObjectKey 基于对象引用 对象唯一性
UniqueKey 随机唯一 强制重建
// GlobalKey 示例
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
formKey.currentState?.validate();

// ValueKey 示例
ListView(
  children: items.map((item) =>
    ListTile(key: ValueKey(item.id), title: Text(item.name))
  ).toList(),
)

21. BuildContext 是什么?

答案

BuildContext 是 Widget 在 Widget 树中的位置引用,本质是 Element 对象

// 向上查找
Theme.of(context);           // 获取主题
Navigator.of(context);       // 获取导航器
MediaQuery.of(context);      // 获取媒体查询
Scaffold.of(context);        // 获取 Scaffold

// InheritedWidget 查找
MyInheritedWidget.of(context);

注意事项

  • initState() 中不能使用 context(Element 未完全挂载)
  • 异步操作后需检查 mounted 状态

22. Widget 有哪些分类?

答案

类型 代表类 作用
组合类 StatelessWidget、StatefulWidget 组合其他 Widget
代理类 InheritedWidget、ParentDataWidget 状态共享、数据传递
绘制类 RenderObjectWidget 真正的布局和绘制

RenderObject 三个子类

  • LeafRenderObjectWidget:叶子节点(无子节点)
  • SingleChildRenderObjectWidget:单子节点
  • MultiChildRenderObjectWidget:多子节点

23. StatefulWidget 的完整生命周期?

答案

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState(); // 1. 创建 State
}

class _MyWidgetState extends State<MyWidget> {
  @override
  void initState() {                    // 2. 初始化(只调用一次)
    super.initState();
  }

  @override
  void didChangeDependencies() {         // 3. 依赖变化
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {   // 4. 构建 UI
    return Container();
  }

  @override
  void didUpdateWidget(MyWidget old) {   // 5. Widget 更新
    super.didUpdateWidget(old);
  }

  @override
  void reassemble() {                    // 6. 热重载时调用
    super.reassemble();
  }

  @override
  void deactivate() {                    // 7. 暂时移除
    super.deactivate();
  }

  @override
  void dispose() {                       // 8. 永久销毁
    super.dispose();
  }
}

生命周期图

createState → initState → didChangeDependencies → build
                                    ↓
                          [setState/父Widget更新]
                                    ↓
                          didUpdateWidget → build
                                    ↓
                          deactivate → dispose

24. InheritedWidget 的原理是什么?

答案

InheritedWidget 用于数据向下传递,避免多层传参:

class ThemeProvider extends InheritedWidget {
  final Color color;

  ThemeProvider({required this.color, required Widget child})
    : super(child: child);

  static ThemeProvider? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
  }

  @override
  bool updateShouldNotify(ThemeProvider oldWidget) {
    return color != oldWidget.color;
  }
}

性能优化原理

  • Element 维护 InheritedWidget 哈希表
  • 查找时间复杂度 O(1)
  • 避免遍历父链(O(N))

25. 热重载(Hot Reload)的原理是什么?

答案

流程

  1. 代码修改保存
  2. IDE 发送变更到 Dart VM
  3. VM 增量编译新代码
  4. 新代码注入到 VM(保留旧实例)
  5. 调用 reassemble()
  6. 触发完整的 build 流程

不支持热重载的场景

  • ❌ 修改 main() 函数
  • ❌ 修改 initState() 方法
  • ❌ 修改全局变量初始化
  • ❌ 修改枚举类型
  • ❌ 修改泛型类型

26. Flutter 与原生如何通信?

答案

三种 Channel

Channel 用途 数据流向
MethodChannel 方法调用 双向请求/响应
EventChannel 事件流 原生 → Flutter
BasicMessageChannel 消息传递 双向自定义编解码
// MethodChannel 示例
const platform = MethodChannel('com.example/battery');

Future<int> getBatteryLevel() async {
  try {
    return await platform.invokeMethod('getBatteryLevel');
  } on PlatformException catch (e) {
    return -1;
  }
}

// EventChannel 示例
const eventChannel = EventChannel('com.example/sensor');
Stream<dynamic> get sensorStream => eventChannel.receiveBroadcastStream();

27. Impeller 与 Skia 的区别?

答案

特性 Skia Impeller
平台 全平台 iOS(默认)、Android(预览)
着色器编译 运行时 预编译
首帧卡顿
Emoji 渲染 可能卡顿 流畅
GPU 内存管理 一般 优化

28. Flutter 的 Layer Tree 是什么?

答案

Layer Tree 是绘制阶段生成的图层树

Layer Tree 结构:
├── TransformLayer(变换层)
├── ClipRectLayer(裁剪层)
├── OpacityLayer(透明度层)
├── PictureLayer(绘制层)
└── ...

用途

  • 优化重绘(只重绘变化的图层)
  • 支持合成效果(透明度、变换等)
  • 提交给 GPU 合成

29. RepaintBoundary 的作用是什么?

答案

RepaintBoundary 用于隔离重绘区域

// 场景:动画只影响一小块区域
Stack(
  children: [
    StaticBackground(),  // 不需要重绘
    RepaintBoundary(
      child: AnimatedWidget(), // 动画只在此区域重绘
    ),
  ],
)

原理

  • 创建独立的绘制边界
  • 子树重绘不影响外部
  • 外部重绘不影响子树

30. Flutter 架构分层是什么?

答案

┌─────────────────────────────────────────┐
│           应用层(Your App)              │
├─────────────────────────────────────────┤
│        Framework 层(Dart)               │
│  ┌─────────────────────────────────────┐ │
│  │ Material / Cupertino Widgets        │ │
│  ├─────────────────────────────────────┤ │
│  │ Widgets Layer                       │ │
│  ├─────────────────────────────────────┤ │
│  │ Rendering Layer                     │ │
│  ├─────────────────────────────────────┤ │
│  │ Foundation / Animation / Gesture    │ │
│  └─────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│          Engine 层(C++)                 │
│  Skia / Impeller / Dart VM / Text       │
├─────────────────────────────────────────┤
│        Embedder 层(平台适配)             │
│  Android / iOS / Web / Desktop          │
└─────────────────────────────────────────┘

三、异步编程xyz(10题)

31. Dart 事件循环是怎样的?

答案

main() {
  print('1. main start');           // 同步

  Future(() => print('4. event'));  // 事件队列

  scheduleMicrotask(              // 微任务队列
    () => print('3. microtask')
  );

  print('2. main end');             // 同步
}

// 输出顺序:1 → 2 → 3 → 4

优先级:同步代码 > 微任务队列 > 事件队列


32. Future 和 Stream 的区别?

答案

特性 Future Stream
返回值次数 一次 多次
使用场景 网络请求、文件读取 按钮点击、WebSocket
订阅方式 .then() / await .listen()
取消 不可取消 可取消
// Future
Future<String> fetchData() async {
  return await http.get(url);
}

// Stream
Stream<int> countStream() async* {
  for (int i = 0; i < 10; i++) {
    yield i;
    await Future.delayed(Duration(seconds: 1));
  }
}

33. Stream 的两种订阅模式?

答案

模式 特点 使用场景
单订阅 只能有一个监听者 文件读取、HTTP 响应
广播 多个监听者 按钮点击、状态变化
// 单订阅(默认)
stream.listen((data) => print(data));

// 转为广播
Stream broadcastStream = stream.asBroadcastStream();
broadcastStream.listen((data) => print('1: $data'));
broadcastStream.listen((data) => print('2: $data'));

34. Isolate 是什么?如何使用?

答案

Isolate 是 Dart 的并发模型,拥有独立的内存和事件循环:

// 方法1:Isolate.run()(推荐)
Future<List<Photo>> loadPhotos() async {
  final jsonString = await rootBundle.loadString('assets/photos.json');

  return await Isolate.run(() {
    final data = jsonDecode(jsonString) as List;
    return data.map((e) => Photo.fromJson(e)).toList();
  });
}

// 方法2:compute()
final result = await compute(parseJson, jsonString);

使用场景

  • JSON 解析(大文件)
  • 图片处理
  • 复杂计算
  • 加密解密

35. async/await 的执行顺序?

答案

Future<void> test() async {
  print('1');
  await Future.delayed(Duration.zero);  // 让出执行权
  print('2');
}

main() {
  print('a');
  test();
  print('b');
}

// 输出:a → 1 → b → 2

原理await 之前同步执行,之后加入微任务队列


36. Future.wait 和 Future.any 的区别?

答案

// Future.wait:等待所有完成
final results = await Future.wait([
  fetchUser(),
  fetchPosts(),
  fetchComments(),
]);
// results = [user, posts, comments]

// Future.any:返回最先完成的
final fastest = await Future.any([
  fetchFromServer1(),
  fetchFromServer2(),
]);
// fastest = 最快返回的结果

37. StreamController 的使用?

答案

class EventBus {
  final _controller = StreamController<Event>.broadcast();

  Stream<Event> get stream => _controller.stream;

  void emit(Event event) {
    _controller.add(event);
  }

  void dispose() {
    _controller.close();
  }
}

// 使用
final bus = EventBus();
bus.stream.listen((event) => print(event));
bus.emit(LoginEvent());

38. FutureBuilder 和 StreamBuilder 的区别?

答案

Widget 数据源 使用场景
FutureBuilder Future(一次性) 网络请求
StreamBuilder Stream(持续) 实时数据
// FutureBuilder
FutureBuilder<User>(
  future: fetchUser(),
  builder: (context, snapshot) {
    if (snapshot.hasData) return UserWidget(snapshot.data!);
    if (snapshot.hasError) return ErrorWidget(snapshot.error!);
    return CircularProgressIndicator();
  },
)

// StreamBuilder
StreamBuilder<int>(
  stream: countStream(),
  builder: (context, snapshot) {
    return Text('Count: ${snapshot.data ?? 0}');
  },
)

39. async* 和 sync* 生成器的区别?

答案

// sync*:同步生成器,返回 Iterable
Iterable<int> syncGenerator() sync* {
  yield 1;
  yield 2;
  yield 3;
}

// async*:异步生成器,返回 Stream
Stream<int> asyncGenerator() async* {
  for (int i = 0; i < 3; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

40. Completer 的作用?

答案

Completer 用于手动完成 Future

Future<String> fetchWithTimeout() {
  final completer = Completer<String>();

  // 设置超时
  Future.delayed(Duration(seconds: 5), () {
    if (!completer.isCompleted) {
      completer.completeError(TimeoutException('Timeout'));
    }
  });

  // 模拟网络请求
  http.get(url).then((response) {
    if (!completer.isCompleted) {
      completer.complete(response.body);
    }
  });

  return completer.future;
}

四、性能优化xyz(10题)

41. 如何减少 Widget 重建?

答案

// 1. 使用 const Widget
const Text('Hello');
const MyWidget();

// 2. 拆分 Widget
class ParentWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ExpensiveWidget(),  // 不会重建
        DynamicWidget(),           // 可能重建
      ],
    );
  }
}

// 3. 使用 Consumer 精确订阅
Consumer<CounterProvider>(
  builder: (context, counter, child) {
    return Text('${counter.value}');
  },
  child: const ExpensiveChild(), // 不会重建
)

// 4. 使用 Selector 订阅单个字段
Selector<AppState, String>(
  selector: (context, state) => state.userName,
  builder: (context, userName, child) {
    return Text(userName);
  },
)

42. 如何优化 ListView 性能?

答案

ListView.builder(
  // 1. 指定固定高度(避免高度计算)
  itemExtent: 80,

  // 2. 设置缓存范围
  cacheExtent: 500,

  // 3. 使用懒加载
  itemCount: items.length,
  itemBuilder: (context, index) {
    // 4. 使用 RepaintBoundary 隔离重绘
    return RepaintBoundary(
      // 5. 使用 const
      child: ListItemWidget(item: items[index]),
    );
  },
)

// 6. 使用 AutomaticKeepAliveClientMixin 保持状态
class _ItemState extends State<Item> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ...;
  }
}

43. 如何避免 saveLayer 导致的性能问题?

答案

saveLayer 是昂贵操作,以下 Widget 会触发:

Widget 替代方案
Opacity 直接设置颜色透明度
ShaderMask 简化效果
ColorFilter 直接应用到 Image
Clip.antiAliasWithSaveLayer 使用 Clip.hardEdge
// ❌ 触发 saveLayer
Opacity(
  opacity: 0.5,
  child: Container(color: Colors.blue),
)

// ✓ 直接设置透明度
Container(
  color: Colors.blue.withOpacity(0.5),
)

44. 如何优化图片加载?

答案

// 1. 设置缓存尺寸
Image.network(
  url,
  cacheWidth: 200,
  cacheHeight: 200,
)

// 2. 预加载图片
precacheImage(NetworkImage(url), context);

// 3. 使用渐进式加载
FadeInImage.memoryNetwork(
  placeholder: kTransparentImage,
  image: url,
)

// 4. 使用缓存库
CachedNetworkImage(
  imageUrl: url,
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
)

// 5. 及时释放
@override
void dispose() {
  imageProvider.evict();
  super.dispose();
}

45. 如何优化动画性能?

答案

// 1. 使用 AnimatedBuilder 而非 setState
AnimatedBuilder(
  animation: controller,
  builder: (context, child) {
    return Transform.rotate(
      angle: controller.value * 2 * pi,
      child: child, // child 不重建
    );
  },
  child: const ExpensiveWidget(),
)

// 2. 使用 RepaintBoundary 隔离重绘
RepaintBoundary(
  child: AnimatedWidget(),
)

// 3. 使用 Transform 而非改变布局
// ❌ 触发布局
Container(
  margin: EdgeInsets.only(left: animation.value),
  child: widget,
)

// ✓ 只触发绘制
Transform.translate(
  offset: Offset(animation.value, 0),
  child: widget,
)

// 4. 使用 vsync
AnimationController(
  vsync: this, // 与屏幕刷新率同步
  duration: Duration(seconds: 1),
)

46. 如何检测和解决内存泄漏?

答案

常见泄漏场景

class _MyWidgetState extends State<MyWidget> {
  StreamSubscription? subscription;
  Timer? timer;
  AnimationController? controller;
  TextEditingController? textController;

  @override
  void initState() {
    super.initState();
    subscription = stream.listen((_) {});
    timer = Timer.periodic(duration, (_) {});
    controller = AnimationController(vsync: this);
    textController = TextEditingController();
  }

  @override
  void dispose() {
    // ✓ 必须释放所有资源
    subscription?.cancel();
    timer?.cancel();
    controller?.dispose();
    textController?.dispose();
    super.dispose();
  }
}

异步回调中的安全检查

Future<void> loadData() async {
  final data = await fetchData();

  // ✓ 检查 mounted 状态
  if (!mounted) return;

  setState(() => this.data = data);
}

47. 如何优化启动性能?

答案

// 1. 延迟初始化非关键服务
void main() {
  WidgetsFlutterBinding.ensureInitialized();

  // 只初始化必需的
  initCriticalServices();

  runApp(MyApp());

  // 延迟初始化其他服务
  Future.delayed(Duration(seconds: 1), () {
    initNonCriticalServices();
  });
}

// 2. 使用懒加载
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FutureBuilder(
        future: loadInitialData(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return SplashScreen();
          }
          return HomeScreen(data: snapshot.data);
        },
      ),
    );
  }
}

// 3. 使用 deferred loading(代码分割)
import 'package:heavy_module/heavy_module.dart' deferred as heavy;

Future<void> loadHeavyModule() async {
  await heavy.loadLibrary();
  heavy.doSomething();
}

48. 如何使用 DevTools 进行性能分析?

答案

1. Performance 视图

  • Flutter Frames Chart:查看每帧的 UI/Raster 时间
  • Frame Analysis:自动检测性能问题
  • Timeline Events:详细追踪事件

2. 关键指标

✓ 绿色帧:< 16ms(正常)
✗ 红色帧:> 16ms(卡顿)

UI Thread:构建和布局时间
Raster Thread:绘制和合成时间

3. 常见优化建议

  • 避免在 build 中创建对象
  • 使用 const Widget
  • 减少 Widget 深度
  • 使用 RepaintBoundary

49. Flutter 3.24+ 性能优化新特性?

答案

1. Impeller 渲染引擎优化

  • 预编译着色器,消除首帧卡顿
  • Emoji 滚动更流畅
  • GPU 内存管理改进

2. 新的 Sliver 组件

CustomScrollView(
  slivers: [
    SliverFloatingHeader(...),     // 浮动头部
    PinnedHeaderSliver(...),       // 固定头部
    SliverResizingHeader(...),     // 可调整大小头部
  ],
)

3. 增强的 Performance 视图

  • 着色器编译追踪
  • 更详细的帧分析
  • 自动性能建议

50. 如何实现高性能的无限滚动列表?

答案

class InfiniteScrollList extends StatefulWidget {
  @override
  State<InfiniteScrollList> createState() => _InfiniteScrollListState();
}

class _InfiniteScrollListState extends State<InfiniteScrollList> {
  final List<Item> items = [];
  final ScrollController controller = ScrollController();
  bool isLoading = false;
  bool hasMore = true;
  int page = 1;

  @override
  void initState() {
    super.initState();
    controller.addListener(_onScroll);
    _loadMore();
  }

  void _onScroll() {
    if (controller.position.pixels >=
        controller.position.maxScrollExtent - 200) {
      _loadMore();
    }
  }

  Future<void> _loadMore() async {
    if (isLoading || !hasMore) return;

    setState(() => isLoading = true);

    try {
      final newItems = await fetchItems(page: page);
      setState(() {
        items.addAll(newItems);
        page++;
        hasMore = newItems.length >= 20;
        isLoading = false;
      });
    } catch (e) {
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: controller,
      itemExtent: 80,                    // 固定高度
      cacheExtent: 500,                  // 缓存范围
      itemCount: items.length + (hasMore ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == items.length) {
          return Center(child: CircularProgressIndicator());
        }
        return RepaintBoundary(          // 隔离重绘
          child: ItemWidget(item: items[index]),
        );
      },
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

五、复杂场景xyz(5题)

51. 如何实现自定义 RenderObject?

答案

class CustomProgressBar extends LeafRenderObjectWidget {
  final double progress;
  final Color color;

  const CustomProgressBar({
    required this.progress,
    required this.color,
  });

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderCustomProgressBar(
      progress: progress,
      color: color,
    );
  }

  @override
  void updateRenderObject(
    BuildContext context,
    RenderCustomProgressBar renderObject,
  ) {
    renderObject
      ..progress = progress
      ..color = color;
  }
}

class RenderCustomProgressBar extends RenderBox {
  double _progress;
  Color _color;

  RenderCustomProgressBar({
    required double progress,
    required Color color,
  })  : _progress = progress,
        _color = color;

  set progress(double value) {
    if (_progress != value) {
      _progress = value;
      markNeedsPaint();  // 触发重绘
    }
  }

  set color(Color value) {
    if (_color != value) {
      _color = value;
      markNeedsPaint();
    }
  }

  @override
  void performLayout() {
    size = constraints.constrain(Size(300, 20));
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final canvas = context.canvas;

    // 背景
    canvas.drawRect(
      Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
      Paint()..color = Colors.grey[300]!,
    );

    // 进度
    canvas.drawRect(
      Rect.fromLTWH(offset.dx, offset.dy, size.width * _progress, size.height),
      Paint()..color = _color,
    );
  }
}

52. 状态管理方案如何选择?

答案

方案 复杂度 适用场景 特点
setState 简单组件 最基础
InheritedWidget 数据传递 Flutter 原生
Provider 中小型应用 官方推荐
Riverpod 现代应用 类型安全、可测试
Bloc 大型应用 事件驱动、清晰分层
GetX 快速开发 轻量、功能全

53. 如何实现国际化(i18n)?

答案

// 1. 定义翻译
class AppLocalizations {
  static Map<String, Map<String, String>> _localizedValues = {
    'en': {'hello': 'Hello', 'world': 'World'},
    'zh': {'hello': '你好', 'world': '世界'},
  };

  static String translate(BuildContext context, String key) {
    Locale locale = Localizations.localeOf(context);
    return _localizedValues[locale.languageCode]?[key] ?? key;
  }
}

// 2. 配置 MaterialApp
MaterialApp(
  localizationsDelegates: [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('en', 'US'),
    Locale('zh', 'CN'),
  ],
)

// 3. 使用
Text(AppLocalizations.translate(context, 'hello'))

54. 如何实现复杂的表单验证?

答案

class FormValidator {
  static String? validateEmail(String? value) {
    if (value?.isEmpty ?? true) return '邮箱不能为空';
    if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
      return '邮箱格式错误';
    }
    return null;
  }

  static String? validatePassword(String? value) {
    if (value?.isEmpty ?? true) return '密码不能为空';
    if (value!.length < 6) return '密码至少6位';
    if (!value.contains(RegExp(r'[A-Z]'))) return '需要包含大写字母';
    return null;
  }
}

class LoginForm extends StatefulWidget {
  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLoading = false;

  Future<void> _submit() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() => _isLoading = true);

    try {
      await login(_emailController.text, _passwordController.text);
      if (!mounted) return;
      Navigator.pushReplacementNamed(context, '/home');
    } catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('登录失败: $e')),
      );
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _emailController,
            validator: FormValidator.validateEmail,
            decoration: InputDecoration(labelText: '邮箱'),
          ),
          TextFormField(
            controller: _passwordController,
            validator: FormValidator.validatePassword,
            obscureText: true,
            decoration: InputDecoration(labelText: '密码'),
          ),
          ElevatedButton(
            onPressed: _isLoading ? null : _submit,
            child: _isLoading
              ? CircularProgressIndicator()
              : Text('登录'),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

55. Flutter 3.24+ 最新特性有哪些?

答案

1. 新的 Sliver 组件

  • SliverFloatingHeader:浮动头部
  • PinnedHeaderSliver:固定头部
  • SliverResizingHeader:可调整大小头部

2. CarouselView(轮播)

CarouselView(
  itemCount: 10,
  itemBuilder: (context, index, realIndex) {
    return Container(color: Colors.primaries[index % 10]);
  },
)

3. TreeView(树形视图)

TreeView(
  nodes: [
    TreeViewNode(title: Text('Parent'), children: [...]),
  ],
)

4. AnimationStatus 增强

if (status.isRunning) { ... }
if (status.isForwardOrCompleted) { ... }

5. Flutter GPU(预览)

  • 直接渲染 3D 图形

6. Web 热重载支持


总结表

分类 核心知识点 题目数
Dart 基础 语法特性、空安全、泛型、扩展方法 15
Flutter 原理 三棵树、渲染流程、生命周期、Key 15
异步编程 Event Loop、Future/Stream、Isolate 10
性能优化 Widget 重建、列表优化、内存管理 10
复杂场景 自定义渲染、状态管理、表单验证 5

掌握这 55 道xyz,可以应对 99% 的 Flutter 面试!🚀

App Groups in iOS

作者 songgeb
2026年1月23日 14:41

参考:developer.apple.com/documentati…

一、什么是 App Group

  • App Group 允许同一开发者团队(Team)下的多个 App访问一个或多个共享空间(Shared Container)
  • 默认情况下(未使用 App Group):
    • 每个 App 都运行在独立进程
    • 拥有独立 沙盒
    • 无法进行数据共享
    • 无法进行 进程间通信
  • 对于 iOS 应用
    • 即使开启了 App Group
    • 只能实现多 App / App Extension 之间的数据或空间共享
    • 无法实现真正的跨进程通信( IPC
  • 对于 macOS 应用
    • App Group 可以放宽沙盒边界
    • 允许通过 Mach IPC、UNIX domain socket 等机制实现 IPC

⚠️ App Group 在 iOS 与 macOS 上的能力存在显著差异

二、App Group 的历史背景

  • App Group 是在 WWDC 2014 中提出的能力
  • iOS 8(以及 OS X 10.10 Yosemite)一起发布
  • 设计初衷是配合 App Extension 的出现:
    • 主 App
    • Widget
    • Share / Action Extension
    • 等多个进程之间的安全数据共享

三、App Group 的基本规则与限制

1. 数量限制

  • 一个开发者账号 最多可以注册 1000 个 App Group
  • 一个 App:
    • 可以不使用 App Group
    • 也可以属于一个或多个 App Group

2. 使用范围

  • 以下组合都可以使用 App Group:
    • App ↔ App Extension
    • App ↔ App
    • App ↔ App Clip

3. Container ID 规则

  • 创建 App Group 时需要设置一个 Container ID
  • Container ID 用于标识共享空间
  • 当 App Group 包含 iOS App(而非 macOS App)时
    • Container ID 必须以 group. 作为前缀

示例:

group.com.company.shared

四、iOS 中 App Group 能做什么,不能做什么

4.1 能做的事情

  • 多进程( App / Extension/App Clip)共享数据

4.2 不能做的事情

  • ❌ 不支持进程间通信(IPC)
  • ❌ 不支持 Mach IPC、socket、shared memory 等机制
  • ❌ 不能假设共享目录的真实路径
  • ❌ 不能假设共享目录一定长期存在

在 iOS 中,App Group 的本质是: 共享存储权限,而不是通信权限

五、iOS App 使用 App Group 共享空间的方式

系统提供了三种主要方式:

5.1 通过 UserDefaults 共享数据

  • 必须使用 init(suiteName:) 初始化

let defaults = UserDefaults(suiteName: "group.com.company.shared") ``defaults?.set("value", forKey: "key")

适用于:

  • 配置项
  • 功能开关
  • 小体量状态数据

5.2 通过共享容器路径读写文件

  • 使用 containerURL(forSecurityApplicationGroupIdentifier:) 获取共享空间 URL

let containerURL = FileManager.default.containerURL( ``forSecurityApplicationGroupIdentifier: "group.com.company.shared" )

说明:

  • 系统只会自动创建 Library/Caches 目录
  • 其他目录需要自行创建
  • 适合存储:
    • JSON
    • SQLite
    • 缓存文件

5.3 App Extension 使用 Background URL Session

  • 对于 App Extension:
    • 使用 URLSessionConfiguration.background
    • 设置 sharedContainerIdentifier
  • 下载的数据会直接存储在 App Group 的共享空间中

适用于:

  • 后台下载
  • Extension 与主 App 共享下载结果

六、工程实践注意事项

  • 不要写死 App Group 的磁盘路径
  • 不要假设共享容器一定存在
    • 当设备上属于同一个app group中的所有应用都卸载后,共享容器也会被删除
  • 多个 App / Extension 需要:
    • 统一目录结构
    • 统一数据格式

七、总结

  • App Group 是 iOS 8 引入的一项共享容器能力
  • 在 iOS 平台上:
    • 它解决的是数据共享问题
    • 而不是进程间通信 问题
  • 合理使用 App Group,可以安全地协调多个 App / Extension 之间的状态与资源

Maintaining shadow branches for GitHub PRs

作者 MaskRay
2026年1月22日 16:00

I've created pr-shadow with vibecoding, a tool that maintains a shadow branch for GitHub pull requests(PR) that never requires force-pushing. This addresses pain points Idescribed in Reflectionson LLVM's switch to GitHub pull requests#Patch evolution.

The problem

GitHub structures pull requests around branches, enforcing abranch-centric workflow. There are multiple problems when you force-pusha branch after a rebase:

  • The UI displays "force-pushed the BB branch from X to Y". Clicking"compare" shows git diff X..Y, which includes unrelatedupstream commits—not the actual patch difference. For a project likeLLVM with 100+ commits daily, this makes the comparison essentiallyuseless.
  • Inline comments may become "outdated" or misplaced after forcepushes.
  • If your commit message references an issue or another PR, each forcepush creates a new link on the referenced page, cluttering it withduplicate mentions. (Adding backticks around the link text works aroundthis, but it's not ideal.)

These difficulties lead to recommendations favoring less flexibleworkflows that only append commits (including merge commits) anddiscourage rebases. However, this means working with an outdated base,and switching between the main branch and PR branches causes numerousrebuilds-especially painful for large repositories likellvm-project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
git switch main; git pull; ninja -C build

# Switching to a feature branch with an outdated base requires numerous rebuilds.
git switch feature0
git merge origin/main # I prefer `git rebase main` to remove merge commits, which clutter the history
ninja -C out/release

# Switching to another feature branch with an outdated base requires numerous rebuilds.
git switch feature1
git merge origin/main
ninja -C out/release

# Listing fixup commits ignoring upstream merges requires the clumsy --first-parent.
git log --first-parent

In a large repository, avoiding rebases isn't realistic—other commitsfrequently modify nearby lines, and rebasing is often the only way todiscover that your patch needs adjustments due to interactions withother landed changes.

In 2022, GitHub introduced "Pull request title and description" forsquash merging. This means updating the final commit message requiresediting via the web UI. I prefer editing the local commit message andsyncing the PR description from it.

The solution

After updating my main branch, before switching to afeature branch, I always run

1
git rebase main feature

to minimize the number of modified files. To avoid the force-pushproblems, I use pr-shadow to maintain a shadow PR branch (e.g.,pr/feature) that only receives fast-forward commits(including merge commits).

I work freely on my local branch (rebase, amend, squash), then syncto the PR branch using git commit-tree to create a commitwith the same tree but parented to the previous PR HEAD.

1
2
3
4
5
6
Local branch (feature)     PR branch (pr/feature)
A A (init)
| |
B (amend) C1 "Fix bug"
| |
C (rebase) C2 "Address review"

Reviewers see clean diffs between C1 and C2, even though theunderlying commits were rewritten.

When a rebase is detected (git merge-base withmain/master changed), the new PR commit is created as a merge commitwith the new merge-base as the second parent. GitHub displays these as"condensed" merges, preserving the diff view for reviewers.

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Initialize and create PR
git switch -c feature
edit && git commit -m feature

# Set `git merge-base origin/main feature` as the initial base. Push to pr/feature and open a GitHub PR.
prs init
# Same but create a draft PR. Repeated `init`s are rejected.
prs init --draft

# Work locally (rebase, amend, etc.)
git fetch origin main:main
git rebase main
git commit --amend

# Sync to PR
prs push "Rebase and fix bug"
# Force push if remote diverged due to messing with pr/feature directly.
prs push --force "Rewrite"

# Update PR title/body from local commit message.
prs desc

# Run gh commands on the PR.
prs gh view
prs gh checks

The tool supports both fork-based workflows (pushing to your fork)and same-repo workflows (for branches likeuser/<name>/feature). It also works with GitHubEnterprise, auto-detecting the host from the repository URL.

Related work

The name "prs" is a tribute to spr, which implements asimilar shadow branch concept. However, spr pushes user branches to themain repository rather than a personal fork. While necessary for stackedpull requests, this approach is discouraged for single PRs as itclutters the upstream repository. pr-shadow avoids this by pushing toyour fork by default.

I owe an apology to folks who receiveusers/MaskRay/feature branches (if they use the defaultfetch = +refs/heads/*:refs/remotes/origin/* to receive userbranches). I had been abusing spr for a long time after LLVM'sGitHub transition to avoid unnecessary rebuilds when switchingbetween the main branch and PR branches.

Additionally, spr embeds a PR URL in commit messages (e.g.,Pull Request: https://github.com/llvm/llvm-project/pull/150816),which can cause downstream forks to add unwanted backlinks to theoriginal PR.

If I need stacked pull requests, I will probably use pr-shadow withthe base patch and just rebase stacked ones - it's unclear how sprhandles stacked PRs.

❌
❌