普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月11日首页

30.99 万元起,搭载华为 ADS4 的 MPV,岚图梦想家冠军版上市

作者 谢东成
2026年3月11日 22:30

3 月 10 日,岚图在武汉的黄金工厂,迎来了岚图梦想家的第 20 万辆整车下线。

更重要的是,这台车正好是同日官宣限量上市的「岚图梦想家冠军版」,而奥运冠军杨威正是这台第 20 万辆岚图梦想家的车主。

就在现场,杨威从岚图汽车董事长、党委书记卢放手中,接过了这部新车的钥匙。

20 万台,对于一台高端新能源 MPV 来说,是一个足以证明市场认可度的数字。

卖了 20 万辆,卷入 30 万元「红海」

如果把中国汽车市场比作一份终极考卷,那「高端 MPV」绝对是过去十几年里最难解的一道压轴大题。

很长一段时间里,这道题的满分标准是由别克 GL8、丰田埃尔法等合资与进口巨头所定义的 。但时间来到 2026 年,你会发现,马路上疾驰的高端 MPV 里,国产自主品牌的身影正变得越来越密集。

在这场破局战中,历经四载、三代进化的岚图梦想家,用 20 万台整车下线的成绩,交出了一份极具分量的答卷 。

但手握 20 万 MPV 用户基本盘的岚图,显然没打算在舒适圈中止步 。

面对腾势 D9、小鹏 X9 强敌环伺、争夺激烈的 30 万级 MPV 市场,岚图选择主动出击,将 40 万级的豪华与智能体验直接下放,推出了售价 30.99 万元的「岚图梦想家 冠军版」 。

不难看出,要在这样一片厮杀红眼的「红海」里抢地盘,就必须得拿出足以破局的诚意。

第一个,是智驾。

之前提起 MPV,可能较少人会先想起「智驾」,而岚图梦想家冠军版身上最大的杀手锏,就是搭载了同级唯一的华为乾崑智驾 ADS 4。

它配备了一颗 192 线激光雷达,具备 250m 的超远探测距离和 30cm 小目标物的精准识别能力,即便在雨雾天气中和夜间也能正常使用。

伴随着智驾而来的,是家庭用户更加在意的「主动安全」。

岚图梦想家冠军版成为国内首个通过 110km/h 超美标高速追尾测试的 MPV,不仅支持 130km/h 的 AEB 自动紧急制动刹停,还在第三排座椅靠背嵌入了高强度钢板,全方位筑牢了主被动的安全防线 。

在看不见的硬件底子上,冠军版也继承了梦想家系列备受好评的底盘素质,冠军版标配了前双叉臂、后五连杆独立悬挂以及智能电动四驱 ,以及 255mm 胎宽的 20 英寸轮胎。

而在座舱内部,支持 -6℃ 到 50℃ 宽温调节的 13L 双门冷暖箱,配合直接接入鸿蒙生态的 17.3 英寸 3K 后排娱乐屏,将实用主义的舒适体验落到了实处 。

然而,在 20 万辆的耀眼数字和携手华为乾崑密切合作的背后,这家有着央企背景的新能源车企,到底藏着怎样的深层思考与战略布局 ?

在发布会后的媒体群访中,岚图汽车董事长卢放与 CBO 邵明峰给出了深度且硬核的解答 。

不牺牲「驾驶者」的舒适平权

「过去一段时间,我们取得了阶段性的成功,证明了央国企在做新能源转型方面,能够在市场上取得领先的地位。」

岚图汽车董事长、党委书记卢放表示 ,「4 年前,中国高端新能源 MPV 市场还处于空白,用户往往只能选择合资燃油 MPV 。」

「岚图梦想家的初衷,就是要用中国自主的高端新能源 MPV 抢占一席之地,而 20 万辆的下线,正是这一初衷开花结果的最好证明 。」

接下来,面对当下越来越「卷」的 30 万级家用 MPV 市场,岚图打出了一张「平权」牌。

岚图汽车 CBO 邵明峰敏锐地指出,过去的 MPV 往往是「爸爸开、全家坐」,但司机的驾驶体验往往被牺牲了。

而岚图梦想家冠军版的核心意义就在于实现「舒适平权」和「科技平权」,将 40 万级 MPV 的标准带入 30 万级市场,把百万级全铝悬架和华为乾崑智驾带给用户,让爸爸们不再将就。

面对市场上诸多友商声称「梦想家有的我都有」的言论,邵明峰毫不客气地回应了两个字:「别信」。

他强调,有些东西是无法复印的。例如岚图坚持的全系四驱和前双叉臂后五连杆底盘悬架,在雪地模式、飞坡过弯时不侧倾的表现,以及紧急情况下的 Z 形避让能力,都是实打实的硬核素质 。在岚图看来,真正的豪华标准永远不会变,那就是安全。

深度捆绑华为,L3 智驾破局

在智能化下半场,岚图与华为的合作并非简单的「买卖关系」。

卢放透露,双方是相互赋能的深度合作,岚图在合作中学习了华为在软件开发和用户体验定义上的流程体系 。

这种「聪明的大脑」与岚图「矫健的体魄(底盘、动力、车身结构)」相结合,旨在共同为用户打造顶尖的综合体验 。

据悉,接下来即将发布的岚图泰山 Ultra 版还将搭载华为更顶级的四激光雷达解决方案,包括首个量产的 896 线激光雷达。

这也就意味着,对于行业热议的 L3 级自动驾驶,岚图早已在暗中蓄力。

目前,岚图 L3 级测试车在武汉的实际道路测试里程已超 11 万公里,模拟测试超 90 万公里 。

卢放表示,岚图即将推出的新车(如泰山 Ultra 及代号「珠峰」的车型)已经具备了 L3 级的软硬件智能架构和五大安全冗余。虽然真正推向市场还需要相关法律法规和基础设施的完善,但技术层面的储备已经就绪。

赴港上市,敲钟在即

如果说产品和技术是岚图的硬实力,那么资本市场的动作则是其未来发展的加速器。

3 月 19 日,岚图汽车将正式在香港联交所主板挂牌上市,迎来「央国企高端新能源第一股」的里程碑时刻 。

对于上市后的打法,卢放强调了初心不改、使命不变:「岚图将继续坚持高质量发展和长期主义。在此之前,岚图已经凭借全价值链的提质降本增效、核心技术自研以及直营模式,在去年实现了盈利,并将毛利率保持在行业高位。」

至于上市后,卢放表示,岚图将以更加开放的姿态融入全球化战略布局,向全球资本市场证明中国高端新能源品牌的真正价值。

从 20 万辆的里程碑,到 30 万级 MPV 市场的重塑,再到赴港上市的资本跨越,「岚图梦想家」系列不仅是一个产品的成功,更映照出了中国汽车品牌向上突围的清晰路径。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


UITableView 在 width=0 时 reloadData 被"空转消费"导致 Cell 显示错乱

作者 songgeb
2026年3月11日 13:30

深入理解代替单纯记忆

本文中的问题和排查过程由作者完成,文章编写由Cursor完成

一、问题现象

一个 UITableView 在特定时序下出现了诡异的显示错乱:

  • 数据源有 2 条数据 [数据 B, 数据 A]numberOfRowsInSection 返回 2
  • 但 UITableView 显示了 2 条完全相同的数据 A
  • 通过日志发现 cellForRowAtIndexPath 只被调用了 1 次(row=1),row=0 从未被请求

数据源没有问题,UITableView 却跳过了 row=0 的 cell 请求。

二、场景结构

出问题的 VC 架构如下:

ContainerVC(容器,通过 frame 动画实现滑入/滑出)
  └── containerView(承载内容的 view,初始位置在屏幕外)
        └── ListVC.view(子 VC,内含 UITableView)

关键行为:

  • ContainerVC 通过 present 弹出,containerView 初始在屏幕外,然后通过 frame 动画滑入
  • ListVCinit 中注册通知,数据变化时调用 reloadData
  • ContainerVC dismiss 后不会释放,下次打开复用同一个实例

三、复现步骤

  1. 打开 ContainerVCcontainerView 滑入,UITableView 显示 [数据 A],正常
  2. 关闭(dismiss),ContainerVC 及其子 VC 仍然存活
  3. 此时外部数据变化,通知触发 reloadData,数据源变为 [数据 B, 数据 A]
  4. 再次打开 ContainerVC

预期:显示 [数据 B, 数据 A]

实际:显示 [数据 A, 数据 A]

四、排查过程

4.1 排除数据源问题

日志确认 numberOfRowsInSection 返回 2,两条数据标识符不同。数据源正确。

4.2 怀疑 reloadData 在 off-screen 时异常

dismiss 后通知仍在触发 reloadData(view.window == nil),怀疑这导致了 UITableView 内部状态不一致。

但通过对照实验推翻了这个假设:我们有另一个功能相同但布局实现不同的 ContainerVC_B。替换后,即使同样在 off-screen 时触发 reloadData,重新打开后 cellForRowAtIndexPath 正确执行了 2 次

结论:off-screen 时的 reloadData 不是问题,问题在 ContainerVC 自身的实现。

4.3 对比两个容器的实现差异

逐行对比发现,关键差异在 ListVC.view 的 AutoLayout 约束上。

ContainerVC_B(正常)—— 约束相对于 containerView:

// containerView 尺寸通过 frame 设定,是固定值
containerView.frame = CGRectMake(0, offScreenY, fixedWidth, fixedHeight);

// ListVC.view 的宽度 = containerView.width = 固定值
[listVC.view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.leading.trailing.equalTo(containerView);
}];

ContainerVC(异常)—— 约束跨越了视图层级:

// containerView 尺寸也是固定的
containerView.frame = CGRect(x: offScreenX, y: 0, width: fixedWidth, height: fixedHeight)

// 但 headerView 的 trailing 锚定到了 VC 主 view 的 safeArea
headerView.snp.makeConstraints { make in
    make.leading.equalToSuperview()                              // = containerView.leading
    make.trailing.equalTo(view.safeAreaLayoutGuide.snp.trailing) // = VC 主 view 的右边缘
}

// ListVC.view 跟着 headerView 走
listVC.view.snp.makeConstraints { make in
    make.leading.trailing.equalTo(headerView) // width = headerView.width
}

这个跨视图层级的约束就是根因。

五、根因分析

5.1 跨视图约束如何导致 width=0

headerViewcontainerView 的子视图,但它的 trailing 约束锚定到了 VC 主 viewsafeAreaLayoutGuide.trailing

AutoLayout 解析约束时,会将所有边的位置转换到共同祖先的坐标系中计算。当 containerView 在屏幕外时:

headerView.leading  = containerView.leading  ≈ 844(屏幕外)
headerView.trailing = view.safeArea.trailing  ≈ 800(屏幕右边缘)

trailing(800) < leading(844) → 宽度为负 → 被压缩为 0

ListVC.viewleading.trailing 跟着 headerViewtableView.width = 0

ContainerVC_B 的约束全部相对于 containerView,后者的尺寸是 frame 设定的固定值,不随位置变化,所以 tableView 始终有有效宽度。

5.2 reloadData 在 width=0 时为什么会导致显示错乱?

根据日志观察到的现象,推测因果链如下:

  1. reloadData 在 width=0 时被触发。UITableView 计算可见行数为 0,因此不调用 cellForRow,也不回收旧 cell。但 UITableView 内部可能认为这次 reload 已经完成。

  2. reload 被"空转消费"—— 流程走了,但实际什么都没刷新。旧的 cell(第一次打开时创建的 CellA)仍然挂在 tableView 的 subview 上。

  3. containerView 滑入屏幕、tableView width 从 0 恢复正常时,触发了 layoutSubviews。但 UITableView 不再将其视为一次完整的 reload,而是当作尺寸变化引起的增量布局

  4. 增量布局中,UITableView 发现 row=0 位置已有一个 cell(上次残留的 CellA),直接复用,不调用 cellForRow。仅对 row=1 调用 cellForRow,返回数据 A 的 cell。

  5. 最终两行都显示数据 A。

六、修复

ListVC.viewleading.trailing 约束改为相对于 containerView

// 修复前:width 间接依赖 headerView(跨视图约束,position-dependent)
listVC.view.snp.makeConstraints { make in
    make.leading.trailing.equalTo(headerView)
}

// 修复后:width 直接依赖 containerView(固定尺寸,position-independent)
listVC.view.snp.makeConstraints { make in
    make.leading.trailing.equalTo(containerView)
}

containerView 的 width 是通过 frame 设定的固定值,不随位置变化。改动后 tableView 在任何时刻都有有效宽度,reloadData 不会被空转消费。

七、总结

归根到底,这是UITableView 的 reloadData 时的一个边界行为

当 tableView 的 bounds 宽度(或高度)为 0 时,reloadData 会走内部流程(查询行数),但可能不会创建或回收任何 cell。后续尺寸恢复时,UITableView 按增量布局处理,可能复用之前残留的旧 cell。

这可能不一定是 UITableView 的 bug,而是合理的优化 —— 没有可见区域时不创建 cell。但如果约束写法导致 tableView 在不该为 0 的时候 width 为 0,这个行为就会引发显示错乱。

排查建议

cellForRowAtIndexPath 的调用次数不符合预期时,优先检查 tableView 在 reloadData 时刻的 frame:

NSLog(@"reloadData: frame=%@, window=%@",
    NSStringFromCGRect(self.tableView.frame),
    self.tableView.window);

如果 width 或 height 为 0,reloadData 就会被空转消费。

# iOS Block 深度解析

作者 忆江南
2026年3月11日 10:44

全面剖析 Block 的本质、底层结构、内存管理、变量捕获、循环引用、线程安全、调试技巧与最佳实践。 力求深入而易懂——用类比代替术语堆砌,用图表代替大段代码。


一、Block 的本质

Block 是 C 语言层面的匿名函数 + 自动捕获上下文变量的能力 的组合体。

它不是 Objective-C 独有的特性,而是 Apple 对 C 语言的扩展(Clang 编译器实现),所以在 C、C++、Objective-C、Objective-C++ 中都可以使用。

一句话概括 Block 的本质:

Block 是一个封装了函数指针和捕获变量的 Objective-C 对象。

这意味着 Block 同时具备两重身份:

  • 作为函数:可以被调用、传参、返回
  • 作为对象:有 isa 指针,可以被 copy/release,参与 ARC 内存管理

类比理解: 普通函数像一台固定在车间里的机器——你去找它,它帮你加工。Block 像一台可以搬走的便携机器——你能带着它走,而且它随身携带了自己需要的原材料(捕获的变量)。

1.1 从编译器视角看 Block 的诞生

Clang 编译 Block 时经历的变换过程:

源代码层               编译器中间表示层              机器码层

^{ NSLog(@"Hi"); }
        │
        ▼
  语法解析为 BlockExpr
        │
        ▼
  分析捕获变量列表
  (遍历 Block 体内所有引用的外部变量)
        │
        ▼
  生成 Block_layout 结构体定义
  (根据捕获变量数量和类型动态确定结构体大小)
        │
        ▼
  将 Block 体内的代码提取为一个独立的 C 函数
  (函数名通常为 __文件名_block_func_序号)
  (第一个参数为 Block 结构体指针)
        │
        ▼
  在原位置生成结构体初始化代码
  (填充 isa、invoke、descriptor、捕获变量)
        │
        ▼
  ARM64 机器码

关键洞察: Block 的"闭包"能力不是运行时魔法,而是编译期的代码变换——编译器帮你把自由变量"打包"进了一个结构体。这就像你要出差,把办公桌上需要的文件全装进行李箱带走,到了酒店打开就能继续工作。

1.2 Block 与其他语言闭包的本质差异

┌──────────────┬────────────────────────────────────────────┐
│    语言       │    闭包实现方式                               │
├──────────────┼────────────────────────────────────────────┤
│ JavaScript   │ 通过作用域链引用外部变量(共享同一个变量)        │
│              │ 闭包和外部代码修改的是同一个变量                 │
│              │ 不需要特殊关键字                               │
├──────────────┼────────────────────────────────────────────┤
│ Swift        │ 默认捕获引用(和 JS 类似,共享变量)             │
│              │ [value] 显式值捕获                            │
│              │ 闭包是引用类型                                 │
├──────────────┼────────────────────────────────────────────┤
│ OC Block     │ 默认值捕获(拷贝一份副本)                      │
│              │ 需要 __block 才能共享变量                      │
│              │ Block 有 Stack/Malloc/Global 三种存储位置      │
│              │ 需要显式/隐式 copy 才能延长生命周期               │
├──────────────┼────────────────────────────────────────────┤
│ C++ Lambda   │ [=] 值捕获,[&] 引用捕获                     │
│              │ 编译为匿名类的 operator()                     │
│              │ 和 OC Block 最相似                            │
└──────────────┴────────────────────────────────────────────┘

OC Block 的独特之处:
  1. 默认值捕获 → 安全但反直觉(修改需要 __block)
  2. 有栈→堆迁移的概念 → 其他语言的闭包都直接在堆上
  3. 是 OC 对象 → 参与引用计数,有循环引用问题

1.3 为什么 Apple 选择了默认"值捕获"

这个设计决策背后有深层考虑:

JavaScript"引用捕获"经常制造 Bugfor (var i = 0; i < 3; i++) {
      setTimeout(function() { console.log(i); }, 0);
  }
  // 输出 3, 3, 3(而非期望的 0, 1, 2)
  // 因为闭包共享了变量 i,循环结束时 i = 3

OC"值捕获"避免了这类问题:
  
  for (int i = 0; i < 3; i++) {
      dispatch_async(queue, ^{ NSLog(@"%d", i); });
  }
  // 输出 0, 1, 2 ✓
  // 每个 Block 在创建时拍了一张 i 的快照

Apple 的设计哲学:
  "大多数场景下,Block 只需要读取变量的值,不需要修改"
  "让安全的事情成为默认,不安全的事情需要显式声明(__block)"
  
  这是典型的 Pit of Success 设计理念 ——
  让开发者默认就掉进成功的坑里,想犯错反而需要额外的努力。

二、Block 的底层结构

编译器会将每个 Block 转换为一个结构体 + 一个函数

Block 变量(指针)
    │
    ▼
┌──────────────────────────────────────┐
│           Block_layout 结构体          │
├──────────────────────────────────────┤
│  isa 指针          → 指向 Block 的类   │
│  flags             → 标志位            │
│  reserved          → 保留字段          │
│  invoke            → 函数指针 (核心)    │
│  descriptor        → 描述信息指针       │
│  ─────────────────────────────────── │
│  captured_var_1    → 捕获的变量 1       │
│  captured_var_2    → 捕获的变量 2       │
│  ...                                  │
└──────────────────────────────────────┘

类比: 把 Block 想象成一个快递包裹:

  • isa 是包裹类型标签(普通件/到付件/国际件)
  • flags 是物流状态码
  • invoke 是"使用说明书"(告诉你怎么打开和使用内容物)
  • descriptor 是"装箱清单"(描述包裹尺寸和特殊处理要求)
  • 捕获的变量就是包裹里的内容物

2.1 flags 标志位详解

flags 不是一个简单的整数,它是一个位域(bitfield),每一位都有特定含义:

flags 位域布局 (32 bit):

 31  30  29  28  27  26  25  24        16  15         1   0
┌───┬───┬───┬───┬───┬───┬───┬──── ─ ────┬───┬──── ─ ───┬───┐
│   │   │   │   │   │   │   │           │   │          │   │
└───┴───┴───┴───┴───┴───┴───┴──── ─ ────┴───┴──── ─ ───┴───┘
  │   │   │   │                   │               │
  │   │   │   │                   │               └── bit 0: BLOCK_DEALLOCATING
  │   │   │   │                   │                   正在被释放
  │   │   │   │                   │
  │   │   │   │                   └── bit 1~15: 引用计数(存储在这里!)
  │   │   │   │                       堆上 Block 的 retainCount
  │   │   │   │
  │   │   │   └── bit 24: BLOCK_NEEDS_FREE
  │   │   │       表示是堆上的 Block,需要 free 释放
  │   │   │
  │   │   └── bit 25: BLOCK_HAS_COPY_DISPOSE
  │   │       表示有 copy_helper 和 dispose_helper
  │   │       (即捕获了对象或 __block 变量)
  │   │
  │   └── bit 26: BLOCK_HAS_CTOR
  │       捕获的变量有 C++ 构造函数
  │
  └── bit 30: BLOCK_HAS_SIGNATURE
      表示有方法签名(可通过 NSMethodSignature 获取参数/返回值类型)

隐藏知识: Block 的引用计数不像普通 OC 对象存在 SideTable 中,而是直接编码在 flags 的 bit 1~15 中。这是一个性能优化——避免了每次 retain/release 都要查 SideTable 的哈希表。这意味着 Block 最大引用计数为 2^15 - 1 = 32767,不过实际场景中绰绰有余。

2.2 descriptor 的多态结构

descriptor 不是固定结构,它根据 Block 捕获的内容有不同的版本

版本 1(不捕获对象/不捕获 __block 变量):
┌─────────────────────────────┐
│  unsigned long reserved     │  → 0unsigned long size         │  → Block 结构体总字节数
└─────────────────────────────┘

版本 2(捕获了对象或 __block 变量,BLOCK_HAS_COPY_DISPOSE = 1):
┌─────────────────────────────┐
│  unsigned long reserved     │
│  unsigned long size         │
│  void (*copy_helper)()      │  → 新增:拷贝时调用
│  void (*dispose_helper)()   │  → 新增:销毁时调用
└─────────────────────────────┘

版本 3(有方法签名,BLOCK_HAS_SIGNATURE = 1):
┌─────────────────────────────┐
│  unsigned long reserved     │
│  unsigned long size         │
│  void (*copy_helper)()      │  → 可能有
│  void (*dispose_helper)()   │  → 可能有
│  const char *signature      │  → 新增:Block 的类型编码字符串
└─────────────────────────────┘
    例如 signature = "v8@?0" 表示 void(^)(void)

这种多态设计的好处:
  - 不捕获对象的 Block,descriptor 更小,节省内存
  - 编译器根据情况选择最紧凑的版本
  
类比:就像飞机上的行李标签——
  国内经济舱行李只需要简单标签(版本 1)
  含易碎品的行李需要额外的特殊处理标签(版本 2)
  国际航班行李还需要海关申报信息(版本 3

2.3 invoke 函数的隐含参数

当你写这样的代码:

  int x = 10;
  void(^block)(int) = ^(int y) { printf("%d", x + y); };
  block(20);

编译器生成的 invoke 函数签名是:

  static void __main_block_func_0(
      struct __main_block_impl_0 *__cself,  // ← 隐含的第一个参数!
      int y                                  // ← 你写的参数
  )

调用过程:
  block(20)
      → block->invoke(block, 20)
         ↑ Block 把自己作为第一个参数传入
         这样 invoke 函数就能访问 Block 结构体中捕获的变量

这和 OC 方法调用极其类似:
  [obj doSomething]objc_msgSend(obj, @selector(doSomething))
  block(20)          → block->invoke(block, 20)

2.4 Block 的类型编码(Type Encoding)

Block 在 runtime 中也有类型签名,这让它可以与 NSInvocation 配合使用:

Block 签名编码规则:

  void(^)(void)             → "v8@?0"
  void(^)(int)              → "v12@?0i8"
  NSString *(^)(int, BOOL)  → "@20@?0i8B12"

编码字符含义:
  v  = void
  @  = 对象指针
  @? = Block 类型(@ 表示对象,? 表示 Block)
  i  = int
  B  = BOOL
  数字 = 参数在栈帧中的偏移量

获取 Block 签名的方式:
  从 descriptor 的 signature 字段读取
  可用 NSMethodSignature 解析为可读格式

这个签名使得 Block 可以被 NSInvocation 动态调用,
也使得 libffi 能够在运行时对 Block 做各种 hook 和转发操作。

三、Block 的三种类型

Block 根据存储位置分为三种类型,这是理解 Block 内存管理的关键:

┌─────────────────────────────────────────────────────────────┐
│                      内存布局                                 │
│                                                              │
│  高地址  ┌──────────────┐                                     │
│         │     栈 Stack   │ ← __NSStackBlock__                 │
│         │   (向下增长)    │   栈上的 Block,离开作用域即销毁       │
│         ├──────────────┤                                     │
│         │              │                                     │
│         │     堆 Heap   │ ← __NSMallocBlock__                 │
│         │              │   堆上的 Block,引用计数管理            │
│         ├──────────────┤                                     │
│         │   全局/静态区   │ ← __NSGlobalBlock__                │
│         │   Data Segment│   全局 Block,程序结束才销毁           │
│         ├──────────────┤                                     │
│  低地址  │   代码区 Text  │                                     │
│         └──────────────┘                                     │
└─────────────────────────────────────────────────────────────┘

3.1 三种类型的判定规则

类型 条件 生命周期 isa 指向
__NSGlobalBlock__ 不捕获任何外部局部变量 与程序同生共死 _NSConcreteGlobalBlock
__NSStackBlock__ 捕获了外部局部变量(MRC 或未被强引用) 与所在栈帧同生共死 _NSConcreteStackBlock
__NSMallocBlock__ Stack Block 经 copy 引用计数为 0 时销毁 _NSConcreteMallocBlock

类比理解:

  • 全局 Block → 写在教科书上的公式——永远在那里,谁都能用
  • 栈 Block → 写在白板上的草稿——会议结束就擦掉
  • 堆 Block → 拍了照片保存在手机里的公式——照片在就在,删了就没了

3.2 三种类型的 retain/copy/release 行为差异

                    retain        copy          release
                    ──────        ────          ───────
GlobalBlock         什么都不做     返回自身        什么都不做
                    (它是不死的)   (不需要 copy)   (不能被释放)

StackBlock          什么都不做     拷贝到堆上      什么都不做
                    (栈管理)      返回堆上新副本   (栈自动管理)
                                  ★这是唯一有效操作

MallocBlock         引用计数 +1   引用计数 +1     引用计数 -1
                                  (不会重新拷贝)   (归零则释放)

关键洞察:
  - 对 StackBlock 做 retain 是无效的(不增加引用计数)
  - 必须 copy 才能将其"救"到堆上
  - 这就是为什么 Block 属性要用 copy 而不是 strong(MRC 时代的历史原因)
  - ARC 下 strongcopy 对 Block 效果相同(编译器自动插入 copy

面试陷阱: "对一个已经在堆上的 Block 再做 copy 会发生什么?"——答案是只增加引用计数(+1),不会创建新副本。这和 NSMutableArray copy 会创建新对象是不同的行为。

3.3 ARC 下的自动 copy —— 编译器到底做了什么

在 ARC 环境下,以下场景编译器会自动将栈 Block copy 到堆上

自动 copy 触发场景:
├── ① Block 作为函数/方法返回值
│      编译器在 return 语句处插入 objc_retainBlock() → 内部调用 _Block_copy()
│
├── ② Block 赋值给 __strong 修饰的变量
│      void(^block)(void) = ^{ ... };
│      编译器在赋值处插入 objc_retainBlock()
│
├── ③ Block 作为 GCD API 的参数
│      dispatch_async 内部实现中调用 _Block_copy()
│
├── ④ Block 作为 Cocoa 框架中含 usingBlock 的方法参数
│      框架内部负责 copy
│
└── ⑤ Block 被传入方法的参数(编译器启发式判断)
       如果方法签名暗示会保存 Block(如 completion handler),编译器插入 copy

编译器不会自动 copy 的场景(ARC 下罕见但存在):
├── Block 作为函数参数传递时,如果调用者没有保存它
│   (此时 Block 可能在栈上,但因为是同步使用所以安全)
└── 使用 __unsafe_unretained 修饰的变量接收 Block

3.4 为什么 Block 要有栈这个中间状态?

设计哲学:性能与安全的权衡

如果 Block 一律在堆上创建(像 Swift 闭包那样):
  ✅ 简单,不需要考虑栈→堆迁移
  ❌ 每次创建 Block 都要 malloc,频繁的堆分配 + GC 压力

OC Block 的策略:
  ✅ 大量临时 Block(如 for 循环中的 Block)直接在栈上创建和销毁
     无 malloc 开销、无引用计数开销
  ✅ 只有需要"逃逸"的 Block 才 copy 到堆上
  ⚠️ 代价是引入了栈→堆迁移的复杂性

类比理解:
  栈 Block → 临时工,干完活就走,不占编制
  堆 Block → 正式员工,办了入职手续(malloc),有档案(引用计数)
  全局 Block → 终身教授,和学校同在

在 ARC 时代,编译器自动处理迁移,开发者几乎无感。
但理解这个机制对排查内存问题至关重要。

3.5 逃逸(Escape)与非逃逸(Non-Escape)

这个概念虽然在 Swift 中用 @escaping 关键字显式化了,但在 OC Block 中一直存在,只是没有语法层面的区分:

什么是逃逸?
  Block 的使用范围"逃"出了它被创建的那个函数作用域。

非逃逸 Block(不需要 copy 到堆上):
  - 在当前函数内同步调用然后丢弃
  - 例如:enumerateObjectsUsingBlocksortUsingComparator
  - 函数结束时 Block 还在栈上,随栈帧销毁即可
  
逃逸 Block(必须 copy 到堆上):
  - 被保存到属性/实例变量中
  - 被异步 dispatch 执行
  - 作为返回值传出当前函数
  - 被传入会保存它的 API(如 NSNotificationCenterblock observer)

逃逸 Block 的特征:
  ┌──────────────────────────────────────────────────────┐
  │ Block 在创建者的作用域结束之后仍然可能被调用 → 逃逸      │
  │ Block 只在创建者的作用域内被调用 → 非逃逸                │
  └──────────────────────────────────────────────────────┘

Swift 把这个概念升级为编译器强制检查:
  - func doWork(completion: @escaping () -> Void)   // 编译器知道会逃逸
  - func doWork(block: () -> Void)                  // 默认非逃逸,性能更好

OC 中虽然没有编译器检查,但 ARC 的自动 copy 机制帮你兜了底:
  该 copy 的时候自动 copy,不需要显式操心。

四、变量捕获机制

这是 Block 最核心、最容易出问题的部分。

4.1 捕获规则总览

┌──────────────────┬────────────────────┬──────────────────────┐
│    变量类型        │    捕获方式          │    Block 内能否修改    │
├──────────────────┼────────────────────┼──────────────────────┤
│ 局部基本类型变量    │ 值拷贝 (copy)       │ 不能(编译报错)       │
│ 局部对象类型变量    │ 指针值拷贝 (copy)    │ 不能改指针指向         │
│                  │                    │ 能改指向对象的属性      │
│ __block 局部变量   │ 封装为堆上结构体引用  │ 能                   │
│ 静态局部变量       │ 指针引用 (不拷贝)     │ 能                   │
│ 全局变量          │ 不捕获 (直接访问)     │ 能                   │
│ self             │ 强引用捕获           │ 能访问属性/方法        │
└──────────────────┴────────────────────┴──────────────────────┘

4.2 局部变量的值捕获 —— 为什么是"快照"

捕获时刻:Block 定义时(不是调用时)

int a = 10;

void(^block)(void) = ^{
    // 这里的 a 是定义时拷贝进来的值 = 10
};

a = 20;
block();  // 输出 10,不是 20

原理: 编译器在 Block 结构体中新增了一个 int a 成员变量,在 Block 创建的那一刻,将外部 a 的值拷贝进去。之后外部 a 的变化与 Block 内部无关。

Block 结构体                外部栈帧
┌──────────────┐           ┌──────────────┐
│ isa           │           │              │
│ invoke        │           │ a = 20       │  ← 外部已改为 20
│ ...           │           │              │
│ a = 10        │ ← 独立副本 │              │
└──────────────┘           └──────────────┘
      ↑ Block 内部读到的始终是 10

为什么编译器禁止修改? 不是技术上不能改(结构体成员当然可以改),而是改了会造成语义困惑——开发者期望改的是外部变量 a,但实际改的是内部副本,外部完全无感。编译器选择了"报错"而非"允许但行为诡异"。

类比: 值捕获就像你拍了一张照片。照片定格了那一瞬间的画面。之后现实场景怎么变化,照片里的内容不会变。而且你在照片上涂改也改不了现实。

4.3 对象类型的值捕获 —— 指针拷贝的微妙之处

对象类型的捕获容易让人困惑,因为需要区分"指针"和"指针所指的对象":

NSMutableArray *arr = [NSMutableArray new];

void(^block)(void) = ^{
    [arr addObject:@"hello"];  // ✅ 合法!
    arr = [NSMutableArray new]; // ❌ 编译报错!
};

为什么前者合法,后者报错?

Block 拷贝的是"指针的值"(即对象的地址),不是对象本身。

┌───── Block 结构体 ─────┐          ┌─── NSMutableArray ───┐
│                        │          │                      │
│  arr = 0x1234 (拷贝) ──┼────→     │  contents: [...]     │
│                        │          │                      │
└────────────────────────┘          └──────────────────────┘
                                          ↑
┌───── 外部栈帧 ─────────┐               │
│                        │               │
│  arr = 0x1234 (原件) ──┼───────────────┘
│                        │
└────────────────────────┘

两个 arr 指针虽然是独立副本,但都指向同一个 NSMutableArray 对象。
所以:
  - [arr addObject:] → 通过指针操作对象 → 合法(没改指针本身)
  - arr = xxx        → 修改指针本身 → 报错(和修改 int a 是一样的道理)

4.4 静态局部变量与全局变量 —— 为什么不需要捕获

为什么静态变量和全局变量不需要"捕获"?

全局变量:
  存储在数据段(Data Segment),地址在编译期确定
  程序任何地方都可以通过固定地址直接访问
  → Block 内直接用地址访问,不需要拷贝到结构体

静态局部变量:
  虽然作用域是局部的,但存储在数据段
  地址在编译期确定,生命周期是全程序
  → Block 捕获的是变量的指针(地址),不是值
  → 通过指针间接访问,所以能读也能改

  Block 结构体中存储的是:int *countPtr = &count;
  访问时:*(countPtr) = *(countPtr) + 1;

对比:
  局部变量 → 值拷贝(因为栈帧会销毁,地址会失效)
  静态变量 → 指针拷贝(地址永远有效)
  全局变量 → 不捕获(直接用符号地址)

类比:
  局部变量 → 别人白板上的草稿,你必须抄一份带走(值拷贝)
  静态变量 → 图书馆书架上的书,你只需要记住书架号(指针拷贝)
  全局变量 → 墙上的公告,不需要抄,大家都能看到(不捕获)

4.5 __block 修饰符的深层原理

__block 不是简单的"允许修改",它触发了一个复杂的底层变换

原始代码:                   编译器转换后:
                            
__block int a = 10;    →    struct __Block_byref_a {
                                void *__isa;
                                __Block_byref_a *__forwarding;  // 关键!
                                int __flags;
                                int __size;
                                int a;           // 真正的值
                            };
                            
                            __Block_byref_a a = {
                                .isa = 0,
                                .__forwarding = &a,  // 指向自己
                                .a = 10
                            };

理解 __block 的类比: 普通变量捕获就像你把书抄了一页带走——你在副本上改东西,原书不变。而 __block 就像把整本书放进一个共享文件柜(堆上的 __Block_byref 结构体),然后给所有需要的人配一把钥匙(指针)——大家打开柜子看到的、改的都是同一本书。

__forwarding 指针的精妙设计

╔══════════════════════════════════════════════════════════════════╗
║  __forwarding 存在的根本原因:                                     ║
║  解决"同一个 __block 变量可能同时被栈上代码和堆上 Block 访问"的问题   ║
╚══════════════════════════════════════════════════════════════════╝

阶段一:Block 还在栈上时

  Stack 栈帧
  ┌─────────────────────┐
  │ __Block_byref_a     │
  │ ├── __forwarding ───┼──→ 指向自己(栈上地址)
  │ └── a = 10          │
  └─────────────────────┘
  
  此时所有访问 a 的代码都通过:byref->__forwarding->a
  由于 __forwarding 指向自己,等价于 byref->a
  看起来多此一举?往下看——

阶段二:Block 被 copy 到堆上

  Stack                        Heap
  ┌─────────────────────┐     ┌─────────────────────┐
  │ __Block_byref_a     │     │ __Block_byref_a     │
  │ ├── __forwarding ───┼──┬──┼── __forwarding ───┼──→ 指向自己(堆上)
  │ └── a = 10          │  │  │ └── a = 10          │
  └─────────────────────┘  │  └─────────────────────┘
                           │         ↑
                           └─────────┘
                     栈上的 __forwarding 被修改!
                     现在指向堆上副本

  关键效果:
  - 栈上代码访问 a → byref_stack->__forwarding->a → 堆上的 a ✓
  - Block 内访问 a  → byref_heap->__forwarding->a  → 堆上的 a ✓
  - 两边修改的是同一个 a!

如果没有 __forwarding:
  - 栈上代码修改 byref_stack->a = 20,改的是栈上的副本
  - Block 内读取 byref_heap->a,读的是堆上的副本
  - 两边不一致!Bug!

类比: __forwarding 就像一个邮件转发地址。一开始你住在家里(栈上),所有信都寄到家里地址。后来你搬到公司(堆上),你在家里设置了邮件转发——所有寄到家里的信都会自动转到公司。这样无论别人往哪个地址寄信,最终都送到你手上(堆上)。

__block 变量被多个 Block 捕获时

__block int a = 10;

void(^block1)(void) = ^{ a = 20; };
void(^block2)(void) = ^{ a = 30; };

底层发生了什么?

block1 copy 到堆时:
  → __Block_byref_a 从栈 copy 到堆(第一次)
  → block1 结构体中保存指向堆上 byref 的指针

block2 copy 到堆时:
  → 发现 __Block_byref_a 已经在堆上了(通过 __forwarding 判断)
  → 不再重复 copy,直接引用计数 +1
  → block2 结构体中保存同一个堆上 byref 的指针

结果:
  block1 和 block2 共享同一个堆上的 __Block_byref_a
  修改的是同一个 a
  __Block_byref_a 的引用计数 = 2(被两个 Block 持有)

__block 与对象类型的特殊交互(MRC vs ARC)

__block 在 MRC 和 ARC 下对对象类型变量的行为完全不同!

MRC 下:
  __block id obj = [[NSObject alloc] init];
  → __Block_byref 结构体中的 obj 不会被 retain
  → 可以用来避免循环引用(因为不强引用)
  → 这是 MRC 时代避免循环引用的手段之一

ARC 下:
  __block id obj = [[NSObject alloc] init];
  → __Block_byref 结构体中的 obj 会被 strong 引用
  → 不能用来避免循环引用!
  → 这是很多从 MRC 迁移到 ARC 的项目踩过的坑

ARC 下避免循环引用应该用 __weak,不是 __block

总结:
  MRC: __block 不 retain 对象 → 可用于打破循环引用
  ARC: __block 会 retain 对象 → 不能打破循环引用,需要 __weak

4.6 对象类型变量的捕获与内存管理

捕获对象时的引用关系:

NSObject *obj = [[NSObject alloc] init];

void(^block)(void) = ^{
    NSLog(@"%@", obj);
};

┌──────────────────┐         ┌───────────────┐
│ Block (堆上)       │         │ NSObject 实例  │
│ ├── isa            │         │               │
│ ├── invoke         │         │  retainCount  │
│ └── obj (strong) ──┼────→    │  (被 Block +1) │
└──────────────────┘         └───────────────┘

Block copy 到堆时,会调用 descriptor 中的 copy_helper,
对捕获的对象执行 _Block_object_assign:
  - 强引用 (默认) → 等同于 retain
  - __weak 修饰    → 弱引用,不增加引用计数
  - __unsafe_unretained → 不增加引用计数,不置 nil(危险)

_Block_object_assign 的内部逻辑

void _Block_object_assign(void *destAddr, const void *object, int flags) {
    
    flags 决定行为:
    
    BLOCK_FIELD_IS_OBJECT (3):    // 捕获的是 OC 对象_Block_retain_object(object)
        → 等价于 [object retain] 或 objc_storeStrong
        → 如果是 __weak,走 objc_initWeak 路径
    
    BLOCK_FIELD_IS_BLOCK (7):     // 捕获的是另一个 Block_Block_copy(object)
        → 被捕获的 Block 也会被 copy 到堆上(递归 copy)
    
    BLOCK_FIELD_IS_BYREF (8):     // 捕获的是 __block 变量_Block_byref_copy(object)
        → 将 __block 结构体 copy 到堆上
        → 修改 __forwarding 指针
    
    BLOCK_FIELD_IS_WEAK (16):     // __weak 修饰的对象objc_initWeak(destAddr, object)
        → 注册到 SideTable 的弱引用表中
}

4.7 self 的捕获 —— 隐式 vs 显式

═══════════════════════════════════════════════════════════════
  self 捕获是循环引用的最大根源,必须深入理解
═══════════════════════════════════════════════════════════════

显式捕获(容易意识到):
  ^{ [self doSomething]; }     // 明确写了 self
  ^{ self.name = @"xxx"; }     // 明确写了 self

隐式捕获(容易忽略!):
  ^{ _name = @"xxx"; }         // 直接访问 ivar,编译器转为 self->_name
                                // 同样强引用捕获了 self!
  
  ^{ [self->_delegate call]; } // 同理

  ^{ _block(); }               // 如果 _block 是实例变量
                                // 也隐式捕获了 self

更隐蔽的情况:
  ^{ doSomething(); }          // 如果 doSomething 是当前类的方法
                                // 编译器转为 [self doSomething]
                                // 隐式捕获 self

  ^{ NSLog(@"%@", _array[0]); }  // _array 是 ivar → 捕获了 self

  ┌──────────────────────────────────────────────────────┐
  │ 规则:Block 内只要访问了实例变量或调用了实例方法,        │
  │       就一定捕获了 self,无论有没有写 "self." 前缀       │
  └──────────────────────────────────────────────────────┘

4.8 捕获变量的内存对齐

Block 结构体中捕获变量的排列遵循 C 语言的内存对齐规则:

假设捕获了以下变量:
  char c;     // 1 byte
  int i;      // 4 bytes  
  double d;   // 8 bytes

在 Block 结构体中的排列:
  
  偏移量 0~7:   Block_layout 固有字段(isa, flags 等)
  ...
  偏移量 N:     char c    (1 byte)
  偏移量 N+1~N+3: padding (3 bytes 填充,对齐到 4)
  偏移量 N+4:   int i     (4 bytes)
  偏移量 N+8:   double d  (8 bytes)

编译器的优化:
  有时编译器会重新排列捕获变量的顺序
  把大类型放前面,小类型放后面
  以减少 padding,使 Block 结构体更紧凑

这对日常开发影响不大,但在分析 crash log 中 Block 结构体
的内存布局时,理解对齐规则有助于定位问题。

五、循环引用的本质与解法

5.1 循环引用的形成

经典循环引用:

self → 强引用 → block → 强引用 → self

┌──────────┐  strong   ┌──────────┐  strong   ┌──────────┐
│  self     │─────────→│  Block   │─────────→│  self     │
│ (对象)     │          │ (堆上)    │          │ (同一个)   │
└──────────┘          └──────────┘          └──────────┘
      ↑                                          │
      └──────────────────────────────────────────┘
      引用计数永远不归零,两者都无法释放 → 内存泄漏

类比: 循环引用就像两个人互相握手不肯先松开——只要对方不松手我也不松手,结果两个人永远僵持在那里。__weak 就是让其中一个人用力气很小的方式搭在对方手上(不算真正的"握"),一旦对方松开,自己的手自动滑落。

5.2 复杂循环引用链(实际项目中更常见)

不是所有循环引用都是直接的 self  block  self

间接循环引用(三角环):
  self  viewModel  completionBlock  self
  
  ViewController 持有 ViewModel
  ViewModel 持有 completionBlock
  completionBlock 捕获了 ViewController(self)
   三者形成环,都无法释放

更隐蔽的多层环:
  self  manager  handler  service  callback  self

  ┌────────┐    ┌─────────┐    ┌─────────┐    ┌──────────┐
   self    │──→│ manager │──→│ handler │──→│ callback 
  └────────┘    └─────────┘    └─────────┘    └──────────┘
                                                  
      └────────────────────────────────────────────┘

NSTimer 的经典循环引用:
  self  timer (strong)
  timer  target: self (strong,NSTimer 强引用 target)
  timer  block (如果用 block API)

  NSTimer 的特殊性:
  - RunLoop 强引用 timer
  - timer 强引用 target (self)
  - 即使 self 不强引用 timer,RunLoop  timer  self 也会导致 self 不释放
  - 必须手动 invalidate timer 才能打破

NSTimer 循环引用的根本原因:
  ┌──────────────────────────────────────────────────────────┐
   NSTimer 的设计缺陷:它强引用 target 直到 invalidate     
   这打破了常规的"弱引用 delegate"范式                      
   解决方案:使用 iOS 10+ 的 block-based API + weakSelf,    
   或者使用 NSProxy 中间人模式                              
  └──────────────────────────────────────────────────────────┘

5.3 解决方案的原理对比

方案一:__weak(推荐)

self  strong  Block  weak  self
                         
                    弱引用不增加引用计数
                    self 释放后自动置 nil

特点:
   安全,self 释放后 weakSelf 自动为 nil
   Block 调用时需要判断 weakSelf 是否为 nil
  ⚠️ Block 执行过程中 self 可能随时被释放(多线程场景)

─────────────────────────────────────────

方案二:__weak + __strong(Weak-Strong Dance)

Block 外部:__weak typeof(self) weakSelf = self;
Block 内部:__strong typeof(weakSelf) strongSelf = weakSelf;

执行流程:
  ① Block 被调用
  ② strongSelf = weakSelf(如果 self 已释放,strongSelf = nil,提前 return)
  ③ strongSelf 临时持有 self,保证 Block 执行期间 self 不被释放
  ④ Block 执行完毕,strongSelf 出栈,临时强引用消失

特点:
   保证 Block 执行期间 self 存活
   Block 执行完后不阻止 self 释放
   最佳实践

─────────────────────────────────────────

方案三:__block + 手动置 nilMRC 遗留思路)

__block typeof(self) blockSelf = self;
self.block = ^{
    [blockSelf doSomething];
    blockSelf = nil;  // 手动打破循环
};

self  Block  blockSelf  self(执行后 blockSelf = nil 打破)

特点:
  ⚠️ Block 必须被执行才能打破循环
  ⚠️ 如果 Block 永远不被调用  内存泄漏
   不推荐

5.4 Weak-Strong Dance 的深层理解

为什么单纯 __weak 在某些场景下不够?

场景:Block 执行到一半时 self 被释放

__weak typeof(self) weakSelf = self;
self.block = ^{
    [weakSelf doStep1];    // ① weakSelf 非 nil,执行成功
    
    // ──── 此时另一个线程释放了 self ────
    
    [weakSelf doStep2];    // ② weakSelf 变成 nil!不执行了
    weakSelf.name = @"xx"; // ③ 也不执行
    // 步骤不完整,数据可能处于不一致状态
};

加了 __strong 后:

__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return;  // self 已死,整体不执行
    
    [strongSelf doStep1];     // ✅ 安全
    [strongSelf doStep2];     // ✅ 安全(strongSelf 临时持有,self 不会中途释放)
    strongSelf.name = @"xx";  // ✅ 安全
    
    // Block 执行完,strongSelf 出栈,临时强引用消失
    // 不影响 self 的正常释放
};

关键理解:
  __strong 创建的是一个临时的、局部的强引用
  它只在 Block 执行期间生效
  Block 不执行时,它不存在,不会造成循环引用
  Block 执行时,它临时延长 self 的生命周期
  Block 执行完,它随着栈帧销毁而消失

类比: Weak-Strong Dance 就像电影院的座位预留机制。__weak 是"不预留座位"——你来了有空位就坐,来晚了位子可能被撤了。__strong 是在进场(Block 开始执行)时确认一下"这个座位还在不在",如果在就暂时锁定它,看完电影(Block 执行完)自动解锁。

5.5 不是所有 Block 都会循环引用

不会循环引用的场景:
├── UIView 动画 Block
│   └── [UIView animateWithDuration:animations:] 
│       系统持有 Block,Block 引用 self,但 self 不持有 Block
│       → 单向引用,不成环
│
├── GCD 一次性 Block
│   └── dispatch_async(queue, ^{ self.xxx; });
│       GCD 持有 Block(直到执行完),Block 引用 self
│       self 不持有 GCD 的 Block → 不成环
│       ⚠️ 但如果 dispatch_after 延时很长,self 的释放会被推迟
│
├── 局部变量 Block
│   └── void(^block)(void) = ^{ self.xxx; }; block();
│       block 是局部变量,函数结束即销毁 → 不成环
│
├── NSArray/NSDictionary 的 enumerate Block
│   └── [array enumerateObjectsUsingBlock:^{ self.xxx; }];
│       Block 同步执行完即释放 → 不成环
│
├── Masonry / SnapKit 的约束 Block
│   └── [view mas_makeConstraints:^{ make.top.equalTo(self.view); }];
│       Block 同步执行完即释放 → 不成环
│
└── 判断标准:
    ┌─────────────────────────────────────────────────┐
    │ 画一条从 self 出发的"持有链"                       │
    │ 如果能绕回 self → 循环引用                         │
    │ 如果不能绕回 self → 安全                           │
    │                                                  │
    │ self → 属性 → Block → self (环!)                │
    │ 系统 → Block → self (不成环,安全)                │
    │ 局部 → Block → self (不成环,安全)                │
    └─────────────────────────────────────────────────┘

5.6 循环引用的检测方法

检测循环引用的实用手段:

1. dealloc 日志法(最简单)
   在类的 dealloc 方法中打印日志
   如果页面退出后看不到日志 → 该对象没有被释放 → 可能存在循环引用

2. Instruments - Leaks
   Xcode 自带工具,能自动检测泄漏的对象
   可以看到泄漏对象的引用关系图
   局限:不是所有循环引用都会被 Leaks 检测到

3. Instruments - Allocations(更可靠)
   查看对象的生命周期(分配和释放历史)
   如果一个对象只有 alloc 没有 dealloc → 泄漏
   可以按类名过滤,非常方便

4. Memory Graph Debugger(推荐)
   Xcode 调试时点击左下角的 Memory Graph 按钮
   会以图形方式展示所有对象的引用关系
   循环引用会被清晰地标注出来(紫色警告图标)
   ⭐ 最直观的检测方式

5. 第三方工具
   MLeaksFinder(腾讯开源):自动检测 UIViewController 的泄漏
   FBRetainCycleDetector(Facebook 开源):运行时检测循环引用
   两者可以配合使用
   
6. Debug Memory Graph + lldb
   在 Memory Graph 中选中可疑对象
   在 lldb 中执行 po 命令查看对象详情
   使用 malloc_history 命令追踪对象分配堆栈

排查思路流水线:
  dealloc 没触发 → Memory Graph 看引用关系 → 找到环 → 分析哪个引用应该用 weak

六、Block 与内存管理的进阶话题

6.1 Block 的 copy 语义链

Block copy 时发生的事情(连锁反应):

Block copy 到堆上
    │
    ├── Block 结构体从栈拷贝到堆(malloc + memcpy)
    │
    ├── 调用 descriptor->copy_helper
    │   │
    │   ├── 对捕获的 OC 对象执行 _Block_object_assign
    │   │   ├── strong 对象 → retain(引用计数 +1)
    │   │   ├── weak 对象   → objc_initWeak(注册弱引用)
    │   │   └── block 对象  → 递归 _Block_copy
    │   │
    │   └── 对 __block 变量执行 _Block_object_assign
    │       ├── __block 结构体从栈 copy 到堆
    │       ├── 修改栈上 __forwarding 指向堆上副本
    │       └── 对 __block 内部的 OC 对象执行相应的 retain/weak
    │
    └── 修改 isa 指针:_NSConcreteStackBlock → _NSConcreteMallocBlock
        修改 flags:设置 BLOCK_NEEDS_FREE 位,引用计数初始化为 1

6.2 Block 的 dispose 语义链

Block 引用计数归零时:

Block release → retainCount == 0
    │
    ├── 调用 descriptor->dispose_helper
    │   │
    │   ├── 对捕获的 OC 对象执行 _Block_object_dispose
    │   │   ├── strong 对象 → release(引用计数 -1)
    │   │   ├── weak 对象   → objc_destroyWeak(注销弱引用)
    │   │   └── block 对象  → 递归 _Block_release
    │   │
    │   └── 对 __block 变量执行 _Block_object_dispose
    │       └── __block 结构体引用计数 -1,归零则:
    │           ├── 对内部 OC 对象执行 release/destroyWeak
    │           └── free(__block 结构体)
    │
    └── free(block) 释放 Block 堆内存

6.3 Block 的 retain/release 实现细节

_Block_copy 的内部逻辑(简化版):

void *_Block_copy(const void *arg) {
    struct Block_layout *src = (struct Block_layout *)arg;
    
    if (src->flags & BLOCK_NEEDS_FREE) {
        // 已经在堆上了 → 只增加引用计数
        latching_incr_int(&src->flags);  // flags 中的引用计数 +1
        return src;
    }
    
    if (src->flags & BLOCK_IS_GLOBAL) {
        // 全局 Block → 什么都不做,返回自身
        return src;
    }
    
    // 栈上 Block → 拷贝到堆上
    struct Block_layout *dst = malloc(src->descriptor->size);
    memmove(dst, src, src->descriptor->size);      // 整体内存拷贝
    
    dst->isa = _NSConcreteMallocBlock;              // 改 isa
    dst->flags |= BLOCK_NEEDS_FREE;                 // 标记为堆 Block
    dst->flags = (dst->flags & ~0xFFFF) | 1;        // 引用计数 = 1
    
    if (dst->flags & BLOCK_HAS_COPY_DISPOSE) {
        dst->descriptor->copy_helper(dst, src);      // 处理捕获变量
    }
    
    return dst;
}

性能洞察:
  - Block copy 涉及 malloc + memmove + 可能的多次 retain
  - 这就是为什么频繁创建和 copy Block 有性能开销
  - 也是为什么 GCD 内部对 Block 的处理做了大量优化

6.4 Block 属性用 copy 还是 strong?

MRC 时代:
  @property (nonatomic, copy) void(^block)(void);
  
  必须用 copy!
  如果用 retain,Block 仍然在栈上,函数返回后 Block 失效 → 野指针 crash
  copy 会把 Block 从栈拷贝到堆上,延长生命周期

ARC 时代:
  @property (nonatomic, copy) void(^block)(void);
  @property (nonatomic, strong) void(^block)(void);
  
  两者效果完全相同!
  ARC 编译器对 Block 赋值时自动插入 _Block_copy
  无论你写 copy 还是 strong,底层都会执行 copy 操作

  但惯例上仍然写 copy,原因:
  ① 代码自文档化——看到 copy 就知道"这是 Block,有特殊的内存语义"
  ② 向后兼容——万一哪天代码被挪到 MRC 环境也能正确工作
  ③ 团队共识——Apple 官方文档和社区都推荐 copy

  ┌─────────────────────────────────────────────────┐
  │ ARC 下用 strong 也完全正确,但写 copy 更规范     │
  └─────────────────────────────────────────────────┘

七、Block 与线程安全

7.1 Block 本身的线程安全性

Block 对象一旦创建完成(copy 到堆上后),其内部状态是只读的。
invoke 指针、descriptor、捕获的变量值都不会再变。

因此:
  ✅ 多线程同时调用(invoke)同一个 Block → 安全(只读操作)
  ✅ 多线程同时对同一个 Block 做 retain/release → 安全
     (引用计数操作是原子的,使用了 OSAtomicCompareAndSwapInt)
  ❌ 如果 Block 捕获了可变对象,多线程调用时对该对象的修改 → 不安全

  ┌──────────────────────────────────────────────────────────┐
  │ Block 的"壳"是线程安全的,但"内容物"不一定是。              │
  │ 就像一个上了锁的保险箱(Block)里面放了一把没有安全锁的刀    │
  │ (NSMutableArray)——保险箱是安全的,但刀可以伤人。            │
  └──────────────────────────────────────────────────────────┘

7.2 捕获变量的线程安全问题

场景一:多个线程通过 Block 读写同一个 __block 变量

  __block int counter = 0;
  
  for (int i = 0; i < 1000; i++) {
      dispatch_async(concurrentQueue, ^{
          counter++;  // 多线程同时 ++ → 数据竞争!结果不可预期
      });
  }
  
  问题:counter++ 不是原子操作(读-改-写三步)
  解决:用 dispatch_barrier_async 或 @synchronized 或 os_unfair_lock

场景二:Block 捕获的对象在另一个线程被释放

  __weak typeof(self) weakSelf = self;
  dispatch_async(bgQueue, ^{
      // 此时 self 可能已经被主线程释放
      [weakSelf doSomething];  // weakSelf 可能为 nil → 消息发给 nil,安全但无效
      
      NSLog(@"%@", weakSelf.name); // 同理,可能返回 nil
  });
  
  这不是 crash,但可能导致逻辑不正确
  → 需要 Weak-Strong Dance

场景三:Block 中修改捕获的可变集合

  NSMutableArray *arr = [NSMutableArray new];
  
  dispatch_async(queue1, ^{ [arr addObject:@"A"]; });
  dispatch_async(queue2, ^{ [arr addObject:@"B"]; });
  
  两个 Block 捕获同一个 arr 指针(指向同一个可变数组)
  同时修改 → crash(NSMutableArray 非线程安全)
  
  解决:
  ① 使用串行队列保护
  ② 每个 Block 使用独立的 copy
  ③ 使用并发队列 + barrier

7.3 Block 与 GCD 的线程交互模式

常见模式及其线程安全分析:

模式 1:主线程 → 后台 → 回主线程

  dispatch_async(bgQueue, ^{
      id result = [self heavyComputation];  // 后台线程
      
      dispatch_async(dispatch_get_main_queue(), ^{
          self.label.text = result;          // 主线程更新 UI
      });
  });
  
  线程安全性:
  - heavyComputation 在后台线程执行 → 不能操作 UI
  - result 是局部变量,被内层 Block 值捕获 → 安全
  - 内层 Block 在主线程执行 → 可以操作 UI ✓
  - 注意:self 被两层 Block 捕获 → 是否循环引用取决于 self 是否持有 queue

模式 2:dispatch_group 汇聚多个异步任务

  dispatch_group_t group = dispatch_group_create();
  __block NSArray *data1, *data2;
  
  dispatch_group_async(group, queue, ^{ data1 = [self fetchData1]; });
  dispatch_group_async(group, queue, ^{ data2 = [self fetchData2]; });
  
  dispatch_group_notify(group, mainQueue, ^{
      [self updateUIWithData1:data1 data2:data2];  // 两个任务都完成后
  });
  
  线程安全性:
  - data1 和 data2 用 __block 修饰,多个 Block 共享同一个堆上变量
  - 但因为两个 async 的 Block 各写各的变量 → 不冲突
  - notify 的 Block 在两个 async 都完成后才执行 → 此时 data1/data2 已写入
  - 安全 ✓(但如果多个 Block 写同一个变量就不安全了)

八、Block 在底层框架中的角色

8.1 GCD 中的 Block

dispatch_async(queue, block)

执行流程:
    │
    ├── 1. Block 被 copy 到堆上(GCD 内部调用 _Block_copy)
    │
    ├── 2. Block 被封装进 dispatch_continuation_t 结构体
    │      ┌──────────────────────────────┐
    │      │ dispatch_continuation_t       │
    │      │ ├── do_vtable (虚表指针)       │
    │      │ ├── dc_func (执行函数)         │
    │      │ ├── dc_ctxt (Block 指针)       │
    │      │ ├── dc_voucher              │
    │      │ └── dc_priority             │
    │      └──────────────────────────────┘
    │      放入 queue 的 FIFO 链表
    │
    ├── 3. 线程池中的 worker thread 取出 continuation
    │      调用 dc_func(dc_ctxt) → block->invoke(block)
    │
    └── 4. 执行完毕后 _Block_release(block)
           Block 引用计数 -1,归零则触发 dispose 链

dispatch_sync 的差异:
  - 同步调用不需要 copy Block(Block 在调用者栈上即可)
  - 调用者线程阻塞等待,Block 在 queue 的线程上执行
  - 执行完后调用者才继续,此时 Block 仍然有效
  - 但要注意死锁:在串行队列中 dispatch_sync 到同一队列 → 死锁!

8.2 RunLoop 中的 Block

CFRunLoopPerformBlock(runloop, mode, block)

    │
    ├── Block 被 copy 到堆上
    ├── 挂载到 RunLoop 指定 mode 的 _blocks 链表
    ├── RunLoop 在对应 mode 的迭代中遍历链表执行
    └── 执行后从链表移除并 release

RunLoop 与 Block 的生命周期关系:
  - Block 提交后到执行前,一直被 RunLoop 持有
  - 如果 RunLoop 切换到其他 mode,Block 不会执行
  - 如果 RunLoop 退出,未执行的 Block 会被 release

performSelector:withObject:afterDelay: 的底层也是 RunLoop + Timer + Block

8.3 Notification/KVO 中的 Block

id token = [[NSNotificationCenter defaultCenter]
    addObserverForName:@"xxx"
    object:nil
    queue:nil
    usingBlock:^(NSNotification *note) {
        // 这个 Block 被 NotificationCenter 持有
        // 直到 removeObserver 才释放
    }];

生命周期陷阱:
  NotificationCenter → observer(内部对象) → Block → self
                                                     ↑
  如果 self 持有 token 并在 dealloc 中 removeObserver:
  - self 的 dealloc 永远不会调用(因为 Block 强引用 self)
  - token 永远不会被 remove
  → 经典死锁式内存泄漏

解决:Block 内必须用 weakSelf

8.4 Block 作为 Associated Object

objc_setAssociatedObject(self, key, block, OBJC_ASSOCIATION_COPY_NONATOMIC);

  - COPY 策略会调用 _Block_copy
  - Block 被关联到对象上,对象释放时 Block 才释放
  - 如果 Block 捕获了该对象 → 循环引用!
  - 这是很多第三方库(如方法交换添加 Block 回调)的常见泄漏原因

8.5 Block 在 KVO 中的新 API

iOS 11+ 提供了基于 Block 的 KVO API:

  self.observation = [self.model observe:@selector(name) 
                                 options:NSKeyValueObservingOptionNew 
                                 changeHandler:^(Model *model, ...) {
      // 注意:Block 参数直接给了被观察对象,不需要 self
      // Apple 有意设计成不需要捕获 self
      NSLog(@"%@", model.name);
  }];

  self.observation 持有 observation token
  observation token 持有 Block
  Block 引用 model(不是 self)
  → 不形成循环引用
  → observation token 在 self dealloc 时被释放
  → 自动移除观察者

这个设计是 Apple 总结了无数 KVO 内存泄漏问题后的改良方案。

九、Block 的性能考量

9.1 Block 的开销分析

Block 的性能开销来源:

1. 创建开销
   ├── 栈 Block:几乎为零(只是栈指针移动 + 结构体初始化)
   ├── 堆 Block:malloc + memcpy + 可能的多次 retain
   └── 全局 Block:零开销(编译期确定)

2. 调用开销
   ├── 通过函数指针间接调用(和 C 函数指针相同)
   ├── 比 OC 方法调用快(没有 objc_msgSend 的查找过程)
   ├── 比直接函数调用慢(多一次指针解引用)
   └── 和 C++ 虚函数调用类似的性能级别

3. 销毁开销
   ├── 栈 Block:零(栈帧弹出即可)
   ├── 堆 Block:dispose_helper + free + 可能的多次 release
   └── 全局 Block:永不销毁

性能对比(从快到慢):
  直接函数调用 ≈ 内联 Block
  > C 函数指针调用 ≈ Block 调用
  > objc_msgSend(方法调用)
  > performSelector

9.2 编译器对 Block 的优化

编译器在开启优化时(-O1 及以上)会对 Block 做以下优化:

1. 内联优化
   如果 Block 在定义后立即调用且只使用一次
   编译器可能将其内联,消除 Block 开销

2. 栈提升为全局
   如果 Block 不捕获变量,即使写在函数内部
   编译器也会将其提升为 GlobalBlock

3. copy 消除
   如果编译器能证明 Block 不会逃逸出当前作用域
   可能跳过 copy 操作

4. 捕获变量合并
   多个 Block 捕获相同变量时,编译器可能优化内存布局

9.3 大量 Block 场景的性能优化建议

场景:高频回调(如滚动监听、动画帧回调)

问题:
  每次回调都创建新 Block → 频繁 malloc/free
  Block 捕获大量对象 → 频繁 retain/release

优化策略:

  ① 复用 Block:将 Block 保存为属性,避免重复创建
     // Bad:每次滚动都创建新 Block
     scrollView.didScroll = ^{ [self handleScroll]; };
     
     // Good:初始化时创建一次
     self.scrollHandler = ^{ [weakSelf handleScroll]; };
     scrollView.didScroll = self.scrollHandler;

  ② 减少捕获变量:只捕获真正需要的变量
     // Bad:隐式捕获整个 self(包含所有 ivar 的引用)
     ^{ _array = ...; _dict = ...; }
     
     // Good:只传入需要的对象
     NSMutableArray *arr = _array;
     ^{ [arr addObject:...]; }

  ③ 考虑用函数指针替代 Block(极致性能场景)
     在 C 层面的高频回调中,函数指针比 Block 更轻量
     因为没有结构体创建、copy、dispose 的开销
     
  ④ 使用 dispatch_block_create 的 DISPATCH_BLOCK_NO_QOS_CLASS 标志
     避免 QoS 传播的额外开销

十、Block 的调试技巧

10.1 在运行时识别 Block 类型

调试时经常需要确认 Block 的类型和捕获信息:

lldb 命令:
  (lldb) po block
  → 输出 Block 的描述,包含类型信息

  (lldb) po [block class]
  → __NSGlobalBlock__ / __NSStackBlock__ / __NSMallocBlock__

  (lldb) po [block superclass]
  → NSBlock

  (lldb) memory read --size 8 --count 5 (void *)block
  → 读取 Block 结构体的前 5 个字段(isa, flags, reserved, invoke, descriptor)

  (lldb) po (void *)((void **)block)[3]
  → 读取 invoke 函数指针地址

  (lldb) image lookup -a <invoke 地址>
  → 反查 invoke 函数对应的源代码位置
     通常输出类似:__ClassName_methodName_block_invoke

10.2 在汇编层面识别 Block 调用

Block 调用在 ARM64 汇编中的特征:

Block 创建:
  通常会看到 __copy_helper_block_ 和 __destroy_helper_block_ 的引用
  以及 ___block_descriptor_ 相关的符号

Block 调用:
  ldr x8, [x0, #16]    // 从 Block 结构体偏移 16 字节处加载 invoke 指针
  blr x8                // 跳转到 invoke 函数执行
  
  ↑ 这两条指令是 Block 调用的标志性模式
  x0 既是 Block 指针,也作为 invoke 的第一个参数(隐含 self)

Block 捕获变量访问:
  在 invoke 函数内部,通过 x0(Block 指针)+ 偏移量 来访问捕获的变量
  ldr x8, [x0, #32]    // 访问第一个捕获变量(偏移量取决于结构体布局)

10.3 排查 Block 相关的 Crash

常见 Block Crash 类型及排查方法:

1. EXC_BAD_ACCESS —— 调用已释放的 Block
   原因:栈 Block 在栈帧销毁后被调用
   特征:crash 在 block->invoke(block, ...) 处
   排查:检查 Block 是否被正确 copy 到堆上
         MRC 下尤其常见

2. EXC_BAD_ACCESS —— Block 内访问已释放的对象
   原因:Block 用 __unsafe_unretained 捕获了一个已释放的对象
   特征:crash 在 Block 内部的 objc_msgSend 处
   排查:将 __unsafe_unretained 改为 __weak

3. Block 为 nil 时调用 → 直接 crash
   原因:Block 指针为 nil 时,调用会触发 EXC_BAD_ACCESS
         因为底层是 block->invoke(block),nil 解引用
   特征:crash 地址通常是 0x10 附近(nil + invoke 偏移量)
   排查:调用前判空 → if (block) { block(); }
   
   ┌──────────────────────────────────────────────────┐
   │ OC 方法调用可以安全地发给 nil → [nil doSomething]    │
   │ Block 调用不能发给 nil → nil() 会 crash!            │
   │ 这是一个容易被忽略的差异。                             │
   └──────────────────────────────────────────────────┘

4. 野 Block(Block 指针指向已被回收的内存)
   原因:使用 __unsafe_unretained 接收 Block,Block 被释放后指针未置 nil
   特征:crash 地址随机,表现不稳定
   排查:不要用 __unsafe_unretained 存储 Block

10.4 使用 clang 查看 Block 编译后的 C++ 代码

在终端中执行以下命令,可以看到 Block 被编译器转换后的 C++ 代码:

  clang -rewrite-objc main.m -o main.cpp

这会把所有 Block 转换为对应的结构体和函数,帮助你理解底层原理。

输出中你会看到:
  - __main_block_impl_0 结构体(Block 的结构体)
  - __main_block_func_0 函数(Block 的 invoke 函数)
  - __main_block_desc_0 结构体(Block 的 descriptor)
  - __Block_byref_xxx 结构体(__block 变量的结构体)

注意:
  这个命令生成的代码是简化版的,和 ARC 实际编译结果有差异。
  它主要用于学习和理解原理,不是 100% 精确的编译输出。
  如果需要精确的汇编输出,使用 Xcode 的 Product → Perform Action → Assemble。

十一、Block 与 Swift 的桥接

11.1 OC Block 在 Swift 中的映射

OC Block 类型与 Swift 闭包类型的对应关系:

  OC:    void(^)(void)                → Swift: () -> Void
  OC:    void(^)(int, NSString *)     → Swift: (Int32, String) -> Void
  OC:    NSString *(^)(int)           → Swift: (Int32) -> String
  OC:    void(^)(BOOL *stop)          → Swift: (UnsafeMutablePointer<ObjCBool>) -> Void

桥接规则:
  ① OC 的 Block 类型自动桥接为 Swift 的 @convention(block) 闭包
  ② 参数和返回值类型按照 Swift-OC 桥接规则转换
  ③ nullable Block 映射为 Optional 闭包

在 Swift 中使用 OC Block API:
  OC 定义:
    - (void)fetchDataWithCompletion:(void(^)(NSArray *data, NSError *error))completion;
  
  Swift 调用:
    obj.fetchData { data, error in
        // data 是 [Any]?,error 是 Error?
    }

逃逸标注的影响:
  OC 中标注了 NS_NOESCAPE 的 Block 参数 → Swift 中映射为非逃逸闭包
  OC 中未标注的 Block 参数 → Swift 中映射为 @escaping 闭包

这意味着:
  如果 OC API 的 Block 参数是同步使用的,应该标注 NS_NOESCAPE
  这样 Swift 调用者不需要写 self.(非逃逸闭包不捕获 self 的强引用)

11.2 Swift 闭包在 OC 中的使用

Swift 闭包可以桥接到 OC Block,但有限制:

可以桥接的:
  ✅ 不捕获泛型类型的闭包
  ✅ 使用 @convention(block) 标注的闭包
  ✅ 返回值和参数都是 OC 可表达类型

不能桥接的:
  ❌ 捕获了 Swift 特有类型(如 struct、enum with associated values)
  ❌ 使用了 Swift only 的特性(如 throws、async)
  
@convention(block) 的作用:
  告诉 Swift 编译器:"把这个闭包按照 OC Block 的 ABI 来生成"
  而不是 Swift 原生闭包的 ABI

  let block: @convention(block) (Int) -> String = { num in
      return "\(num)"
  }
  
  这个闭包现在是一个合法的 OC Block 对象
  可以传给任何接受 Block 的 OC API

11.3 Swift 闭包 vs OC Block 的关键差异

┌───────────────────┬─────────────────────┬────────────────────────┐
   维度                OC Block              Swift 闭包            
├───────────────────┼─────────────────────┼────────────────────────┤
 默认捕获方式        值捕获                引用捕获                
 修改外部变量        需要 __block           默认就可以              
 存储位置          //全局             堆上(编译器优化除外)    
 逃逸标注           无(手动注意)          @escaping 编译器强制     
 循环引用处理        weakSelf/strongSelf   [weak self] 捕获列表     
 nil 安全           nil Block 调用 crash   Optional 闭包安全       
 类型系统           弱类型(仅 runtime)    强类型(编译期检查)     
 内存管理           ARC + 手动 copy       ARC                 
└───────────────────┴─────────────────────┴────────────────────────┘

Swift 的设计吸取了 OC Block 的教训:
  - 默认引用捕获  不需要 __block 的心智负担
  - @escaping 强制标注  编译期就发现逃逸问题
  - 闭包直接在堆上  没有栈堆迁移的复杂性
  - 捕获列表 [weak self]  比 weakSelf/strongSelf 更简洁
  - Optional 闭包  nil 安全,不会 crash

十二、Block 的常见坑与反模式

12.1 十大常见 Block 陷阱

陷阱 1:忘记 Block 可以为 nil
  self.completionHandler(result);
  // 如果 completionHandler 为 nil → crash!
  
  正确做法:
  if (self.completionHandler) {
      self.completionHandler(result);
  }

─────────────────────────────────────────

陷阱 2:在 dealloc 中依赖 weakSelf
  __weak typeof(self) weakSelf = self;
  self.block = ^{
      [weakSelf cleanup];  // dealloc 流程中 weakSelf 可能已经是 nil!
  };
  
  dealloc 时 weak 引用可能已经被清零(取决于时机)

─────────────────────────────────────────

陷阱 3:Block 内创建局部 strong 引用后以为不会循环引用
  __weak typeof(self) weakSelf = self;
  self.block = ^{
      __strong typeof(weakSelf) strongSelf = weakSelf;
      strongSelf.anotherBlock = ^{
          [strongSelf doSomething];  // strongSelf 是局部变量,但被内层 Block 捕获
          // self → block → 外层 Block → strongSelf 被内层 Block 捕获为强引用
          // 内层 Block 被赋值给 self.anotherBlock → 形成循环引用!
      };
  };
  
  Weak-Strong Dance 只保护一层,嵌套 Block 需要再次 weak!

─────────────────────────────────────────

陷阱 4:在栈 Block 出作用域后使用(MRC)
  void(^block)(void);
  {
      int x = 42;
      block = ^{ NSLog(@"%d", x); };
  }
  block();  // crash!Block 在栈上,出作用域后失效

─────────────────────────────────────────

陷阱 5:误以为 copy 会创建独立副本
  void(^block)(void) = ^{ NSLog(@"hello"); };
  void(^block2)(void) = [block copy];
  // block2 == block(堆上 Block copy 只增加引用计数)
  // 不像 NSMutableArray copy 会创建新对象

─────────────────────────────────────────

陷阱 6:dispatch_after 的 Block 延长了对象生命周期
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC), 
                 dispatch_get_main_queue(), ^{
      [self doSomething];  // self 至少活到 30 秒后
  });
  
  用户关闭页面后,ViewController 30 秒内不会被释放
  这不是"循环引用",但是"生命周期延长"

─────────────────────────────────────────

陷阱 7:在 Block 内使用 C 数组
  int arr[3] = {1, 2, 3};
  void(^block)(void) = ^{
      NSLog(@"%d", arr[0]);  // 编译错误!C 数组不能被 Block 捕获
  };
  
  C 数组不是一等公民,不能被值拷贝
  解决:使用 __block 修饰,或者用 NSArray/指针代替

─────────────────────────────────────────

陷阱 8:同步 Block 中的 return 语义
  - (BOOL)check {
      [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {          if ([obj isKindOfClass:[NSString class]]) {
              return;  // 这个 return 只退出 Block,不退出 check 方法!
          }
      }];
      return NO;  // 无论如何都会执行到这里
  }
  
  Block 内的 return 退出的是 Block,不是外层方法

─────────────────────────────────────────

陷阱 9:多次调用一次性 Block
  if (self.completion) {
      self.completion(result);
      self.completion(result);  // 二次调用!可能导致重复操作
  }
  
  最佳实践:调用后立即置 nil
  if (self.completion) {
      void(^completion)(id) = self.completion;
      self.completion = nil;
      completion(result);
  }

─────────────────────────────────────────

陷阱 10:忽略 Block 的 copy 开销(性能敏感场景)
  for (int i = 0; i < 10000; i++) {
      self.handler = ^{ ... };  // 每次循环都创建新 Block 并 copy 到堆上
  }
  
  在热路径中频繁创建和赋值 Block 可能导致性能问题
  解决:Block 不变时在循环外创建一次

12.2 Block 的最佳实践总结

✅ DO(推荐做法):
  1. Block 属性用 copy(即使 ARC 下 strong 等效,copy 更清晰)
  2. 使用 Weak-Strong Dance 处理循环引用
  3. 调用 Block 前检查是否为 nil
  4. Completion Block 调用后置 nil
  5. 使用 typedef 为复杂 Block 类型定义别名
  6. API 中的 Block 参数注明是否逃逸(NS_NOESCAPE)
  7. 嵌套 Block 中的每一层都检查循环引用

❌ DON'T(避免做法):
  1. 不要在 Block 内直接访问 ivar(容易隐式捕获 self)
  2. 不要用 __unsafe_unretained 代替 __weak(野指针风险)
  3. 不要在 ARC 下用 __block 来打破循环引用(ARC 下 __block 会 retain)
  4. 不要在不确定是否为 nil 的情况下直接调用 Block
  5. 不要在高频回调中频繁创建新 Block
  6. 不要在 Block 内做耗时操作而不考虑线程
  7. 不要忘记 NSTimer/NotificationCenter 的 Block 生命周期管理

十三、Block vs Delegate vs Notification —— 如何选择

三种回调机制的对比:

┌──────────────┬──────────────┬──────────────┬──────────────┐
│   维度        │   Block       │   Delegate    │ Notification │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 关系         │ 一对一        │ 一对一        │ 一对多       │
│ 耦合度       │ 高(代码内联)│ 低(协议隔离)│ 最低(解耦)  │
│ 代码位置     │ 就地书写      │ 分散在方法中  │ 分散 + 跨文件│
│ 类型安全     │ 中            │ 高            │ 低           │
│ 内存风险     │ 循环引用      │ 较少          │ 忘记 remove  │
│ 适合场景     │ 一次性回调    │ 多方法协议    │ 广播通知     │
│ 调试难度     │ 较容易        │ 中            │ 难追踪       │
└──────────────┴──────────────┴──────────────┴──────────────┘

选择建议:

  单一回调(如网络请求完成)→ Block
    优势:代码集中,上下文清晰
    例:[api fetchDataWithCompletion:^(Data *d) { ... }];

  多个回调方法(如 UITableView 的数据源)→ Delegate
    优势:职责明确,方法独立,可选实现
    例:UITableViewDataSource 有多个 required/optional 方法

  一对多广播(如登录状态变化通知所有页面)→ Notification
    优势:完全解耦,任意对象可监听
    例:用户登出后通知所有页面刷新

  混合使用:实际项目中经常组合使用
    例:网络层用 Block 回调给业务层,
        业务层通过 Notification 广播给 UI 层

十四、知识体系脑图

Block
├── 本质
│   ├── C 语言扩展(Clang 实现)
│   ├── 匿名函数 + 上下文捕获
│   ├── 底层是 OC 对象(有 isa)
│   ├── 编译期变换:源码 → 结构体 + 函数
│   └── 默认值捕获的设计哲学(Pit of Success)
│
├── 底层结构
│   ├── Block_layout 结构体
│   │   ├── isa → 类型标识(Global/Stack/Malloc)
│   │   ├── flags → 位域(引用计数 + 多种标志)
│   │   ├── invoke → 函数指针(隐含 self 参数)
│   │   └── descriptor → 多态结构(size/copy/dispose/signature)
│   ├── 捕获变量存储在结构体尾部(动态大小)
│   └── 类型编码(Type Encoding)与 NSMethodSignature
│
├── 三种类型
│   ├── GlobalBlock(不捕获局部变量,在数据段)
│   ├── StackBlock(捕获变量,在栈上,生命周期同栈帧)
│   ├── MallocBlock(copy 后在堆上,引用计数管理)
│   ├── 为什么有栈?性能优化,避免不必要的 malloc
│   └── 逃逸与非逃逸的概念
│
├── 变量捕获(核心难点)
│   ├── 局部基本类型 → 值拷贝(快照语义)
│   ├── 局部对象 → 指针拷贝 + ARC 内存管理(strong/weak)
│   ├── __block → 封装为 __Block_byref 结构体
│   │   ├── __forwarding 保证栈堆访问一致性
│   │   ├── 多 Block 共享时引用计数管理
│   │   └── MRC vs ARC 下对对象的不同行为
│   ├── 静态变量 → 指针捕获(地址不变,无需拷贝值)
│   ├── 全局变量 → 不捕获(直接按地址访问)
│   └── self 的隐式捕获(访问 ivar 也会捕获 self)
│
├── 循环引用
│   ├── 本质:强引用环导致引用计数无法归零
│   ├── 直接环 vs 间接环(多层持有链)
│   ├── __weak(打破强引用)
│   ├── __weak + __strong(Weak-Strong Dance,保证执行完整性)
│   ├── NSTimer 的特殊循环引用
│   ├── 判断标准:从 self 出发能否画回 self
│   └── 检测方法(Memory Graph / Instruments / MLeaksFinder)
│
├── 内存管理
│   ├── copy 链:malloc → memcpy → copy_helper → retain/weak/递归copy
│   ├── dispose 链:dispose_helper → release/destroyWeak → free
│   ├── 引用计数存储在 flags 位域中(非 SideTable)
│   ├── ARC 下编译器自动 copy5 种场景)
│   └── copy vs strong 属性修饰符的选择
│
├── 线程安全
│   ├── Block 对象本身的线程安全性(只读 + 原子引用计数)
│   ├── 捕获变量的线程安全问题
│   ├── __block 变量的数据竞争
│   └── GCD + Block 的常见线程安全模式
│
├── 性能
│   ├── 创建:栈≈0 / 堆=malloc+copy / 全局=0
│   ├── 调用:比 objc_msgSend 快,比直接调用慢一点
│   ├── 销毁:栈=0 / 堆=dispose+free
│   ├── 编译器优化:内联、全局提升、copy 消除
│   └── 高频场景的优化建议
│
├── 框架应用
│   ├── GCD(dispatch_continuation_t 封装)
│   ├── RunLoop(Block 链表管理)
│   ├── Notification/KVO(Block 生命周期陷阱)
│   ├── Associated Object(COPY 策略注意循环引用)
│   └── KVO 新 API 的设计改进
│
├── 调试技巧
│   ├── lldb 命令查看 Block 类型和捕获信息
│   ├── 汇编层面识别 Block 调用
│   ├── 常见 Block Crash 类型及排查
│   └── clang -rewrite-objc 查看编译产物
│
├── Swift 桥接
│   ├── OC Block → Swift 闭包的类型映射
│   ├── @convention(block) 的作用
│   ├── NS_NOESCAPE 对 Swift 侧的影响
│   └── Swift 闭包 vs OC Block 的关键差异
│
├── 常见坑与反模式
│   ├── nil Block 调用 crash
│   ├── 嵌套 Block 的循环引用
│   ├── dispatch_after 延长对象生命周期
│   ├── Block 内 return 的语义
│   ├── C 数组不能被 Block 捕获
│   └── 一次性 Block 多次调用
│
└── 设计选择
    ├── Block vs Delegate vs Notification 的取舍
    └── Block 回调的最佳实践

十五、面试高频考点速查

问题 核心答案
Block 的本质是什么? 封装了函数指针和捕获变量的 OC 对象(结构体),有 isa 指针
Block 的 invoke 函数有什么特点? 第一个隐含参数是 Block 结构体自身指针,类似 OC 的 self
Block 有几种类型?怎么判定? Global(不捕获局部变量)、Stack(捕获了,在栈上)、Malloc(copy 后在堆上)
什么时候 Block 会从栈 copy 到堆? 作为返回值、赋值给 strong 变量、传给 GCD、ARC 编译器自动处理
为什么要有栈 Block? 性能优化,临时 Block 无需 malloc/free,生命周期随栈帧
Block 的引用计数存在哪里? flags 字段的 bit 1~15,不在 SideTable 中
为什么局部变量在 Block 内不能修改? 捕获的是值副本,修改副本无意义且语义混乱,编译器直接禁止
__block 的底层原理? 将变量封装为 __Block_byref 堆上结构体,通过 __forwarding 指针保证栈堆访问一致
__forwarding 为什么存在? 解决 Block copy 后栈上代码和堆上 Block 访问同一个 __block 变量的一致性问题
多个 Block 捕获同一个 __block 变量会怎样? 共享同一个堆上 __Block_byref 结构体,引用计数管理
__block 在 MRC 和 ARC 下有什么区别? MRC 下不 retain 对象(可打破循环引用),ARC 下会 retain(不能打破循环引用)
访问 ivar 会捕获 self 吗? 会,编译器将 _name 转为 self->_name,隐式强引用捕获 self
循环引用怎么产生的? self 持有 Block,Block 强引用捕获 self,形成引用环
__weak__unsafe_unretained 区别? weak 对象释放后自动置 nil;unsafe_unretained 不置 nil,可能野指针
Weak-Strong Dance 的意义? weak 避免循环引用,strong 保证 Block 执行期间 self 不被中途释放
Block 捕获 self 一定循环引用吗? 不一定,只有 self(直接或间接)持有 Block 时才会形成环
Block 和 C 函数指针的区别? Block 能捕获上下文,是 OC 对象,参与 ARC;函数指针都不能
Block 和 Swift 闭包的主要区别? Block 默认值捕获,Swift 默认引用捕获;Block 有栈→堆迁移,Swift 闭包直接在堆上
Block 调用比 objc_msgSend 快还是慢? 快,Block 通过函数指针直接调用,省去了方法查找(SEL→IMP)的过程
Block 为 nil 时调用会怎样? Crash(EXC_BAD_ACCESS),不像 OC 消息发送给 nil 是安全的
_Block_object_assign 做了什么? 根据 flags 对捕获变量做 retain/initWeak/递归copy/byref_copy 等操作
GCD Block 需要 weakSelf 吗? 通常不需要,因为 self 不持有 GCD Block,不成环。但长延时的 dispatch_after 会推迟 self 释放
Block 的 copy 属性在 ARC 下还有意义吗? 功能上 strong 等效,但 copy 更具自文档性,是社区推荐写法
如何检测循环引用? Xcode Memory Graph Debugger、Instruments Leaks/Allocations、MLeaksFinder
什么是逃逸 Block? 超出创建它的函数作用域后仍可能被调用的 Block,必须 copy 到堆上
NSTimer 为什么容易循环引用? NSTimer 强引用 target,RunLoop 强引用 timer,即使 self 不强引用 timer,仍可能泄漏
Block 线程安全吗? Block 对象本身线程安全(只读),但捕获的可变对象的操作不是线程安全的

单元测试系列:如何测试不愿暴露的私有状态

作者 visual_zhang
2026年3月10日 22:59

引言

在多人协作的大型工程中,编写单元测试时有一个绑定出现的矛盾:生产代码追求封装,尽可能把实现细节藏在 private 后面;测试代码需要观测和控制内部状态,才能验证逻辑是否正确。

当你依赖的某个属性被另一个团队从 internal 改成了 private,你的测试就从"编译通过"变成了"编译失败"。这在日常开发中反复上演。

本文聚焦于一个具体问题:当被测对象的关键状态被封装为私有时,测试该怎么写? 我们按照推荐优先级介绍三种模式,并讨论如何让这类测试在多人协作中保持可维护。


一、问题的本质

以一个典型场景为例:一个管理器(Manager)内部持有若干数据源(DataSource),它的公有方法 totalCount() 聚合所有数据源的计数。你想测试的是聚合逻辑——非子集的数据源应该被加总,子集的应该被排除。

要测试这个逻辑,你需要让不同数据源返回不同的计数值。但数据源存储在管理器的 private 属性中,计数值也是数据源的 private 属性。你"两头都摸不到"。

直觉的反应是"把它改成 internal 就好了"——但在多人协作的项目中,放松封装来迁就测试往往会带来更大的问题。被放松的接口会被其他模块误用,而且你也不一定有权修改别人的代码。

所以我们需要在不修改生产代码的前提下解决这个问题。


二、三种模式

模式一:通过公有行为间接验证

原则:不直接访问私有状态,而是通过被测对象的公有方法观察其行为。

// 不要直接读取私有变量
XCTAssertEqual(object.privateCount, 10)

// 通过公有接口验证行为
object.performAction()
XCTAssertEqual(object.publicResult(), expectedValue)

适用场景:被测对象有足够的公有 API 来覆盖验证需求。

局限:当你需要设置特定的内部状态来测试某个分支时(例如"当未读数为 10 时,聚合应返回 10"),仅靠公有 API 可能无法将对象置于期望状态。此时需要下一个模式。

模式二:子类覆写

当需要控制被测对象内部依赖的返回值时,可以在测试文件中定义一个子类,覆写返回内部状态的方法:

private class StubDataSource: RealDataSource {
    private var mockValues: [QueryType: Int] = [:]

    func setValue(_ value: Int, for type: QueryType) {
        mockValues[type] = value
    }

    override func getValue(for type: QueryType) -> Int {
        return mockValues[type] ?? 0
    }
}

被测对象调用 getValue(for:) 时,实际执行的是 Stub 的逻辑,返回我们预设的值。不需要修改任何生产代码。

适用条件:被覆写的方法是非 final 的。

注意:Stub 子类应保持轻量,只覆写必要的方法。如果父类初始化有副作用(注册通知、启动定时器等),需要留意。

模式三:运行时注入

模式二解决了"让依赖返回可控值"的问题,但还有一个问题:如何把 Stub 塞进被测对象?

理想情况下,被测对象应通过构造器或属性注入依赖。但在大型存量项目中,很多类的依赖是内部创建并存储在 private 属性中的,没有公开的注入点。

此时,对于 NSObject 子类,可以借助 Objective-C 运行时直接操作 ivar:

private func setIvar<T>(_ name: String, on object: AnyObject, value: inout T) {
    let cls: AnyClass = type(of: object)
    guard let ivar = class_getInstanceVariable(cls, name) else {
        XCTFail("Cannot find ivar '(name)' — property may have been renamed.")
        return
    }

    let actualSize = computeIvarSize(ivar, in: cls)
    guard actualSize == MemoryLayout<T>.size else {
        XCTFail("Ivar '(name)' size mismatch — type may have changed.")
        return
    }

    let offset = ivar_getOffset(ivar)
    let ptr = Unmanaged.passUnretained(object).toOpaque().advanced(by: offset)
    ptr.assumingMemoryBound(to: T.self).pointee = value
}

这段代码包含两层防护,对应两类变更场景:

生产代码变更 防护机制 测试行为
属性被改名 class_getInstanceVariable 返回 nil XCTFail + 安全返回
属性名不变,类型变了 MemoryLayout<T>.size 与 ivar 实际大小不匹配 XCTFail + 安全返回

这确保了不论生产代码如何变化,测试都报错(failure)而非崩溃(crash)

限制:仅适用于 NSObject 子类。纯 Swift 类没有 ObjC 运行时元数据,此方法不可用。

选择优先级

优先级 模式 条件 风险
1 通过公有行为验证 公有 API 足以覆盖
2 子类覆写 方法非 final
3 运行时注入 NSObject 子类,无注入点 中(需防护)

模式三是"最后手段",不是常规工具。如果你发现自己频繁使用它,更值得推动的是让生产代码提供正式的依赖注入接口。


三、让这类测试在协作中存活

解决了技术问题之后,还有一个同样重要的问题:在多人协作的环境中,这些测试能否被团队中的其他人理解和维护?

3.1 封装脆弱操作,暴露清晰意图

运行时注入是"脆弱"的——它依赖属性名字符串、内存布局等编译器无法检查的假设。关键原则是:把所有脆弱操作封装在一个辅助方法中,让每个测试方法只表达业务意图。

// 辅助方法封装了所有运行时细节
private func injectStubDataSources(_ entries: [(id: String, isSubset: Bool, ds: StubDataSource)]) {
    // ... runtime injection logic ...
}

// 测试方法只表达意图
func test_totalCount_excludesSubset() {
    let primary = makeStub(count: 10)
    let subset = makeStub(count: 5)
    injectStubDataSources([
        (id: "primary", isSubset: false, ds: primary),
        (id: "filter",  isSubset: true,  ds: subset)
    ])

    XCTAssertEqual(manager.totalCount(), 10)
}

这样做的好处:

  • 单点维护:属性改名或类型变更时,只需修改 injectStubDataSources 一处。

  • 可读性:团队成员读到测试方法时,看到的是"注入一个非子集数据源(10)和一个子集数据源(5),期望聚合结果为 10",不需要理解运行时细节。

3.2 用命名传递信息

在多人协作中,测试方法名是最重要的"文档"。一个好的命名应该在不打开方法体的情况下就能传达:测什么、在什么条件下、期望什么结果。

test_[被测方法]_[场景]

test_totalCount_excludesSubsetDataSources
test_totalCount_multipleDataSources_sumsNonSubset
test_totalCount_allSubset_returnsZero

当这些测试出现在 CI 的失败报告中时,任何人——即使从未接触过这个模块——都能从方法名推断出问题所在。

3.3 报错,不要崩溃

这是使用 unsafe 技术时最重要的设计原则。

  • 失败(Failure) :CI 报告中标注 test_totalCount_excludesSubset FAILED: Cannot find ivar 'dataContextMap'。开发者 5 秒内定位问题。

  • 崩溃(Crash) :CI 报告中只有 EXC_BAD_ACCESS (code=1, address=0x...)。开发者需要调试半小时。

每一步 unsafe 操作前都必须有 guard ... else { XCTFail(...); return } 的防护链。没有例外。


小结

处理私有成员的测试难题,本质上是在封装性可测试性之间找到平衡。在不修改生产代码的前提下,三种模式提供了递进的解决方案:

  1. 优先通过公有行为验证——最安全,零风险。

  2. 子类覆写控制返回值——利用多态,风险低。

  3. 运行时注入作为兜底——突破封装,但必须有防护。

技术方案之外,同样重要的是协作层面的设计:将脆弱操作封装在一处、用命名传递意图、确保测试报错而非崩溃。这些原则让测试不仅"能用",而且"能活"——在团队成员轮换、生产代码持续演进的环境中,持续发挥保护作用。

# Flutter Engine、Dart VM、Runner、iOS 进程与线程 —— 深度解析

作者 忆江南
2026年3月10日 21:57

一、整体架构总览

┌─────────────────────────────────────────────────────────┐
│                    iOS 进程 (Process)                     │
│  ┌───────────────────────────────────────────────────┐  │
│  │              Runner (iOS Host App)                 │  │
│  │  ┌─────────────────────────────────────────────┐  │  │
│  │  │           FlutterEngine 实例                  │  │  │
│  │  │  ┌───────────────────────────────────────┐  │  │  │
│  │  │  │            Dart VM                     │  │  │  │
│  │  │  │  ┌─────────────────────────────────┐  │  │  │  │
│  │  │  │  │       Dart Isolate (main)        │  │  │  │  │
│  │  │  │  │    (你写的 Dart 业务代码)          │  │  │  │  │
│  │  │  │  └─────────────────────────────────┘  │  │  │  │
│  │  │  │  ┌─────────────────────────────────┐  │  │  │  │
│  │  │  │  │    Dart Isolate (spawned)        │  │  │  │  │
│  │  │  │  │    (compute / Isolate.spawn)     │  │  │  │  │
│  │  │  │  └─────────────────────────────────┘  │  │  │  │
│  │  │  └───────────────────────────────────────┘  │  │  │
│  │  │                                             │  │  │
│  │  │  ┌──────────┐ ┌──────────┐ ┌────────────┐  │  │  │
│  │  │  │ UI Thread│ │IO Thread │ │ GPU Thread  │  │  │  │
│  │  │  │(Platform)│ │          │ │ (Raster)    │  │  │  │
│  │  │  └──────────┘ └──────────┘ └────────────┘  │  │  │
│  │  └─────────────────────────────────────────────┘  │  │
│  │                                                    │  │
│  │  ┌──────────────────────────────────────────────┐ │  │
│  │  │  Native iOS 代码 (AppDelegate, ViewController)│ │  │
│  │  └──────────────────────────────────────────────┘ │  │
│  └───────────────────────────────────────────────────┘  │
│                                                          │
│  iOS Main Thread ─── GCD Queues ─── 其他系统线程           │
└─────────────────────────────────────────────────────────┘

二、逐层深度解析

2.1 iOS 进程 (Process)

iOS 进程是最顶层的容器,是操作系统分配资源的基本单位。

  • 每个 iOS App 运行在独立的沙盒进程中,由 launchd 守护进程启动
  • 进程拥有独立的虚拟内存空间(通常 4GB 虚拟地址空间)
  • 进程内包含:代码段、数据段、堆、栈、内存映射区域
  • 一个进程内可以有多个线程

关键点: 无论你的 Flutter App 多复杂,在 iOS 上它始终运行在一个进程内。Runner、FlutterEngine、Dart VM 都是这个进程内的组成部分。

iOS 进程
├── 进程内存空间
│   ├── Runner 的 Native 代码 (Objective-C/Swift)
│   ├── Flutter Engine 的 C++ 代码
│   ├── Dart VM 的运行时
│   ├── Dart 堆内存 (Dart Heap)
│   └── 共享库 (dylib)
├── 文件描述符表
├── 信号处理表
└── 线程表 (所有线程)

2.2 iOS 线程 (Thread)

线程是CPU 调度的基本单位,同一进程内的线程共享内存空间。

iOS 中的关键线程类型

线程 说明
Main Thread UI 线程,所有 UIKit 操作必须在此执行
GCD Worker Threads libdispatch 管理的线程池
pthread POSIX 线程,底层线程创建方式

Flutter 创建的线程

Flutter Engine 启动后会在 iOS 进程内创建4 个核心线程(Task Runner):

┌──────────────────────────────────────────────────────┐
│                  Flutter 四大线程                       │
├──────────────┬───────────────────────────────────────┤
│ Platform     │ 复用 iOS Main Thread                    │
│ Thread       │ 处理 Platform Channel、插件调用、原生交互   │
├──────────────┼───────────────────────────────────────┤
│ UI Thread    │ 独立 pthread                            │
│ (Dart Thread)│ 运行 Dart 代码、构建 Widget Tree/Layer Tree│
├──────────────┼───────────────────────────────────────┤
│ Raster       │ 独立 pthread                            │
│ Thread       │ GPU 光栅化,将 Layer Tree 转为 GPU 指令    │
├──────────────┼───────────────────────────────────────┤
│ IO Thread    │ 独立 pthread                            │
│              │ 图片解码、资源加载等耗时 IO 操作             │
└──────────────┴───────────────────────────────────────┘

重要区分: Flutter 的 "UI Thread" 不是 iOS 的 Main Thread。Flutter 的 Platform Thread 才是 iOS 的 Main Thread。

2.3 Runner (iOS Host App)

Runner 是 Flutter 工程中 ios/Runner 目录下的 iOS 宿主工程,本质上就是一个标准的 iOS App。

Runner 的职责

Runner (Xcode Project)
├── AppDelegate.swift / .m
│   └── 创建 FlutterEngine / FlutterViewController
├── Info.plist
│   └── App 配置、权限声明
├── Assets.xcassets
│   └── App Icon、LaunchImage
├── Main.storyboard (可选)
│   └── LaunchScreen
└── Frameworks/
    ├── Flutter.framework        ← Flutter Engine 二进制
    └── App.framework            ← 编译后的 Dart 代码 (AOT)

Runner 的生命周期

iOS 系统启动进程
    │
    ▼
main() 函数执行
    │
    ▼
UIApplicationMain() 
    │
    ▼
AppDelegate 初始化
    │
    ├── 创建 FlutterEngine 实例
    │       │
    │       ├── 初始化 Dart VM
    │       ├── 创建 4 个 Task Runner (线程)
    │       └── 加载 Dart AOT snapshot
    │
    ├── 创建 FlutterViewController
    │       │
    │       ├── 关联 FlutterEngine
    │       ├── 创建 Metal/OpenGL 渲染表面
    │       └── 注册 Platform Channels
    │
    └── App 进入 RunLoop

Runner 与 FlutterEngine 的关系

// 典型的 AppDelegate 代码
@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
    // FlutterAppDelegate 内部持有 FlutterEngine
    // 它本质上是一个 UIApplicationDelegate 的实现
    
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // 此时 FlutterEngine 已经被创建并启动
        // Dart VM 已经初始化
        // main.dart 的 main() 已经开始执行
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

核心理解:Runner 是"壳",FlutterEngine 是"核心"。 Runner 负责 iOS 层面的事务(权限、推送、深链接等),FlutterEngine 负责 Flutter 的一切。

2.4 Flutter Engine

Flutter Engine 是用 C/C++ 编写的核心运行时,是连接 Dart 代码和底层操作系统的桥梁。

Engine 的组成

FlutterEngine
├── Dart Runtime (Dart VM)
│   ├── Dart Isolate 管理
│   ├── GC (垃圾回收器)
│   └── JIT / AOT 编译器
│
├── Shell (壳层)
│   ├── Platform Shell (iOS 适配层)
│   │   ├── Metal 渲染后端
│   │   ├── 触摸事件转发
│   │   └── 系统 API 桥接
│   └── Task Runner 管理
│       ├── PlatformTaskRunner → iOS Main Thread
│       ├── UITaskRunner → Dart 执行线程
│       ├── RasterTaskRunner → GPU 光栅化线程
│       └── IOTaskRunner → IO 线程
│
├── Skia / Impeller (图形引擎)
│   ├── 2D 渲染 API
│   ├── 文字排版 (txt 库 / libtxt)
│   └── 图片解码
│
├── Text Layout (文字排版引擎)
│
└── Platform Channel 机制
    ├── MethodChannel
    ├── BasicMessageChannel
    └── EventChannel

Engine 在 iOS 中的存在形式

Flutter.framework (约 40~50 MB, Release 模式)
├── Flutter (Mach-O 动态库)
│   ├── Dart VM 运行时
│   ├── Skia / Impeller 图形库
│   ├── Shell 层 (iOS 适配)
│   └── ICU 国际化数据
└── Headers/
    ├── FlutterEngine.h
    ├── FlutterViewController.h
    ├── FlutterPlugin.h
    └── FlutterChannels.h

App.framework (Dart 业务代码编译产物)
├── App (Mach-O 动态库)
│   ├── Dart AOT Snapshot (机器码)
│   └── 资源数据
└── flutter_assets/
    ├── kernel_blob.bin (Debug 模式)
    ├── vm_snapshot_data
    ├── isolate_snapshot_data
    └── AssetManifest.json

2.5 Dart VM

Dart VM 是 FlutterEngine 内部的虚拟机运行时,负责执行 Dart 代码。

Dart VM 的两种运行模式

┌─────────────────────────────────────────┐
│              Debug 模式 (JIT)              │
│                                          │
│  Dart 源码 → Kernel Binary → JIT 编译     │
│           → 解释执行 + 热编译为机器码        │
│                                          │
│  特点:支持 Hot Reload / Hot Restart       │
│       有 Dart VM 完整编译器                 │
│       性能较低                             │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│            Release 模式 (AOT)              │
│                                          │
│  Dart 源码 → AST → IR → ARM64 机器码       │
│           → 直接 CPU 执行                  │
│                                          │
│  特点:不支持 Hot Reload                   │
│       无 JIT 编译器 (体积更小)              │
│       性能接近原生                          │
└─────────────────────────────────────────┘

Dart VM 的内存结构

Dart VM 内存布局
├── New Space (新生代)
│   ├── Semi-Space A (活跃区)
│   └── Semi-Space B (备用区)
│   └── Scavenger GC (复制算法, STW 很短)
│
├── Old Space (老生代)
│   ├── 大对象区
│   └── 普通对象区
│   └── Mark-Sweep / Mark-Compact GC
│
├── Code Space (代码区)
│   └── AOT 编译后的机器码
│
├── Image Space (镜像区)
│   ├── vm_snapshot_data
│   └── isolate_snapshot_data
│
└── Isolate 独有内存
    ├── 每个 Isolate 有独立的堆
    └── Isolate 间不共享内存 (通过消息传递通信)

Dart Isolate 与 iOS 线程的关系

┌─────────────────────────────────────────────────┐
│                                                  │
│  Dart Isolate ≠ iOS Thread                       │
│  但 Dart Isolate 运行在 iOS Thread 之上            │
│                                                  │
│  ┌─────────────┐      ┌─────────────────┐       │
│  │ Root Isolate │ ──→  │ UI Thread        │       │
│  │ (main)       │      │ (固定绑定)        │       │
│  └─────────────┘      └─────────────────┘       │
│                                                  │
│  ┌─────────────┐      ┌─────────────────┐       │
│  │ Spawned      │ ──→  │ Dart VM 线程池中   │       │
│  │ Isolate      │      │ 的某个 pthread    │       │
│  └─────────────┘      └─────────────────┘       │
│                                                  │
│  一个 Isolate 在同一时刻只在一个线程上运行          │
│  但 Dart VM 可以将 Isolate 调度到不同线程上         │
│                                                  │
└─────────────────────────────────────────────────┘

关键区别:

  • Isolate 是 Dart 的并发模型,拥有独立的堆内存,通过 SendPort/ReceivePort 通信
  • Thread 是 OS 级别的执行单元,共享进程内存
  • Root Isolate 始终运行在 Flutter 的 UI Thread
  • Isolate.spawn() 创建的 Isolate 运行在 Dart VM 管理的线程池中

三、协作流程深度解析

3.1 一帧的渲染流程

时间线 ──────────────────────────────────────────────────────→

VSync 信号到达 (16.67ms 一次 @60fps)
    │
    │  ① Platform Thread (iOS Main Thread)
    │  ┌──────────────────────────────┐
    │  │ 接收 VSync 回调               │
    │  │ 通过 CADisplayLink            │
    │  │ 通知 UI Thread 开始新一帧      │
    │  └──────────────┬───────────────┘
    │                 │
    │  ② UI Thread (Dart Thread)
    │  ┌──────────────▼───────────────┐
    │  │ 执行 Dart 代码                │
    │  │ ├── Build Phase (Widget)      │
    │  │ ├── Layout Phase (大小/位置)   │
    │  │ ├── Paint Phase (绘制指令)     │
    │  │ └── 生成 Layer Tree            │
    │  └──────────────┬───────────────┘
    │                 │
    │  ③ Raster Thread (GPU Thread)
    │  ┌──────────────▼───────────────┐
    │  │ 接收 Layer Tree               │
    │  │ ├── Skia/Impeller 光栅化      │
    │  │ ├── 生成 GPU 指令             │
    │  │ └── 提交到 Metal/OpenGL       │
    │  └──────────────┬───────────────┘
    │                 │
    │  ④ iOS 合成器 (Core Animation)
    │  ┌──────────────▼───────────────┐
    │  │ 合成 Flutter 层与原生 UI 层    │
    │  │ 提交到屏幕显示                 │
    │  └──────────────────────────────┘

3.2 Platform Channel 调用流程

Dart 代码 (UI Thread)                     Native 代码 (Platform Thread)
       │                                           │
       │  MethodChannel.invokeMethod('getBattery')  │
       │  ─────────────────────────────────────→    │
       │  [序列化为二进制消息]                         │
       │  [从 UI Thread 调度到 Platform Thread]       │
       │                                           │
       │                            ┌──────────────▼──────┐
       │                            │ iOS Main Thread      │
       │                            │ 执行原生代码          │
       │                            │ UIDevice.current     │
       │                            │   .batteryLevel      │
       │                            └──────────────┬──────┘
       │                                           │
       │  ←─────────────────────────────────────   │
       │  [结果序列化]                               │
       │  [从 Platform Thread 调度回 UI Thread]      │
       │                                           │
       ▼                                           ▼
  收到 Future 结果                            调用完成

3.3 多 Engine 场景 (Add-to-App)

iOS 进程
├── Runner
│   ├── FlutterEngine A (主引擎)
│   │   ├── Dart VM (进程内唯一,共享)  ◄──── 重要!
│   │   ├── Root Isolate A
│   │   ├── 4 个线程 (Platform/UI/Raster/IO)
│   │   └── FlutterViewController A
│   │
│   ├── FlutterEngine B (第二个引擎)
│   │   ├── Dart VM (复用同一个)  ◄──── 同一个 Dart VM
│   │   ├── Root Isolate B (独立的 Isolate)
│   │   ├── 4 个线程 (Platform共享/UI独立/Raster独立/IO独立)
│   │   └── FlutterViewController B
│   │
│   └── FlutterEngineGroup (管理多引擎)
│       └── 共享 Dart VM,减少约 99% 的额外内存开销

核心要点: 一个 iOS 进程中只有一个 Dart VM 实例,但可以有多个 FlutterEngine,每个 Engine 有自己独立的 Root Isolate。


四、关系总结图

┌─────────────────────────────────────────────────────┐
│                                                      │
│  包含关系 (从外到内):                                  │
│                                                      │
│  iOS 进程                                             │
│    └── Runner (iOS Host App)                         │
│          └── FlutterEngine                           │
│                ├── Dart VM (进程唯一)                  │
│                │     └── Dart Isolate (可多个)         │
│                ├── Skia / Impeller                    │
│                └── Shell (平台适配层)                   │
│                                                      │
│  运行关系 (线程维度):                                  │
│                                                      │
│  iOS Main Thread ═══ Flutter Platform Thread          │
│       │                    │                          │
│       │                    ├── 插件调用                 │
│       │                    ├── 原生 UI 交互             │
│       │                    └── 生命周期管理              │
│       │                                               │
│  Flutter UI Thread ─── Dart Root Isolate 运行于此       │
│       │                    │                          │
│       │                    ├── Widget 构建              │
│       │                    ├── 布局计算                 │
│       │                    └── 绘制指令生成              │
│       │                                               │
│  Flutter Raster Thread ─── GPU 光栅化于此               │
│       │                                               │
│  Flutter IO Thread ─── 资源加载/图片解码于此             │
│       │                                               │
│  Dart VM 线程池 ─── spawned Isolate 运行于此            │
│                                                      │
└─────────────────────────────────────────────────────┘

五、常见误区澄清

误区 1:Flutter 的 UI Thread 就是 iOS 的 Main Thread

错!

  • Flutter Platform Thread = iOS Main Thread
  • Flutter UI Thread 是单独的 pthread,专门运行 Dart 代码
  • 在 Dart 代码里调用 Platform Channel 时,消息从 UI Thread 发送到 Platform Thread(即 iOS Main Thread)

误区 2:Dart Isolate 就是一个线程

错!

  • Isolate 是 Dart 的并发抽象,拥有独立的堆内存
  • Isolate 运行在线程之上,但两者不是一一对应关系
  • Dart VM 内部使用线程池来调度 Isolate
  • Root Isolate 固定绑定在 UI Thread 上,但 spawned Isolate 可能被调度到不同的线程

误区 3:多个 FlutterEngine 就有多个 Dart VM

错!

  • 一个 iOS 进程中只有一个 Dart VM
  • 多个 FlutterEngine 共享同一个 Dart VM
  • 每个 FlutterEngine 有自己独立的 Root Isolate
  • 使用 FlutterEngineGroup 可以高效创建多引擎,内存开销极小

误区 4:Runner 就是 Flutter

错!

  • Runner 只是一个标准的 iOS App 工程(壳)
  • 真正的 Flutter 运行时是 Flutter.framework 中的 FlutterEngine
  • Runner 可以同时包含原生 Swift/ObjC 代码和 Flutter 页面
  • 在 Add-to-App 场景中,Runner 甚至不叫 Runner,就是你已有的 iOS 工程

六、实际影响与性能调优启示

场景 涉及组件 优化方向
界面卡顿 UI Thread (Dart) 减少 build() 复杂度,使用 const Widget
光栅化卡顿 Raster Thread 减少 saveLayerclipPath 等 GPU 密集操作
平台通信慢 Platform Thread ↔ UI Thread 减少 Channel 调用频率,批量传输数据
图片加载慢 IO Thread 预缓存、降低分辨率、使用 precacheImage
内存爆炸 Dart VM (Isolate Heap) 控制 Isolate 数量、及时释放大对象
原生插件阻塞 Platform Thread (Main Thread) 插件内部开子线程处理耗时逻辑
App 启动慢 Engine 初始化 + Dart VM 启动 预热 Engine (FlutterEngine 提前初始化)

天味食品:2025年净利润同比下降8.79%,拟10派5.5元

2026年3月11日 20:54
36氪获悉,天味食品发布2025年年度报告。报告显示,公司实现营业收入34.49亿元,同比下降0.79%;归属于上市公司股东的净利润5.70亿元,同比下降8.79%。公司拟向全体股东每股派发现金红利0.55元(含税),即每10股派5.5元(含税)。

被CRUD拖垮的第5年,我用Cursor 一周"复仇":pxcharts-vue开源,一个全栈老兵的AI编程账本

作者 徐小夕
2026年3月11日 20:52

今天继续和大家聊聊,我们开源的 pxcharts-vue 多维表格的诞生故事。

图片

开源地址:github.com/MrXujiang/p…

演示地址:test.admin.mvtable.com/mvtable

一、开篇:那个写不动代码的凌晨

记得是5年前的一个夜晚,凌晨两点左右,我对着第46个几乎相同的表单页面发呆。

需求文档上写着:"再做一个支持关联查询的动态表格,下周上线。"

我机械地复制着上一版的CRUD代码,改字段名、调接口、写校验。Vue文件超过1000行,methods里塞着十几个功能相似但不敢重构的方法——怕牵一发而动全身。

这是我做全栈的第5年。从Vue2到Vue3,从jQuery到React,技术栈在升级,但日常还是改不完的表单、写不完的列表、调不完的接口

我自嘲是"高级CRUD工程师",但那个凌晨,我真的写不动了。

不是身体累,是认知上的绝望:我知道接下来的5年,如果继续这样"人肉搬砖",只会从"写不动"变成"不敢写"——新技术层出不穷,而我被困在业务逻辑的重复劳动里。

转机出现在三年后。

Cursor 的Agent模式刚更新,我抱着"试试又不会死"的心态,把曾经折磨我两周的多维表格需求丢给了AI。

图片

经过3天多和AI辩证推演之后,pxcharts-vue 的核心架构跑通了。我盯着屏幕上自动生成的Composition API代码,第一反应不是兴奋,是后怕——如果AI早来两年,我这5年到底在忙什么?

这篇文章,是我作为"全栈老兵"的AI编程账本。不吹不黑,只记录真实的效率数字、踩过的坑、以及那个凌晨之后,我对职业价值的重新思考。


二、产品画像:pxcharts-vue是什么?(技术人的"复仇工具")

图片

先介绍这次"复仇"的成果。pxcharts-vue 不是又一个Element Plus的封装,而是面向复杂业务场景的"关系型多维表格引擎"

它解决的是我5年来反复遇到的三个痛点:

1. 平面表格 vs 立体数据

传统表格是Excel思维:行是记录,列是属性。但真实业务是关系型的——订单关联客户、任务关联项目、SKU关联SPU。我们用"关联列"把这种关系可视化:选客户时自动带出合同,选商品时自动填充价格,底层是外键约束,表层是下拉选择。

图片

这 revenge 了我过去写过的无数遍onChange联动逻辑。

在多维表格设计中,我们完全对标了钉钉AI表格和飞书多维表格的字段设计,实现了多种表格业务字段,并支持随时编辑修改:

图片

当然有些字段比较复杂,AI无法完全理解和实现,其中40%的工作量是我们手敲代码实现的。

2. 一份数据,多种视角

图片

同一份项目数据,产品经理要看甘特图,运营要看看板,财务要看表格汇总。pxcharts-vue 实现了视图层与数据层解耦:底层是统一的数据模型,上层是表格、看板、表单等渲染适配器。

这 revenge 了我过去为"换个展示方式"而写的冗余接口。

3. 公式字段:把Excel能力Web化(React版本中实现了)

支持跨表引用、聚合计算、条件判断,非技术用户能配出"自动计算提成"的复杂逻辑。对于开发者,这意味着业务规则从后端Java代码前移到了前端配置层,需求变更不用重新部署。

这 revenge 了我过去凌晨两点还在改的"紧急加字段"需求。

技术栈:Vue3 + TypeScript + Vite,纯前端实现,零后端依赖,开箱即用。


三、复仇实录:一周重构的流水线与真实账本

这次开发全程在Cursor Composer的Agent模式下完成,我们自己研发的工作量占比40%。

我记录了一套"老兵式AI协作流"——不是盲目信任,是有策略地外包

plan 1:架构设计(从"人肉画图"到"对话式架构")

过去的我:  打开Draw.io画组件关系图,纠结半小时目录结构,再花2小时搭Vite脚手架。

AI模式:

我:基于Vue3实现一个多维表格内核,需要支持列定义、数据编辑、视图切换,采用模块化架构,优先使用Composition API和<script setup>语法。Cursor:生成项目结构 + 核心类型定义 + 基础组件框架

耗时:30分钟 vs 过去的4小时。

关键干预:  强制要求AI先生成ARCHITECTURE.md设计文档,确认模块边界后再生成代码。这是从"边想边写"的混乱中保留下来的人类架构师尊严

plan 2:核心功能(关联列与视图系统)

关联列功能:

我:需要实现表与表之间的关联,类似数据库外键约束,支持多选、级联筛选、自动回填。Cursor:生成基于Proxy的响应式关联逻辑 + 选择器组件 + 数据联动机制

过去需要2天,现在4小时。  但AI生成的第一版用了递归遍历,大数据量时卡顿明显。我要求它改用虚拟滚动+懒加载,它给出了基于vue-virtual-scroller的优化方案。

视图系统: AI建议使用策略模式管理不同视图,我确认方案后,它生成了TableStrategy、KanbanStrategy、GanttStrategy三个类,统一实现render()接口。

耗时:6小时 vs 过去的3天。

plan 3:公式引擎与边界加固

这是最复杂的模块。我采用Plan Mode

  1. 先让AI出《公式引擎设计文档》:语法解析(PEG.js)、沙箱执行(Web Worker)、错误处理机制
  2. 人工Review确认安全方案(禁用eval,使用白名单函数)
  3. 再让AI生成代码

发现的问题:  AI生成的初始版本用了new Function()执行公式,我立即叫停——这是XSS漏洞温床。CodeRabbit 的研究证实,AI代码引入安全漏洞的概率是人类代码的2.74倍。最终改用受限沙箱+语法树解析

耗时:1.5天 vs 过去的5天。

效率账本(真实数字)

环节 传统开发(第5年的我) AI辅助开发(复仇模式) 效率倍数
脚手架与架构 3天 2小时 8x
关联列逻辑 3天 1天 3x
视图切换系统 5天 1天 5x
公式引擎 5天 1天 5x
安全加固与优化 2-3天 1天 2x
总计 18-20天 4.2天 4x

整体效率提升约230% ,与GitClear对高AI使用率开发者的调研数据(4-10倍产出提升)基本吻合。

当然客观的说,我们工程师也花了大概30%-40%的经历攻克AI无法解决的问题,但是AI Coding的整体提效还是很显著的。


四、账本B面:AI编程的隐性成本与"复仇"的代价

但这不是爽文。

图片

一周交付的背后,我们付出了传统开发不会有的代价。这是账本必须记录的B面

1. 安全债务:AI的"自信"是危险的

pxcharts-vue 初期版本中,AI生成的表格解析渲染器存在原型链污染漏洞——它从某个Stack Overflow回答中学到了"巧妙"的对象合并技巧,但那是有安全缺陷的过时方案。

CodeRabbit 分析了数百万行AI生成代码,发现:

  • 引入XSS漏洞的概率:人类代码的2.74倍
  • 硬编码机密信息的概率:人类代码的2.1倍

我的对策:  核心安全模块(公式沙箱、数据校验)必须人工Review,AI仅辅助生成单元测试用例。

2. 可维护性陷阱:你成了"代码陌生人"

Day 2下午,AI生成了50行复杂的视图切换逻辑。当时我看懂了大意,觉得"没问题"。一周后回看,我盯着那团递归+闭包的组合,完全想不起来为什么这样写、边界条件是什么

GitClear的研究警告:AI辅助代码的撤销率(Churn rate)比人类代码高40% ,意味着更多返工。

我的对策:  强制要求AI生成 "逻辑注释" ——不是解释语法,而是解释设计决策("为什么用递归而非迭代""此处假设数据量小于1万条")。关键算法必须人工复述原理,确保"我懂我的代码"。

3. 架构一致性危机:AI的"创意"是混乱的

不同会话的AI会给出风格迥异的方案。早期关联列用Options API,后期视图系统被建议改成Composition API,导致代码风格混杂——就像一个项目里有5个不同架构师的手笔

我的对策:  建立《AI编程规范文档》(.cursorrules),固化:

  • 技术栈:Vue3 + <script setup> + TypeScript严格模式
  • 设计模式:优先组合式函数,类仅用于策略模式
  • 命名规范:组件PascalCase,组合式函数useXxx,工具函数纯函数优先

这让AI在约束内发挥,而非"自由创作"。

4. 幻觉税:为AI的"自信"买单

图片

视图切换的虚拟滚动功能,AI生成的代码在1000条数据时完美运行,10000条时白屏。它没有考虑内存溢出边界,也没有提示"此处需要性能测试"。

这类问题只能靠人工测试发现。AI编程省下的时间,部分要返还到更严格的测试环节


五、老兵的新战场:AI时代,全栈工程师该专注什么?

图片

pxcharts-vue 开源后,我一直在想:如果AI能写代码,我这5年积累的经验还有什么价值?答案在开发过程中逐渐清晰——

1. 从"实现者"到"架构守门员"

AI擅长生成"能跑的代码",但不懂业务领域的架构权衡

pxcharts-vue 的数据模型设计(平面表 vs 树形结构)、状态管理方案(Pinia vs 纯响应式)、视图渲染策略(Canvas vs DOM),这些决策需要人类对业务场景的深度理解。

新角色:  不是写代码,是设计代码的生成规则

凭借我之前在大厂做技术架构的经验,我能很快给出AI高效的架构和解决思路,所以这也要求我们有一定的技术背景,才能更好的让AI为我们服务。

2. 从"调试bug"到"设计防错机制"

AI代码的bug更隐蔽——它很少犯语法错误,但常犯逻辑假设错误("假设用户不会同时编辑两个单元格")。我的新工作是预判这些假设,在设计阶段就加入防御性机制。

新角色:  不是修bug,是设计让bug无法发生的系统

3. 从"技术执行"到"AI流程设计"

这次3天重构,真正的生产力提升不是来自Cursor本身,而是我设计的分层协作流程

  • 生成层(工具函数):100%信任AI
  • 业务层(组件逻辑):AI生成+人工Review,70%信任
  • 核心层(公式引擎):AI辅助设计,人工实现,30%信任

新角色:  不是写代码,是设计人机协作的流水线


六、开源的思考:不止于代码,是"复仇经验"的共享

选择开源 pxcharts-vue,除了技术分享,我还想验证一个假设:AI编程时代,开源的价值会从"代码"转向"流程"

传统开源是"拿我的代码用",未来可能是"拿我的Prompt用"——如何让AI生成高质量的Vue3组件?如何设计安全的公式引擎?如何避免我踩过的坑?

我后续会分享《pxcharts-vue AI开发手册》,包含:

  • 架构设计、高性能表格技术实践
  • 安全审计清单(AI代码常见漏洞模式)
  • 性能优化策略(虚拟滚动、大数据渲染、内存管理)

如果你也在用AI编程工具,欢迎来 留言区 交流。

我们可以一起探索:当AI成为标配,人类开发者的"复仇"该指向什么?


结语:账本结算,复仇之后

5年前那个凌晨两点写不动代码的我,不会想到三年后会写下这篇文章。

pxcharts-vue 的一周重构,是效率的胜利,也是一次职业价值的重新校准。AI编程确实"复仇"了CRUD的重复劳动,但它也暴露了人类开发者的软肋——我们过去引以为傲的"编码速度",在AI面前不值一提。

新的竞争力在于:架构设计的品味、安全风险的嗅觉、人机协作的智慧,以及对自己代码的深刻理解

盛科通信:近期国家集成电路产业投资基金减持1%公司股份

2026年3月11日 20:50
36氪获悉,盛科通信公告,公司收到持股5%以上股东国家集成电路产业投资基金股份有限公司的告知函,其在2026年3月2日至2026年3月11日期间,通过集中竞价方式减持公司股份410万股,占公司总股本的比例由13.00%减少至12.00%,权益变动触及1%刻度。此次权益变动为非第一大股东减持所致,不触及要约收购,不会导致无控股股东及实际控制人的情况发生变化,不会对公司治理结构及持续经营产生重大影响。

热门中概股美股盘前涨跌不一,蔚来跌超2%

2026年3月11日 20:38
36氪获悉,热门中概股美股盘前涨跌不一,截至发稿,B站、蔚来跌超2%,阿里巴巴跌超1%,腾讯音乐跌0.86%,京东跌0.16%,拼多多跌0.15%;理想汽车涨超2%,小鹏汽车涨超1%,爱奇艺涨0.71%,微博涨0.2%。

存储产能争夺战再升温,AMD欲联手三星电子锁定HBM供给

2026年3月11日 20:17
知情人士表示,AMD首席执行官苏姿丰将于下周在韩国会见韩国科技霸主三星电子会长李在镕,双方将重点讨论在用于人工智能芯片组件的高带宽存储(即HBM存储系统)供应保障方面开展积极合作。知情人士表示,预计苏姿丰还将与韩国最大互联网门户和搜索引擎提供商Naver讨论更广泛的AI算力基础设施合作前景。(财联社)

巴西最大糖业公司Raizen达成126亿美元债务重组协议

2026年3月11日 20:15
3月11日,据报道,巴西最大的糖业和乙醇公司Raizen表示,已与债权人和债券持有人达成庭外债务重组协议,涉及约651亿雷亚尔(约合126.1亿美元)的债务。Raizen是巴西企业集团Cosan SA和石油巨头壳牌公司的合资企业,此前报道称,该公司正在考虑潜在的资产出售,并停止建设新工厂的项目,以努力减少债务。(界面)

OpenClaw会疯狂扣钱吗?腾讯云回应

2026年3月11日 20:13
36氪获悉,腾讯云发文回应与OpenClaw及腾讯“龙虾”相关的安全问题。就OpenClaw是否会“疯狂”地扣用户的钱,腾讯云提到,社交平台上流传着一张截图,显示一名用户在腾讯云公益装机活动上安装OpenClaw之后出现高额费用“偷跑”,累计200多元。腾讯云回应称,经后台排查及核实,这200多元是这名用户此前的历史模型调用费用。“OpenClaw安装本身免费,但如果使用过程中调用大模型,就会产生token费用,目前基本所有Agent工具都是类似情况。”腾讯云称。
❌
❌