阅读视图

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

# iOS Block 深度解析

全面剖析 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 对象本身线程安全(只读),但捕获的可变对象的操作不是线程安全的

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

一、整体架构总览

┌─────────────────────────────────────────────────────────┐
│                    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 提前初始化)

# iOS weak 原理详解

一、weak 是什么

weak 是 Objective-C / Swift 中的一种弱引用修饰符。它的核心行为只有两条:

  1. 不增加引用计数:持有对象但不影响对象的生命周期
  2. 对象释放时自动置 nil:不会产生野指针

这个"自动置 nil"是 weak 最关键的特性,也是它和 __unsafe_unretained(不置 nil,会变野指针)的根本区别。


二、为什么需要 weak

循环引用问题

两个对象互相强引用,谁都释放不了:

A 强引用 BB 的引用计数 +1B 强引用 AA 的引用计数 +1)

想释放 A → 但 B 还在引用 AA 释放不了
想释放 B → 但 A 还在引用 BB 释放不了
→ 内存泄漏

把其中一方改成 weak 就打破了循环:

A 强引用 BB 的引用计数 +1B 弱引用 AA 的引用计数不变)

外部释放 AA 的引用计数归零 → A 被销毁 → A 释放对 B 的强引用 → B 也被销毁

常见场景:delegate、block 捕获 self、父子视图关系。


三、weak 的底层实现:SideTable

这是 weak 原理的核心。Runtime 维护了一套 SideTable 数据结构来管理 weak 引用。

3.1 整体架构

全局有一个 SideTable 数组(StripedMap<SideTable>)
包含 64 个 SideTable(根据对象地址哈希分配到不同的表,减少锁竞争)

每个 SideTable 包含三样东西:
+---------------------------+
|  spinlock_t 自旋锁         |   用于多线程安全
+---------------------------+
|  RefcountMap 引用计数表     |   存储对象的引用计数(非 isa 优化时)
+---------------------------+
|  weak_table_t 弱引用表     |   存储所有 weak 指针的信息
+---------------------------+

3.2 weak_table_t 的结构

weak_table_t
+-----------------------------------+
|  weak_entry_t *weak_entries       |   哈希数组
|  size_t num_entries               |   当前条目数
|  uintptr_t mask                   |   哈希掩码(数组大小 - 1)
|  uintptr_t max_hash_displacement  |   最大哈希冲突偏移
+-----------------------------------+

每个 weak_entry_t 对应一个被弱引用的对象:
weak_entry_t
+-----------------------------------+
|  referent(被弱引用的对象地址)      |   Key:对象是谁
|  referrers(弱引用指针的地址数组)   |   Value:谁在弱引用它
+-----------------------------------+

用人话说就是:Runtime 维护了一张大表,Key 是对象地址,Value 是所有指向这个对象的 weak 指针的地址列表

类比理解:

想象一个"粉丝登记簿"。每个明星(对象)有一页,上面记着所有粉丝(weak 指针)的联系方式。明星退役(对象释放)时,工作人员翻到那一页,逐个通知粉丝"他退了"(置 nil),然后撕掉这一页。


四、weak 的完整生命周期

4.1 创建 weak 引用时发生了什么

当你写 __weak id weakObj = obj; 时,Runtime 调用 objc_initWeak,完整流程:

objc_initWeak(&weakObj, obj)
    |
    v
storeWeak(&weakObj, obj)
    |
    v
1. 根据 obj 的地址,哈希计算找到对应的 SideTable
    |
    v
2. 加锁(SideTable  spinlock)
    |
    v
3.  weak_table 中查找 obj 对应的 weak_entry_t
   - 如果不存在:创建一个新的 weak_entry_t,插入 weak_table
   - 如果已存在:直接使用
    |
    v
4.  &weakObj(weak 指针的地址)添加到 weak_entry_t  referrers 数组中
    |
    v
5. 解锁
    |
    v
6. 返回 obj(weakObj 现在指向 obj,但不增加引用计数)

4.2 读取 weak 引用时发生了什么

当你使用 weakObj 时(比如 [weakObj doSomething]),Runtime 调用 objc_loadWeakRetained

objc_loadWeakRetained(&weakObj)
    |
    v
1. 读取 weakObj 当前指向的对象
    |
    v
2. 如果对象正在被释放(deallocating)→ 返回 nil
    |
    v
3. 如果对象还活着  对它做一次 retain(引用计数 +1)
    |
    v
4. 返回对象(调用方用完后会 release)

为什么读取时要 retain? 防止你拿到对象后、使用之前的瞬间,对象被其他线程释放。retain 一下确保对象在使用期间不会消失。

这也是为什么常见的模式是:

__weak typeof(self) weakSelf = self;
[obj doSomething:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;  // 读取时 retain
    if (!strongSelf) return;                           // 如果已释放就退出
    [strongSelf doWork];                               // 安全使用
}];

__strong typeof(weakSelf) strongSelf = weakSelf 这一步就触发了 retain,保证 block 执行期间 self 不会消失。

4.3 对象释放时发生了什么(最关键的部分)

当一个被弱引用的对象引用计数归零时,dealloc 过程中会清理所有 weak 引用。

完整流程:

对象引用计数归零
    |
    v
objc_object::rootDealloc()
    |
    v
object_dispose()
    |
    v
objc_destructInstance(obj)
    |
    v
clearDeallocating(obj)
    |
    v
clearDeallocating_slow(obj)
    |
    v
1. 根据 obj 地址找到对应的 SideTable
    |
    v
2. 加锁
    |
    v
3.  weak_table 中查找 obj 对应的 weak_entry_t
    |
    v
4. 遍历 weak_entry_t  referrers 数组
   对每个 weak 指针地址:*referrer = nil  (置 nil!)
    |
    v
5.  weak_table 中删除这个 weak_entry_t
    |
    v
6. 解锁
    |
    v
7. 释放对象内存(free)

第 4 步就是 weak 自动置 nil 的核心:Runtime 遍历所有指向这个对象的 weak 指针,把它们全部设为 nil。

4.4 weak 引用被覆盖或销毁时

当 weak 指针指向新对象或超出作用域时,Runtime 调用 objc_destroyWeak

objc_destroyWeak(&weakObj)
    |
    v
storeWeak(&weakObj, nil)
    |
    v
1. 找到旧对象的 SideTable
    |
    v
2. 从旧对象的 weak_entry_t  referrers 中移除 &weakObj
    |
    v
3. 如果 referrers 为空了,删除这个 weak_entry_t

五、SideTable 的哈希设计

5.1 为什么用 64 个 SideTable

如果只有一个全局的 SideTable,所有线程操作 weak 引用时都要抢同一把锁,性能极差。

64 个 SideTable 通过对象地址哈希分散到不同的表上,不同表用不同的锁,大幅减少了锁竞争。

对象 A(地址 0x1000)→ 哈希 → SideTable[3]  → 锁3
对象 B(地址 0x2000)→ 哈希 → SideTable[17] → 锁17
对象 C(地址 0x3000)→ 哈希 → SideTable[3]  → 锁3(和A竞争,但概率低)

5.2 weak_entry_t 内部的优化

weak_entry_t 内部存储 referrers(弱引用指针数组)有两种模式:

  • 内联模式(inline):当弱引用数量不超过 4 个时,直接用一个固定大小的数组存储(WEAK_INLINE_COUNT = 4),避免堆内存分配
  • 动态模式(outline):超过 4 个时,切换为动态分配的哈希数组

大多数对象的 weak 引用数量不超过 4 个(通常就一两个 delegate),所以内联模式覆盖了大部分场景,性能更好。


六、weak 和 isa 的关系

6.1 isa 中的 weakly_referenced 位

现代 Objective-C 使用"优化的 isa"(Non-pointer isa),把引用计数和一些标志位直接存在 isa 指针里:

isa 指针(64位):
| 1bit  | 1bit        | 1bit            | 33bit    | ...   |
| nonptr| has_assoc   | has_cxx_dtor    | shiftcls | ...   |
|       |             |                 |          |       |
|       |             |                 |          | weakly_referenced (1bit)

weakly_referenced 位标记这个对象是否有 weak 引用。dealloc 时,如果这个位是 0,就跳过 weak 清理流程,加速释放。

dealloc 快速路径:
  if (!nonpointer) → 慢路径
  if (weakly_referenced) → 需要清理 weak 表 → 慢路径
  if (has_assoc) → 需要清理关联对象 → 慢路径
  if (has_cxx_dtor) → 需要调用 C++ 析构 → 慢路径
  否则 → 直接 free,最快

所以一个没有 weak 引用、没有关联对象、没有 C++ 析构的纯 OC 对象,释放速度是最快的。


七、weak 的性能开销

weak 不是"免费的",它有实实在在的性能开销:

操作 开销
创建 weak 引用 哈希查找 SideTable + 加锁 + 插入 weak_entry
读取 weak 引用 读取 + retain + autorelease(或 release)
对象释放时 哈希查找 + 加锁 + 遍历所有 weak 指针置 nil + 删除条目

对比 __unsafe_unretained:创建和读取都只是普通的指针赋值和读取,几乎零开销。代价是对象释放后变成野指针。

实际影响:在绝大多数场景下,weak 的开销完全可以忽略。但在极端高频的场景下(比如每秒创建销毁上万个弱引用对象),可以考虑用 __unsafe_unretained 配合手动管理来优化。


八、weak 和 autoreleasepool 的关系

在 MRC 和早期 ARC 实现中,读取 weak 变量会自动将对象注册到 autoreleasepool:

id obj = objc_loadWeak(&weakObj);
// 等价于:
id obj = objc_loadWeakRetained(&weakObj);
objc_autorelease(obj);

这意味着在一个循环里频繁读取 weak 变量,会导致 autoreleasepool 膨胀:

for (int i = 0; i < 100000; i++) {
    NSLog(@"%@", weakObj);  // 每次读取都往 pool 里加一个
}
// pool 里积累了 10 万个对象,直到 pool drain 才释放

解决方案:在循环外用 strong 变量接住,循环里用 strong 变量。

现代 ARC(编译器优化后)在很多场景下已经不走 autorelease 了,但理解这个机制仍然重要。


九、Tagged Pointer 和 weak

Tagged Pointer 是苹果对小对象(短 NSString、小 NSNumber 等)的优化:把值直接编码在指针里,不是真正的堆对象。

对 Tagged Pointer 做 weak 引用时:

  • 因为它不是真正的对象,没有引用计数,不会被"释放"
  • Runtime 检测到是 Tagged Pointer 后,不会走 SideTable 的注册/清理流程
  • weak 指针直接存储 Tagged Pointer 的值,永远不会被置 nil

十、Swift 的 weak 和 Objective-C 的区别

维度 Objective-C weak Swift weak
类型 可以是任意 OC 对象 必须是 Optional 类型
类限制 只能用于 class 类型(AnyObject)
底层机制 SideTable Swift 有自己的实现,但原理类似
unowned 没有直接等价物 有 unowned(类似 unsafe_unretained,但 debug 模式有检查)

Swift 的 unowned vs weak

  • weak:可选类型,对象释放后变 nil,有 SideTable 开销
  • unowned:非可选类型,假设对象一定还活着。释放后访问会在 debug 模式 crash(比野指针更安全)。性能比 weak 好(不走 SideTable)

十一、常见面试问题

Q1:weak 是怎么实现自动置 nil 的?

Runtime 维护了一个全局的 SideTable 结构,其中的 weak_table 以对象地址为 Key,以所有指向该对象的 weak 指针地址数组为 Value。对象 dealloc 时,Runtime 从表中找到所有 weak 指针,逐个置 nil,然后删除表项。

Q2:weak 和 assign 有什么区别?

assign 只是简单的指针赋值,对象释放后指针变成野指针(指向已释放的内存)。weak 会在对象释放时自动置 nil,安全。assign 用于基本类型(int、CGFloat 等),weak 用于对象类型。

Q3:为什么 weak 比 strong 慢?

strong 只是引用计数的原子操作(+1/-1)。weak 需要额外的哈希查找、加锁、SideTable 操作。读取时还需要 retain 保证线程安全。但在绝大多数场景下差异可忽略。

Q4:一个对象可以有多少个 weak 引用?

理论上没有限制。weak_entry_t 内部先用 4 个内联槽位,超过后切换为动态哈希数组,可以按需增长。

Q5:weak 对象在什么线程被置 nil?

在触发 dealloc 的那个线程。谁释放了最后一个 strong 引用,就在谁的线程上走 dealloc 流程,进而清理 weak 表。


十二、一句话总结

weak 的本质就是 Runtime 维护了一张"对象 -> 弱指针列表"的全局哈希表(SideTable 中的 weak_table)。创建 weak 引用时注册,读取时 retain 保安全,对象释放时遍历置 nil 后删除条目。代价是哈希查找和加锁的开销,换来的是零野指针的安全性。

# iOS 电量优化详解

一、电量是怎么被消耗的?

手机电池的电量本质上就是电能。App 的各种操作最终都会驱动硬件工作,硬件工作就要消耗电能。

主要的耗电硬件:

┌────────────────────────────────────────────────────┐
│                    App 的各种操作                     │
├──────┬──────┬──────┬──────┬──────┬──────┬──────────┤
│ CPU  │ GPU  │ 网络  │ 定位  │ 屏幕 │ 传感器│ 蓝牙/NFC │
│      │      │模块   │模块   │背光  │      │          │
├──────┴──────┴──────┴──────┴──────┴──────┴──────────┤
│                    电池                              │
└────────────────────────────────────────────────────┘

关键认知:硬件有两种状态——空闲态和活跃态。

空闲态几乎不耗电,活跃态耗电量可能是空闲态的 10-100 倍。电量优化的核心就是:尽量让硬件处于空闲态,减少活跃态的持续时间。


二、iOS 的电量管理机制

2.1 合并唤醒(Coalescing)

iOS 不会让硬件被频繁地"唤醒-休眠-唤醒-休眠"。它会把多个 App 的小任务合并到同一个时间窗口集中处理。

不合并:
  App1 ─▮─────────▮─────────▮─────────   (每 10 秒唤醒一次)
  App2 ──────▮─────────▮─────────▮────   (每 10 秒唤醒一次)
  CPU   ─▮───▮──▮──▮───▮──▮──▮───▮──▮   (被唤醒了 9 次)

合并后:
  App1 ─▮─────────▮─────────▮─────────
  App2 ─▮─────────▮─────────▮─────────   (和 App1 对齐)
  CPU   ─▮─────────▮─────────▮─────────   (只被唤醒 3 次)

对开发者的启示: 不要自己用精确的 Timer 去定时做事,用系统提供的 API(如 BGTaskScheduler),让系统帮你合并。

2.2 能量计量(Energy Gauges)

iOS 在系统层面持续监控每个 App 的能量消耗。如果你的 App 耗电异常:

  • 设置 → 电池 里会显示高耗电
  • 系统可能会限制你的后台执行时间
  • App Store 审核可能因为耗电问题被拒
  • 用户看到你耗电高就卸载了

2.3 后台执行限制

iOS 对后台 App 的电量管控非常严格:

状态 允许做什么 时间限制
前台 任何事 无限制
后台(刚切走) 完成当前任务 约 30 秒(可申请延长到 ~3 分钟)
后台(挂起) 什么都不能做 0(被冻结)
后台模式(音乐/导航/VoIP等) 特定任务 持续但受监控

App 被挂起后,CPU 完全不分配给它,所以不耗电。 这是 iOS 比 Android 省电的核心原因之一。


三、八大耗电场景与优化

3.1 CPU —— 最大的耗电户

为什么耗电

CPU 频率越高、负载越重、持续时间越长,耗电越多。

常见问题

问题 场景
死循环 / 忙等待 while(flag) {} 没有 sleep
过度计算 主线程做复杂的 JSON 解析、图片处理
Timer 间隔太短 每 0.01 秒刷新一次,但界面根本看不出差别
后台还在跑 切后台了 Timer 还在走

优化策略

1. 避免忙等待,用事件驱动替代轮询

❌ 轮询:每 0.1 秒检查一次数据有没有准备好
   while (!dataReady) { usleep(100000); }

✅ 事件驱动:数据好了通知我
   NotificationCenter / KVO / Completion Handler / Combine

2. Timer 的电量陷阱

NSTimer / DispatchSourceTimer 默认是精确触发的,会阻止 CPU 进入深度休眠。

优化方式:给 Timer 加 tolerance(容差)

timer.tolerance = interval * 0.1  // 允许 10% 的偏差

加了 tolerance 后,系统可以把你的 Timer 和其他 Timer 合并触发,减少 CPU 唤醒次数。

苹果的建议:tolerance 至少设为间隔的 10%。

3. 用合适的 QoS(Quality of Service)

iOS 的任务队列有不同的优先级,低优先级的任务系统会安排在"电量友好"的时间执行:

QoS 级别 用途 CPU 调度
.userInteractive UI 更新、动画 最高优先级,立即执行
.userInitiated 用户触发的操作(点击后加载) 高优先级
.default 默认 中等
.utility 长时间任务(下载、导入) 低优先级,省电模式可能延迟
.background 用户不关心何时完成(预加载、备份) 最低,系统自行安排

原则:不需要立即响应的任务,用 .utility.background 系统会在电量充足或充电时才执行这些任务。

3.2 网络 —— 隐形的耗电大户

为什么网络特别耗电

蜂窝网络模块(4G/5G)有三种功耗状态:

空闲态(Idle)─── 几乎不耗电
    │  有数据要发送
    ▼
升频态(Ramp Up)─── 功耗急剧上升(从空闲到全速需要 1-2 秒)
    │
    ▼
全速态(Active)─── 高功耗传输数据
    │  数据传完
    ▼
拖尾态(Tail)─── 仍保持高功耗约 10-15 秒!等待可能的后续请求
    │  超时无新数据
    ▼
空闲态(Idle)

关键问题在"拖尾态": 传完数据后,蜂窝模块不会立刻休眠,而是保持活跃 10-15 秒等待新数据。如果你的 App 每 20 秒发一个小请求,蜂窝模块就永远无法进入空闲态。

❌ 零散请求(蜂窝模块永远醒着):
  请求──拖尾──请求──拖尾──请求──拖尾──请求──拖尾
  ████████████████████████████████████████████  全程高功耗

✅ 批量请求(只唤醒一次):
  ──────────批量请求──拖尾──────────────────────
  ░░░░░░░░░░█████████████░░░░░░░░░░░░░░░░░░░░  大部分时间低功耗

优化策略

1. 请求合并(Batching)

不要每个事件都立即发网络请求。把多个请求攒在一起,一次性发送。

例如:埋点数据不要实时上报,累积 20 条或间隔 30 秒批量上报。

2. 避免轮询,用推送替代

❌ 每 30 秒轮询一次服务器检查新消息
✅ 用 APNs 推送通知客户端有新消息

3. 适配网络类型

  • WiFi 比蜂窝省电得多(没有拖尾态问题)
  • 大文件下载、数据同步等操作尽量在 WiFi 环境下进行
  • NWPathMonitorReachability 判断当前网络类型

4. 减少数据传输量

  • 开启 HTTP 压缩(gzip / br)
  • 用 HTTP/2 的头部压缩
  • 图片用 WebP / HEIF 替代 PNG/JPEG
  • API 只返回需要的字段(GraphQL 的优势)
  • 合理使用缓存(URLCacheETagLast-Modified

5. 超时和重试策略

  • 设置合理的超时时间(不要太长等不来也不放手)
  • 重试用指数退避(1s → 2s → 4s → 8s),不要固定间隔疯狂重试
  • 失败后等 WiFi 或充电时再重试

3.3 定位 —— 精度越高越耗电

各精度的耗电对比

精度 API 耗电 适用场景
最佳精度 kCLLocationAccuracyBest 极高(GPS 全速运转) 导航
10 米 kCLLocationAccuracyNearestTenMeters 跑步记录
100 米 kCLLocationAccuracyHundredMeters 附近商家
公里级 kCLLocationAccuracyKilometer 天气、城市级服务
3公里级 kCLLocationAccuracyThreeKilometers 很低 粗略地理围栏
显著位置变化 startMonitoringSignificantLocationChanges 极低 只在基站切换时触发

GPS 芯片功耗约 25-35mW,WiFi 定位约 5-10mW,基站定位约 1-2mW。

优化策略

1. 用够了就关

开始定位 → 拿到位置 → 立即 stopUpdatingLocation

很多 App 犯的错误:开启定位后忘了关,GPS 一直在后台运转。

2. 用最低够用的精度

外卖 App 展示附近餐厅用 100 米精度足够了,不需要 Best。只有导航才需要最高精度。

3. 用"显著位置变化"替代持续定位

如果你只需要在用户换了个区域时更新内容(比如新闻 App 根据城市推荐),用 startMonitoringSignificantLocationChanges。它基于基站切换触发,几乎不额外耗电。

4. distanceFilter 过滤无意义的更新

locationManager.distanceFilter = 50  // 移动 50 米以上才回调

默认是 kCLDistanceFilterNone(每次都回调),设一个合理的值可以大幅减少回调次数。

5. allowsBackgroundLocationUpdates 谨慎使用

只有导航、运动记录等真正需要后台定位的场景才开启。开启后要搭配 pausesLocationUpdatesAutomatically = true,让系统在检测到用户静止时自动暂停。

3.4 GPU / 图形渲染

耗电的渲染操作

操作 为什么耗电
离屏渲染 需要额外的帧缓冲区,GPU 要来回切换上下文
大量透明度混合 每一层都要计算混合,层越多越慢
大图缩小显示 GPU 要对大图做缩放计算
实时模糊(UIBlurEffect) 每帧都要对底层内容做高斯模糊
高帧率动画 120Hz 的计算量是 60Hz 的两倍

优化策略

1. 避免不必要的离屏渲染

触发离屏渲染的操作:
  - cornerRadius + masksToBounds(圆角裁剪)
  - shadow(阴影,没有设 shadowPath 时)
  - mask(遮罩)
  - group opacity(组透明度)

优化方式:
  - 圆角:用贝塞尔曲线预先裁剪成圆角图片,或在绘图时直接画圆角
  - 阴影:设置 shadowPath,避免实时计算阴影形状
  - 模糊:对静态内容用截图+模糊的方式,而不是实时 UIVisualEffectView

2. 图片大小匹配显示大小

一张 3000x3000 的图显示在 100x100 的 ImageView 里,GPU 每帧都要缩放。应该在加载时就缩放到显示尺寸。

3. 降低不必要的帧率

不是所有动画都需要 60fps / 120fps。滚动和交互动画需要高帧率,但一个缓慢变化的进度条用 30fps 就够了。

CADisplayLink 可以设置 preferredFramesPerSecond

3.5 蓝牙(BLE)

两种扫描模式的耗电差异

模式 耗电 说明
主动扫描(Active Scan) 蓝牙模块持续发射扫描请求
被动监听 只监听广播包

优化策略

  • 扫描到目标设备后立即停止扫描
  • 设置 CBCentralManagerScanOptionAllowDuplicatesKey = NO,避免重复上报同一个设备
  • 后台扫描比前台限制更严格,系统会自动降低扫描频率
  • 不需要实时数据时,用 notify 替代 read(让外设主动通知,而不是 App 轮询读取)

3.6 后台任务

beginBackgroundTask 的正确用法

切后台时申请额外执行时间来完成当前任务:

关键要点:
① 一定要在超时回调里调用 endBackgroundTask,否则系统会杀掉你的 App
② 不要用它来"偷偷"执行长时间任务
③ 系统给的时间在 iOS 13+ 只有约 30 秒(以前是 3 分钟)

BGTaskScheduler(iOS 13+)

用于安排后台任务,系统会在合适的时间执行:

类型 用途 触发条件
BGAppRefreshTask 数据刷新(拉新闻、同步) 系统根据用户使用习惯决定
BGProcessingTask 重计算任务(数据库清理、ML训练) 通常在充电 + WiFi 时

系统会综合考虑电量、网络、充电状态、用户使用习惯来决定何时执行你的任务。 你只需要提交任务,不需要操心何时执行。

3.7 推送通知

静默推送的耗电陷阱

静默推送(content-available: 1)会唤醒 App 在后台执行代码。如果推送频率太高(比如每分钟一次),相当于 App 每分钟被唤醒一次,持续消耗 CPU 和网络。

苹果会限制频率: 如果系统检测到你的静默推送太频繁,会开始丢弃推送。

优化: 静默推送只用于"有重要数据需要预加载"的场景,不要当成轮询的替代品。

3.8 传感器

传感器 耗电 优化
加速度计 用 CMMotionManager 的合理更新频率,不用就 stop
陀螺仪 同上
磁力计(指南针) headingFilter 过滤微小变化
气压计 按需使用
摄像头 极高 分辨率调到够用即可,不用就释放
麦克风 用 VAD(语音活动检测)避免持续录音

四、电量监控与测量

4.1 开发阶段

Instruments - Energy Log

Xcode 的 Instruments 提供 Energy Log 模板,能看到:

  • CPU 活动(Overhead 级别:0-20)
  • 网络活动
  • 定位活动
  • GPU 活动
  • 前台/后台状态

每个指标用 0-20 的等级表示功耗水平。

Xcode Energy Gauges

Debug Navigator 里实时显示 Energy Impact(低/中/高/极高),直观但粗略。

Energy Impact 的颜色含义:

  • 绿色(低):正常
  • 黄色(中):有优化空间
  • 红色(高/极高):需要关注

sysdiagnose

在设备上触发 sysdiagnose(同时按 音量上 + 音量下 + 电源键),生成一份详细的系统诊断报告,包含详细的电量日志。

4.2 线上监控

MetricKit(iOS 13+)

苹果提供的官方线上性能监控框架,每 24 小时汇总一次数据:

Metric 说明
MXCPUMetric CPU 使用指令数
MXGPUMetric GPU 使用时间
MXNetworkTransferMetric 网络传输量(上/下行)
MXLocationActivityMetric 定位活动时间
MXCellularConditionMetric 蜂窝信号质量(信号差时更耗电)
MXAppRunTimeMetric 前台/后台运行时间

这些数据以直方图形式提供,包含 P50/P90/P99 分位值。 可以帮你了解真实用户的耗电情况。

Xcode Organizer - Energy Reports

Xcode → Window → Organizer → Energy,可以看到线上用户的能量报告。如果你的 App 被系统判定为"耗电异常",这里会有日志。

4.3 电量归因:到底是谁在耗电?

当发现 App 耗电高时,排查思路:

1. CPU 高?
   └── 用 Time Profiler 找到热点函数
       └── 是主线程还是子线程?
       └── 是否有不必要的循环/计算?
       └── 后台是否有 Timer 在跑?

2. 网络频繁?
   └── 用 Network instrument 看请求频率和数据量
       └── 是否有轮询?
       └── 请求是否可以合并?
       └── 是否在蜂窝网络下做了大量传输?

3. 定位一直开着?
   └── 检查 CLLocationManager 的 start/stop 配对
       └── 精度是否过高?
       └── 后台是否还在定位?

4. GPU 负载高?
   └── 用 Core Animation instrument 检查离屏渲染
       └── 是否有不必要的透明度混合?
       └── 帧率是否过高?

五、Low Power Mode(低电量模式)适配

用户开启低电量模式后,系统会:

  • 降低 CPU/GPU 频率
  • 减少后台活动
  • 降低屏幕亮度
  • 关闭 5G(降回 4G)
  • 停止自动下载和邮件获取

你的 App 应该监听并适配:

检测方式 说明
ProcessInfo.processInfo.isLowPowerModeEnabled 查询当前状态
NSProcessInfoPowerStateDidChangeNotification 监听状态变化

适配建议:

  • 低电量模式下降低动画帧率或关闭动画
  • 停止非关键的后台数据同步
  • 降低定位精度
  • 减少网络请求频率
  • 延迟非紧急的计算任务

六、优化原则总结

六字箴言:少做、晚做、批量做

原则 含义 例子
少做 能不做就不做 不需要的数据不请求,不在屏的 View 不渲染
晚做 能推迟就推迟 非关键 SDK 延迟初始化,后台任务等充电时做
批量做 能合并就合并 网络请求合并,埋点批量上报

优化优先级

按耗电影响从大到小:

1. 🔴 网络(尤其是蜂窝网络的拖尾效应)
2. 🔴 定位(GPS 持续开启)
3. 🟡 CPU(后台 Timer、忙等待、过度计算)
4. 🟡 GPU(离屏渲染、高帧率)
5. 🟢 蓝牙(持续扫描)
6. 🟢 传感器(持续采集)

一张检查清单

□ Timer 是否设置了 tolerance?
□ 切后台后是否停止了不必要的 Timer / 定位 / 蓝牙扫描?
□ 网络请求是否有合并?是否有缓存?
□ 定位精度是否是最低够用的?用完是否关闭了?
□ 是否有轮询可以用推送替代?
□ 后台任务是否用了 BGTaskScheduler 而不是自己计时?
□ 图片是否压缩到了合适的尺寸?
□ 是否适配了低电量模式?
□ 大型计算任务是否标记了合适的 QoS?
□ 是否用 MetricKit 监控了线上耗电数据?

HTTP 各版本演进与 HTTPS 原理详解

一、先搞清楚 HTTP 是什么

HTTP(HyperText Transfer Protocol)就是浏览器和服务器之间"对话"的规则。你在浏览器输入一个网址,浏览器按照 HTTP 规则发一个请求,服务器按照 HTTP 规则回一个响应。

浏览器:「我要 /index.html」          →  请求(Request)
服务器:「给你,200 OK,内容如下...」   ←  响应(Response

HTTP 从 1991 年诞生到现在,经历了 5 个大版本。每个版本都是为了解决上一个版本的痛点。


二、HTTP/0.9(1991)—— 最原始的版本

特点

  • 只支持 GET 方法
  • 没有请求头、没有响应头
  • 只能传 HTML 文本
  • 响应完就断开连接

一次对话长这样

请求:GET /hello.html
响应:<html>Hello World</html>
(连接断开)

就这么简单粗暴。没有状态码,没有 Content-Type,啥都没有。

存在的问题

  • 只能传 HTML,不能传图片、CSS、JS
  • 没有任何元数据(不知道内容多大、什么类型、什么编码)
  • 每次请求都要新建连接

三、HTTP/1.0(1996)—— 有模有样了

解决了什么

新增能力 说明
请求头 & 响应头 可以携带元数据了(Content-Type、Content-Length 等)
多种方法 GET、POST、HEAD
状态码 200、404、500 等,知道请求成功还是失败了
Content-Type 可以传图片、音频、视频,不再局限于 HTML
版本号 请求行里带上 HTTP/1.0

一次对话长这样

请求:
GET /logo.png HTTP/1.0
Host: www.example.com
Accept: image/png

响应:
HTTP/1.0 200 OK
Content-Type: image/png
Content-Length: 4096

(图片二进制数据)
(连接断开)

存在的问题

最大的问题:短连接。 每个请求都要经历 TCP 三次握手 → 传数据 → 四次挥手。

一个网页有 1 个 HTML + 10 个图片 + 3 个 CSS + 5 个 JS = 19 个请求,就要建立 19 次 TCP 连接。

连接1[三次握手] → GET /index.html[响应][四次挥手]
连接2[三次握手] → GET /style.css[响应][四次挥手]
连接3[三次握手] → GET /logo.png[响应][四次挥手]
...重复 19

每次握手和挥手都要消耗时间和系统资源,极其浪费

虽然有些实现支持非标准的 Connection: keep-alive,但这不是规范的一部分,行为不统一。


四、HTTP/1.1(1997)—— 用了二十多年的主力

解决了什么

1. 持久连接(Keep-Alive)—— 最重要的改进

默认开启 TCP 连接复用。一个连接可以发多个请求,不用每次都握手挥手。

HTTP/1.0:
  连接1 → 请求1 → 响应1 → 断开
  连接2 → 请求2 → 响应2 → 断开
  连接3 → 请求3 → 响应3 → 断开

HTTP/1.1:
  连接1 → 请求1 → 响应1 → 请求2 → 响应2 → 请求3 → 响应3 → 断开

2. 管线化(Pipelining)

理论上可以不等响应就发下一个请求:

客户端:请求1 → 请求2 → 请求3 →
                                  等待...
服务端:                    ← 响应1 ← 响应2 ← 响应3

但实际几乎没人用(原因见下面的问题)。

3. 分块传输(Chunked Transfer)

不需要预先知道内容的总大小,边生成边发送:

HTTP/1.1 200 OK
Transfer-Encoding: chunked

5\r\n
Hello\r\n
6\r\n
 World\r\n
0\r\n
\r\n

适用场景:服务端流式输出(比如 ChatGPT 的逐字输出)。

4. 其他新增

能力 说明
Host 头必选 一个 IP 可以托管多个域名(虚拟主机)
Cache-Control 更精细的缓存控制(替代 Expires)
Range 请求 支持断点续传(下载了一半断了,可以接着下)
100 Continue 先问服务器"我要发一个大文件,你准备好了吗?"
PUT / DELETE / OPTIONS / PATCH 更多的方法,RESTful API 的基础

存在的问题

问题一:队头阻塞(Head-of-Line Blocking)

这是 HTTP/1.1 最致命的问题。

虽然支持管线化,但响应必须按请求的顺序返回。如果第一个请求处理很慢,后面的请求即使已经处理完了,也必须排队等着。

请求顺序:请求1(慢查询)→ 请求2(静态图片)→ 请求3(CSS)

实际情况:
  请求1 ████████████████████░░░░░(处理中...3秒)
  请求2 ░░░░░░░░░░░░░░░░░░░████░  (早就好了,但必须等请求1)
  请求3 ░░░░░░░░░░░░░░░░░░░░░░██  (也在排队等)

就像高速公路只有一条车道,前面的大卡车开得慢,后面的跑车再快也超不过去。

问题二:并发限制的妥协

为了绕开队头阻塞,浏览器的做法是 对同一个域名开 6 个并行 TCP 连接

但这导致了新的问题:

  • 每个连接都要三次握手 + TLS 握手,开销不小
  • 服务器要维护大量连接
  • 催生了"域名分片"等 hack 手段(把资源分散到 cdn1.xxx.com、cdn2.xxx.com 来突破 6 个限制)

问题三:头部冗余

每个请求都要带上完整的头部(Cookie、User-Agent、Accept 等),这些头部在同一个连接上几乎不变,但每次都要重复发送。一个大 Cookie 可能有 1-2KB,100 个请求就白白多传 100-200KB。

问题四:只能客户端主动

服务器不能主动给客户端推数据,只能客户端请求、服务器响应。想实现"服务器推送"只能用长轮询或 WebSocket。


五、HTTP/2(2015)—— 质的飞跃

核心思想

在一条 TCP 连接上实现真正的并行传输。

解决了什么

1. 二进制分帧(Binary Framing)—— 基础革新

HTTP/1.x 是文本协议(人能直接看懂),HTTP/2 改成了二进制协议。

所有数据被拆分成更小的 帧(Frame),每个帧有一个 Stream ID,标记它属于哪个请求/响应。

HTTP/1.1(文本):
  GET /index.html HTTP/1.1\r\n
  Host: example.com\r\n
  \r\n

HTTP/2(二进制帧):
  ┌──────────┬───────────┬──────────────┐
  │ Length:9Type:HEADStream ID: 1HEADERS 帧
  ├──────────┴───────────┴──────────────┤
  │ :method = GET, :path = /index.html  │
  └─────────────────────────────────────┘

2. 多路复用(Multiplexing)—— 最重要的改进

多个请求/响应可以在同一个 TCP 连接上交错传输,互不阻塞。

HTTP/1.1:6 条连接,每条排队
  连接1: [请求1 ████████████]
  连接2: [请求2 ████]
  连接3: [请求3 ██████]
  连接4: [请求4 ███]
  连接5: [请求5 ████████]
  连接6: [请求6 ██]

HTTP/2:1 条连接,交错并行
  连接1: [帧1a][帧2a][帧3a][帧1b][帧2b][帧3b][帧1c][帧2c]...
          ↑stream1  ↑stream2  ↑stream3  ↑stream1  ↑stream2

类比理解:

HTTP/1.1 就像有 6 条单行道,每条道上车要排队。HTTP/2 就像有一条超宽的高速公路,所有车可以同时跑,通过车牌号(Stream ID)区分。

这彻底解决了 HTTP 层的队头阻塞,也不再需要"域名分片"等 hack 手段。

3. 头部压缩(HPACK)

HTTP/2 用 HPACK 算法压缩头部:

  • 维护一个静态表(61 个常见头部,如 :method: GETcontent-type: text/html),用索引号代替完整字符串
  • 维护一个动态表,记录当前连接用过的头部,后续只发索引号
  • 对值做 Huffman 编码 压缩
第一次请求:
  Cookie: session=abc123def456...   (完整发送,同时存入动态表,索引 62)

第二次请求:
  62                                 (只发一个索引号,省掉了几百字节)

效果:头部大小减少 85-95%

4. 服务器推送(Server Push)

服务器可以主动推送客户端可能需要的资源。

客户端请求 /index.html
服务器响应 /index.html
服务器主动推送 /style.css   ← 不用等客户端解析 HTML 后再请求
服务器主动推送 /app.js      ← 提前到达,减少等待

但实际效果争议较大——很难准确预测客户端需要什么,推错了反而浪费带宽。Chrome 已在 2022 年移除了对 Server Push 的支持。

5. 流优先级(Stream Priority)

可以给不同的请求设置优先级。比如 CSS 优先级高于图片,因为 CSS 会阻塞渲染。

存在的问题

TCP 层的队头阻塞 —— HTTP/2 的阿喀琉斯之踵

HTTP/2 解决了 HTTP 层的队头阻塞,但底下的 TCP 层还有队头阻塞

TCP 保证数据按序交付。如果一个 TCP 包丢失了,即使后面的包已经到达,TCP 也不会把它们交给应用层,而是等丢失的包重传回来。

TCP 传输:包1 → 包2(丢了!) → 包3 → 包4 → 包5

TCP 层:包1 ✓   包2 ?等重传...  包3-5 已到但不能用
         └── 所有 Stream 都被阻塞!

HTTP/1.1 开 6 个连接,一个连接丢包只影响那一个连接上的请求。HTTP/2 所有请求共用一个连接,一个包丢失会阻塞所有请求

在网络质量差(丢包率 > 2%)的环境下,HTTP/2 的表现可能反而不如 HTTP/1.1

TLS 握手开销

HTTP/2 虽然协议本身不强制加密,但所有浏览器都要求 HTTP/2 必须走 HTTPS。TLS 握手需要额外的 1-2 个 RTT。


六、HTTP/3(2022)—— 换掉了 TCP

核心思想

既然 TCP 的队头阻塞无法在 TCP 层面解决,那就 不用 TCP 了,改用 QUIC(基于 UDP)。

QUIC 是什么

QUIC(Quick UDP Internet Connections)是 Google 设计的传输层协议,跑在 UDP 上。你可以把它理解为"重新实现了一个更好的 TCP"。

HTTP/2 的协议栈:          HTTP/3 的协议栈:
┌──────────┐              ┌──────────┐
│  HTTP/2  │              │  HTTP/3  │
├──────────┤              ├──────────┤
│   TLS    │              │   QUIC   │ ← 把 TLS 融合进来了
├──────────┤              ├──────────┤
│   TCP    │              │   UDP    │
├──────────┤              ├──────────┤
│    IP    │              │    IP    │
└──────────┘              └──────────┘

解决了什么

1. 彻底消灭队头阻塞

QUIC 在传输层就支持多路复用。每个 Stream 独立管理自己的数据包顺序,一个 Stream 丢包不影响其他 Stream

HTTP/2 + TCP:
  Stream 1 的包丢了 → 所有 Stream 被阻塞等重传

HTTP/3 + QUIC:
  Stream 1 的包丢了 → 只有 Stream 1 等重传,Stream 2/3/4 正常收发

2. 0-RTT 连接建立

TCP + TLS 需要的握手:

TCP 三次握手:   1 RTT(客户端→服务器→客户端)
TLS 1.2 握手:  2 RTT
TLS 1.3 握手:  1 RTT
──────────────────
总计:TCP + TLS 1.3 = 2 RTT(数据才能开始传)

QUIC 的握手:

首次连接:1 RTT(QUIC 把传输层握手和加密握手合并了)
重连(0-RTT):0 RTT(第一个包就可以带数据!)

类比理解:

TCP + TLS 就像打电话:先拨号等接通(TCP),再输密码验证身份(TLS),然后才能说话。QUIC 就像发微信:直接把消息和身份验证一起发出去,对方收到就能回。重连时更像对方还认识你,直接开聊。

3. 连接迁移

TCP 连接用"源IP + 源端口 + 目标IP + 目标端口"四元组标识。你手机从 WiFi 切到 4G,IP 变了,TCP 连接就断了,必须重新握手。

QUIC 用一个 Connection ID 标识连接。IP 变了没关系,只要 Connection ID 不变,连接就能无缝切换。

TCP:WiFi → 4G = 连接断开 → 重新三次握手 + TLS 握手 → 恢复
QUIC:WiFi → 4G = IP 变了 → Connection ID 没变 → 无缝继续

对移动端体验提升巨大(电梯、地铁、从室内走到室外)。

4. 改进的头部压缩(QPACK)

HPACK 依赖严格的包顺序(因为动态表需要同步更新),和 QUIC 的乱序特性冲突。QPACK 解决了这个问题,允许在乱序到达的情况下也能正确解压头部。

存在的问题

问题 说明
UDP 被运营商/防火墙限制 部分网络环境会限速或丢弃 UDP 包,需要回退到 TCP
中间设备不友好 很多老旧的路由器、防火墙对 UDP 支持不好
CPU 开销 QUIC 在用户态实现(不在内核里),加解密和拥塞控制消耗更多 CPU
生态还在成熟 服务端支持(Nginx 2022 才正式支持)、调试工具都还在完善
无法利用 TCP 的内核优化 TCP 经过几十年优化,内核的 TCP 栈非常高效;QUIC 在用户态,暂时没法比

七、各版本一张图对比

        HTTP/0.9    HTTP/1.0    HTTP/1.1      HTTP/2        HTTP/3
年份      1991        1996        1997         2015          2022
传输层    TCP         TCP         TCP          TCP           UDP(QUIC)
连接方式  短连接      短连接      持久连接      多路复用       多路复用
并发能力  无          无          管线化(废了)   Stream并行     Stream并行
头部格式  无          文本        文本          HPACK压缩      QPACK压缩
加密      无          可选        可选          事实强制       强制(内置TLS)
队头阻塞  -           有          有(HTTP层)    有(TCP层)      无!
握手RTT   1(TCP)      1(TCP)      1(TCP)       2-3(TCP+TLS)  0-1(QUIC)
服务器推  无          无          无            支持(已废弃)    无

八、HTTPS 详解

8.1 为什么需要 HTTPS?

HTTP 是明文传输,存在三大安全风险:

风险 场景 后果
窃听 连公共 WiFi 时,路由器能看到所有内容 密码、银行卡号泄露
篡改 运营商在网页中插入广告 页面被注入恶意代码
冒充 钓鱼网站伪装成银行 用户被骗输入密码

HTTPS 就是 HTTP + TLS,用加密解决这三个问题:

安全需求 解决方案
防窃听 对称加密(AES),加密传输内容
防篡改 消息认证码(MAC),验证数据完整性
防冒充 数字证书 + 非对称加密(RSA/ECDSA),验证服务器身份

8.2 对称加密 vs 非对称加密

理解 HTTPS 之前,必须搞清楚这两种加密方式。

对称加密

加密和解密用同一把钥匙

明文 "Hello" + 密钥 K → 加密 → 密文 "x7$f2"
密文 "x7$f2" + 密钥 K → 解密 → 明文 "Hello"
  • 优点:速度快(AES 可达 GB/s 级别)
  • 缺点:密钥怎么安全地传给对方?如果密钥被截获,加密就白搭

常见算法:AES、ChaCha20

非对称加密

有两把钥匙:公钥(公开)和 私钥(保密)。公钥加密的只有私钥能解,私钥加密的只有公钥能解。

公钥加密:明文 + 公钥 → 密文(只有私钥能解)
私钥签名:数据 + 私钥 → 签名(公钥可验证)
  • 优点:不需要传递私钥,天然解决密钥分发问题
  • 缺点:速度极慢(比对称加密慢 100-1000 倍)

常见算法:RSA、ECDSA、Ed25519

HTTPS 的选择:混合加密

既然对称加密快但密钥分发难,非对称加密安全但太慢,那就结合使用

① 用非对称加密安全地交换一个"临时密钥"(只需要一次,慢就慢吧)
② 之后所有数据都用这个临时密钥做对称加密(快!)

8.3 TLS 握手流程(TLS 1.2)

这是 HTTPS 最核心的部分——建立安全连接的过程。

客户端                                              服务器
  │                                                    │
  │ ① ClientHello                                      │
  │    - 支持的 TLS 版本                                 │
  │    - 支持的加密套件列表                               │
  │    - 客户端随机数(Client Random)                    │
  │ ─────────────────────────────────────────────────►  │
  │                                                    │
  │                              ② ServerHello         │
  │    - 选定的 TLS 版本                                 │
  │    - 选定的加密套件                                   │
  │    - 服务器随机数(Server Random)                    │
  │                              ③ 服务器证书            │
  │                              ④ ServerHelloDone     │
  │ ◄─────────────────────────────────────────────────  │
  │                                                    │
  │ ⑤ 验证证书(证书链 → 根证书)                          │
  │ ⑥ 生成预主密钥(Pre-Master Secret)                  │
  │ ⑦ 用服务器公钥加密预主密钥                             │
  │ ⑧ ClientKeyExchange(发送加密后的预主密钥)            │
  │ ⑨ ChangeCipherSpec("我准备好了,之后都加密")         │
  │ ⑩ Finished(第一条加密消息)                          │
  │ ─────────────────────────────────────────────────►  │
  │                                                    │
  │                    ⑪ 用私钥解密得到预主密钥             │
  │                    ⑫ 双方各自计算会话密钥               │
  │                       = f(Client Random +           │
  │                          Server Random +            │
  │                          Pre-Master Secret)         │
  │                    ⑬ ChangeCipherSpec                │
  │                    ⑭ Finished                       │
  │ ◄─────────────────────────────────────────────────  │
  │                                                    │
  │ ═══════ 之后所有数据用会话密钥做对称加密 ═══════════    │

需要 2 个 RTT 才能开始传数据。

为什么要用三个随机数?

最终的会话密钥由三个随机数共同生成:

会话密钥 = PRF(Pre-Master Secret, Client Random, Server Random)
  • Client Random:防止服务器重放攻击
  • Server Random:防止客户端重放攻击
  • Pre-Master Secret:只有双方知道的秘密(通过非对称加密安全交换)

三个随机数混合,即使其中一个被猜到,也无法推导出会话密钥。

8.4 TLS 1.3(2018)—— 更快更安全

TLS 1.3 是对 TLS 1.2 的大幅简化和优化。

主要改进

改进 TLS 1.2 TLS 1.3
握手 RTT 2 RTT 1 RTT(首次),0 RTT(重连)
密钥交换 RSA 或 ECDHE 只支持 ECDHE(前向安全)
加密套件 几十种 精简到 5 种
废弃的算法 - 砍掉 RC4、DES、3DES、MD5、SHA-1 等不安全算法
握手加密 握手过程明文 握手消息也加密(ServerHello 之后)

TLS 1.3 握手流程(只需 1 RTT)

客户端                                              服务器
  │                                                    │
  │ ClientHello                                        │
  │    - 客户端随机数                                    │
  │    - 支持的加密套件                                   │
  │    - 客户端的 ECDHE 公钥 ← 关键!直接带上了            │
  │ ─────────────────────────────────────────────────►  │
  │                                                    │
  │                              ServerHello           │
  │                              + 服务器的 ECDHE 公钥   │
  │                              + 证书                 │
  │                              + Finished            │
  │ ◄─────────────────────────────────────────────────  │
  │                                                    │
  │ 双方用 ECDHE 算出相同的密钥                           │
  │ Finished                                           │
  │ ─────────────────────────────────────────────────►  │
  │                                                    │
  │ ═══════ 1 RTT 后就可以传数据了 ═══════════           │

为什么快了? 因为 TLS 1.2 客户端要等拿到服务器证书和公钥后才能开始密钥交换,而 TLS 1.3 客户端直接在 ClientHello 里就把自己的 ECDHE 公钥发出去了(赌服务器会接受),省了一个来回。

0-RTT 重连

如果之前连过这个服务器,客户端缓存了一个 PSK(Pre-Shared Key):

客户端:ClientHello + PSK + 加密的应用数据 →
                                           服务器直接解密处理

第一个包就能带业务数据,延迟降到极致。

安全代价: 0-RTT 数据没有前向安全性,而且可能被重放攻击。所以只适合幂等请求(GET),不适合会产生副作用的操作(POST 转账)。

8.5 数字证书:怎么证明"你是你"

问题

非对称加密解决了"传密钥"的问题,但引入了新问题:你怎么知道拿到的公钥是真的?

中间人攻击:

客户端 ←→ 中间人(伪装成服务器) ←→ 真正的服务器

中间人把自己的公钥发给客户端
客户端以为这是服务器的公钥,用它加密数据
中间人解密 → 看到明文 → 用服务器真正的公钥重新加密 → 发给服务器

解决:数字证书 + CA

引入一个双方都信任的第三方 —— CA(Certificate Authority,证书颁发机构)

① 服务器生成公私钥对
② 服务器把公钥 + 域名信息提交给 CA
③ CA 验证服务器确实拥有这个域名
④ CA 用自己的私钥对"服务器公钥 + 域名信息"做数字签名
⑤ CA 颁发证书(包含:服务器公钥 + 域名 + CA 签名 + 有效期等)
⑥ 客户端收到证书后,用 CA 的公钥验证签名
⑦ 签名正确 → 公钥可信 → 建立加密连接

类比理解:

就像你去政务大厅办事,需要身份证(证书)。身份证是公安局(CA)发的,上面有你的照片(公钥)和钢印(CA 签名)。办事员通过钢印确认身份证是真的,从而相信你就是本人。

证书链

CA 的公钥又是谁来保证的?答案是证书链

根证书(Root CA)         预装在操作系统/浏览器中,无条件信任
  
  └── 中间证书(Intermediate CA)  CA 签发的
        
        └── 服务器证书(End Entity)    中间 CA 签发的

验证过程从下往上:

  1. 用中间 CA 的公钥验证服务器证书的签名
  2. 用根 CA 的公钥验证中间 CA 证书的签名
  3. 根 CA 证书在本地受信任列表中 → 整条链可信

为什么要分层?如果根 CA 直接签发所有证书,一旦根 CA 私钥泄露,后果不堪设想。分层后,即使中间 CA 出问题,只需要吊销这个中间 CA,不影响其他。

8.6 前向安全(Forward Secrecy)

问题

如果用 RSA 做密钥交换(TLS 1.2 的一种模式),一旦服务器私钥泄露,攻击者可以:

  1. 解密之前录制的所有流量中的 Pre-Master Secret
  2. 从而推导出所有历史会话密钥
  3. 所有历史通信全部暴露

解决:ECDHE 密钥交换

每次连接都生成临时的(Ephemeral) 公私钥对,用完即销毁。

连接1:临时密钥对 A → 会话密钥 X → 销毁临时密钥对 A
连接2:临时密钥对 B → 会话密钥 Y → 销毁临时密钥对 B
连接3:临时密钥对 C → 会话密钥 Z → 销毁临时密钥对 C

即使服务器的长期私钥泄露,也无法解密之前的通信,因为临时密钥已经不存在了。

这就是"前向安全"(Forward Secrecy)。TLS 1.3 强制要求使用 ECDHE,所有连接都具有前向安全性。

8.7 HTTPS 的性能影响

开销 说明 缓解方案
握手延迟 TLS 1.2 多 2 RTT,TLS 1.3 多 1 RTT 升级 TLS 1.3、会话复用、0-RTT
加解密 CPU AES 加解密消耗 CPU 现代 CPU 都有 AES-NI 硬件加速,开销几乎可忽略
证书传输 证书链可能 3-5KB OCSP Stapling 减少验证开销
内存 每个连接需要维护加密上下文 影响很小

现代环境下 HTTPS 的额外开销已经非常小了,Google 的数据显示 HTTPS 只增加了不到 2% 的 CPU 负载和不到 10ms 的延迟。

8.8 常见 HTTPS 相关概念

概念 解释
HSTS 服务器告诉浏览器"以后只用 HTTPS 访问我",防止降级攻击
OCSP Stapling 服务器主动把证书有效性证明发给客户端,省去客户端向 CA 查询
CT(Certificate Transparency) 所有颁发的证书必须公开记录,防止 CA 偷偷签发恶意证书
SNI 客户端在 TLS 握手时告诉服务器要访问哪个域名(一个 IP 上多个 HTTPS 站点)
ESNI / ECH 加密 SNI,防止中间人知道你在访问哪个网站
证书固定(Pinning) App 内置预期的证书指纹,防止中间人使用合法但非预期的证书
Let's Encrypt 免费、自动化的 CA,推动了 HTTPS 的全面普及
双向认证(mTLS) 不仅服务器要证书,客户端也要证书(常见于企业内部、金融系统)

九、一张图总结

1991  HTTP/0.9 ─── 只能传 HTML,连个状态码都没有
        
1996  HTTP/1.0 ─── 有了头部、状态码、多媒体,但每次请求都要新建连接
        
1997  HTTP/1.1 ─── 持久连接、缓存控制、断点续传,但有队头阻塞
                     └── HTTPS(TLS 1.0~1.2) 解决安全问题
        
2015  HTTP/2 ──── 二进制分帧、多路复用、头部压缩,但 TCP 层仍有队头阻塞
                     └── TLS 1.3:1-RTT 握手、前向安全
        
2022  HTTP/3 ──── 换用 QUIC(UDP),彻底消灭队头阻塞,0-RTT 连接,连接迁移

每一代都在解决上一代留下的问题,同时也带来了新的挑战。技术演进就是这样一步步往前走的。

iOS 应用启动流程与优化详解

一、什么算"启动"?

从用户点击 App 图标,到第一个页面完整渲染出来,这段时间就是启动时间。

苹果把启动分为两个阶段:

用户点击图标
    │
    ▼
┌──────────────────────────────┐
│         Pre-main 阶段         │  ← 系统在干活,你的代码还没执行
│  (dyld 加载 → Runtime 初始化)  │
└──────────────┬───────────────┘
               │
               ▼  main() 函数被调用
┌──────────────────────────────┐
│         Post-main 阶段        │  ← 你的代码开始执行
│  (AppDelegate → 首页渲染完成)  │
└──────────────────────────────┘
               │
               ▼
          用户看到首页

苹果的标准:冷启动应在 400ms 以内完成,超过 20 秒系统会杀掉 App(Watchdog 机制)。

冷启动 vs 温启动 vs 热启动

类型 条件 耗时
冷启动 App 不在内存中,从零开始加载 最长
温启动 App 刚被杀掉,部分数据还在系统缓存中 中等
热启动 App 在后台,从挂起状态恢复 最短(几乎瞬间)

启动优化主要针对冷启动,因为它是最慢的。


二、Pre-main 阶段:系统在干什么?

从点击图标到 main() 函数执行,中间经历了以下步骤:

① 内核 fork 进程,加载可执行文件(Mach-O)
                    │
                    ▼
② dyld 接管,开始加载动态库
                    │
                    ▼
③ Rebase & Bind:修复指针、绑定外部符号
                    │
                    ▼
④ Objc Runtime 初始化:注册类、处理 Category
                    │
                    ▼
⑤ 执行 +load 方法和 C++ 静态构造函数
                    │
                    ▼
⑥ 调用 main()

2.1 加载可执行文件(Mach-O)

iOS 的可执行文件格式叫 Mach-O(Mach Object)。内核先 fork 出一个新进程,把 Mach-O 文件映射到内存中。

Mach-O 的结构:

┌─────────────────┐
│    Header        │ ← 架构信息(arm64)、文件类型
├─────────────────┤
│  Load Commands   │ ← 告诉 dyld 需要加载哪些动态库、各段放在哪
├─────────────────┤
│   __TEXT 段       │ ← 代码(只读)
├─────────────────┤
│   __DATA 段       │ ← 全局变量、指针(可读写)
├─────────────────┤
│  __LINKEDIT 段    │ ← 符号表、签名信息
└─────────────────┘

2.2 dyld 加载动态库

dyld(dynamic link editor) 是苹果的动态链接器,负责把 App 依赖的所有动态库(.dylib / .framework)加载到内存中。

一个普通 App 依赖的动态库数量:

  • 系统库(UIKit、Foundation、CoreGraphics 等):100~400 个
  • 第三方库(如果用了动态 framework):几个到几十个

每个动态库的加载过程:

  1. 从磁盘找到 .dylib 文件
  2. 验证代码签名(安全检查)
  3. 映射到内存
  4. 如果这个库还依赖其他库,递归加载(这就是为什么依赖关系复杂时会很慢)

2.3 Rebase & Bind

由于 ASLR(地址空间布局随机化)的存在,每次启动时 Mach-O 被加载到内存的地址都不同。但代码里的指针是编译时确定的固定地址,所以需要修正。

Rebase(内部指针修正):

  • 把 Mach-O 内部指向自己的指针,加上一个随机偏移量(slide)
  • 比如:代码里写的是 0x1000,ASLR slide 是 0x5000,修正后变成 0x6000

Bind(外部符号绑定):

  • 把 Mach-O 引用的外部符号(比如 UIKit 里的 UIViewController)绑定到实际的内存地址
  • 需要在符号表中查找,比 Rebase 更慢

类比理解:

Rebase 就像搬家后更新通讯录里自己家人的地址(内部),Bind 就像更新朋友的地址(外部,需要打电话问)。

2.4 Objc Runtime 初始化

  • 注册所有 Objective-C 类到全局类表
  • 处理 Category(把 Category 中的方法附加到对应的类上)
  • 确保 selector 唯一性

类越多,这一步越慢。 如果你的项目有上万个类,这里的耗时就很可观。

2.5 Initializers(+load 和静态构造函数)

这是 Pre-main 阶段最后一步,也是开发者唯一能直接控制的部分

  • 执行所有类的 +load 方法(按编译顺序,先父类后子类,先主类后 Category)
  • 执行 C++ 的全局/静态对象的构造函数
  • 执行标记了 __attribute__((constructor)) 的 C 函数

这些代码在 main() 之前就执行了,而且是在主线程上同步执行。如果你在 +load 里做了耗时操作(比如 Swizzle 大量方法、读文件、网络请求),启动就会被拖慢。


三、Post-main 阶段:你的代码在干什么?

main()
  │
  ▼
UIApplicationMain()
  │
  ▼
application:didFinishLaunchingWithOptions:
  │  ← 大量初始化代码通常堆在这里
  │     SDK 初始化、数据库初始化、推送注册、路由注册...
  ▼
创建 UIWindow、设置 rootViewController
  │
  ▼
首页 viewDidLoad → viewWillAppear → viewDidAppear
  │
  ▼
首帧渲染完成 → 用户看到界面

这个阶段的耗时主要来自 didFinishLaunchingWithOptions 中的各种初始化。


四、dyld 版本演进与优化

这是重点中的重点。苹果在 dyld 上做了三次大的版本迭代,每次都大幅优化了启动速度。

4.1 dyld 1.0(远古时代,macOS 早期)

最初的版本,设计简单粗暴:

  • 全量加载:启动时把所有动态库一次性全部加载到内存
  • 无缓存:每次启动都重新解析、绑定
  • 无优化:Rebase/Bind 逐个处理,没有批量优化

问题:随着系统库越来越多,启动速度越来越慢。

4.2 dyld 2.0(iOS 3.1 ~ iOS 12)

这是大家最熟悉的版本,做了很多重要优化:

核心改进

优化项 做了什么 效果
共享缓存(dyld shared cache) 把几百个系统库预先合并成一个大的缓存文件 系统库加载速度大幅提升
懒绑定(Lazy Binding) 外部符号不在启动时全部绑定,而是在第一次调用时才绑定 减少启动时的 Bind 耗时
符号缓存 缓存已解析的符号地址 避免重复查找

共享缓存(dyld shared cache)详解

这是 dyld 2 最重要的优化。

问题: 一个 App 可能依赖 300+ 个系统动态库。如果每次启动都逐个加载、解析,太慢了。

解决: 苹果在系统更新(或首次启动)时,预先把所有系统库打包合并成一个大文件,叫 dyld shared cache。存放在 /System/Library/dyld/

打包前:                          打包后:
UIKit.framework    ─┐
Foundation.framework ├──→  dyld_shared_cache_arm64
CoreGraphics.framework│     (一个约 1-2GB 的文件)
libsystem.dylib     ─┘

所有系统库的 Rebase/Bind 已经预先完成
所有系统库共用一个地址空间

好处:

  • 系统库启动时只需映射这一个文件,不需要逐个解析
  • Rebase/Bind 已经预先做完,启动时不需要再做
  • 所有 App 共享同一份缓存,节省内存

懒绑定(Lazy Binding)

dyld 2 引入了 PLT(Procedure Linkage Table) 机制:

启动时:
  外部函数调用 → PLT 桩函数 → dyld_stub_binder(绑定真实地址并修改 PLT 条目)

首次调用后:
  外部函数调用 → PLT 桩函数 → 直接跳到真实地址(已经绑定好了)

意思是:你的 App 引用了 UIKit 的 100 个函数,启动时不会全部绑定。而是在你第一次调用某个函数时,才去解析它的真实地址。这样启动时的 Bind 工作就分散到了运行时。

dyld 2 的残留问题

尽管有了很多优化,dyld 2 仍然是串行、逐步执行的:

解析 Mach-O → 查找依赖库 → 逐个加载 → Rebase → Bind → 初始化
              └── 每一步都在主线程上同步执行 ──┘

而且:

  • 第三方动态库没法享受 shared cache
  • 每次启动还是要做一遍 Rebase/Bind(对 App 自身的 Mach-O)
  • 安全校验(代码签名)也是启动时做的

4.3 dyld 3.0(iOS 13+,重大重构)

dyld 3 是一次架构级别的重写,核心思想是:把能预先做的工作提前到"启动之外"去做。

三层架构

┌─────────────────────────────────────────────┐
│            ① 进程外的 Mach-O 解析器            │
│    (App 安装/更新时运行,不在启动路径上)         │
│                                             │
│    - 解析 Mach-O header 和依赖关系             │
│    - 查找所有依赖库的位置                       │
│    - 执行安全校验(代码签名)                    │
│    - 把结果写入 启动闭包(Launch Closure)        │
└────────────────────┬────────────────────────┘
                     │ 预先计算好的结果
                     ▼
┌─────────────────────────────────────────────┐
│             ② 启动闭包缓存                     │
│                                             │
│    一个预先序列化好的数据结构,包含:              │
│    - 所有 dylib 的加载地址                      │
│    - 所有需要的 Rebase/Bind 信息                │
│    - 初始化顺序                                │
│    - 已验证的代码签名结果                        │
└────────────────────┬────────────────────────┘
                     │ 直接读取缓存
                     ▼
┌─────────────────────────────────────────────┐
│             ③ 进程内的引擎                     │
│     (真正在 App 启动时运行的部分)               │
│                                             │
│    - 读取启动闭包(一次 mmap)                   │
│    - 按预先计算好的结果直接加载                   │
│    - 极少的运行时计算                           │
└─────────────────────────────────────────────┘

启动闭包(Launch Closure)

这是 dyld 3 最核心的概念。

类比: dyld 2 就像每次做菜都要翻菜谱、找食材、洗切配。dyld 3 相当于提前把所有食材洗好切好配好放在盒子里(启动闭包),做菜时直接下锅就行。

闭包在什么时候创建?

  • App 安装时
  • App 更新时
  • 系统更新时(shared cache 变了)

闭包里存了什么?

  • 完整的依赖关系图
  • 每个 dylib 的磁盘路径和内存加载地址
  • Rebase/Bind 所需的全部信息
  • 代码签名验证结果(通过/失败)
  • 初始化器的执行顺序

dyld 3 vs dyld 2 对比

维度 dyld 2 dyld 3
Mach-O 解析 每次启动都做 安装时做好,缓存到闭包
依赖库查找 每次启动都在文件系统搜索 闭包里已记录完整路径
代码签名校验 每次启动都验证 安装时验证,结果缓存
Rebase/Bind 计算 每次启动都计算 闭包里已预计算
安全性 解析器在进程内,有被攻击风险 解析器在进程外,更安全
启动速度 快 40%+

4.4 dyld 4.0(iOS 16+ / WWDC 2022)

dyld 4 没有大的架构变化,主要是在 dyld 3 基础上做了进一步优化:

主要改进

优化项 说明
统一两种模式 dyld 3 有"有闭包"和"无闭包"两种路径(模拟器上不用闭包),dyld 4 统一成一种
Just-In-Time 加载 更激进的懒加载,某些 dylib 推迟到真正使用时才加载
页面级别的按需加载 不再把整个 dylib 映射进来,而是按页(Page)按需加载
更好的 Swift 支持 优化了 Swift metadata 的初始化
Compact Info 用更紧凑的格式存储链接信息,减少 __LINKEDIT 段的大小
Pre-warming 系统会在后台预热高频 App 的启动闭包

4.5 dyld 各版本一张图总结

dyld 1   →   dyld 2      →   dyld 3      →   dyld 4
(原始)       (iOS 3.1)        (iOS 13)        (iOS 16)
  │             │                │                │
  │        共享缓存            启动闭包           统一架构
  │        懒绑定             进程外解析          按页懒加载
  │        符号缓存            签名缓存           Swift 优化
  │             │                │                │
  ▼             ▼                ▼                ▼
全量加载      减少重复工作      大量工作移到      极致的懒加载
每次解析      分散绑定时机      安装/更新时       页级按需加载

五、启动优化实战指南

5.1 Pre-main 阶段优化

减少动态库数量

每多一个动态库,就多一次查找、加载、签名校验的过程。

做法 效果
合并自己的动态 framework 直接减少加载次数
能用静态库就不用动态库 静态库在编译时已合并进主二进制,启动时无额外加载
控制 Pods 的 use_frameworks! 改用 use_frameworks! :linkage => :static
苹果建议:第三方动态库不超过 6 个 超过就考虑合并

减少 Rebase/Bind

  • 减少 Objective-C 类的数量(合并功能相近的类)
  • 减少 Category 的数量
  • 减少 C++ 虚函数
  • 用 Swift Struct 代替 OC 对象(Struct 不需要 Rebase)

干掉 +load

+load 是启动优化的头号敌人。

原方案 优化方案
+load 中做 Method Swizzling 移到 +initialize(首次使用时才触发)
+load 中注册路由 改用编译期方案(__attribute__((section)) 写入 Mach-O 段)
+load 中初始化 SDK 移到 didFinishLaunching 或更晚

+initialize vs +load 的关键区别:

  • +load:App 启动时全部执行,即使这个类从未被使用
  • +initialize:某个类第一次收到消息时才执行,懒加载

二进制重排(Page Fault 优化)

这是近年最热门的 Pre-main 优化手段。

问题: App 启动时需要执行很多函数,但这些函数分散在不同的内存页上。每访问一个新页面就会触发一次 Page Fault(缺页中断),内核需要从磁盘加载这一页到物理内存。每次 Page Fault 大约耗时 0.1~1ms。

启动时可能触发几百到上千次 Page Fault,累计就是几百毫秒。

解决: 把启动时需要执行的函数重新排列,让它们尽量排在相邻的内存页上,减少 Page Fault 次数。

优化前:
┌──────┬──────┬──────┬──────┬──────┐
│ Page1│ Page2│ Page3│ Page4│ Page5│
│ A    │ X    │ B    │ Y    │ C    │   启动需要 AB→C,触发 3 次 Page Fault
│      │      │      │      │      │
└──────┴──────┴──────┴──────┴──────┘

优化后:
┌──────┬──────┬──────┬──────┬──────┐
│ Page1│ Page2│ Page3│ Page4│ Page5│
│ A    │ X    │ Y    │      │      │   启动需要 AB→C,只触发 1 次 Page Fault
│ B    │      │      │      │      │
│ C    │      │      │      │      │
└──────┴──────┴──────┴──────┴──────┘

怎么做?

  1. 用 Clang 的 -fsanitize-coverage 插桩,收集启动时调用的所有函数的顺序
  2. 生成一个 order 文件,列出这些函数的符号名
  3. 在 Xcode 的 Build Settings 中设置 Order File 路径
  4. 链接器会按照这个顺序重新排列函数在二进制中的位置

5.2 Post-main 阶段优化

分级初始化

不要把所有 SDK 初始化都堆在 didFinishLaunchingWithOptions 里。

┌─────────────────────────────────────────────────────┐
│                   分级初始化策略                       │
├─────────────┬──────────────────┬────────────────────┤
│   必须立即做   │    首页出现后做    │    用到时才做       │
│              │                  │                    │
│  崩溃统计     │  推送注册         │  分享 SDK          │
│  日志系统     │  数据统计 SDK     │  地图 SDK          │
│  网络库初始化  │  ABTest          │  支付 SDK          │
│  数据库核心表  │  开屏广告         │  蓝牙/定位         │
│              │                  │  AI 相关 SDK       │
└─────────────┴──────────────────┴────────────────────┘

首页渲染优化

优化手段 说明
首页用纯代码布局 避免 xib/storyboard 解析的开销
首页数据缓存 先展示上次的缓存数据,再异步请求新数据
预加载 viewDidLoad 发起网络请求,不要等 viewDidAppear
骨架屏 先展示骨架屏,给用户"已经在加载"的感觉
减少首页层级 AutoLayout 约束越少越好,层级越浅越好

子线程分担

把不依赖 UI 的初始化工作放到子线程:

主线程:UI 配置 → rootVC 创建 → 首页渲染
子线程:SDK 初始化 / 数据库 Migration / 缓存预热

注意:UIKit 相关的操作必须在主线程,但大部分 SDK 的 init 是线程安全的。


六、启动耗时测量

6.1 Pre-main 耗时

在 Xcode 的 Scheme → Arguments → Environment Variables 中添加:

DYLD_PRINT_STATISTICS = 1        // 基础信息
DYLD_PRINT_STATISTICS_DETAILS = 1  // 详细信息

会输出类似:

Total pre-main time:  420.17 milliseconds (100.0%)
         dylib loading time: 154.88 milliseconds (36.8%)
        rebase/binding time:  37.43 milliseconds (8.9%)
            ObjC setup time:  52.29 milliseconds (12.4%)
           initializer time: 175.54 milliseconds (41.7%)

每一项对应的优化方向一目了然。

6.2 Post-main 耗时

main() 开头和首页 viewDidAppear 各打一个时间戳,相减就是 Post-main 耗时。

更精细的测量可以用 Instruments 的 App Launch 模板(Xcode 11+),它会自动标注各阶段的耗时。

6.3 MetricKit(线上监控)

iOS 13+ 提供了 MetricKit 框架,可以在线上采集启动耗时数据:

  • MXAppLaunchMetric:冷启动 / 恢复启动的耗时分布
  • 以直方图形式提供 P50 / P90 / P99 数据

七、优化优先级总结

按性价比从高到低排列:

优先级 优化项 预期收益 难度
★★★★★ 删除 +load,改用 +initialize 立竿见影
★★★★★ didFinishLaunching 分级初始化 几十到几百 ms
★★★★☆ 减少动态库数量 / 改用静态库 每个库 5-10ms
★★★★☆ 首页数据缓存 体感提升明显
★★★☆☆ 减少 OC 类数量 / 用 Swift Struct Rebase 阶段提升
★★★☆☆ 子线程并行初始化 分担主线程压力
★★☆☆☆ 二进制重排 约 10-30% Page Fault 减少
★★☆☆☆ 骨架屏 / 闪屏优化 体感优化(非真正提速)

八、一张图总结全流程

用户点击图标
    │
    ├── 内核 fork 进程,加载 Mach-O
    │
    ├── dyld 启动
    │     ├── [dyld 3/4] 读取启动闭包(大量工作已预先完成)
    │     ├── 加载动态库(系统库走 shared cache,极快)
    │     ├── Rebase(修复内部指针)
    │     ├── Bind(绑定外部符号,非懒绑定部分)
    │     └── 加载完成
    │
    ├── Objc Runtime 初始化
    │     ├── 注册所有类
    │     └── 处理 Category
    │
    ├── Initializers
    │     ├── +load 方法(尽量消灭它们!)
    │     └── C++ 静态构造函数
    │
    ╞══════════════════════ main() ═══════════════
    │
    ├── UIApplicationMain
    │
    ├── didFinishLaunchingWithOptions
    │     ├── 🔴 必须立即做的初始化
    │     ├── 🟡 延迟到首页出现后
    │     └── 🟢 延迟到用到时
    │
    ├── 首页 ViewController 初始化
    │     ├── viewDidLoad(发起网络请求)
    │     ├── viewWillAppear
    │     └── viewDidAppear ← 首帧渲染完成
    │
    ▼
用户看到首页 ✅

对组件化与模块化的思考与总结

 

前言

前段时间反复研读了蘑菇街 App 的组件化之路蘑菇街 App 的组件化之路·续iOS应用架构谈 组件化方案,然后又找到了其它一些研究组件化、模块化方案的文章,但是总觉得差点什么,所以还是决定从头开始思考。文章的标题起的好宽泛,感觉给自己挖了个深坑-。-,其实只是自己对组件化、模块化的一些看法、总结。

为什么

先总结下为什么要大动干戈的对代码分模块、拆组件。

代码量膨胀,不利于维护,更不利于新功能的开发

现在随便开发一个App的代码行数都是数以万计的,如果不对代码做合理的拆分,那简直就是灾难性的,估计只有最初的开发人员知道如何维护修改,如果换人开发的话,难以下手,更不用说开发新功能了。

不同业务代码耦合严重,难以多人合作,职责不分明

多人一起开发时,如果代码结构、模块化的不好,就很难对不同业务划分出分界线,难以明确各自的职责,牵一发动全身,出了问题更是容易相互扯皮(这个时候只能说一句“怪我咯o(╯□╰)o”),更不用提合并代码时的冲突了。

所以,合理的组织代码,划分模块、拆分组件是项目可以高效迭代的基础。

疑问

那到底什么是模块化、组件化?查资料的时候一会儿模块,一会儿组件,有什么联系,有什么区别?有人说这只是叫法习惯问题,知道大概意思就好,不用咬文嚼字,但是总觉得没有个“定义”感觉不踏实,所以还是求助了万能的维基百科=。=

模块化

维基百科的Modular programming的开头定义如下:

Modular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality.

接着,在Key aspects部分的开头也说了:

With modular programming, concerns are separated such that modules perform logically discrete functions, interacting through well-defined interfaces.

可以总结为:模块化的目的在于将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块之间通过接口调用。

当然,模块化编程的具体概念是包含了很多内容的,读者可以详细阅读下维基百科的定义。

组件化

关于组件化,能找到的比较接近的就是维基百科的Component-based software engineering,其开头内容如下:

Component-based software engineering (CBSE), also known as component-based development (CBD), is a branch of software engineering that emphasizes the separation of concerns in respect of the wide-ranging functionality available throughout a given software system. It is a reuse-based approach to defining, implementing and composing loosely coupled independent components into systems.

乍一看,这不是跟模块化Modular programming的定义很相似嘛=。=
的确,文中也提到组件化跟模块化是很类似的,都是主要为了对一个系统做拆分,比如文中提到:

All system processes are placed into separate components so that all of the data and functions inside each component are semantically related (just as with the contents of classes). Because of this principle, it is often said that components are modular and cohesive.

同时,组件还具有其他属性,如可替代性(substitutable),通过接口(interface)访问,可重用性(Reusability)等,读者可自行阅读。

对比

难道模块化跟组件化真的是完全一样的?的确,很多时候两者的概念完全可以相互替换,在实践中更是经常混用。

在求助谷歌,甚至阅读了大量的前端技术等其它技术领域的组件化、模块化的文章后,我觉得如果真要将它们两者做个对比,大概总结如下:

  • 模块化强调的是拆分,无论是从业务角度还是从架构、技术角度,模块化首先意味着将代码、数据等内容按照其职责不同分离,使其变得更加容易维护、迭代,使开发人员可以分而治之。
  • 组件化则着重于可重用性,不管是界面上反复使用的用户头像按钮,还是处理数据的流程中的某个部件,只要可以被反复使用,并且进行了高度封装,只能通过接口访问,就可以称其为“组件”。

当然,并不是说模块就不能被复用,还是要根据实际情况来看,使系统更加容易维护,开发更加方便,才是最终目的。

如何拆分

无论是模块化还是组件化,首先肯定是做拆分,但是如何拆分?怎么下手?依照什么标准?
下面简单总结一些方法。

横向拆分业务、功能模块

很多时候,一个完整的软件程序是同时为多种业务服务的,所有可以优先按照业务的不同,将整个系统进行拆分。

如一个电商类型的App,就可以分出商品浏览模块、订单模块、购物车模块、消息模块、支付模块等。又如微信这种社交型应用,可以拆分出联系人模块、朋友圈模块、聊天模块、消息模块等。

其实就是从用户使用的角度,按照功能的不同划分模块,当然,这种业务模块是要由各种技术模块作支撑的。

横向拆分业务模块示例编辑

纵向拆分技术、架构模块

如果脱离业务,只从技术角度来看,则可以尝试纵向对系统拆分模块。

其实这里的纵向拆分跟对系统的架构做分层有点像=。=,现如今只要需要联网请求API的App都免不了有网络请求、数据缓存、数据加工处理、数据展示、反馈用户操作等行为,所有这些环节层层递进才能完成一个功能。

当开始着手规划一个完整软件系统,或者说App时,就可以按照这些环节划分模块,纵向分层次的组合,搭建出一个以技术模块组成的简易系统架构图,方便后续的开发,如下图。

纵向拆分技术模块示例编辑

大体上的技术模块划分好以后,就可以按照具体的需求,实现每个技术模块,乃至细分出更多的子模块,如缓存模块可能由键值对缓存(NSUserDefaults)、数据库缓存(SQLite、Realm)、图片缓存等子模块组成,根据具体情况而定。

从界面入手,拆分可视化组件

现在再来看看如何从界面入手拆分可复用的组件。假如有如下布局的界面:

从界面入手拆分可视化组件编辑

很多时候,像界面里面的“搜索框”、“头像按钮”、“内容框”和显示提示用的“加载中”HUD,甚至整个内容的Cell,都是可能在很多地方出现的,而且本身的样式、功能比较集中。
如头像可能要支持点击跳转,头像图片圆角,内容框有特定的Padding和字体大小等,所以可以将这些界面上的元素“提”出来,单独封装成一个组件,供整个App复用。或者直接用第三方的组件,如图中的“加载中”HUD,就可以用SVProgressHUD、MBProgressHUD等开源库。

其实这里的组件有种sunnyxx大大提到过的“Self-Manager”的味道=。=,组件本身负责自己的所有功能、样式,参考:iOS 开发中的 Self-Manager 模式。当然跟前端的组件化也挺像的,如React里面的component,样式、功能都封装到component里面,以便更好地解耦复用。

从数据入手,拆分数据加工组件

再来看看从数据入手,拆分可复用的组件。假如有如下数据处理流程:

一数据处理流程示例编辑

其实大部分时候,拆分模块、组件都是以清晰的流程、逻辑为基础的,就如上图的过程,当流程清晰后,可以拆分复用的组件也就“出来了”。

如从JSON数据实例化出对应的Entity对象,这个功能就是一个完整独立的组件,当然实际开发中会用Mantle、JSONModel等库实现。

以此类推,校验、格式化日期(如“几秒钟前、几天前”)、多语言等环节,都可以独立成一个个的组件。

当然,这里的组件一般是指能在多个模块使用的功能组件,如果只是在某个界面上才用的,倒不如放到ViewModel、Presenter等这些直接跟界面有关的类里面。

小节

上面的几种方法比较适合不知道如何下手时使用=。=,真正的开发中,还是要根据实际情况考虑,情况也会复杂些。不过倒是可以总结几点原则:

  • 单一职责,意味着一个模块、一个组件只做一件事,绝不多做。
  • 正交性,意思是不重复,一个模块跟另一个模块的职责是正交的,没有重叠,组件也是一样。
  • 单向依赖,模块之间最多是单向的依赖,如果出现A依赖B,B也依赖A,那么要么是A、B应该属于一个模块,要么就是整体的拆分有问题。一个完整的软件系统的模块依赖应该是一张有向无环图。(当然这是最终理想=。=)
  • 紧凑性,模块、组件对外暴露的接口、属性应该尽可能的少,接口的参数个数也要少。
  • 面向接口,模块、组件对外提供服务时最好是面向接口的,以便后期可以灵活的变更实现。

最后

一切为了更加干净整洁的代码,“May the clean code be with you”

参考

Flutter GetX 深入浅出详解

一、GetX 是什么?

GetX 是 Flutter 生态中的一个 全家桶式框架,它不只是一个状态管理方案,而是把状态管理、路由导航、依赖注入三件事打包在了一起。

很多人第一次接触 GetX 的感受是:"怎么什么都能做?" —— 这既是它的优势,也是它的争议所在。

GetX 的三大核心模块

GetX
 ├── 状态管理(State Management)  ── 替代 setState / Provider / Bloc
 ├── 路由管理(Route Management)  ── 替代 Navigator 系列 API
 └── 依赖注入(Dependency Injection)── 替代 Provider / get_it

为什么这么多人用?

一个字:简单

用 Bloc 写一个计数器功能,你需要:Event 类 + State 类 + Bloc 类 + BlocProvider + BlocBuilder,至少 4~5 个文件。

用 GetX 写同样的功能:一个 Controller 类 + Obx(() => Text()),两行搞定。


二、状态管理

2.1 两种响应式风格

GetX 提供了两种状态管理方式,理解它们的区别是用好 GetX 的第一步。

简单状态管理(GetBuilder)

  • 手动调用 update() 通知刷新
  • 类似 setState(),但作用域更小
  • 性能好,适合不需要频繁自动响应的场景

工作原理很朴素:Controller 内部维护一个监听者列表,调用 update() 时遍历通知所有 GetBuilder Widget 重建。本质就是一个 手动版的观察者模式

响应式状态管理(Obx)

  • 变量用 .obs 标记,变化时自动触发 UI 刷新
  • 不需要手动调用 update()
  • 类似 Vue 的响应式、MobX 的 observable
// 声明
var count = 0.obs;

// 修改(自动触发 UI 刷新)
count.value++;

// UI 监听
Obx(() => Text('${controller.count}'))

2.2 .obs 的底层原理

.obs 是 GetX 最核心的魔法。要理解它,需要拆解三个问题:

问题一:.obs 做了什么?

当你写 var count = 0.obs,实际上是把一个普通的 int 包装成了一个 RxInt 对象。这个 Rx 对象内部持有:

  • 真正的值(_value
  • 一个监听者列表(Stream

它本质上就是一个 带通知能力的值容器

问题二:修改值时发生了什么?

当你写 count.value++Rx 对象的 set value 被触发。在 setter 内部:

  1. 更新 _value
  2. 通过 Stream 广播一个"值变了"的事件
  3. 所有订阅了这个 Stream 的监听者收到通知

问题三:Obx 怎么知道要监听哪些变量?

这是 GetX 最巧妙的设计。Obx 并不需要你手动告诉它"我依赖了哪些变量",它是 自动收集依赖 的。

原理分三步:

  1. Obx 在首次 build 时,先打开一个"全局监听开关"
  2. 执行你传入的 builder 函数(比如 () => Text('${count.value}')
  3. count.value 的 getter 被调用时,Rx 对象检测到"监听开关"是打开的,就把自己注册到 Obx 的依赖列表中
  4. builder 执行完毕,关闭"监听开关"

之后任何被收集到的 Rx 变量发生变化,Obx 就会自动重建。

Obx 首次 build
  → 开启"依赖收集模式"
  → 执行 builder: () => Text('${count.value}')
    → count.value 的 getter 被调用
    → count(Rx 对象)发现正在收集依赖,把自己注册进去
  → 关闭"依赖收集模式"
  → 依赖收集完成:[count]

之后 count.value++ 触发
  → Rx 广播变化
  → Obx 收到通知,重新执行 builder

这个机制和 Vue 3 的 watchEffect、MobX 的 autorun 原理几乎一模一样 —— 基于 getter 劫持的自动依赖收集

2.3 GetBuilder 的底层原理

相比之下,GetBuilder 的原理简单得多:

  1. GetBuilderinitState 时,把自己注册到 Controller 的监听者列表
  2. Controller 调用 update(),遍历列表,调用每个 GetBuildersetState
  3. GetBuilderdispose 时,从列表中移除自己

没有 Stream、没有依赖收集,就是最朴素的 观察者模式 + setState

2.4 两种方式怎么选?

场景 推荐 原因
表单页面、简单列表 GetBuilder 手动控制,性能开销最小
数据频繁联动、多变量交叉依赖 Obx + .obs 自动依赖收集,代码更简洁
超大列表、高性能场景 GetBuilder + update([id]) 可以精确控制刷新范围

三、依赖注入

3.1 是什么?

GetX 内置了一套依赖注入系统,核心 API 就两个:

  • Get.put(Controller()) —— 注册
  • Get.find<Controller>() —— 获取

你可以把它理解成一个 全局的"服务柜台":先把东西放进去,需要的时候按类型取出来。

3.2 底层原理

GetX 内部维护了一个 全局的 Map<String, Object>,key 是类型名(或类型名 + tag),value 是实例。

全局容器(简化理解):
{
  "HomeController": HomeController 实例,
  "UserService": UserService 实例,
  "ApiClient_v2": ApiClient 实例(带 tag)
}

Get.put() 就是往 Map 里写,Get.find() 就是从 Map 里读。

3.3 四种注册方式的区别

方式 何时创建 何时销毁 适用场景
Get.put() 立即创建 手动或路由关闭时 页面 Controller
Get.lazyPut() 首次 find 同上 可能用不到的依赖
Get.putAsync() 立即创建(支持异步) 同上 需要异步初始化的服务
Get.create() 每次 find 都新建 不自动销毁 每次需要新实例的场景

3.4 SmartManagement:自动内存管理

GetX 最被低估的能力之一。它有一套 智能内存管理机制,可以在路由关闭时自动销毁关联的 Controller。

三种模式:

  • full(默认):不被任何路由或 Widget 使用的 Controller 自动销毁
  • onlyBuilder:只有通过 GetBuilder / GetX 使用的 Controller 才自动管理
  • keepFactory:销毁实例但保留工厂函数,下次 find 时重新创建

这解决了 Flutter 状态管理中一个常见的痛点:谁来负责销毁 Controller? 在 Provider/Bloc 中你需要手动处理,GetX 帮你自动化了。


四、路由管理

4.1 为什么要替换 Flutter 原生路由?

Flutter 原生路由的痛点:

  • 跳转需要 context,在非 Widget 层(Service、Controller)中很难拿到
  • 传参和接收返回值写法繁琐
  • 路由动画自定义复杂

GetX 的路由通过全局 NavigatorKey 持有 Navigator 的引用,所以 不需要 context 就能跳转。

4.2 底层原理

GetX 路由的核心做了两件事:

第一:全局 NavigatorKey

GetX 在 GetMaterialApp 初始化时,创建了一个全局的 GlobalKey<NavigatorState>,保存在静态变量中。之后所有路由操作都通过这个 key 拿到 Navigator,不再依赖 context。

第二:路由与依赖注入联动

这是 GetX 路由最独特的地方。当你用 Get.to(HomePage()) 跳转时:

  1. 创建一个路由条目
  2. 如果 HomePage 关联了 Controller(通过 GetBuilderBindings),自动 put 进依赖容器
  3. 当路由 pop 时,自动 delete 关联的 Controller
Get.to(HomePage())
  → 创建路由
  → 自动注册 HomeController(如果有 Binding)
  → 用户在 HomePage 操作...
  → Get.back()
  → 路由 pop
  → 自动销毁 HomeController
  → 内存释放

这就形成了一个 路由驱动的生命周期管理:Controller 的生死和页面的进出自动绑定。

4.3 Bindings:依赖与路由的桥梁

Bindings 是连接路由和依赖注入的纽带。它定义了"进入某个页面时需要准备哪些依赖"。

你可以把它类比为 iOS 的 viewDidLoad —— 页面加载时做初始化工作,页面销毁时自动清理。

4.4 中间件(Middleware)

GetX 路由支持中间件,可以在路由跳转前/后插入逻辑:

  • 登录拦截:未登录自动跳转登录页
  • 权限检查:没有权限的页面拒绝访问
  • 埋点:自动记录页面访问

中间件按优先级执行,可以中断跳转(返回 null 表示拦截),和 Web 框架的中间件概念一致。


五、GetX 的其他能力

GetX 是个全家桶,除了三大核心模块,还打包了很多实用工具:

能力 说明
国际化(i18n) 'hello'.tr 即可翻译,动态切换语言
主题切换 Get.changeTheme() 一行切换深色/浅色
网络请求 GetConnect 封装了 HTTP 客户端
本地存储 GetStorage 类似 SharedPreferences 但更快
响应式表单验证 配合 .obs 做实时校验
Snackbar / Dialog / BottomSheet 不需要 context 的全局弹窗
Worker ever / debounce / interval 等响应式工具

Worker 机制

Worker 是 GetX 响应式系统中很实用的工具,用于对 .obs 变量的变化做 节流、防抖、一次性监听 等处理:

Worker 行为
ever(count, callback) 每次变化都执行
once(count, callback) 只在第一次变化时执行
debounce(count, callback) 停止变化后一段时间才执行(搜索场景)
interval(count, callback) 变化期间按固定间隔执行(节流)

底层实现就是对 Rx 的 Stream 做了 listen / first / debounceTime / throttle 等 Dart Stream 操作的封装。


六、GetX 的底层架构总结

把所有模块串起来,GetX 的底层可以概括为三个核心机制:

1. Rx + Stream:响应式引擎

.obs 变量(Rx 对象)
  └── 内部持有 Stream
        └── Obx / Worker 订阅 Stream
              └── 变量变化 → Stream 广播 → 订阅者响应

这是 Dart 语言自带的 Stream 机制,GetX 没有发明新东西,只是在 Stream 之上做了 语法糖封装.obsObxever 等),降低了使用门槛。

2. 全局 Map:依赖注入容器

静态 Map<String, InstanceInfo>
  └── Get.put() 写入
  └── Get.find() 读取
  └── Get.delete() 删除
  └── SmartManagement 自动清理

没有复杂的 IoC 容器,就是一个 Map。简单直接。

3. 全局 NavigatorKey:脱离 context 的路由

GetMaterialApp 初始化 → 持有全局 NavigatorKey
  └── Get.to() / Get.back() → 通过 Key 拿到 Navigator → 执行路由操作
  └── 路由变化 → 触发 Bindings → 联动依赖注入的创建/销毁

七、GetX 的争议

赞成派观点

  • 开发效率极高:原型开发、中小项目飞速
  • 学习曲线平缓:API 直觉化,新手友好
  • 全家桶一站式:不用在多个库之间做选型和协调

反对派观点

  • 过度封装:把 Flutter 的很多设计理念(如 BuildContext、InheritedWidget)绕过了,新手可能对 Flutter 本身理解不深
  • 隐式行为多:自动依赖收集、自动销毁,出了问题难以调试
  • 大型项目维护难:全局状态 + 隐式依赖,随着项目变大,依赖关系会变得不透明
  • 和 Flutter 官方方向渐行渐远:Flutter 团队推崇的是 Riverpod / Provider 思路

客观建议

项目类型 推荐度 建议
个人项目 / Demo 强烈推荐 快速出活
中小型商业项目 推荐 配合良好的分层架构使用
大型团队协作项目 谨慎 建议考虑 Riverpod / Bloc,或严格约束 GetX 的使用范围
学习 Flutter 阶段 不推荐先学 先理解 Flutter 原生机制,再用 GetX 提效

八、GetX vs 其他状态管理方案

维度 GetX Provider Riverpod Bloc
学习成本
模板代码量 极少
依赖 context 不需要 需要 不需要 需要
内置路由
内置依赖注入 自身就是 DI 自身就是 DI 无(需配合)
可测试性
官方推荐 是(早期) 是(现在) 社区主流
适合规模 小中型 中型 中大型 大型

九、一句话总结

GetX 的哲学是 "约定优于配置,简单优于正确"。它牺牲了一些架构上的严谨性,换来了极致的开发效率。理解它的底层原理(Rx Stream + 全局 Map + 全局 NavigatorKey),你就能用好它,也知道它的边界在哪里。

iOS 可视化埋点与无痕埋点详解

一、为什么需要不同的埋点方式?

最早大家都是 手动写代码埋点:在每个按钮点击、页面出现的地方,手动调用 track("事件名")。这种方式最精确,但有两个痛点:

  1. 每加一个埋点就要改代码、发版,周期太长
  2. 埋点需求爆炸式增长,开发根本忙不过来

于是业界开始思考:能不能让机器自动采集?能不能让运营自己配置?

这就催生了三种埋点方式的演进:

手动代码埋点 → 无痕埋点(全自动) → 可视化埋点(半自动)
维度 代码埋点 无痕埋点 可视化埋点
谁来埋 开发 机器自动 运营圈选
需要发版吗 需要 不需要 不需要
能带业务参数吗 能(商品ID、金额等) 不能 有限支持
数据量 按需 巨大 按需
精确度 最高 最低 中等

二、无痕埋点(全埋点)

一句话理解

不写任何埋点代码,SDK 自动采集用户的所有操作。

原理:偷梁换柱

iOS 有个强大的 Runtime 机制叫 Method Swizzling —— 可以在运行时把系统方法的实现"偷偷换掉"。

举个例子,iOS 中所有按钮点击最终都会走 UIControlsendAction:to:forEvent: 方法。SDK 做的事情就是:

原本的调用链:
  用户点击按钮 → sendAction → 执行业务逻辑

Swizzle 之后:
  用户点击按钮 → SDK 拦截,记录"谁在哪个页面点了什么" → 再调用原始 sendAction → 执行业务逻辑

业务方完全无感知,SDK 悄悄在中间插了一层数据采集。

SDK 需要 Hook 哪些地方?

拦截点 能采集到什么
UIControl 的点击事件 按钮、开关、滑块等操作
UITableView 的 Cell 点击 列表项点击
UIViewController 的页面出现 页面浏览量(PV)
UIGestureRecognizer 手势操作

核心难题:怎么标识"点的是哪个按钮"?

SDK 需要给每个 UI 元素生成一个 唯一标识(ViewPath),方式是沿着 View 层级往上爬,记录每一层的类名和位置:

UIWindow / UINavigationController / 首页VC / UIView / UITableView / 第3个Cell / 购买按钮

转化成路径就是:

UIWindow[0]/UINavigationController[0]/HomeVC[0]/UIView[0]/UITableView[0]/Cell[3]/UIButton[0]

这就像是给每个按钮一个"门牌号"。

致命缺陷

  1. 门牌号不稳定:UI 稍微改一下层级(比如在按钮外面多套一层 View),路径就变了,之前的数据就对不上了

  2. 只知道行为,不知道内容:SDK 能告诉你"用户点了第3个 Cell 里的按钮",但不知道那个 Cell 显示的是什么商品、多少钱

  3. 数据量爆炸:用户每一次点击、每一次滑动都会上报,90% 的数据可能没人看


三、可视化埋点

一句话理解

在无痕埋点的基础上,加了一个"后台圈选"的功能。运营在后台看到 App 截图,用鼠标点选要追踪的元素,SDK 只上报被选中的事件。

工作流程

第一步:App 和后台建立 WebSocket 连接

第二步:App 截图 + View 树结构 → 发给后台
       (后台能看到 App 当前界面的"透视图")

第三步:运营在后台的截图上点击"立即购买"按钮
       → 后台自动识别出这个按钮的 ViewPath
       → 运营给它命名为 "click_buy_button"

第四步:后台把配置下发给 SDK
       { viewPath: "xxx", eventName: "click_buy_button" }

第五步:SDK 在 Hook 点拦截事件时,拿当前元素的 ViewPath 去配置表里匹配
       → 匹配到了才上报,匹配不到就忽略

和无痕埋点的本质区别

无痕埋点:先采集所有数据 → 后期在数据平台筛选(先采后筛)
可视化埋点:先配置要采什么 → 只采集配置过的(先筛后采)

这就像是:

  • 无痕埋点 = 装了 360 度全景摄像头,24 小时录像,需要的时候回看
  • 可视化埋点 = 在关键位置装定向摄像头,只拍你关心的区域

优势

  • 运营自助:不需要开发介入,运营在后台圈选即可生效
  • 动态生效:配置下发后立即生效,不需要发版
  • 数据可控:只采集被圈选的事件,数据量小

局限

  • 依赖 ViewPath 稳定性:和无痕埋点一样,如果 UI 层级变了,之前圈选的配置就失效了
  • 无法携带复杂业务参数:你能圈选"购买按钮被点击",但很难自动带上"买的是哪个商品"
  • WebView / H5 页面支持复杂:需要额外注入 JS SDK

四、实战中怎么选?

成熟的 App 不会只用一种,而是 混合使用

场景 推荐方式 原因
页面 PV、App 启动/退出 无痕埋点 标准化行为,不需要业务参数
运营活动按钮、Tab 切换 可视化埋点 需求变化快,运营自助配置
支付、注册、加购、分享 代码埋点 需要精确的业务参数(金额、商品ID)

一个简单的原则:

数据越重要、越需要业务参数的事件,越应该用代码埋点;越通用、越标准化的行为,越适合自动采集。


五、ViewPath 稳定性:所有自动埋点方案的阿喀琉斯之踵

ViewPath 不稳定是无痕埋点和可视化埋点最大的技术挑战。业界的应对思路:

  1. 用 accessibilityIdentifier 做锚点:给关键元素设置固定 ID,优先用 ID 而不是层级位置来标识
  2. 模糊匹配:不要求路径完全一致,允许中间层级有增减,只要首尾和关键节点匹配度达到 80% 就算命中
  3. 哈希指纹:结合元素类型、文本内容、相对位置等多维度信息生成指纹,不完全依赖层级路径

六、SwiftUI 时代的新挑战

传统方案依赖 UIKit 的 View 层级树,但 SwiftUI 的渲染机制完全不同 —— 开发者写的 Button 和实际渲染出来的 View 层级之间没有稳定的对应关系。

目前的解决方向:

  • 利用 SwiftUI 的 ViewModifier 机制,做类似 .tracked("buy_button") 的声明式埋点
  • 借助 Accessibility Tree(辅助功能树)作为更稳定的元素标识来源
  • 通过 SwiftUI Introspect 获取底层 UIKit View 做桥接

这个领域还在发展中,还没有像 UIKit 时代那样成熟的方案。


七、隐私合规

  • 截图上传时必须对密码框、身份证号等敏感区域做 模糊处理
  • 不采集键盘输入内容
  • 需要在隐私政策中明确告知用户数据采集范围
  • 遵循 GDPR / 中国个人信息保护法
  • SDK 必须提供关闭开关

八、业界参考

平台 特点
GrowingIO 国内可视化埋点先驱,圈选体验好
神策 Sensors Analytics 全埋点 + 可视化 + 代码埋点全覆盖,iOS SDK 开源
Mixpanel 可视化埋点 + 代码埋点,国际主流
Heap 全埋点理念的代表,"Capture Everything"
Firebase Analytics Google 出品,自动事件 + 自定义事件

iOS 深度解析


目录

  1. iOS 启动流程
  2. 启动优化
  3. 网络优化
  4. RunLoop
  5. Runtime
  6. 卡顿监控
  7. AFNetworking
  8. SDWebImage

1. iOS 启动流程

1.1 启动的宏观阶段划分

iOS App 的启动可分为两个大阶段:pre-main 阶段(main 函数执行之前)和 post-main 阶段(main 函数执行之后到首帧渲染完成)。

  • 冷启动(Cold Launch):App 完全不在内存中,需要从磁盘加载所有资源,经历完整的 pre-main 和 post-main 流程。
  • 热启动(Warm Launch):App 进程虽然被终止,但部分数据仍然在系统内核的页缓存中(page cache),此时 dyld 加载速度会更快。
  • 恢复启动(Resume):App 只是从后台切回前台,不涉及进程创建,严格意义上不算"启动"。

1.2 Pre-main 阶段详解

1.2.1 内核阶段(Kernel)

当用户点击 App 图标时,系统通过 launchd 进程(PID=1)fork 出一个新的进程。内核为新进程完成以下工作:

  • 创建进程:分配 PID,创建虚拟内存空间(每个进程都有独立的 4GB/16EB 虚拟地址空间)。
  • ASLR(Address Space Layout Randomization):生成一个随机偏移值(slide),将 Mach-O 的加载基地址随机化,防止固定地址攻击。ASLR 是在内核层面实现的,每次启动 slide 不同。
  • 加载可执行文件:将 Mach-O 的头部和 Load Commands 映射到虚拟内存中(注意是映射,不是全部读入物理内存,利用的是 mmap 和按需缺页机制)。

1.2.2 dyld 阶段(Dynamic Linker)

dyld(dynamic link editor)是 Apple 的动态链接器,它是第一个在用户态运行的代码。Apple 在 iOS 13/macOS 11 之后将 dyld 升级到了 dyld3 和后来的 dyld4,引入了启动闭包(Launch Closure)机制。

dyld 的核心工作流程:

a) 加载动态库(Load Dylibs)

dyld 根据 Mach-O 的 LC_LOAD_DYLIB 等 Load Commands,递归地加载所有依赖的动态库。每个动态库自身也可能依赖其他动态库,形成一棵依赖树。系统共享库(如 UIKit、Foundation)通过 dyld shared cache(共享缓存)提前合并优化,存放在 /System/Library/Caches/com.apple.dyld/ 下,加载速度极快。

动态库的加载过程:

  • 解析 Mach-O Header,验证魔数(Magic Number)、CPU 架构、文件类型。
  • 读取 Load Commands,确定各 Segment(__TEXT__DATA__LINKEDIT)的内存映射方式。
  • 调用 mmap() 将文件内容映射到虚拟内存。
  • 由于使用了 Copy-on-Write(COW)技术,只读段可以被多个进程共享物理内存。

b) Rebase(基址重定位)

由于 ASLR 的存在,Mach-O 中所有写死的内部指针地址都需要加上 slide 偏移量。这个过程就是 Rebase。

Rebase 主要操作 __DATA 段中的指针。现代的 chained fixups(链式修正)格式将 rebase 信息直接编码在指针值中,减少了 __LINKEDIT 的大小,也加速了处理。

Rebase 的性能瓶颈不在于计算(加法操作极快),而在于 Page Fault:当访问尚未加载到物理内存的虚拟页时,会触发缺页中断,内核需要从磁盘读取对应的页并进行解密验证(如果开启了代码签名验证)。

c) Bind(符号绑定)

Bind 处理的是对外部动态库符号的引用。App 中调用的 NSLogobjc_msgSend 等函数,在编译时并不知道它们的真实地址,需要在运行时通过符号名查找。

  • Lazy Binding(懒绑定):大部分外部函数调用使用懒绑定,第一次调用时才通过 dyld_stub_binder 查找真实地址并回填到 __DATA.__la_symbol_ptr(Lazy Symbol Pointer)中,后续调用直接跳转,不再走 dyld。
  • Non-Lazy Binding(非懒绑定):部分符号(如 Objective-C 类引用、全局变量指针)需要在启动时立即绑定,存放在 __DATA.__nl_symbol_ptr(Non-Lazy Symbol Pointer)中。
  • Weak Binding(弱绑定)__attribute__((weak)) 修饰的符号需要搜索所有已加载的镜像来确定是否有强定义覆盖,开销较大。

d) dyld3/dyld4 的 Launch Closure

dyld3 引入了 Launch Closure(启动闭包)机制——将首次启动时的解析结果(依赖关系、rebase/bind 信息、初始化顺序等)序列化保存到磁盘。后续启动时直接读取闭包文件,跳过大量解析工作。

dyld4 进一步引入了 PrebuiltLoaderSet,对 App 的启动路径做了更激进的预计算。

1.2.3 Objective-C Runtime 初始化

dyld 在完成所有动态库的加载和绑定后,会调用注册的初始化函数。ObjC Runtime 的初始化是其中最重要的一步:

  • map_images:当新的 Mach-O 镜像被映射到内存时调用。Runtime 解析 __DATA.__objc_classlist__DATA.__objc_catlist(Category 列表)、__DATA.__objc_protolist(Protocol 列表)等 section,将类、分类、协议注册到全局表中。
  • 类的实现(Realize):将类从磁盘格式转换为运行时格式,设置 superclass 指针、method list、ivar layout 等。这个过程是懒加载的——只有第一次使用类时才会 realize。
  • Category 的附加:将 Category 中的方法、属性、协议"织入"到对应的类中。方法会被插入到方法列表的前面,这就是 Category 能"覆盖"原类方法的原因。
  • load_images:调用所有类和 Category 的 +load 方法。调用顺序:先按编译顺序调用父类的 +load,再调用子类的,最后调用 Category 的。+load 在所有类完成注册后、任何 +initialize 之前执行。

1.2.4 C++ 静态初始化器

所有标记了 __attribute__((constructor)) 的函数以及 C++ 全局对象的构造函数会在此阶段被调用。它们通过 __DATA.__mod_init_func section 记录。

1.2.5 执行 main 函数

完成以上所有步骤后,dyld 调用 App 可执行文件的入口点,即 main() 函数。

1.3 Post-main 阶段详解

1.3.1 UIApplicationMain

main() 函数通常只做一件事:调用 UIApplicationMain()。这个函数完成:

  • 创建 UIApplication 单例对象。
  • 创建 App Delegate 对象。
  • 启动主线程的 RunLoop(CFRunLoopGetMain())。
  • 加载 Info.plist,如果指定了 Main Storyboard,则加载并实例化初始 ViewController。

1.3.2 Application Lifecycle Callbacks

按照 iOS 13+ 的 Scene-based Life Cycle(多窗口架构):

  1. application:didFinishLaunchingWithOptions: — App 级别的初始化入口。
  2. scene:willConnectToSession:options: — Scene 连接。
  3. sceneWillEnterForeground: — 即将进入前台。
  4. sceneDidBecomeActive: — 已激活,用户可交互。

1.3.3 首帧渲染(First Frame Render)

首帧渲染标志着用户可以看到 App 的实际界面。系统在第一次 CATransaction commit 时将渲染树提交给 Render Server(一个独立进程 backboardd),完成 GPU 合成并上屏。

Apple 的 App Launch InstrumentCA::Transaction::commit() 中第一帧绘制完成作为启动结束的标志。

1.4 Mach-O 文件格式补充

Mach-O 是 macOS/iOS 的可执行文件格式,理解它对理解启动流程至关重要:

区域 内容
Header 魔数、CPU 类型、文件类型(MH_EXECUTE/MH_DYLIB)、Load Commands 数量
Load Commands 描述文件布局的元数据:段的位置和大小、动态库依赖、入口点、代码签名位置等
__TEXT 只读、可执行:机器码(__text)、ObjC 方法名(__objc_methname)、字符串常量(__cstring)等
__DATA 可读写:全局变量、ObjC 类数据、符号指针表等
__DATA_CONST 启动后只读:ObjC 类列表、协议列表等(rebase/bind 后被 mprotect 设为只读)
__LINKEDIT 动态链接器使用的元数据:符号表、字符串表、rebase/bind 操作码、代码签名等

2. 启动优化

2.1 度量体系

2.1.1 Apple 官方指标

  • TTID(Time to Initial Display):App 进程创建到第一帧渲染完成的时间。Apple 建议冷启动控制在 400ms 以内。
  • MetricKitMXAppLaunchMetric 提供生产环境的启动耗时数据(p50/p90/p99)。
  • DYLD_PRINT_STATISTICS:设置此环境变量可在控制台输出 pre-main 阶段各步骤的耗时。

2.1.2 自建度量

+load 或进程创建时记录起始时间戳,在首帧 viewDidAppear:CADisplayLink 回调中记录结束时间戳,差值即为端到端启动时间。注意要使用 mach_absolute_time()clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) 获取高精度时间,避免使用 NSDate(会受 NTP 校时影响)。

2.2 Pre-main 阶段优化

2.2.1 减少动态库数量

每个自定义动态库都会增加 dyld 的加载、rebase、bind 开销。Apple 建议自定义动态库不超过 6 个

优化手段:

  • 将多个小型动态 framework 合并为一个。
  • 能用静态库的场景优先使用静态库(静态库在编译链接阶段就合并到了主二进制中,不增加 dyld 的运行时负担)。
  • 使用 xcframework 统一管理多架构,避免重复链接。

2.2.2 减少 ObjC 元数据

  • 减少类和 Category 的数量:每个 ObjC 类都需要在 map_images 阶段注册到 Runtime 的全局类表中,每个 Category 都需要被合并到宿主类。大量无用的类会拖慢这个过程。
  • 清理无用代码:使用 LinkMap 文件分析各模块大小,结合 AppCode 的 Inspect Code 或开源工具(如 fuiperiphery)找出未使用的类和方法。
  • Swift 优势:Swift 的结构体和枚举不经过 ObjC Runtime,不产生 map_images 的注册开销。能用 Swift 值类型代替 ObjC 类的场景应优先考虑。

2.2.3 消灭 +load 方法

+load 方法在启动的极早期串行执行(持有 Runtime 的全局锁),任何耗时操作都会直接阻塞启动。

替代方案:

  • +initialize:懒加载,在类第一次收到消息时调用,且只调用一次(线程安全由 Runtime 保证)。将初始化逻辑从 +load 迁移到 +initialize 可以将开销延后到实际使用时。
  • __attribute__((constructor)) 也应减少:与 +load 类似,在 main() 之前执行。

2.2.4 二进制重排(Binary Reordering)

原理:App 启动时并非所有代码都会被立即执行。由于虚拟内存的分页机制(iOS 上每页 16KB),启动时执行的函数如果分散在不同的页中,会导致大量 Page Fault。每次 Page Fault 需要从磁盘读取一页并进行代码签名验证(对于加密的 App),耗时约 0.10.3ms。如果启动路径上有 2000 次 Page Fault,累计开销可达 200600ms。

做法

  1. 使用 Clang 的 SanitizerCoverage-fsanitize-coverage=func,trace-pc-guard)编译代码,在每个函数入口插入回调,记录启动路径上所有被调用的函数及其顺序。
  2. 生成 Order File.order 文件),按启动调用顺序列出函数符号。
  3. 在 Xcode 的 Build Settings 中设置 Order File 路径,链接器会按指定顺序排布函数,使启动路径上的函数尽量集中在连续的页中,减少 Page Fault。

效果:对于大型 App,Page Fault 次数可减少 30%70%,带来 100300ms 的启动提升。

2.2.5 dyld3/dyld4 闭包缓存

现代 iOS 系统已默认使用 dyld3 闭包。开发者能做的是确保不破坏闭包缓存的有效性——每次 App 更新后首次启动闭包需要重新生成,这属于不可避免的开销。

2.3 Post-main 阶段优化

2.3.1 任务分级与延迟加载

didFinishLaunchingWithOptions: 中的初始化任务按优先级分为三类:

优先级 任务类型 执行时机
P0 崩溃监控、AB 实验框架 didFinishLaunching 最前面,同步执行
P1 网络库初始化、用户登录态恢复 didFinishLaunching 中异步执行
P2 分享 SDK、推送注册、非首屏功能 首帧渲染后延迟执行(通过 RunLoop idle 或延时 dispatch)

关键原则:首帧渲染前只做必须做的事

2.3.2 首页渲染优化

  • 缓存上次的首页截图:在启动时展示缓存截图(skeleton screen 或快照),让用户感知到"已打开",待真实数据加载完成后替换。
  • 减少首页视图层级:使用 Instruments 的 View Debugger 分析视图层级深度,减少不必要的嵌套。
  • 避免首帧同步网络请求:使用本地缓存数据渲染首帧,网络数据到达后差量更新。

2.3.3 子线程预加载

将不需要在主线程执行的初始化任务放到并发队列中并行执行:

  • 数据库初始化和预热。
  • 预加载常用的图片资源到内存缓存。
  • 预建立 HTTP/2 连接(TCP + TLS 握手)。

注意:UIKit 操作必须在主线程,CoreData 的 NSManagedObjectContext 要注意线程隔离。

2.3.4 启动任务调度框架

大型 App 通常会搭建启动任务调度框架,支持:

  • 声明式地定义任务、依赖关系和线程要求。
  • 自动拓扑排序确定执行顺序。
  • 并行执行无依赖关系的任务。
  • 监控每个任务的耗时,自动上报异常。

2.4 持续劣化防护

  • CI 卡口:在 CI 流水线中集成启动耗时测试(使用 XCTest + MetricKit 或自定义打点),设置阈值,超标则阻断合入。
  • LinkMap 体积监控:监控二进制体积增长(尤其是 __DATA 段的增长),它与 rebase/bind 耗时正相关。
  • +load 扫描:通过静态分析工具在编译期扫描新增的 +load 方法。

3. 网络优化

3.1 网络请求的全链路分析

一次 HTTPS 请求的完整链路:

DNS 解析 → TCP 三次握手 → TLS 握手 → 请求发送 → 服务器处理 → 响应接收 → 数据解析

每个环节都有优化空间。

3.2 DNS 优化

3.2.1 传统 DNS 的问题

  • 解析延迟:首次解析需要递归查询根域名服务器 → 顶级域名服务器 → 权威域名服务器,耗时 50~200ms,极端情况下可达数秒。
  • DNS 劫持:运营商 LocalDNS 可能返回篡改的 IP 地址,将用户引导到广告页或错误服务器。
  • 调度不精准:运营商 DNS 的出口 IP 与用户的实际 IP 可能不在同一地区,导致 CDN 调度到非最优节点。
  • DNS 缓存不可控:系统 DNS 缓存(res_9_getaddrinfo)的 TTL 由服务端控制,App 无法主动管理。

3.2.2 HTTPDNS

HTTPDNS 通过 HTTP/HTTPS 协议直接向 DNS 服务商(如阿里云 HTTPDNS、腾讯云 HTTPDNS)发送域名解析请求,绕过运营商 LocalDNS。

核心优势:

  • 防劫持:使用 HTTPS 通道加密传输,运营商无法篡改。
  • 精准调度:可以携带客户端真实 IP(EDNS Client Subnet),CDN 能调度到最优节点。
  • 可控缓存:App 自主管理 DNS 缓存和预解析策略。

实现要点:

  • 预解析:App 启动时对常用域名发起预解析,将结果缓存在本地。
  • 缓存策略:本地维护 IP 缓存池,设置合理的 TTL。TTL 过期后异步刷新,期间仍使用旧 IP("乐观缓存"策略),避免解析等待。
  • 降级机制:HTTPDNS 服务异常时自动降级到系统 DNS。
  • SNI 问题:使用 HTTPDNS 后,HTTPS 请求的 Host 头是 IP 地址,需要手动设置 SNI(Server Name Indication)字段为原始域名,否则 TLS 握手会因证书不匹配而失败。在 NSURLSession 中需要实现 URLSession:didReceiveChallenge:completionHandler: 代理方法处理证书验证。

3.2.3 DNS-over-HTTPS (DoH) / DNS-over-TLS (DoT)

iOS 14+ 原生支持 DoH/DoT(通过 NEDNSSettingsManager),但这是系统级别的配置,App 级别的定制灵活性不如 HTTPDNS。

3.3 连接优化

3.3.1 连接复用

  • HTTP/1.1 Keep-Alive:在同一个 TCP 连接上串行发送多个请求,避免每次请求都建立新连接。但存在 队头阻塞(Head-of-Line Blocking) 问题——前一个请求未完成时后续请求必须等待。
  • HTTP/2 多路复用(Multiplexing):在单个 TCP 连接上并行发送多个请求/响应,通过帧(Frame)和流(Stream)的概念实现真正的并发。一个连接可以同时承载上百个请求。但 TCP 层的队头阻塞依然存在——一个丢包会阻塞整个连接上的所有流。
  • HTTP/3 (QUIC):基于 UDP,在传输层消除了队头阻塞。每个流独立进行丢包重传,互不影响。同时集成了 TLS 1.3,握手延迟更低(0-RTT/1-RTT)。iOS 15+ 的 NSURLSession 默认支持 HTTP/3。

3.3.2 预连接(Pre-connect)

在用户可能发起请求之前,提前完成 TCP + TLS 握手,使后续请求可以直接发送数据。

实现方式:使用 NSURLSession 的连接预热 API,或自行管理连接池。

3.3.3 连接迁移(Connection Migration)

传统 TCP 连接以四元组(源 IP、源端口、目的 IP、目的端口)标识,当用户从 WiFi 切换到蜂窝时,源 IP 变化导致连接断开。QUIC 使用 Connection ID 标识连接,网络切换时连接不中断,实现无缝迁移。

3.4 数据传输优化

3.4.1 数据压缩

  • Gzip/Brotli:在 HTTP 响应头中设置 Content-Encoding: gzip/br。Brotli 压缩率比 gzip 高 15~25%,特别适合文本类数据。NSURLSession 自动处理 gzip 解压。
  • Protocol Buffers / FlatBuffers:使用二进制序列化替代 JSON。Protobuf 体积比 JSON 小 310 倍,解析速度快 20100 倍。适用于高频接口和大数据量场景。
  • 增量更新(Delta Sync):只传输变化的部分,而非全量数据。可以使用 JSON Patch(RFC 6902)或自定义 diff 算法。

3.4.2 请求合并与批处理

将多个小请求合并为一个批量请求,减少网络往返次数(RTT)。例如将 10 个独立的埋点上报请求合并为 1 个批量请求。

3.4.3 精简数据

  • 按需请求字段:使用 GraphQL 或接口的 fields 参数,只请求客户端真正需要的字段,减少无用数据传输。
  • 分页加载:对列表类数据实施分页,避免一次加载全量数据。

3.5 缓存策略

3.5.1 HTTP 缓存

  • 强缓存Cache-Control: max-age=3600Expires 头。在有效期内直接使用本地缓存,不发起网络请求。
  • 协商缓存ETag / If-None-MatchLast-Modified / If-Modified-Since。客户端携带标识请求服务器,若资源未变则返回 304,节省传输带宽。
  • NSURLSession 的缓存策略:通过 NSURLRequest.cachePolicy 控制,NSURLCache 自动管理磁盘和内存缓存。

3.5.2 业务层缓存

  • 将接口返回数据持久化到本地(SQLite、文件),优先展示缓存数据,网络数据到达后更新 UI("先展示后刷新"策略)。
  • 对于不频繁变化的数据(如配置信息),使用较长的本地缓存有效期。

3.6 弱网优化

  • 超时策略:针对不同网络质量动态调整超时时间。WiFi 下 15s,4G 下 20s,3G/2G 下 30s。
  • 重试策略:指数退避(Exponential Backoff)+ 抖动(Jitter)。避免重试风暴压垮服务器。只对幂等请求(GET、PUT)重试,POST 请求需要业务层保证幂等性。
  • 网络质量检测:通过 NWPathMonitor(Network Framework)实时监听网络状态变化,结合 RTT、丢包率估算网络质量,动态降级(如切换到低分辨率图片)。
  • 多通道竞速:在 WiFi 和蜂窝同时可用时,并行发起请求,取先返回的结果。NSURLSessionConfiguration.multipathServiceType 支持 MPTCP(Multipath TCP)。

3.7 安全层优化

  • TLS 1.3:将握手往返从 2-RTT(TLS 1.2)减少到 1-RTT,支持 0-RTT 恢复(PSK,Pre-Shared Key)。iOS 12.2+ 默认支持。
  • 证书固定(Certificate Pinning):在 App 内预埋服务器证书的公钥哈希,防止中间人攻击。需要注意证书轮换的运维流程。
  • OCSP Stapling:服务器在 TLS 握手时主动提供证书状态(是否被吊销),避免客户端额外查询 OCSP 服务器。

3.8 监控体系

  • URLSessionTaskMetrics(iOS 10+):提供每个请求的详细时间线——DNS 解析时间、连接建立时间、TLS 握手时间、请求发送时间、响应接收时间等。这是做网络性能分析的核心数据源。
  • 端到端监控指标:成功率、平均耗时、P99 耗时、DNS 解析耗时、首字节时间(TTFB)、错误类型分布等。
  • 网络链路追踪:在请求头中注入 Trace ID,贯穿客户端 → CDN → 网关 → 后端服务,实现全链路问题定位。

4. RunLoop

4.1 RunLoop 的本质

RunLoop 本质上是一个 事件循环(Event Loop) 机制。它让线程在没有任务时进入休眠(不消耗 CPU),在有任务时被唤醒处理事件。没有 RunLoop 的线程执行完任务就会退出;有了 RunLoop,线程可以常驻内存,随时响应事件。

RunLoop 与线程是一一对应的关系:

  • 主线程的 RunLoop 在 UIApplicationMain 中自动创建和启动。
  • 子线程的 RunLoop 默认不创建,需要手动调用 [NSRunLoop currentRunLoop]CFRunLoopGetCurrent() 时才会懒加载创建。
  • RunLoop 保存在一个全局的 CFMutableDictionaryRef 中,以 pthread_t 作为 key。

4.2 RunLoop 的核心架构

4.2.1 三大核心对象

a) CFRunLoopSource(输入源)

  • Source0(非端口事件源):不能主动唤醒 RunLoop,需要手动调用 CFRunLoopSourceSignal() 标记为待处理,再调用 CFRunLoopWakeUp() 唤醒 RunLoop。触摸事件、performSelector:onThread: 等使用 Source0 分发。
  • Source1(端口事件源):基于 Mach Port,能主动唤醒 RunLoop。系统内核通过 Mach Port 发送消息来通知事件,如硬件事件(触摸/锁屏/摇晃)首先由 IOKit 通过 Mach Port 传递给 SpringBoard,再由 SpringBoard 通过 Mach Port 分发给对应的 App 进程。App 内部的 Source1 接收到事件后,通常会封装成 Source0 在主线程 RunLoop 中处理。

b) CFRunLoopTimer(定时器源)

基于时间的触发器,与 NSTimer 是 toll-free bridged 的。Timer 的触发时间并非绝对精确——它依赖于 RunLoop 的运行状态。如果 RunLoop 正在处理一个耗时任务,Timer 的回调会被延迟到当前任务完成后才执行。Timer 有一个 tolerance(容差)属性,系统可以在 fireDate ± tolerance 范围内选择最佳触发时机以节能。

c) CFRunLoopObserver(观察者)

可以监听 RunLoop 的状态变化:

状态 含义
kCFRunLoopEntry 即将进入 RunLoop
kCFRunLoopBeforeTimers 即将处理 Timer
kCFRunLoopBeforeSources 即将处理 Source
kCFRunLoopBeforeWaiting 即将进入休眠
kCFRunLoopAfterWaiting 刚从休眠中唤醒
kCFRunLoopExit 即将退出 RunLoop

4.2.2 RunLoop Mode

RunLoop 在某一时刻只能运行在一个 Mode 下。每个 Mode 包含独立的 Source/Timer/Observer 集合。切换 Mode 时,当前 Mode 下的 Source/Timer/Observer 不会被处理。

常用 Mode:

  • kCFRunLoopDefaultModeNSDefaultRunLoopMode:默认 Mode,App 空闲时运行在此 Mode。
  • UITrackingRunLoopMode:ScrollView 滑动时切换到此 Mode。这就是为什么 NSTimer 在 Default Mode 下注册时,滑动 ScrollView 期间 Timer 不触发——因为 RunLoop 此时运行在 Tracking Mode 下。
  • kCFRunLoopCommonModesNSRunLoopCommonModes:这不是一个真正的 Mode,而是一个"模式集合"的标记。被标记为 Common 的 Source/Timer/Observer 会被同步到所有被标记为 Common 的 Mode 中。默认情况下 Default Mode 和 Tracking Mode 都是 Common Mode。将 Timer 添加到 Common Modes 可以让它在滑动时也能触发。

4.3 RunLoop 的运行机制(核心循环)

RunLoop 的核心运行逻辑(简化版):

  1. 通知 Observer:即将进入 RunLoop(kCFRunLoopEntry)。
  2. 通知 Observer:即将处理 Timer(kCFRunLoopBeforeTimers)。
  3. 通知 Observer:即将处理 Source0(kCFRunLoopBeforeSources)。
  4. 处理所有待处理的 Source0 事件。
  5. 如果有 Source1(Mach Port 消息)待处理,跳转到步骤 9 直接处理。
  6. 通知 Observer:即将进入休眠(kCFRunLoopBeforeWaiting)。
  7. 休眠,等待唤醒。线程通过 mach_msg() 系统调用陷入内核态,让出 CPU。可以被以下事件唤醒:
    • Mach Port 消息到达(Source1 事件、Timer 触发、CFRunLoopWakeUp() 调用)。
    • 超时(RunLoop 有一个超时参数)。
    • 被外部手动唤醒。
  8. 通知 Observer:刚从休眠中被唤醒(kCFRunLoopAfterWaiting)。
  9. 处理唤醒事件:
    • 如果是 Timer 到期:处理 Timer 回调。
    • 如果是 dispatch_main_queue 的 block:执行 block(GCD 派发到主队列的任务通过 RunLoop 的 Source1 唤醒主线程执行)。
    • 如果是 Source1 事件:处理 Source1 回调。
  10. 判断是否需要退出(Mode 中没有任何 Source/Timer、被外部停止、超时等)。
  11. 如果不退出,跳转到步骤 2 继续循环。
  12. 通知 Observer:即将退出 RunLoop(kCFRunLoopExit)。

4.4 RunLoop 与系统功能的关系

4.4.1 AutoreleasePool

主线程 RunLoop 注册了两个 Observer 与 AutoreleasePool 配合:

  • 第一个 Observer 监听 kCFRunLoopEntry(优先级最高,保证在所有回调之前):调用 _objc_autoreleasePoolPush() 创建自动释放池。
  • 第二个 Observer 监听 kCFRunLoopBeforeWaiting(优先级最低,保证在所有回调之后):调用 _objc_autoreleasePoolPop() 释放旧池中的对象,再调用 _objc_autoreleasePoolPush() 创建新池。同时监听 kCFRunLoopExit:调用 _objc_autoreleasePoolPop() 做最终释放。

这意味着主线程上被 autorelease 的对象会在每次 RunLoop 循环即将休眠时被释放。

4.4.2 事件响应

硬件事件(触摸)传递链:

  1. 硬件产生中断 → IOKit.framework 封装为 IOHIDEvent。
  2. 通过 Mach Port 传递给 SpringBoard 进程。
  3. SpringBoard 判断前台 App,通过 Mach Port 传递给 App 进程。
  4. App 主线程 RunLoop 的 Source1 被唤醒,回调 __IOHIDEventSystemClientQueueCallback()
  5. Source1 内部触发 Source0(__UIApplicationHandleEventQueue())。
  6. Source0 中进行 Hit Test、手势识别、UIResponder 事件分发。

4.4.3 UI 刷新

setNeedsLayoutsetNeedsDisplay 等调用不会立即触发布局/绘制,而是标记为"需要更新"。主线程 RunLoop 注册了一个 Observer 监听 kCFRunLoopBeforeWaitingkCFRunLoopExit,在回调中遍历所有标记了需要更新的视图,执行实际的 layout、display、render 操作,最终打包提交给 Render Server。

这就是 Core Animation 的 Transaction 机制

4.4.4 GCD 与 RunLoop

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会唤醒主线程的 RunLoop(通过向 RunLoop 的 dispatch port 发送 Mach 消息),RunLoop 在循环中检测到 dispatch port 有消息后,会调用 _dispatch_main_queue_callback_4CF() 来执行 block。

4.4.5 performSelector:afterDelay:

performSelector:withObject:afterDelay: 实际上是创建了一个 Timer 添加到当前线程的 RunLoop 中。如果当前线程没有 RunLoop(子线程默认没有),这个方法不会执行。

4.5 RunLoop 的实际应用

  • 常驻子线程:为子线程创建 RunLoop 并添加一个永不触发的 Port(防止 RunLoop 因没有 Source/Timer 而退出),使线程常驻内存,随时可以接收任务。AFNetworking 2.x 和 SDWebImage 早期版本都使用过这个技巧。
  • NSTimer 滑动不停:将 Timer 添加到 NSRunLoopCommonModes
  • 卡顿监控:通过 Observer 监听 RunLoop 状态,检测主线程 Source 处理或休眠前等待是否超时(详见卡顿监控章节)。
  • 线程保活(Thread Keep-Alive):网络库中用于在子线程持续接收回调。
  • 任务拆分:将大量计算任务拆分成小块,每次 RunLoop 循环处理一块,避免长时间阻塞主线程(类似协程的思想)。

5. Runtime

5.1 Runtime 的本质

Objective-C Runtime 是一个用 C/C++/汇编编写的运行时库,它实现了 ObjC 的面向对象特性和动态性。ObjC 是一门动态语言——许多决定(调用哪个方法、对象是什么类型)被推迟到运行时。

核心思想:消息发送(Messaging)。ObjC 中的方法调用 [obj method] 会被编译器转换为 objc_msgSend(obj, @selector(method)),由 Runtime 在运行时查找并执行对应的实现。

5.2 对象模型

5.2.1 对象(id / objc_object)

每个 ObjC 对象本质上是一个结构体,其第一个成员是 isa 指针,指向该对象所属的类。

从 ARM64 开始,Apple 使用了 Tagged PointerNon-pointer ISA 优化:

Tagged Pointer:对于 NSNumberNSDate、短字符串等小对象,指针本身就直接存储了对象的值,不需要在堆上分配内存。判断方法:指针的最高位(ARM64)或最低位(x86_64)为 1 则是 Tagged Pointer。Tagged Pointer 不是真正的对象,没有 isa、没有 retain/release 开销,内存效率和访问速度极高。

Non-pointer ISA(优化的 isa):在 64 位系统上,isa 不再是单纯的类指针。64 位中只有 33~44 位用于存储类地址,其余位存储了:

  • 引用计数extra_rc,19 位,存储引用计数减 1 的值)。当 extra_rc 溢出时,将一半的引用计数转存到 SideTable 的 RefcountMap 中,has_sidetable_rc 标志位置 1。
  • 是否有关联对象has_assoc)。
  • 是否有 C++ 析构函数has_cxx_dtor)。
  • 是否使用了弱引用weakly_referenced)。
  • 是否正在释放deallocating)。

5.2.2 类(objc_class)

类也是一个对象(元类的实例),继承自 objc_object。关键成员:

  • isa:指向元类(metaclass)。
  • superclass:指向父类。
  • cache:方法缓存(cache_t),使用哈希表存储最近调用的方法,加速消息发送。
  • bits / class_rw_t
    • class_ro_t(Read-Only):编译期确定的只读数据——方法列表、属性列表、ivar 列表、协议列表、实例大小等。存储在 Mach-O 的 __DATA_CONST 段中。
    • class_rw_t(Read-Write):运行时创建的可读写数据,包含对 class_ro_t 的引用,以及运行时动态添加的方法、属性、协议列表。
    • class_rw_ext_t:iOS 14+ 优化,只有在类被运行时修改过(如添加了 Category、使用了 class_addMethod)时才会创建 class_rw_ext_t,约 90% 的类不需要,节省大量内存(Apple 称全系统节省约 14MB)。

5.2.3 元类(Metaclass)

  • 实例对象的 isa → 类对象。
  • 类对象的 isa → 元类对象。
  • 元类对象的 isa → 根元类(NSObject 的元类)。
  • 根元类的 isa → 自身。
  • 根元类的 superclass → NSObject 类。

这个链条解释了为什么实例方法存储在类中,类方法存储在元类中——消息发送总是沿着 isa 链查找方法。

5.3 消息发送机制(objc_msgSend)

5.3.1 快速查找(缓存查找)

objc_msgSend 是用汇编语言编写的(ARM64),追求极致性能。

执行流程:

  1. 判断 receiver 是否为 nil(Tagged Pointer 的特殊处理)。
  2. 通过 receiver 的 isa 找到类对象。
  3. 在类的 cache_t(方法缓存)中查找 SEL 对应的 IMP。cache_t 是一个开放寻址的哈希表,使用 SEL 的地址值做 mask 运算得到索引,查找效率接近 O(1)。
  4. 如果命中缓存(Cache Hit),直接跳转到 IMP 执行——整个过程几十纳秒,纯汇编实现。

5.3.2 慢速查找(方法列表查找)

缓存未命中时,进入 C/C++ 实现的 lookUpImpOrForward 函数:

  1. 在当前类的 class_rw_t 中搜索方法列表。方法列表已按 SEL 地址排序(在类 realize 时排序),使用二分查找,时间复杂度 O(log n)。
  2. 如果未找到,沿 superclass 链向上逐级查找父类的方法列表(每级都先查缓存再查方法列表)。
  3. 如果一直到 NSObject(根类)都未找到,进入消息转发流程。
  4. 如果找到了,将 SEL→IMP 的映射写入当前类的 cache_t(注意是写入最初接收消息的类的缓存,不是找到方法的那个父类的缓存)。

5.3.3 方法缓存(cache_t)的实现细节

  • 哈希表使用 掩码(mask) 而非取模,因为 mask 可以用位与运算(& mask)替代除法,更快。
  • 缓存容量始终是 2 的幂次,初始容量为 4(ARM64)。
  • 当缓存使用率超过 3/4(75%) 时,容量翻倍并清空所有旧缓存(而非 rehash),因为 Apple 认为缓存的时间局部性很强,旧缓存大概率不再需要。
  • 类在第一次收到消息时分配缓存。

5.4 消息转发机制(Message Forwarding)

当消息发送的快速查找和慢速查找都未找到方法实现时,进入消息转发的三个阶段:

5.4.1 第一阶段:动态方法解析(Dynamic Method Resolution)

Runtime 调用:

  • 实例方法:+resolveInstanceMethod:
  • 类方法:+resolveClassMethod:

在这个方法中,类有机会动态地为 SEL 添加一个 IMP(通过 class_addMethod)。如果返回 YES 且添加了方法,Runtime 会重新执行消息发送流程。

应用场景:@dynamic 属性的实现、Core Data 的 NSManagedObject 动态生成属性的 getter/setter。

5.4.2 第二阶段:快速转发(Fast Forwarding / Forwarding Target)

Runtime 调用 -forwardingTargetForSelector:

在这个方法中,可以返回另一个对象来处理这条消息(消息转发给备用接收者)。这一步效率很高,因为直接对新对象执行 objc_msgSend,不需要创建 NSInvocation

应用场景:多重代理(将消息转发给多个对象)、组合模式的简化实现。

5.4.3 第三阶段:完整转发(Normal Forwarding)

Runtime 依次调用:

  1. -methodSignatureForSelector::返回方法的类型签名(NSMethodSignature),描述参数类型和返回值类型。
  2. -forwardInvocation::接收一个封装了完整调用信息的 NSInvocation 对象,可以修改目标、参数、甚至调用多次。

这是最灵活但最慢的阶段,NSInvocation 的创建涉及堆分配和参数拷贝。

如果以上三个阶段都未处理,最终调用 -doesNotRecognizeSelector:,抛出经典的 "unrecognized selector sent to instance" 异常。

5.5 Method Swizzling

通过 Runtime 函数交换两个方法的 IMP,实现 AOP(面向切面编程)。

核心 API:

  • method_exchangeImplementations:交换两个 Method 的 IMP。
  • class_replaceMethod:替换某个 SEL 的 IMP。
  • method_setImplementation:设置某个 Method 的 IMP。

陷阱与最佳实践

  • 必须在 +load 中执行(或用 dispatch_once 保证只执行一次),避免竞态条件。
  • 必须调用原始实现:Swizzle 后的方法中要调用"看似递归实际不是"的原始方法(因为 IMP 已经交换了)。
  • 父类方法问题:如果当前类没有实现目标方法(继承自父类),直接交换会影响父类。正确做法是先 class_addMethod 尝试添加,成功则只需 class_replaceMethod 替换父类的实现到当前类的新 SEL,失败(说明当前类已有实现)才 method_exchangeImplementations
  • _cmd 问题:Swizzle 后方法内部的 _cmd 值是交换后的 SEL,可能导致日志、KVO 等依赖 _cmd 的逻辑出错。

5.6 关联对象(Associated Objects)

通过 objc_setAssociatedObject / objc_getAssociatedObject 为已存在的类动态添加"属性"(实际是绑定的键值对)。

内部存储结构

全局维护一个 AssociationsManager(自带锁),内部是一个 AssociationsHashMap

AssociationsHashMap: { 对象地址(disguised_ptr_t) → ObjectAssociationMap }
ObjectAssociationMap: { key(const void*) → ObjcAssociation(policy + value) }
  • 关联对象不存储在对象本身的内存中,而是存储在全局的哈希表中,以对象地址为 key。
  • 对象销毁时(dealloc),Runtime 检查 isa 的 has_assoc 标志位,如果为 1,则调用 _object_remove_associations() 清除该对象的所有关联对象。
  • 关联策略:OBJC_ASSOCIATION_ASSIGN(弱引用)、OBJC_ASSOCIATION_RETAIN_NONATOMIC(强引用,非原子)、OBJC_ASSOCIATION_COPY_NONATOMIC(拷贝)等,语义与 property 属性一致。

5.7 Category 的实现原理

Category 在编译后生成 category_t 结构体,包含:方法列表、属性列表、协议列表(但没有 ivar 列表,这就是 Category 不能添加实例变量的原因——实例变量列表在编译期确定,存储在 class_ro_t 中,不可修改)。

加载过程

  1. map_images 阶段,Runtime 遍历所有镜像的 __objc_catlist section,收集所有 Category。
  2. 调用 attachCategories() 将 Category 的方法列表倒序插入到类的方法列表数组的前面(使用 attachListsATTACH_EXISTING 方式)。
  3. 因此,后编译的 Category 的方法会排在最前面,最先被找到——这就是 Category "覆盖"原类方法的真相(原方法仍然存在,只是排在后面不会被优先找到)。

多个 Category 有同名方法时:取决于编译顺序(Build Phases → Compile Sources 中的文件顺序),最后编译的 Category 的方法排在最前面。

5.8 Weak 引用的实现

全局 Weak 表:Runtime 维护一个全局的 SideTable(实际上是一个 StripedMap,包含 64 个 SideTable 以减少锁竞争),每个 SideTable 包含:

  • spinlock_t:自旋锁,保护并发访问。
  • RefcountMap:存储对象的额外引用计数(extra_rc 溢出时使用)。
  • weak_table_t:弱引用表,核心结构。

weak_table_t 是一个哈希表,以对象地址为 key,value 是 weak_entry_t,包含所有指向该对象的 weak 指针的地址。

weak 指针的赋值过程

  1. 调用 objc_initWeak()(或 objc_storeWeak())。
  2. 如果旧值非 nil,从旧对象的 weak_entry_t 中移除该 weak 指针。
  3. 如果新值非 nil,将该 weak 指针注册到新对象的 weak_entry_t 中。

对象销毁时清除 weak 引用

  1. dealloc_objc_rootDeallocrootDeallocobject_disposeobjc_destructInstance
  2. objc_destructInstance 中:清除关联对象 → 清除弱引用(weak_clear_no_lock)→ 清除 SideTable 引用计数。
  3. weak_clear_no_lock:遍历对象的 weak_entry_t 中所有 weak 指针地址,将它们全部置为 nil。

这就是 weak 指针在对象销毁后自动变为 nil 的底层机制。

5.9 KVO 的底层实现

KVO(Key-Value Observing)完全依赖 Runtime 实现:

  1. 当对某个对象的属性添加 KVO 观察时,Runtime 动态创建一个该对象所属类的子类(命名为 NSKVONotifying_OriginalClass)。
  2. 将对象的 isa 指向这个动态子类(isa swizzling)。
  3. 动态子类重写了被观察属性的 setter 方法,在 setter 中插入:
    • willChangeValueForKey: → 调用原始 setter → didChangeValueForKey:
    • didChangeValueForKey: 内部触发 observeValueForKeyPath:ofObject:change:context: 回调。
  4. 动态子类还重写了 class 方法(返回原类而非 NSKVONotifying_ 前缀的子类,对外隐藏 KVO 的实现细节),以及 dealloc(清理观察)和 _isKVOA(标识 KVO 类)。

6. 卡顿监控

6.1 卡顿的定义与原理

iOS 设备的屏幕刷新率通常为 60Hz(ProMotion 设备最高 120Hz),意味着每帧的渲染时间预算为 16.67ms(60fps)或 8.33ms(120fps)。如果主线程在一帧的时间内未完成 UI 更新的所有工作(布局计算、绘制、图层合成提交),就会导致掉帧(Frame Drop),用户感知为卡顿。

渲染流水线(Render Pipeline):

App 进程(CPU)                      Render Server(GPU)
┌─────────────────┐                  ┌──────────────────┐
│ Layout          │                  │ 图层树解码       │
│ Display (Draw)  │ ──Commit──────→  │ 纹理上传         │
│ Prepare         │   Transaction    │ 合成渲染         │
│ Commit          │                  │ 显示             │
└─────────────────┘                  └──────────────────┘
        ← 一帧 16.67ms →                 ← 一帧 16.67ms →

CPU 和 GPU 是流水线式工作的。CPU 在当前帧完成布局和绘制后提交给 GPU,GPU 在下一帧完成合成渲染。任一环节超时都会导致掉帧。

6.2 卡顿的常见原因

CPU 侧

  • 复杂布局计算:Auto Layout 的约束求解是多项式时间复杂度,视图层级深、约束多时开销显著。
  • 文本计算与渲染NSAttributedString 的排版(Text Kit / Core Text)、行高计算、折行计算。
  • 图片解码UIImage 在首次渲染时才进行解码(从 PNG/JPEG 压缩格式解码为位图),大图的解码可能耗时数十毫秒。
  • 对象创建与销毁:大量对象的 alloc/dealloc(尤其涉及 ARC 的 retain/release 操作和 SideTable 锁竞争)。
  • 数据库/文件 I/O:主线程同步读写磁盘。
  • 锁等待:主线程等待子线程持有的锁。

GPU 侧

  • 离屏渲染(Offscreen Rendering)cornerRadius + masksToBoundsshadowmaskgroup opacity 等会触发离屏渲染,GPU 需要额外创建帧缓冲区。
  • 过度绘制(Overdraw):大量重叠的不透明图层导致 GPU 重复渲染。
  • 大图纹理:超大图片上传到 GPU 的纹理缓存,占用大量显存和带宽。
  • 图层爆炸:大量 CALayer 导致合成开销增大。

6.3 卡顿监控方案

6.3.1 方案一:RunLoop Observer 监控

原理:主线程的所有任务都在 RunLoop 中执行。通过监听 RunLoop 的状态变化,检测两个关键时间间隔:

  • kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting(Source 处理阶段):如果这个间隔过长,说明 Source0 事件处理耗时过久(如触摸事件处理中有耗时操作)。
  • kCFRunLoopAfterWaiting 到下一次 kCFRunLoopBeforeWaiting(被唤醒后的处理阶段):如果这个间隔过长,说明被唤醒后的任务处理耗时过久。

实现思路

  1. 在主线程注册一个 CFRunLoopObserver,监听所有状态变化。
  2. 在 Observer 回调中记录状态变化的时间戳和当前状态。
  3. 创建一个子线程,用信号量(dispatch_semaphore)定期检测(如每 50ms 一次)主线程 RunLoop 是否长时间停留在某个状态。
  4. 如果连续多次(如 3 次)检测到主线程处于同一个状态超过阈值(如 250ms),判定为卡顿。
  5. 在子线程中抓取主线程的调用堆栈。

卡顿判定策略

  • 超过 1 帧(16ms):微卡顿,通常不记录。
  • 超过 3 帧(50ms):轻微卡顿。
  • 超过 250ms:明显卡顿,需要记录堆栈。
  • 超过 3s:严重卡顿(ANR),需要立即上报。

6.3.2 方案二:子线程 Ping(心跳检测)

原理:子线程定期向主线程发送一个"心跳"任务(通过 dispatch_async 派发到主队列),如果主线程在规定时间内未能执行该任务,则认为主线程被阻塞。

实现思路

  1. 子线程设置一个 flag 为 false,通过 dispatch_async(dispatch_get_main_queue(), ^{ flag = true; }) 发送心跳。
  2. 子线程等待一段时间(如 500ms 或 1s)。
  3. 检查 flag:如果仍为 false,说明主线程在此期间一直忙碌,判定为卡顿。
  4. 抓取主线程堆栈。

优缺点比较

  • RunLoop Observer 方案更精确,能定位到具体的 RunLoop 阶段,但实现复杂。
  • 心跳检测方案简单可靠,但只能检测到"主线程忙",无法区分是哪种任务导致的。

6.3.3 方案三:CADisplayLink 帧率监控

利用 CADisplayLink 的回调计算实际帧率。CADisplayLink 会在每次屏幕刷新前调用回调,如果两次回调的间隔超过 16.67ms,说明发生了掉帧。

局限性:只能检测掉帧的发生和严重程度,无法直接获取卡顿原因的堆栈信息。通常作为辅助监控手段,与上述方案配合使用。

6.3.4 方案四:基于 MetricKit(iOS 14+)

MXHangDiagnostic 提供系统级别的卡顿诊断信息,包括卡顿时长和调用堆栈。MXCPUExceptionDiagnostic 报告 CPU 异常使用情况。

优点是零性能开销(系统在后台采集),缺点是数据延迟(次日推送),适合线上监控而非实时调试。

6.4 堆栈采集

卡顿检测到后,最关键的是采集主线程的调用堆栈,用于定位卡顿的根因。

6.4.1 基于 mach_thread API

使用 task_threads() 获取所有线程列表,通过 thread_get_state() 获取目标线程(主线程)的寄存器状态(包含 PC、FP、LR 等),然后沿着 Frame Pointer(FP)链回溯调用栈,结合 DWARF 调试信息或 dSYM 文件符号化。

6.4.2 基于 backtrace() / backtrace_symbols()

标准 POSIX 接口,但只能获取当前线程的堆栈,无法跨线程采集。

6.4.3 基于 PLCrashReporter

开源的崩溃报告库,提供了安全的跨线程堆栈采集能力(信号安全、锁安全),是业界常用方案。

6.5 堆栈聚合与分析

  • 调用树合并:将多次采集的堆栈按调用路径合并成火焰图/调用树,识别热点函数。
  • 符号化:将内存地址转换为函数名+偏移量,需要对应版本的 dSYM 文件。使用 atos 命令或 dwarfdump 工具。
  • 去噪:过滤系统框架的堆栈帧(如 CFRunLoopRunSpecificmach_msg_trap),聚焦业务代码。

6.6 治理策略

  • 文本异步计算:使用 NSAttributedStringboundingRectWithSize: 在子线程预计算文本高度。
  • 图片异步解码:在子线程用 CGBitmapContextCreate + CGContextDrawImage 强制解码图片,主线程直接使用解码后的位图。
  • 预排版/预计算:Cell 的高度、布局信息在数据到达时在子线程预计算完成,主线程直接使用。
  • 按需加载:屏幕外的 Cell 不进行复杂渲染。
  • 减少离屏渲染:用 UIBezierPath + CAShapeLayer 替代 cornerRadius + masksToBounds;用 shadowPath 替代自动计算的阴影。
  • 异步绘制:使用 drawRect: 在后台线程绘制位图,再赋值给 CALayer.contents(参考 Texture/AsyncDisplayKit 框架的思想)。

7. AFNetworking

7.1 整体架构

AFNetworking 是 iOS/macOS 上最流行的网络库。目前主流版本为 AFNetworking 4.x,完全基于 NSURLSession(3.x 开始移除了 NSURLConnection 支持)。

核心架构分层:

┌────────────────────────────────────────────┐
│           AFHTTPSessionManager            │  ← 最高层:便捷 HTTP 接口
│     (GET/POST/PUT/DELETE 等快捷方法)       │
├────────────────────────────────────────────┤
│           AFURLSessionManager             │  ← 核心层:Session 管理
│   (NSURLSession delegate 的完整实现)       │
├────────────────────────────────────────────┤
│  AFURLRequestSerialization                │  ← 请求序列化
│  (HTTP/JSON/PropertyList Request)         │
├────────────────────────────────────────────┤
│  AFURLResponseSerialization               │  ← 响应反序列化
│  (HTTP/JSON/XML/Image/PropertyList)       │
├────────────────────────────────────────────┤
│  AFSecurityPolicy                         │  ← 安全策略(HTTPS/证书验证)
├────────────────────────────────────────────┤
│  AFNetworkReachabilityManager             │  ← 网络状态监听
└────────────────────────────────────────────┘

7.2 AFURLSessionManager 深入解析

7.2.1 核心职责

AFURLSessionManager 是整个库的心脏,它:

  • 持有并管理一个 NSURLSession 实例。
  • 实现了 NSURLSessionDelegateNSURLSessionTaskDelegateNSURLSessionDataDelegateNSURLSessionDownloadDelegate 四个协议的所有关键方法。
  • 维护一个 mutableTaskDelegatesKeyedByTaskIdentifier 字典,将每个 NSURLSessionTask 映射到一个 AFURLSessionManagerTaskDelegate 对象,实现任务级别的回调隔离。

7.2.2 线程安全设计

  • 使用 NSLock(名为 lock)保护 mutableTaskDelegatesKeyedByTaskIdentifier 字典的并发访问。
  • NSURLSession 的 delegate 回调在一个专用的串行 OperationQueueoperationQueue.maxConcurrentOperationCount = 1)上执行,保证回调的串行化,避免多线程问题。
  • 完成回调(success/failure block)默认 dispatch 到主队列(completionQueue 默认为 dispatch_get_main_queue()),保证 UI 更新的线程安全。开发者也可以自定义 completionQueuecompletionGroup

7.2.3 任务代理(AFURLSessionManagerTaskDelegate)

每个 NSURLSessionTask 对应一个 AFURLSessionManagerTaskDelegate 实例,它负责:

  • 收集响应数据:在 URLSession:dataTask:didReceiveData: 中将接收到的数据追加到 mutableData 中。
  • 跟踪上传/下载进度:通过 NSProgress 对象提供 KVO 兼容的进度更新。
  • 任务完成时:根据 responseSerializer 反序列化响应数据,在 completionQueue 上回调 success/failure block。

7.2.4 KVO 与通知机制

AFNetworking 大量使用了 KVO 和 NSNotification:

  • NSURLSessionTaskstate 属性进行 KVO 观察,当任务状态变为 completed 时自动清理。
  • 任务 resume/suspend/complete 时发送全局通知(如 AFNetworkingTaskDidResumeNotification),方便外部监听(如网络活动指示器 AFNetworkActivityIndicatorManager)。
  • 使用 Method Swizzling 交换了 NSURLSessionTaskresumesuspend 方法,在调用时发送通知。这是因为 NSURLSession 不对 task 的 state 变化发送 KVO 通知,AF 需要自己实现。

7.3 请求序列化(AFURLRequestSerialization)

7.3.1 AFHTTPRequestSerializer

基础的 HTTP 请求序列化器:

  • 设置通用 HTTP Header(User-Agent、Accept-Language、Authorization 等)。
  • 将参数字典编码为 URL query string(GET/HEAD/DELETE)或 HTTP body(POST/PUT/PATCH)。
  • 参数编码规则:对键值对进行百分号编码(Percent Encoding),嵌套字典和数组使用方括号语法(key[subkey]=valuekey[]=value)。
  • multipartFormData:支持 multipart/form-data 编码,用于文件上传。内部使用 AFMultipartBodyStream(自定义的 NSInputStream 子类)实现流式上传,避免将整个文件载入内存。

7.3.2 AFJSONRequestSerializer

继承自 AFHTTPRequestSerializer,将参数字典使用 NSJSONSerialization 编码为 JSON 格式放入 HTTP Body,设置 Content-Typeapplication/json

7.4 响应序列化(AFURLResponseSerialization)

响应序列化器负责验证响应的合法性并将数据转换为目标格式。

7.4.1 验证机制

所有序列化器都继承自 AFHTTPResponseSerializer,它的 validateResponse:data:error: 方法检查:

  • HTTP 状态码是否在 acceptableStatusCodes(默认 200~299)范围内。
  • 响应的 Content-Type 是否在 acceptableContentTypes 集合中。

如果验证失败,生成对应的 NSErrorAFURLResponseSerializationErrorDomain),并将响应数据放入 error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] 中,方便调试。

7.4.2 AFJSONResponseSerializer

使用 NSJSONSerialization 将 Data 解析为字典/数组。支持自动移除 JSON 中的 NSNull 值(removesKeysWithNullValues 属性)。

7.4.3 AFImageResponseSerializer

将 Data 解码为 UIImage。支持自动解压(inflate)——在子线程强制解码图片位图,避免在主线程首次渲染时的解码开销(与 SDWebImage 的思路一致)。

7.5 安全策略(AFSecurityPolicy)

7.5.1 三种验证模式

模式 说明 安全级别
AFSSLPinningModeNone 使用系统默认的证书链验证
AFSSLPinningModeCertificate 将服务器证书与 App 内预埋的证书进行完整比对 最高
AFSSLPinningModePublicKey 只比对证书中的公钥(Public Key) 高(推荐)

7.5.2 证书验证流程

  1. 获取服务器返回的证书链(SecTrustRef)。
  2. 设置锚点证书(Anchor Certificates)为 App 预埋的证书。
  3. 调用 SecTrustEvaluateWithError() 进行系统级证书链验证。
  4. 根据 Pinning Mode:
    • Certificate Mode:逐一比对证书的 DER 编码数据。
    • PublicKey Mode:提取证书的公钥数据进行比对。
  5. validatesDomainName:是否验证证书中的域名与请求域名匹配。

7.5.3 公钥固定的优势

比证书固定更灵活——即使服务器更换了证书(只要使用相同的密钥对),App 无需更新。

7.6 网络可达性(AFNetworkReachabilityManager)

基于 SCNetworkReachability(SystemConfiguration 框架),监听网络状态变化。

核心流程:

  1. 使用 SCNetworkReachabilityCreateWithAddressSCNetworkReachabilityCreateWithName 创建 reachability 引用。
  2. 设置回调函数,当网络状态变化时触发。
  3. 将 reachability 引用加入 RunLoop(CFRunLoopGetMain())以持续监听。
  4. 回调中解析 SCNetworkReachabilityFlags,判断:
    • 是否可达(kSCNetworkReachabilityFlagsReachable)。
    • 是否通过 WWAN(kSCNetworkReachabilityFlagsIsWWAN)。

注意:SCNetworkReachability 检测的是"是否有网络路径",不是"是否能真正连通互联网"。飞行模式能检测到,但连上 WiFi 但无法上网的情况检测不到。

7.7 与 Alamofire 的对比

Alamofire 是 AFNetworking 作者在 Swift 生态下的重写,核心思想一致但做了现代化改进:

对比维度 AFNetworking Alamofire
语言 Objective-C Swift
并发模型 GCD + NSOperationQueue Swift Concurrency (async/await)
请求构建 Mutable URL Request 链式调用 + Request 协议
响应处理 Block 回调 Result + async/await
拦截器 需自行封装 内置 RequestInterceptor 协议
重试 需自行实现 内置 RetryPolicy

8. SDWebImage

8.1 整体架构

SDWebImage 是 iOS 上最广泛使用的图片加载和缓存库。其核心设计哲学是将复杂的图片加载流程封装为简洁的 API(如 sd_setImageWithURL:),同时提供高度可定制的扩展点。

架构分层:

┌──────────────────────────────────────────────────┐
│              UIView+WebCache                     │  ← 最上层:UIKit 扩展
│  (UIImageView / UIButton 的分类方法)              │
├──────────────────────────────────────────────────┤
│              SDWebImageManager                   │  ← 核心调度器
│  (协调缓存查找和网络下载)                          │
├──────────────┬───────────────────────────────────┤
│ SDImageCache │  SDWebImageDownloader             │  ← 缓存 / 下载
│ (内存+磁盘)   │  (网络下载管理)                    │
├──────────────┴───────────────────────────────────┤
│ SDWebImageDownloaderOperation                    │  ← 下载操作
│ (基于 NSURLSessionDataTask 的下载单元)             │
├──────────────────────────────────────────────────┤
│ SDImageCoder / SDImageTransformer                │  ← 编解码 / 变换
│ (PNG/JPEG/GIF/WebP/HEIF 编解码, 圆角/缩放等)      │
└──────────────────────────────────────────────────┘

8.2 加载流程全景

当调用 [imageView sd_setImageWithURL:url] 时,完整的执行流程:

Step 1:取消旧任务 取消该 UIImageView 上一次尚未完成的图片加载任务(通过关联对象存储的 operation key)。这避免了 Cell 复用场景下的图片错乱问题。

Step 2:设置占位图 如果提供了 placeholder,立即在主线程设置占位图。

Step 3:查询缓存 SDWebImageManager 调用 SDImageCache 查询缓存:

  • 内存缓存查询SDMemoryCache(基于 NSCache)中以 URL 的 MD5/SHA256 哈希为 key 查找。命中则直接返回。
  • 磁盘缓存查询:如果内存未命中,在串行 I/O 队列ioQueue)中异步查询磁盘缓存。磁盘缓存使用文件存储,文件名为 URL 的 MD5 哈希值。查询过程包括:
    1. 检查文件是否存在(fileExistsAtPath:)。
    2. 读取文件数据。
    3. 对图片进行解码(从 PNG/JPEG 数据解码为位图)。
    4. 将解码后的图片写入内存缓存(回填)。

Step 4:网络下载 如果缓存完全未命中(或设置了 SDWebImageRefreshCached 选项),启动网络下载:

  • SDWebImageDownloader 创建或复用一个 SDWebImageDownloaderOperation
  • 同一个 URL 的多次请求会被合并(Coalescing)——只发一次网络请求,结果回调给所有等待者。这通过 URLOperations 字典(以 URL 为 key)实现。
  • 下载操作基于 NSURLSessionDataTask

Step 5:图片处理 下载完成后:

  1. 在子线程进行图片解码(Decode)。
  2. 如果设置了 SDImageTransformer(如圆角、缩放、高斯模糊),在子线程执行变换。
  3. 将处理后的图片同时写入内存缓存和磁盘缓存。

Step 6:回调主线程 在主线程设置 imageView.image,触发 UI 更新。支持渐变动画(SDWebImageTransition)。

8.3 缓存机制深入解析

8.3.1 内存缓存(SDMemoryCache)

继承自 NSCache,具备以下特性:

  • 自动淘汰:当系统内存紧张时,NSCache 会自动释放对象。开发者可以设置 countLimit(最大数量)和 totalCostLimit(最大总开销,以图片像素数为 cost)。
  • 线程安全NSCache 内部使用锁保护,可以在任意线程安全访问。
  • 弱引用表(mapTable):SDWebImage 额外维护了一个 NSMapTable(weakToStrongObjects),当 NSCache 因内存压力淘汰了某张图片时,如果该图片仍被某个 UIImageView 持有(强引用),通过 mapTable 仍然可以找到它,避免不必要的重新解码/下载。

8.3.2 磁盘缓存(SDDiskCache)

  • 存储格式:原始的图片数据(未解码的 PNG/JPEG/WebP 数据),不是解码后的位图。这大幅减少了磁盘空间占用。
  • 文件命名:URL 的 MD5 哈希值作为文件名,避免特殊字符问题。
  • 过期策略:默认缓存保留 1 周maxDiskAge = 60 * 60 * 24 * 7)。
  • 容量限制:可设置 maxDiskSize(最大磁盘缓存大小),超限时按最近最久未使用(LRU) 策略淘汰——根据文件的 NSFileModificationDate(修改日期)排序,优先删除最旧的文件,直到缓存大小降至限制的一半。
  • 清理时机
    • App 进入后台时(UIApplicationDidEnterBackgroundNotification)触发异步清理。
    • App 终止时(UIApplicationWillTerminateNotification)触发清理。
    • 开发者手动调用 clearDiskOnCompletion:

8.3.3 缓存 Key 的计算

默认使用完整的 URL 字符串作为缓存 key。开发者可以通过 SDWebImageManagercacheKeyFilter block 自定义 key 生成逻辑(例如去除 URL 中的 token 参数,使相同内容的不同签名 URL 共享缓存)。

如果使用了 SDImageTransformer,变换后的图片使用 originalKey + transformerKey 作为缓存 key,与原图分开缓存。

8.4 图片解码机制

8.4.1 为什么需要预解码

UIImageimageWithData: 创建的图片是未解码的——它只是持有压缩的图片数据。只有在图片首次被渲染到屏幕上时(CALayerdisplay 方法中),Core Animation 才会调用解码器将其解码为位图。这个解码发生在主线程,可能导致掉帧。

SDWebImage 的策略是在子线程提前解码(Force Decode / Decompressing),将位图缓存到内存中,主线程直接使用解码后的位图,消除主线程解码开销。

8.4.2 解码实现

解码的核心步骤:

  1. 创建 CGBitmapContext(位图上下文),指定颜色空间、每像素字节数、Alpha 通道信息。
  2. 使用 CGContextDrawImageCGImageRef 绘制到上下文中——这一步触发实际的解码。
  3. 从上下文中获取解码后的 CGImageRef,创建新的 UIImage

内存占用计算:一张 1000×1000 的图片解码后占用 1000 × 1000 × 4 bytes = 4MB(RGBA 格式,每像素 4 字节)。因此,SDWebImage 提供了 SDImageCoderDecodeScaleDownLimitBytes 选项,对超大图片进行降采样后再解码,避免内存暴涨。

8.4.3 渐进式解码(Progressive Decoding)

对于 JPEG 等支持渐进式加载的格式,SDWebImage 可以在下载过程中边下载边解码。每接收一段数据就解码一次,UI 上展示从模糊到清晰的渐进效果。

通过 SDImageCoderProgressiveCoder 协议实现,每次调用 updateIncrementalData:finished: 更新数据并产生部分解码的图片。

8.4.4 编解码器架构(SDImageCoder)

SDWebImage 5.x 使用了协议化的编解码器架构:

  • SDImageCoder 协议定义了 canDecodeFromData:decodedImageWithData:encodedDataWithImage: 等方法。
  • 内置编解码器:SDImageIOCoder(PNG/JPEG/TIFF/GIF 静图)、SDImageGIFCoder(GIF 动图)、SDImageAPNGCoder(APNG)。
  • 可扩展:通过 SDImageCodersManager 注册自定义编解码器,如 SDImageWebPCoder(WebP 支持)、SDImageHEICCoder(HEIC 支持)。
  • 解码器按注册的逆序遍历(后注册的优先),调用 canDecodeFromData: 判断哪个解码器能处理当前数据格式。

8.5 下载机制深入

8.5.1 SDWebImageDownloader

  • 维护一个 NSOperationQueuedownloadQueue),控制最大并发下载数(默认 6)。
  • 支持 LIFO(后进先出)和 FIFO(先进先出)两种执行顺序。LIFO 适合瀑布流场景——用户快速滑动时,最新可见的 Cell 的图片优先下载。通过设置 Operation 之间的依赖关系实现 LIFO。
  • 支持 HTTP Header 自定义、认证(URLCredential)、超时配置等。

8.5.2 SDWebImageDownloaderOperation

继承自 NSOperation,内部封装了一个 NSURLSessionDataTask

关键设计:

  • 回调合并:使用 callbackBlocks 数组存储所有对同一 URL 的下载回调。当下载完成时,遍历数组逐一回调。
  • 后台下载:支持 App 进入后台后继续下载(通过 UIApplication.beginBackgroundTaskWithExpirationHandler:)。
  • 响应数据拼接:在 URLSession:dataTask:didReceiveData: 中将数据追加到 NSMutableDataimageData),下载完成后一次性交给解码器。
  • 取消机制:调用 cancel 时取消 NSURLSessionDataTask,从 callbackBlocks 中移除对应的回调。如果所有回调都被移除,则取消整个下载任务。

8.5.3 URL 请求去重(Coalescing)

SDWebImageDownloader 维护一个 URLOperations 字典(以 URL 为 key,以 SDWebImageDownloaderOperation 为 value)。当新请求到来时:

  • 如果该 URL 已有进行中的下载操作,直接将新的回调添加到现有 Operation 的 callbackBlocks 中,不创建新的网络请求。
  • 如果没有,创建新的 Operation 并加入队列。

这种设计在列表场景下极为高效——同一张头像被多个 Cell 引用时,只会发起一次网络请求。

8.6 UIView+WebCache 的设计

通过 ObjC Runtime 的关联对象机制,为 UIImageView 等视图绑定当前的加载操作。

核心流程:

  1. 调用 sd_setImageWithURL: 时,先通过 sd_cancelCurrentImageLoad 取消当前关联的旧操作。
  2. 使用 objc_setAssociatedObject 将新的 SDWebImageCombinedOperation 关联到视图上。
  3. 加载完成或 Cell 复用时,通过 objc_getAssociatedObject 获取并取消/检查操作状态。

这解决了经典的 Cell 复用导致图片错乱问题:当 Cell 被复用时,旧 Cell 的下载完成回调中设置的图片会被忽略(因为旧操作已被取消)。

8.7 动图支持

8.7.1 GIF / APNG

SDWebImage 使用 SDAnimatedImageView(继承自 UIImageView)播放动图。其内部实现:

  • 使用 CADisplayLink 驱动动画帧切换。
  • 按需解码:不一次性解码所有帧(一个 GIF 可能有数百帧,全部解码会占用大量内存),而是维护一个帧缓存(NSMutableDictionary),预解码当前帧附近的若干帧(预取缓冲区),按需释放远离当前播放位置的帧。
  • 帧缓冲区大小根据可用内存动态调整。

8.7.2 WebP / HEIF

通过可插拔的编解码器支持:

  • SDImageWebPCoder:使用 libwebp 库进行 WebP 编解码。
  • SDImageHEICCoder:使用系统 ImageIO 框架进行 HEIF 编解码(iOS 11+)。

8.8 性能优化细节

  • 异步 I/O:磁盘缓存的所有读写操作都在专用的串行 ioQueue 上异步执行,不阻塞主线程。
  • 解码降采样:对于超大图片(如 4000×3000 的相机照片),先使用 CGImageSourceCreateThumbnailAtIndex 进行降采样到目标显示尺寸,再解码。这比先解码再缩放效率高得多——直接操作压缩数据,内存峰值大幅降低。
  • 内存警告响应:监听 UIApplicationDidReceiveMemoryWarningNotification,立即清空内存缓存(NSCacheremoveAllObjects)。
  • URL 黑名单:对于下载失败的 URL(非超时错误),加入 failedURLs 集合,短期内不再重试,避免无效请求浪费资源(可通过 SDWebImageRetryFailed 选项关闭此行为)。
  • Prefetch(预加载)SDWebImagePrefetcher 支持批量预加载图片到缓存中,适用于已知用户即将浏览的内容(如下一页的列表数据)。

8.9 SDWebImage 5.x 的架构升级

SDWebImage 5.x 相比 4.x 做了大量架构优化:

特性 4.x 5.x
编解码 硬编码在内部 协议化(SDImageCoder)
缓存 固定实现 协议化(SDImageCache Protocol)
下载 固定实现 协议化(SDImageLoader Protocol)
变换 需第三方库 内置 SDImageTransformer
动图 FLAnimatedImage 依赖 内置 SDAnimatedImage
指标 SDImageLoadIndicator

协议化设计使得每个组件都可以被替换为自定义实现,极大提升了灵活性。


总结

上述八个知识点构成了 iOS 开发中性能优化与底层原理的核心体系:

  • 启动流程启动优化帮助我们理解 App 从点击图标到用户可见的完整链路,并从 pre-main 和 post-main 两个阶段系统性地优化启动速度。
  • 网络优化覆盖了从 DNS 到数据传输、从连接管理到弱网对抗的全链路优化策略。
  • RunLoop 是 iOS 事件驱动模型的基石,理解它才能理解触摸事件、Timer、UI 刷新等核心机制的运作方式。
  • Runtime 是 Objective-C 动态性的根基,消息发送、方法缓存、消息转发、KVO、Category 等特性都建立在它之上。
  • 卡顿监控将 RunLoop 和性能分析结合,提供了从检测到治理的完整方案。
  • AFNetworkingSDWebImage 作为两个最经典的第三方库,它们的架构设计、线程安全策略、性能优化思路值得深入学习和借鉴。

Swift 全面深入指南

本文从底层原理、横向对比、纵向深度、性能优化、难点问题、高难度原理六大维度,对 Swift 语言进行全面、细致、深入的梳理。


第一部分:Swift 基础与底层原理


1. 值类型 vs 引用类型

1.1 核心区别

维度 值类型 (Value Type) 引用类型 (Reference Type)
代表 struct, enum, tuple class, closure
存储 栈(小对象)/ 堆(大对象或含引用)
赋值语义 拷贝(Copy-on-Write 优化) 共享引用
线程安全 天然线程安全(独立副本) 需要同步机制
引用计数 有 ARC
继承 不支持(enum/struct) 支持(class)
deinit 不支持 支持
Identity 无 === 操作 有 === 操作

1.2 底层内存布局

struct 布局:

  • struct 的成员按声明顺序存储(有对齐填充)
  • 小 struct(通常 ≤ 3 个 word,即 24 字节 on 64-bit)直接在栈上分配
  • 大 struct 或含引用类型成员时,可能会被编译器优化到堆上(间接存储)
  • 作为协议的 existential container 时,超过 3 word 会触发堆分配

class 布局:

  • 堆上分配,包含:
    • isa 指针(8 字节):指向类的元数据(metadata),用于动态派发
    • 引用计数(8 字节):strong count + unowned count + weak count(打包在一个 64-bit InlineRefCounts 中)
    • 实例变量:按声明顺序排列,有对齐
  • 总 overhead 至少 16 字节(isa + refcount),加上 malloc 的 16 字节对齐 overhead

1.3 Copy-on-Write (COW) 深入

标准库 COW 实现(Array/Dictionary/Set/String):

  • 内部持有一个引用类型的 buffer(如 _ArrayBuffer
  • 赋值时只复制 buffer 的引用(引用计数 +1),O(1)
  • 写入前检查 isKnownUniquelyReferenced(&buffer)
    • 如果引用计数 == 1,直接修改(无拷贝)
    • 如果引用计数 > 1,先深拷贝 buffer,再修改
  • 注意:自定义 struct 不会自动获得 COW,需要手动实现

自定义 COW 模式:

final class Storage<T> {
    var value: T
    init(_ value: T) { self.value = value }
}
struct COWWrapper<T> {
    private var storage: Storage<T>
    init(_ value: T) { storage = Storage(value) }
    var value: T {
        get { storage.value }
        set {
            if !isKnownUniquelyReferenced(&storage) {
                storage = Storage(newValue)
            } else {
                storage.value = newValue
            }
        }
    }
}

1.4 struct 中包含引用类型的代价

  • struct 拷贝时,内部引用类型成员的引用计数也要 +1
  • 如果 struct 有 N 个引用类型成员,一次拷贝就有 N 次 retain
  • 这就是为什么大量包含引用类型的 struct 拷贝比纯 class 更慢

2. 内存管理 — ARC 深入

2.1 ARC 的本质

  • ARC 是编译器在编译期自动插入 retain/release 调用
  • 不是 GC(垃圾回收),没有 stop-the-world
  • 引用计数操作是原子的(使用 atomic_fetch_add 等),保证线程安全
  • 每次 retain/release 有 CPU 开销(原子操作 + 内存屏障)

2.2 引用计数的存储结构(Swift 5+)

InlineRefCounts (8 bytes):
┌─────────────────────────────────────────────────┐
│ strong RC (32 bit) │ unowned RC (31 bit) │ flags │
└─────────────────────────────────────────────────┘
  • strong count:强引用计数。变为 0 时触发 deinit,释放实例内存(如果无 unowned/weak 引用)
  • unowned count:unowned 引用计数 + 1(自身占 1)。变为 0 时释放 side table / 对象内存
  • weak count:存储在 side table 中。当有 weak 引用时,对象会创建 side table
  • Side Table:当需要 weak 引用或引用计数溢出时,从 InlineRefCounts 切换到指向 side table 的指针

2.3 strong / weak / unowned 深入对比

维度 strong weak unowned
引用计数 +1 strong RC 不增加 strong RC,增加 weak RC(side table) +1 unowned RC
解引用速度 最快(直接访问) 较慢(需要检查 side table) 快(直接访问,但有运行时检查)
置 nil 不会 对象释放后自动置 nil 不会(对象释放后访问触发 fatal error)
Optional 不要求 必须 Optional 不要求 Optional
内存释放时机 strong RC = 0 时 deinit 不影响释放 不影响 deinit,但影响内存回收
适用场景 默认所有权 delegate、可能为 nil 的反向引用 生命周期确定不短于自身的引用

unowned 的危险与底层:

  • unowned 引用在对象 deinit 后,内存不会立即释放(因为 unowned count > 0)
  • 访问已 deinit 的 unowned 引用会触发 runtime trap(不是野指针,是确定性崩溃)
  • unowned(unsafe) 可以跳过检查,行为类似 C 的悬垂指针,性能最高但最危险

weak 的底层机制:

  • weak 引用不直接指向对象,而是通过 side table 间接引用
  • 对象 deinit 时,runtime 遍历 side table 将所有 weak 引用置 nil
  • 这就是为什么 weak 必须是 Optional —— 因为可能被置 nil
  • weak 的读取需要加锁(原子操作),有性能开销

2.4 循环引用的三种场景与解决

场景一:两个对象互相持有

class A { var b: B? }
class B { var a: A? }  // 循环引用!
// 解决:B 中用 weak var a: A?

场景二:闭包捕获 self

class ViewController {
    var handler: (() -> Void)?
    func setup() {
        handler = { self.doSomething() }  // self 持有 handler,handler 捕获 self
    }
}
// 解决:handler = { [weak self] in self?.doSomething() }

场景三:嵌套闭包中的 capture list

handler = { [weak self] in
    guard let self = self else { return }
    // 这里 self 是 strong 的局部变量,闭包执行期间不会释放
    someAsyncCall {
        self.doSomething()  // 安全,因为外层已经 guard 了
    }
}

2.5 Autorelease Pool 在 Swift 中的角色

  • Swift 原生对象不使用 autorelease(ARC 直接管理)
  • 但与 ObjC 交互时(调用返回 ObjC 对象的方法),仍可能进入 autorelease pool
  • autoreleasepool { } 在 Swift 中仍然可用,用于循环中大量创建临时 ObjC 对象时控制内存峰值

3. 协议 (Protocol) 底层原理

3.1 协议的两种使用方式

作为泛型约束(Static Dispatch):

func process<T: MyProtocol>(_ value: T) { value.doSomething() }
  • 编译期确定类型,静态派发
  • 编译器为每个具体类型生成特化版本(monomorphization / specialization)
  • 性能最优,等同于直接调用

作为存在类型(Dynamic Dispatch):

func process(_ value: MyProtocol) { value.doSomething() }
// Swift 5.6+ 显式写法:func process(_ value: any MyProtocol)
  • 运行时通过 Existential Container 动态派发
  • 有性能开销

3.2 Existential Container 详细结构

Existential Container (5 words = 40 bytes on 64-bit):
┌──────────────────────────────────────────┐
│  Value Buffer (3 words = 24 bytes)       │  ← 存储值或指向堆的指针
│  Metadata Pointer (1 word = 8 bytes)     │  ← 指向类型元数据
│  PWT Pointer (1 word = 8 bytes)          │  ← Protocol Witness Table 指针
└──────────────────────────────────────────┘

Value Buffer 策略:

  • 值 ≤ 24 字节:inline 存储,直接放在 buffer 中(无堆分配)
  • 值 > 24 字节:buffer 中存指向堆分配内存的指针
  • 这就是为什么小 struct 遵循协议时没有额外堆分配

Protocol Witness Table (PWT):

  • 每个「类型 + 协议」组合有一张 PWT
  • PWT 是一个函数指针数组,每个协议方法对应一个条目
  • 调用协议方法时:从 existential container 取出 PWT → 查表 → 间接调用
  • 类似于 C++ 的 vtable,但针对的是协议而非类继承

Value Witness Table (VWT):

  • 每个类型有一张 VWT,描述该类型的内存操作
  • 包含:size、alignment、copy、move、destroy 等函数指针
  • 存在类型赋值/拷贝时,通过 VWT 执行正确的内存操作

3.3 协议组合与多协议 existential

func process(_ value: ProtocolA & ProtocolB) { ... }
  • existential container 会包含多个 PWT 指针(每个协议一个)
  • 容器大小 = 24(buffer)+ 8(metadata)+ 8 × N(N 个协议的 PWT)

3.4 Class-Only Protocol 的优化

protocol MyDelegate: AnyObject { ... }
  • 编译器知道遵循者一定是 class(引用类型)
  • existential container 退化为:1 个 word 的引用 + metadata + PWT
  • 不需要 24 字节的 value buffer
  • 可以使用 weak/unowned 修饰

3.5 协议扩展 vs 协议要求方法的派发差异

protocol Greetable {
    func greet()  // 协议要求:PWT 动态派发
}
extension Greetable {
    func greet() { print("Hello") }     // 默认实现
    func farewell() { print("Bye") }    // 扩展方法:静态派发!
}
struct Person: Greetable {
    func greet() { print("Hi, I'm a person") }
    func farewell() { print("See you") }
}

let p: Greetable = Person()
p.greet()     // "Hi, I'm a person" —— 动态派发,走 PWT
p.farewell()  // "Bye" —— 静态派发!走协议扩展的默认实现

关键区别:

  • 协议要求中声明的方法 → 在 PWT 中有条目 → 动态派发 → 能被遵循者重写
  • 仅在协议扩展中定义的方法 → PWT 中无条目 → 静态派发 → 根据编译期类型决定

4. 泛型底层原理

4.1 泛型的实现方式

Swift 泛型采用类型擦除 + 运行时传递元数据的策略(不同于 C++ 的完全模板实例化):

  • 编译器生成一份泛型函数的代码(不是每个类型一份)
  • 运行时通过隐藏参数传递 type metadatawitness table
  • 但在开启优化(-O)时,编译器会进行泛型特化(specialization),为常用类型生成直接调用的版本

4.2 泛型特化 (Specialization)

func swap<T>(_ a: inout T, _ b: inout T) { ... }
// 编译器优化后可能生成:
// swap_Int(...)  ← 针对 Int 的特化版本
// swap_String(...)  ← 针对 String 的特化版本
// swap_generic(...)  ← 通用版本(需要 metadata)

特化条件:

  • 编译器能看到泛型函数的实现(同一模块 或 @inlinable
  • 能确定具体类型
  • 优化级别 -O 或 -Osize

跨模块特化:

  • 默认不能跨模块特化(泛型函数实现不可见)
  • @inlinable 将函数体暴露给其他模块,允许跨模块特化
  • @frozen 将 struct 布局暴露给其他模块

4.3 Type Erasure(类型擦除)模式

问题: 带 associatedtype 的协议不能直接作为存在类型

protocol Iterator {
    associatedtype Element
    func next() -> Element?
}
// let iter: Iterator  ← 编译错误(Swift 5.6 以前)
// let iter: any Iterator  ← Swift 5.7+ 部分支持

经典手动类型擦除:

struct AnyIterator<Element>: IteratorProtocol {
    private let _next: () -> Element?
    init<I: IteratorProtocol>(_ iterator: I) where I.Element == Element {
        var iter = iterator
        _next = { iter.next() }
    }
    func next() -> Element? { _next() }
}

原理: 用闭包捕获具体类型实例,对外暴露统一的泛型接口,擦除了具体类型信息。

4.4 some vs any(Swift 5.7+)

维度 some Protocol (Opaque Type) any Protocol (Existential Type)
底层 编译期确定的固定类型(对调用者隐藏) 运行时动态类型(existential container)
派发 静态派发 动态派发(PWT)
性能 高(无间接开销) 低(堆分配 + 间接调用)
类型一致性 同一函数返回的 some P 保证是同一类型 不保证
适用 返回值、属性 参数、集合元素

5. 方法派发 (Method Dispatch) 全面解析

5.1 四种派发方式

派发方式 速度 机制 适用场景
内联 (Inline) 最快 编译器将函数体直接插入调用点 小函数、@inline(__always)
静态派发 (Static/Direct) 编译期确定函数地址,直接 call struct 方法、final 方法、private 方法
虚表派发 (V-Table) 通过类的虚函数表间接调用 class 的非 final 方法
消息派发 (Message) ObjC runtime 的 objc_msgSend @objc dynamic 方法

5.2 Swift class 的虚函数表

Class Metadata:
┌──────────────────────┐
│  isa (指向 metaclass)  │
│  superclass pointer   │
│  cache (ObjC 兼容)     │
│  data (ObjC 兼容)      │
│  ...                  │
│  V-Table:             │
│    [0] → method1()    │
│    [1] → method2()    │
│    [2] → method3()    │
│    ...                │
└──────────────────────┘
  • 子类的 vtable 包含父类的所有方法条目 + 自己新增的
  • override 时,子类 vtable 中对应位置替换为子类的函数指针
  • 调用时:metadata → vtable[index] → 间接跳转

5.3 各场景的派发方式总结

声明位置 修饰符 派发方式
struct 方法 静态
enum 方法 静态
class 方法 虚表 (V-Table)
class 方法 final 静态
class 方法 private 静态(隐式 final)
class 方法 @objc dynamic 消息 (objc_msgSend)
protocol 要求方法 泛型约束 <T: P> 静态(特化后)/ Witness Table
protocol 要求方法 存在类型 any P PWT 动态派发
protocol 扩展方法 静态
extension of class 静态(不在 vtable 中!)

重要陷阱:class 的 extension 中定义的方法是静态派发!

class Base {
    func inVTable() { print("Base") }  // vtable
}
extension Base {
    func notInVTable() { print("Base ext") }  // 静态派发!
}
class Sub: Base {
    override func inVTable() { print("Sub") }  // OK
    // override func notInVTable() { }  // 编译错误!不能 override
}
let obj: Base = Sub()
obj.inVTable()      // "Sub" —— 动态派发
obj.notInVTable()   // "Base ext" —— 静态派发

5.4 @objc 与 dynamic 的区别

修饰符 作用 派发方式
@objc 将方法暴露给 ObjC runtime 仍然是 vtable(Swift 侧)
dynamic 使用 ObjC 消息派发 objc_msgSend
@objc dynamic 暴露给 ObjC 且使用消息派发 objc_msgSend(可被 KVO/method swizzling)

6. 闭包 (Closure) 底层原理

6.1 闭包的内存结构

闭包在 Swift 中是一个引用类型,底层结构:

Closure = 函数指针 + 上下文 (Context)
┌─────────────────────────┐
│  Function Pointer       │  → 指向闭包体的代码
│  Context (Capture List)  │  → 堆上分配的捕获变量
└─────────────────────────┘
  • 如果闭包不捕获任何变量,退化为普通函数指针(无堆分配)
  • 捕获变量时,编译器创建堆上的 context 对象,存储捕获的变量

6.2 捕获语义

默认捕获:引用捕获(变量)

var x = 10
let closure = { print(x) }
x = 20
closure()  // 输出 20 —— 捕获的是变量本身(引用)

底层:编译器将 x 从栈上提升到堆上的一个 Box 中,闭包和外部代码共享同一个 Box。

capture list 捕获:值捕获

var x = 10
let closure = { [x] in print(x) }
x = 20
closure()  // 输出 10 —— 捕获的是值的拷贝

6.3 逃逸闭包 vs 非逃逸闭包

维度 @escaping 非逃逸(默认)
生命周期 超出函数作用域 函数返回前执行完毕
堆分配 必须堆分配 context 编译器可能优化到栈上
捕获 self 需要显式 self. 不需要
性能 有堆分配开销 可能零开销

withoutActuallyEscaping 允许将非逃逸闭包临时当作逃逸闭包使用(高级场景)。

6.4 @autoclosure

  • 表达式自动包装为闭包,延迟求值
  • 常用于 assert?? 等需要短路求值的场景
  • 底层就是一个无参闭包 () -> T
func logIfTrue(_ condition: @autoclosure () -> Bool) {
    if condition() { print("True") }
}
logIfTrue(2 > 1)  // 2 > 1 被自动包装为 { 2 > 1 }

7. 枚举 (Enum) 底层原理

7.1 简单枚举的内存布局

enum Direction { case north, south, east, west }
// sizeof = 1 字节(只需要区分 4 个 case,1 字节足够 256 个)
  • 底层就是一个整数 tag(鉴别器/discriminator)
  • case 数量 ≤ 256 → 1 字节;≤ 65536 → 2 字节;依此类推

7.2 关联值枚举的内存布局

enum Result {
    case success(Int)    // payload: 8 字节
    case failure(String) // payload: 16 字节
}
// sizeof = max(payload) + tag = 16 + 1 = 17,对齐到 8 → 24 字节
  • 采用 tagged union 策略
  • 大小 = max(所有 case 的 payload) + tag 的大小(可能利用 spare bits 优化)

7.3 Optional 的底层:枚举的极致优化

// Optional<T> 就是:
enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

指针类型的 Optional 优化:

  • Optional<AnyObject> 只占 8 字节(和非 Optional 一样!)
  • 因为指针不可能为 0x0,所以 none 用全零表示,some 用有效指针值
  • 这叫 spare bit optimization —— 利用值中不可能出现的 bit pattern 作为 tag
  • 同理 Optional<Bool> = 1 字节(Bool 只有 0/1,用 2 表示 none)

7.4 indirect enum

indirect enum Tree {
    case leaf(Int)
    case node(Tree, Tree)
}
  • 没有 indirect 时,Tree 大小会无限递归(编译错误)
  • indirect 让关联值通过堆上的 Box 间接引用
  • 底层类似于 case node(Box<Tree>, Box<Tree>),Box 是引用类型

8. Struct vs Class 的性能深入对比

8.1 分配与释放

操作 struct (栈) class (堆)
分配 移动栈指针(1条指令) malloc 系统调用(涉及锁、空闲链表搜索)
释放 移动栈指针 free + 引用计数归零检查
速度比 ~1ns ~25-100ns

8.2 引用计数开销

  • class 每次传递都要 retain(原子操作 ~5ns)
  • 多线程下原子操作可能导致 cache line bouncing
  • struct 无引用计数,但含引用类型成员时有间接引用计数开销

8.3 缓存友好性

  • struct 的数组 [MyStruct]:连续内存,cache 友好
  • class 的数组 [MyClass]:数组存的是指针,实际对象分散在堆上,cache miss 率高
  • 这在遍历大数据集时差距巨大

9. String 底层原理

9.1 Small String Optimization (SSO)

  • Swift String 占 16 字节(2 个 word)
  • 短字符串(≤ 15 字节 UTF-8)直接 inline 存储在这 16 字节中,无堆分配
  • 长字符串在堆上分配 buffer,16 字节中存 buffer 指针 + 长度 + flags
  • 判断标志位在最高位

9.2 String 的字符模型

  • Swift 的 Character扩展字形簇 (Extended Grapheme Cluster)
  • 一个 Character 可能对应多个 Unicode 标量(如 emoji 👨‍👩‍👧‍👦 = 7 个标量)
  • 因此 String.count 是 O(n) 复杂度(需要遍历确定字形簇边界)
  • String.Index 不是整数,是不透明的偏移量,因为字符宽度不固定

9.3 String 的多种视图

视图 元素 场景
string.utf8 UTF8.CodeUnit (UInt8) 网络传输、C 交互
string.utf16 UTF16.CodeUnit (UInt16) NSString 兼容
string.unicodeScalars Unicode.Scalar Unicode 处理
string (默认) Character 用户可见字符

9.4 String 与 NSString 的桥接

  • Swift String 和 NSString 可以零成本桥接(toll-free bridging 的 Swift 版本)
  • 但底层编码不同:Swift 用 UTF-8,NSString 用 UTF-16
  • 桥接时可能触发转码,有性能开销
  • String 被传给 ObjC API 时可能创建临时 NSString(autorelease 对象)

10. 属性 (Property) 的底层机制

10.1 存储属性 vs 计算属性

类型 内存 本质
存储属性 占实例内存 实际的内存字段
计算属性 不占内存 getter/setter 方法
lazy 属性 Optional 存储 首次访问时初始化

10.2 属性观察器 (willSet/didSet) 底层

var name: String {
    willSet { print("将变为 \(newValue)") }
    didSet { print("已从 \(oldValue) 变为 \(name)") }
}

编译器展开为:

var _name: String
var name: String {
    get { _name }
    set {
        let oldValue = _name
        // willSet(newValue)
        _name = newValue
        // didSet(oldValue)
    }
}

注意:init 中赋值不会触发 willSet/didSet。

10.3 Property Wrapper 底层

@propertyWrapper struct Clamped {
    var wrappedValue: Int { ... }
    var projectedValue: Clamped { self }
}
struct Config {
    @Clamped var volume: Int
}

编译器展开为:

struct Config {
    private var _volume: Clamped
    var volume: Int {
        get { _volume.wrappedValue }
        set { _volume.wrappedValue = newValue }
    }
    var $volume: Clamped { _volume.projectedValue }
}

10.4 KeyPath 底层

  • KeyPath 是一个类型安全的属性引用,底层是一个对象
  • 编译器生成一系列 offset/accessor 信息
  • 支持组合(\Base.a.b.c)、运行时读写
  • WritableKeyPathReferenceWritableKeyPath 等继承层级
  • KeyPath 的读取性能接近直接属性访问(编译器优化后)

11. 并发 (Concurrency) — Swift Concurrency

11.1 Actor 模型

actor BankAccount {
    var balance: Double = 0
    func deposit(_ amount: Double) { balance += amount }
}

底层原理:

  • Actor 内部有一个串行执行器 (Serial Executor)
  • 所有对 actor 的方法调用被排列在执行器的队列中
  • 保证同一时刻只有一个任务在执行 actor 的代码
  • 跨 actor 调用需要 await(可能涉及线程切换)

Actor 隔离 (Isolation):

  • Actor 的属性和方法默认是 isolated 的
  • 外部访问需要 await(异步访问)
  • nonisolated 标记的方法可以不需要 await(不能访问 mutable 状态)

11.2 Structured Concurrency

async let result1 = fetchData1()
async let result2 = fetchData2()
let combined = await (result1, result2)
  • 子任务的生命周期绑定到父作用域
  • 父任务取消时,子任务自动取消
  • 子任务完成前,父作用域不会退出

11.3 Task 与 TaskGroup

Task:

  • Task { } 创建非结构化的顶级任务
  • Task.detached { } 创建完全独立的任务(不继承 actor context)
  • Task 持有对结果的引用,可以 await task.value

TaskGroup:

await withTaskGroup(of: Int.self) { group in
    for i in 0..<10 {
        group.addTask { await compute(i) }
    }
    for await result in group { ... }
}

11.4 Sendable 协议

  • 标记类型可以安全跨并发域传递
  • 值类型自动满足 Sendable(如果所有成员都是 Sendable)
  • class 需要满足:final + 所有属性 immutable (let) + Sendable
  • @Sendable 标记闭包,禁止捕获可变状态
  • 编译器在严格并发检查模式下会检查 Sendable 合规性

11.5 async/await 底层 —— Continuation

func fetchData() async -> Data { ... }
  • 编译器将 async 函数转换为状态机
  • 每个 await 点是一个 suspension point(挂起点)
  • 函数被分割为多个 continuation(延续)
  • 挂起时不阻塞线程,线程可以执行其他任务
  • 恢复时通过 continuation 跳回正确的执行点
  • 这就是 协程 (Coroutine) 的实现

与 GCD 的区别:

维度 GCD Swift Concurrency
线程模型 每个 block 可能在不同线程 协程,挂起不占线程
线程爆炸 容易创建过多线程 协作式线程池(线程数 ≤ CPU 核心数)
结构化 Task 层级结构,自动取消传播
安全性 手动保证 Actor 隔离,Sendable 检查

12. 类型系统高级特性

12.1 Phantom Type(幻影类型)

enum Kilometers {}
enum Miles {}
struct Distance<Unit> {
    let value: Double
}
// Distance<Kilometers> 和 Distance<Miles> 是不同类型
// Unit 从未被使用为值,只在类型层面区分 → 零开销

12.2 Result Builder

@resultBuilder struct ArrayBuilder {
    static func buildBlock(_ components: Int...) -> [Int] {
        components
    }
}
  • 编译器将 DSL 块内的语句转换为 buildBlock/buildOptional/buildEither 等方法调用
  • SwiftUI 的 @ViewBuilder 就是 result builder

12.3 Metatype(元类型)

let type: Int.Type = Int.self  // Int 的元类型
let obj = type.init(42)        // 用元类型创建实例
  • .self 获取类型本身的值
  • .Type 是类型的元类型
  • type(of: instance) 获取运行时动态类型
  • 对于 class,type(of:) 返回的可能是子类类型

第二部分:第三方常用库原理


1. Alamofire

1.1 架构分层

Request → SessionManager → URLSession → URLSessionTask
   ↑          ↑                ↑
Encoding   ServerTrust     Interceptor

1.2 核心原理

  • 基于 URLSession 封装,使用 URLSessionDelegate 统一管理回调
  • 请求拦截器 (RequestInterceptor)adapt 修改请求(如添加 token),retry 处理重试
  • 响应序列化:通过 ResponseSerializer 协议将 Data 转为目标类型
  • 请求链:请求排队 → 适配 → 发送 → 验证 → 序列化 → 回调
  • 证书锁定 (Certificate Pinning):通过 ServerTrustManager 实现,防中间人攻击

1.3 重要设计模式

  • Builder 模式:链式调用 .validate().responseDecodable(of:)
  • 命令模式Request 封装了一次完整请求的所有信息
  • 策略模式ParameterEncoding 协议的不同实现(URL/JSON/Custom)

2. Kingfisher

2.1 核心架构

KingfisherManager
  ├── ImageDownloader(网络下载)
  └── ImageCache
        ├── MemoryCache(NSCache)
        └── DiskCache(FileManager)

2.2 缓存策略

  • 内存缓存:基于 NSCache,系统内存紧张时自动清理
  • 磁盘缓存:文件名 = URL 的 MD5 哈希,支持过期时间和大小限制
  • 查找顺序:内存 → 磁盘 → 网络下载
  • 缓存键:默认为 URL 字符串,可自定义 CacheKeyFilter

2.3 图片处理管线

  • Processor:下载后/缓存前进行图片处理(裁剪、模糊、圆角等)
  • 处理后的图片以 原始key + processor标识 为 key 缓存
  • 支持渐进式 JPEG 加载、GIF 动画、SVG

2.4 性能优化细节

  • 下载使用 URLSession,支持 HTTP/2 多路复用
  • 图片解码在后台线程,避免阻塞主线程
  • 支持下载优先级和取消(cell 复用时取消旧请求)
  • ImagePrefetcher 预加载机制

3. SnapKit

3.1 底层原理

  • 本质是对 Auto Layout 的 NSLayoutConstraint 的 DSL 封装
  • 链式调用:每个方法返回 ConstraintMaker/ConstraintDescription
  • make.top.equalTo(view).offset(10) 最终等价于创建一个 NSLayoutConstraint
  • 约束更新snp.updateConstraints 找到已有约束修改 constant,比重新创建高效
  • 约束引用:可以保存约束引用后续修改 constraint.update(offset: 20)

3.2 与原生 API 的性能对比

  • SnapKit 在约束创建阶段有少量 wrapper 开销(可忽略)
  • 约束解算性能完全等同于原生 Auto Layout(最终都走同一个 Cassowary 算法引擎)
  • 主要价值是可读性和维护性

4. RxSwift / Combine 响应式框架

4.1 核心概念对比

概念 RxSwift Combine
数据流 Observable Publisher
消费者 Observer Subscriber
取消 Disposable / DisposeBag AnyCancellable
背压 无原生支持 Demand 机制
调度器 Scheduler Scheduler
Subject PublishSubject/BehaviorSubject PassthroughSubject/CurrentValueSubject

4.2 RxSwift 底层原理

  • Observable 是一个持有 subscribe 闭包的结构
  • subscribe 时创建 Sink(桥梁),连接 Observable 和 Observer
  • 操作符(map/filter 等)创建新的 Observable,形成链式管道
  • DisposeBag 在 deinit 时调用所有 Disposable 的 dispose,断开链条

4.3 Combine 底层原理

  • 基于 Publisher-Subscriber 协议
  • 背压 (Backpressure):Subscriber 通过 Demand 控制接收速率
  • Publisher 是值类型(struct),Subscriber 是引用类型(class)
  • sink/assign 等返回 AnyCancellable,释放即取消订阅

5. SwiftUI 数据流原理

5.1 属性包装器对比

Property Wrapper 所有权 触发刷新 适用场景
@State View 拥有 值变化时 View 内部简单状态
@Binding 不拥有(引用) 值变化时 父子 View 双向绑定
@ObservedObject 不拥有 objectWillChange 时 外部注入的 ObservableObject
@StateObject View 拥有 objectWillChange 时 View 创建的 ObservableObject
@EnvironmentObject 不拥有 objectWillChange 时 跨层级的 ObservableObject
@Environment 不拥有 值变化时 系统环境值

5.2 View 的 diff 更新机制

  • SwiftUI View 是值类型 struct,每次状态变化创建新的 View 值
  • SwiftUI 通过 attribute graph 追踪依赖关系
  • 只有依赖的数据发生变化的 View 才会被重新 body 求值
  • body 返回的新旧 View tree 做 structural diff,只更新差异部分

第三部分:开发难点与解决方案


1. 内存泄漏排查

1.1 常见泄漏场景

场景 根因 解决
闭包捕获 self 循环引用 [weak self] / [unowned self]
delegate 强引用 循环引用 delegate 用 weak 声明
Timer 持有 target Timer → self → Timer Timer.scheduledTimer(withTimeInterval:repeats:block:) + [weak self]
NotificationCenter addObserver iOS 8 以下需手动 remove block-based API + [weak self]
DispatchWorkItem 捕获 闭包内持有 self 取消 workItem 或 [weak self]
WKWebView 与 JS 交互 WKScriptMessageHandler 被 WKUserContentController 强持有 使用中间代理对象弱引用 self

1.2 排查工具链

  1. Xcode Memory Graph Debugger:可视化对象引用关系,直接定位循环引用
  2. Instruments - Leaks:运行时检测内存泄漏
  3. Instruments - Allocations:查看对象生命周期和内存分配
  4. deinit 打印:在 deinit 中加 print 确认对象释放
  5. MLeaksFinder(第三方):自动检测 ViewController 泄漏

1.3 难点:隐蔽的循环引用

// 难以发现的泄漏:闭包嵌套
class ViewModel {
    var onUpdate: (() -> Void)?
    func start() {
        NetworkManager.shared.request { [weak self] data in
            self?.onUpdate = {
                // 这里隐式捕获了 self(strong),因为 onUpdate 是 self 的属性
                // 而 self?.onUpdate = ... 外层已经是 weak self
                // 但 inner closure 没有 weak!
                self?.process(data)  // 如果这里 self 已经 unwrap 为 strong...
            }
        }
    }
}

2. 多线程数据竞争

2.1 经典问题

var array = [Int]()
DispatchQueue.concurrentPerform(iterations: 1000) { i in
    array.append(i)  // 崩溃!Array 非线程安全
}

2.2 解决方案对比

方案 优点 缺点
Serial DispatchQueue 简单直观 完全串行,性能差
Concurrent Queue + Barrier 读并发,写独占 代码稍复杂
NSLock / pthread_mutex 最轻量 需要手动 lock/unlock
os_unfair_lock 最快的互斥锁 不支持递归
Actor (Swift 5.5+) 编译器保证安全 异步调用
@Atomic property wrapper 属性级别保护 单次操作安全,复合操作不安全

2.3 Barrier 读写锁模式

class ThreadSafeArray<T> {
    private var array = [T]()
    private let queue = DispatchQueue(label: "safe", attributes: .concurrent)

    func read<R>(_ block: ([T]) -> R) -> R {
        queue.sync { block(array) }  // 并发读
    }
    func write(_ block: @escaping (inout [T]) -> Void) {
        queue.async(flags: .barrier) { block(&self.array) }  // 独占写
    }
}

3. 大量数据的列表性能

3.1 问题

  • 大量 cell 导致滚动卡顿
  • 图片加载闪烁
  • 内存暴涨

3.2 解决方案

问题 解决方案
cell 创建开销 复用机制 dequeueReusableCell
图片解码卡主线程 异步解码 + 缓存解码后的 bitmap
复杂 cell 布局 预计算 cell 高度,缓存布局结果
透明度 / 离屏渲染 避免 cornerRadius + masksToBounds,用 CAShapeLayer 或预渲染圆角图
大量图片内存 Kingfisher/SDWebImage 的缩略图 + downsampling
Diff 更新 DiffableDataSource / IGListKit / 手动 diff 只更新变化的 cell

4. 启动优化

4.1 启动阶段拆解

冷启动:
  1. 内核创建进程
  2. dyld 加载 → 动态库绑定 → rebase/bind
  3. +load / __attribute__((constructor))
  4. Runtime 初始化(ObjC class 注册、category attach)
  5. main() 函数
  6. AppDelegate → UIWindow → 首屏渲染

4.2 优化手段

阶段 优化方式
dyld 减少动态库数量(合并为 1 个);使用静态库
+load 移到 +initialize 或懒加载
二进制 二进制重排(Profile-Guided Optimization),减少 Page Fault
main 后 延迟非必要初始化,首屏数据预加载/缓存
渲染 简化首屏 UI,避免首屏大量 Auto Layout

5. 崩溃治理

5.1 常见崩溃类型

崩溃 原因 Swift 中的表现
EXC_BAD_ACCESS 野指针 / 访问已释放内存 极少(ARC + 值类型),除非 unowned(unsafe) 或 Unsafe 指针
EXC_BREAKPOINT trap 指令 fatalError、force unwrap nil、数组越界
SIGABRT abort() 断言失败、unrecognized selector(ObjC 交互)
OOM 内存超限 无 crash log(Jetsam),需要 MetricKit

5.2 Swift 特有崩溃

  • Force unwrap nillet x: Int = optional! —— 最常见
  • Array index out of range:下标越界
  • Unowned reference after dealloc:访问已释放的 unowned 对象
  • Exhaustive switch:enum 新增 case 但 switch 未覆盖(@unknown default)

第四部分:性能优化深入细节


1. 编译器优化

1.1 关键编译选项

选项 含义 效果
-Onone 无优化(Debug) 保留所有调试信息
-O 标准优化(Release) 内联、泛型特化、死代码消除
-Osize 优化体积 减少内联,优先选择小代码
-Ounchecked 移除安全检查 数组越界、溢出检查被移除,危险但最快
WMO (Whole Module Optimization) 全模块优化 跨文件内联/特化/去虚拟化

1.2 WMO 的重要性

  • 非 WMO 模式下,每个文件单独编译,看不到其他文件的实现
  • WMO 允许编译器将非 public/非 open 的 class 方法去虚拟化(直接调用)
  • internal 方法从 vtable 派发降级为静态派发
  • 自动推断 final(如果子类在整个模块中不存在)

1.3 帮助编译器优化的编码技巧

技巧 原因
final 修饰不需要继承的 class 静态派发
private / fileprivate 编译器可推断 final,静态派发
用 struct 而非 class 无引用计数,栈分配
避免过大的协议 existential 减少堆分配
@inlinable 暴露关键路径 跨模块内联优化
@frozen 标记稳定的 struct/enum 允许编译器直接操作内存布局
减少不必要的 Optional 减少分支和 unwrap 开销

2. 内存优化

2.1 减少堆分配

场景 优化
小对象 用 struct 替代 class
协议类型 用泛型约束替代 existential
闭包 非逃逸闭包(编译器可栈分配)
String 短字符串利用 SSO
数组 Array.reserveCapacity(_:) 预分配,避免多次扩容拷贝

2.2 减少引用计数操作

  • 减少 class 实例的传递次数
  • struct 中减少引用类型成员数量
  • let 代替 var(编译器可以省略某些 retain/release)
  • 考虑 Unmanaged<T> 手动管理引用计数(高性能场景)

2.3 内存对齐与布局优化

// 不好:padding 浪费
struct Bad {
    let a: Bool    // 1 byte + 7 padding
    let b: Int64   // 8 bytes
    let c: Bool    // 1 byte + 7 padding
}  // 总共 24 bytes

// 好:重排成员减少 padding
struct Good {
    let b: Int64   // 8 bytes
    let a: Bool    // 1 byte
    let c: Bool    // 1 byte + 6 padding
}  // 总共 16 bytes

Swift 编译器不会自动重排 struct 成员(为了保持 ABI 兼容),需要手动优化。


3. 集合操作优化

3.1 Lazy Collection

// 非 lazy:创建 3 个中间数组
let result = array.filter { $0 > 0 }.map { $0 * 2 }.prefix(5)

// lazy:单次遍历,按需计算,无中间数组
let result = array.lazy.filter { $0 > 0 }.map { $0 * 2 }.prefix(5)
  • lazy 将操作转为惰性求值
  • 只遍历一次,遇到满足条件的前 5 个就停止
  • 适合大数组 + 链式操作 + 只取部分结果

3.2 Dictionary 性能

  • Dictionary 使用开放寻址 + 线性探测哈希表
  • 负载因子超过 75% 自动扩容(容量翻倍 + rehash)
  • Dictionary.reserveCapacity(_:) 可预分配
  • 自定义 Hashable 时注意 hash 分布均匀性
  • Dictionary(grouping:by:) 比手动 for 循环分组更高效

3.3 ContiguousArray vs Array

  • Array 需要兼容 NSArray 桥接,有额外判断开销
  • ContiguousArray 保证连续内存存储,不支持 NSArray 桥接
  • 存储非 class、非 @objc 类型时两者等效
  • 存储 class 类型且确定不需要 ObjC 桥接时,ContiguousArray 更快

4. 字符串性能

4.1 避免频繁拼接

// 差:每次 += 可能触发拷贝和堆分配
var s = ""
for i in 0..<1000 { s += "\(i)" }

// 好:预分配
var s = ""
s.reserveCapacity(4000)
for i in 0..<1000 { s += "\(i)" }

// 更好:用数组 join
let s = (0..<1000).map(String.init).joined()

4.2 子串 Substring

  • Substring 与原 String 共享底层 buffer(COW)
  • 长期持有 Substring 会阻止原 String buffer 释放
  • 短期使用 Substring,长期存储时转为 String(substring)

5. 减少动态派发

5.1 性能对比数据

派发方式 相对开销
内联 0(最快)
静态派发 1x
vtable 派发 ~1.1x - 1.5x(间接跳转 + 可能的 cache miss)
PWT 派发 ~1.5x - 2x(多一次间接寻址)
objc_msgSend ~3x - 5x(查找 IMP 缓存)

5.2 优化方法

  1. struct > class(天然静态派发)
  2. final class / final method(静态派发)
  3. private / fileprivate method(隐式 final)
  4. 泛型约束 <T: P> > 存在类型 any P(可特化为静态派发)
  5. WMO 开启(自动去虚拟化)

第五部分:八股文中的横向对比


1. 值类型 vs 引用类型(深度对比)

对比维度 值类型 引用类型
拷贝语义 深拷贝(COW 优化后延迟拷贝) 浅拷贝(共享引用)
身份判断 无法判断「同一个」(只有值相等) === 判断同一实例
多态 协议实现 + 泛型 继承 + 协议
线程安全 天然安全 需同步
析构 无 deinit 有 deinit
内存位置 栈/内联(优先)
引用计数 有(ARC)
适用场景 数据模型、算法、并发安全 共享状态、标识语义、继承层级

选择原则: 默认用 struct,只在需要共享状态、继承、deinit、identity 时用 class。


2. struct vs class vs enum vs actor

特性 struct class enum actor
类型 引用 引用
继承 不支持 支持 不支持 不支持
协议遵循 支持 支持 支持 支持
deinit
可变性 mutating 自由修改 mutating 隔离保护
线程安全 拷贝安全 需手动 拷贝安全 编译器保证
引用计数
内存 栈优先 栈优先

3. let vs var(底层差异)

维度 let var
可变性 不可变 可变
编译器优化 更多(常量折叠、省略 retain/release) 较少
线程安全 安全(不可变) 不安全
引用类型 引用不可变(属性仍可变) 引用可变

4. map vs flatMap vs compactMap

方法 签名 作用
map (T) -> U 1:1 转换
flatMap (T) -> [U] 1:N 转换后展平
compactMap (T) -> U? 1:1 转换,自动过滤 nil
let a = [[1,2],[3,4]]
a.map { $0 }        // [[1,2],[3,4]]
a.flatMap { $0 }    // [1,2,3,4]

let b = ["1","a","3"]
b.compactMap { Int($0) }  // [1, 3]

Optional 上的 flatMap:

let x: Int? = 5
x.flatMap { $0 > 3 ? $0 : nil }  // Optional(5)
x.map { $0 > 3 ? $0 : nil }      // Optional(Optional(5)) → Int??

5. GCD vs Operation vs Swift Concurrency

维度 GCD Operation Swift Concurrency
抽象层级 低(C API) 中(ObjC 对象) 高(语言级别)
取消 手动检查 isCancelled 属性 结构化自动传播
依赖管理 手动 dispatch_group/barrier addDependency async let / TaskGroup
线程控制 QoS + target queue maxConcurrentOperationCount 协作式线程池
线程爆炸 容易 容易 不会(线程数 ≤ 核心数)
错误处理 无内建 无内建 throws + try await
安全保证 Actor + Sendable

6. weak vs unowned vs unowned(unsafe)

维度 weak unowned unowned(unsafe)
类型 Optional 非 Optional 非 Optional
对象释放后 自动 nil trap 崩溃 野指针(UB)
性能开销 Side table + 原子操作 较少 零额外开销
安全性 最安全 安全(确定性崩溃) 最危险
适用场景 delegate、不确定生命周期 确定不会先于 self 释放 极致性能,生命周期绝对保证

7. Any vs AnyObject vs any Protocol vs some Protocol

类型 含义 底层
Any 任意类型(值/引用) existential container (32 bytes)
AnyObject 任意引用类型 单指针 (8 bytes)
any Protocol 任意遵循 P 的类型 existential container
some Protocol 某个特定的遵循 P 的类型(编译期确定) 无 container,直接值

8. 访问控制对比

级别 可见范围 编译器优化影响
open 任何模块可继承和 override 不能优化派发
public 任何模块可访问,不可继承 override 不能优化派发(外部可能做协议遵循等)
internal 同一模块(默认) WMO 下可推断 final
fileprivate 同一文件 可推断 final
private 同一声明作用域 隐式 final,静态派发

9. 闭包 vs 函数 vs 方法

维度 全局函数 实例方法 闭包
类型 (Args) -> Return (Self) -> (Args) -> Return(柯里化) (Args) -> Return
捕获 隐式捕获 self 显式/隐式捕获环境
堆分配 无(作为闭包传递时有) 有(逃逸时)

10. throws vs Result vs Optional

方式 适用场景 性能 链式处理
throws 同步错误处理 正常路径零开销(Swift 使用 error return) do-catch
Result<T, E> 异步回调 / 存储结果 enum 开销(极小) map/flatMap
Optional<T> 值可能不存在 最小 map/flatMap/??

第六部分:高难度深底层原理


1. Swift Runtime 与 Metadata 系统

1.1 类型元数据 (Type Metadata)

每个 Swift 类型在运行时都有一个元数据 (Metadata) 记录:

Struct Metadata:
┌─────────────────────────┐
│ Kind (标识类型种类)        │  ← struct/class/enum/optional/tuple...
│ Type Descriptor          │  → 指向类型描述符(名称、字段、泛型参数等)
│ Value Witness Table Ptr  │  → VWT(size/alignment/copy/destroy 等操作)
└─────────────────────────┘

Class Metadata (ISA):
┌─────────────────────────┐
│ Kind                     │
│ SuperClass Pointer       │  → 父类元数据
│ Cache / Data (ObjC兼容)   │
│ Flags                    │
│ Instance Size            │
│ Instance Alignment       │
│ Type Descriptor          │
│ V-Table entries...       │  → 虚函数表
└─────────────────────────┘

1.2 泛型 Metadata 的懒创建

  • 泛型类型如 Array<Int> 的 metadata 是运行时按需创建
  • 首次使用 Array<Int> 时,runtime 用模板 + Int.self 的 metadata 组合生成
  • 生成后缓存在全局表中(线程安全的 concurrent hash map)
  • 这就是为什么泛型类型的首次使用可能比后续使用略慢

1.3 Mirror 反射的底层

let mirror = Mirror(reflecting: someInstance)
for child in mirror.children { ... }
  • Mirror 通过 Type Descriptor 中的字段描述信息获取属性名和偏移量
  • 通过 Value Witness Table 中的操作函数读取字段值
  • 属于「有限反射」—— 只能读取,不能修改(不像 Java/ObjC 的完全运行时反射)
  • Release 模式下如果类型信息被 strip,反射能力会受限

2. SIL (Swift Intermediate Language)

2.1 编译流程

Swift Source → AST → SIL (raw) → SIL (canonical) → SIL (optimized) → LLVM IR → Machine Code
                ↑         ↑              ↑                 ↑
            解析/类型检查  SILGen     强制诊断/优化      LLVM 优化

2.2 SIL 的作用

  • 类型检查之后、LLVM 之前的中间表示
  • 比 LLVM IR 更高级,保留了 Swift 的类型信息
  • 用于:
    • ARC 优化:合并/消除冗余的 retain/release
    • 泛型特化:生成具体类型的特化版本
    • 去虚拟化:将 vtable 调用转为直接调用
    • 内联:将小函数体直接插入调用点
    • 诊断:检测未初始化变量、排他性访问违规等

2.3 查看 SIL

swiftc -emit-sil file.swift  # 优化前的 SIL
swiftc -emit-sil -O file.swift  # 优化后的 SIL

SIL 中可以直接看到 retain/release 的插入位置、dispatch 方式、内联决策等。


3. 排他性访问 (Exclusivity Enforcement)

3.1 原则

Swift 保证同一时刻不能同时存在对同一变量的读访问和写访问(Law of Exclusivity)。

3.2 静态检查

var x = 1
swap(&x, &x)  // 编译错误!同时对 x 进行两个写访问

3.3 动态检查

var array = [1, 2, 3]
// 运行时可能崩溃:对 array 同时读 (subscript) 和写 (modifyElement)
extension Array {
    mutating func modifyFirst(using: (inout Element) -> Void) {
        using(&self[0])  // self 正在被修改,又通过 subscript 修改
    }
}

3.4 底层实现

  • 编译器在变量的访问开始/结束时插入 begin_access / end_access 标记
  • 栈上变量:编译器静态证明(大部分情况)
  • 堆上变量/全局变量:运行时维护访问记录栈,检测冲突
  • Debug 模式检查更严格,Release 中部分检查被优化掉

4. 内存安全与 Unsafe API

4.1 Swift 的安全保证

  • 变量使用前必须初始化
  • 数组下标自动检查越界
  • 整数溢出自动检测(Debug 模式)
  • Optional 强制解包检查
  • 排他性访问检查

4.2 Unsafe 指针体系

类型 含义 等价 C 类型
UnsafePointer<T> 只读指针 const T*
UnsafeMutablePointer<T> 可变指针 T*
UnsafeRawPointer 无类型只读指针 const void*
UnsafeMutableRawPointer 无类型可变指针 void*
UnsafeBufferPointer<T> 只读指针 + 长度 const T* + size_t
UnsafeMutableBufferPointer<T> 可变指针 + 长度 T* + size_t
OpaquePointer 不透明指针 C 的 opaque struct pointer
Unmanaged<T> 手动管理引用计数的引用 CFTypeRef

4.3 使用场景

  • C 库交互(Core Audio, Metal, 网络底层等)
  • 高性能数据处理(避免 ARC / 边界检查开销)
  • 内存映射文件操作

4.4 常见陷阱

// 危险:指针悬垂
var ptr: UnsafeMutablePointer<Int>?
do {
    var x = 42
    ptr = UnsafeMutablePointer(&x)
}
ptr?.pointee  // 未定义行为!x 已超出作用域

// 正确:使用 withUnsafe 系列方法
withUnsafePointer(to: &x) { ptr in
    // ptr 仅在此闭包内有效
}

5. ABI 稳定性 (Swift 5+)

5.1 什么是 ABI 稳定

  • ABI (Application Binary Interface):二进制层面的接口约定
  • 包括:函数调用约定、类型内存布局、name mangling、元数据格式、runtime 接口
  • Swift 5 之后 ABI 稳定 → Swift runtime 嵌入 OS → App 不再需要内嵌 Swift runtime → 包体积减小

5.2 Library Evolution

  • @frozen:向编译器承诺 struct/enum 的布局不会变化
    • 编译器可以直接根据偏移量访问成员(更快)
    • 不加 @frozen 时,编译器通过间接方式访问(支持未来布局变化)
  • @inlinable:向编译器暴露函数体,允许跨模块内联
  • 这些是标准库和系统框架使用的属性

5.3 Module Stability

  • Swift 5.1+ 模块稳定:.swiftinterface 文件替代 .swiftmodule
  • 不同编译器版本编译的模块可以互相兼容

6. 类型转换的底层机制

6.1 as / as? / as! 的区别

操作 检查时机 失败行为 底层
as 编译期 编译错误 无运行时开销(类型已知)
as? 运行时 返回 nil metadata 比较
as! 运行时 trap 崩溃 metadata 比较 + 强制

6.2 is 检查的底层

if value is MyClass { ... }
  • 值类型:编译期确定(静态检查)
  • 引用类型:运行时检查 isa 指针链(遍历继承链)
  • 协议类型:检查 type metadata 中的 protocol conformance 表

6.3 Protocol Conformance 查找

  • Swift runtime 维护一个全局的 Protocol Conformance Table
  • 表项格式:(TypeDescriptor, ProtocolDescriptor) → WitnessTable
  • as? SomeProtocol 时,runtime 在表中查找当前类型是否遵循该协议
  • 查找结果会被缓存

7. Swift 与 Objective-C 互操作底层

7.1 桥接机制

Swift 类型 ObjC 类型 桥接方式
String NSString 按需转换(UTF-8 ↔ UTF-16)
Array NSArray 包装/拆包
Dictionary NSDictionary 包装/拆包
Int/Double NSNumber 装箱/拆箱
struct 不可桥接 需要手动封装为 class

7.2 @objc 的代价

  • 标记为 @objc 的方法会生成 ObjC 兼容的调用入口
  • Swift class 继承 NSObject 时,会注册到 ObjC runtime
  • ObjC 方法调用走 objc_msgSend,比 Swift vtable 慢 3-5 倍
  • 每个 @objc 方法增加约 100 字节的二进制体积

7.3 Dynamic Member Lookup

@dynamicMemberLookup
struct JSON {
    subscript(dynamicMember member: String) -> JSON { ... }
}
let value = json.user.name  // 编译器转换为 subscript 调用
  • 编译期将 .member 语法转为 subscript 调用
  • 不涉及 ObjC runtime,纯 Swift 实现
  • 用于 DSL、动态语言桥接等

8. Move Semantics 与 Ownership(Swift 5.9+)

8.1 consuming / borrowing 参数

func process(_ value: consuming MyStruct) {
    // value 的所有权被转移到此函数,调用方不能再使用
}
func inspect(_ value: borrowing MyStruct) {
    // 只读借用,不拷贝,不转移所有权
}

8.2 ~Copyable(不可拷贝类型)

struct FileHandle: ~Copyable {
    let fd: Int32
    deinit { close(fd) }  // struct 有了 deinit!
}
  • 不可拷贝类型保证唯一所有权
  • 赋值 = 移动(原变量失效)
  • 可以为 struct 添加 deinit(资源清理)
  • 类似 Rust 的 ownership 模型
  • 消除不必要的引用计数开销

8.3 意义

  • 零成本抽象的资源管理(RAII)
  • 编译器保证资源不会被重复释放或遗忘释放
  • 为 Swift 引入更精细的内存控制能力

9. Result Type 与 Error Handling 底层

9.1 throws 的底层实现

Swift 的 throws 不使用异常表(不同于 C++/Java):

// 函数签名实际上是:
func foo() throws -> Int
// 底层等价于:
func foo() -> (Int, Error?)
  • 通过隐藏的返回值寄存器传递 Error
  • 正常路径零开销(no error → 直接返回结果)
  • 错误路径有 Error 对象创建的开销
  • 这就是为什么 try 的正常路径性能很好

9.2 typed throws (Swift 5.9+)

func parse() throws(ParseError) -> AST { ... }
  • 限定了错误类型,避免 existential Error 的开销
  • 调用方可以直接 catch 具体类型,无需 as? 转换

10. @dynamicCallable 与语言扩展能力

@dynamicCallable
struct PythonObject {
    func dynamicallyCall(withArguments args: [Any]) -> PythonObject { ... }
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Any>) -> PythonObject { ... }
}
let result = pythonObj(1, 2, name: "test")  // 编译器转为 dynamicallyCall
  • 编译器将函数调用语法重写为 dynamicallyCall 方法调用
  • 用于与 Python/Ruby 等动态语言桥接
  • TensorFlow for Swift 等项目大量使用

11. Opaque Return Type 与 Reverse Generics

11.1 some 的底层

func makeShape() -> some Shape {
    Circle()
}
  • 编译器知道返回类型是 Circle,但对调用方隐藏
  • 不使用 existential container,直接返回 Circle 的值
  • 零额外开销(等同于直接返回 Circle)
  • 但保持了 API 的抽象性(future-proof)

11.2 与 existential 的根本差异

// some:编译期确定类型,运行时无开销
func a() -> some Collection { [1,2,3] }
// a() 和 a() 保证是同一类型(Int Array)

// any:运行时动态类型,有 container 开销
func b() -> any Collection { Bool.random() ? [1] : Set([1]) }
// b() 每次可能不同类型

12. Memory Layout 工具

MemoryLayout<Int>.size        // 8(实际占用字节)
MemoryLayout<Int>.stride      // 8(数组中相邻元素的间距)
MemoryLayout<Int>.alignment   // 8(对齐要求)

MemoryLayout<Bool>.size       // 1
MemoryLayout<Bool>.stride     // 1
MemoryLayout<Bool>.alignment  // 1

MemoryLayout<Optional<Int>>.size    // 9(8 + 1 tag)
MemoryLayout<Optional<Int>>.stride  // 16(对齐到 8 的倍数)

MemoryLayout<String>.size     // 16(SSO 结构)
MemoryLayout<String>.stride   // 16

这些在需要与 C 交互、手动管理内存、优化内存布局时非常关键。


附录:高频面试题速查

# 问题 核心关键词
1 struct 和 class 的区别? 值/引用、栈/堆、COW、ARC、继承
2 Swift 的方法派发有几种? 静态、vtable、PWT、objc_msgSend
3 ARC 和 GC 的区别? 编译期插入 vs 运行时扫描、确定性 vs 非确定性、无停顿 vs STW
4 weak 和 unowned 的区别? Optional/非Optional、side table、释放后行为
5 什么是 Existential Container? 5 words、value buffer、metadata、PWT
6 什么是 COW? isKnownUniquelyReferenced、延迟拷贝
7 泛型约束和存在类型的区别? 静态/动态派发、特化、性能差异
8 闭包是值类型还是引用类型? 引用类型、函数指针+context、堆分配
9 Swift 的 String 为什么不能用 Int 下标? 变长 UTF-8、扩展字形簇、O(n) 遍历
10 Optional 底层是什么? 枚举 .none/.some、spare bit 优化
11 some 和 any 的区别? opaque type vs existential、静态/动态、性能
12 Actor 怎么保证线程安全? 串行执行器、isolation、await
13 async/await 底层原理? 协程、状态机、continuation、不阻塞线程
14 throws 的性能开销? 正常路径零开销、隐藏返回寄存器
15 @frozen 和 @inlinable 的作用? ABI 稳定、跨模块优化、库演进
16 什么是 WMO? 全模块优化、去虚拟化、跨文件内联
17 ~Copyable 是什么? 不可拷贝类型、唯一所有权、move semantics
18 协议扩展方法为什么不能多态? PWT 无条目、静态派发
19 class extension 的方法能 override 吗? 不能、不在 vtable 中、静态派发
20 排他性访问是什么? Law of Exclusivity、begin/end_access、读写冲突检测

Flutter深度全解析

涵盖底层原理、第三方库、疑难杂症、性能优化、横向纵向对比,面试+实战全方位覆盖


目录


第一部分:Flutter 底层原理与核心机制

一、Flutter 架构分层详解

1.1 整体架构三层模型

Flutter 架构自上而下分为三层:

层级 组成 语言 职责
Framework 层 Widgets、Material/Cupertino、Rendering、Animation、Painting、Gestures、Foundation Dart 提供上层 API,开发者直接使用
Engine 层 Skia(渲染引擎)、Dart VM、Text Layout(LibTxt)、Platform Channels C/C++ 底层渲染、文字排版、Dart 运行时
Embedder 层 平台相关代码(Android/iOS/Web/Desktop) Java/Kotlin/ObjC/Swift/JS 平台嵌入、表面创建、线程设置、事件循环

1.2 Framework 层细分

  • Foundation 层:最底层,提供基础工具类(ChangeNotifier、Key、UniqueKey 等)
  • Animation 层:动画系统(Tween、AnimationController、CurvedAnimation)
  • Painting 层:Canvas 相关的绘制能力封装(TextPainter、BoxDecoration、Border 等)
  • Gestures 层:手势识别(GestureDetector 底层 GestureRecognizer 竞技场机制)
  • Rendering 层:布局与绘制的核心(RenderObject 树)
  • Widgets 层:Widget 声明式 UI 框架,组合模式
  • Material/Cupertino 层:两套设计语言风格的组件库

1.3 Engine 层核心组件

  • Skia:2D 渲染引擎,Flutter 不依赖平台 UI 控件,直接通过 Skia 绘制像素
  • Dart VM:运行 Dart 代码,支持 JIT(开发期)和 AOT(发布期)两种编译模式
  • Impeller:Flutter 3.x 引入的新渲染引擎,替代 Skia 的部分功能,解决 Shader 编译卡顿问题
  • LibTxt/HarfBuzz/ICU:文字排版、字形渲染、国际化支持

二、三棵树机制(核心中的核心)

2.1 Widget Tree(组件树)

  • Widget 是不可变的配置描述,是 UI 的蓝图(Blueprint)
  • 每次 setState 都会重新构建 Widget Tree(轻量级,不涉及实际渲染)
  • Widget 是 @immutable 的,所有字段都是 final
  • Widget 通过 createElement() 创建对应的 Element
  • 同类型 Widget 有相同的 runtimeTypekey 时可以复用 Element

2.2 Element Tree(元素树)

  • Element 是 Widget 和 RenderObject 之间的桥梁
  • Element 是可变的,持有 Widget 引用,管理生命周期
  • Element 分为两大类:
    • ComponentElement:组合型,自身不参与渲染,只是组合其他 Widget(StatelessElement、StatefulElement)
    • RenderObjectElement:渲染型,持有 RenderObject,参与实际布局和绘制
  • Element 的核心方法:
    • mount():Element 首次插入树中
    • update(Widget newWidget):Widget 重建时更新 Element
    • unmount():从树中移除
    • deactivate():临时移除(GlobalKey 可重新激活)
    • activate():重新激活

2.3 RenderObject Tree(渲染对象树)

  • 真正负责布局(Layout)和绘制(Paint)
  • 实现 performLayout() 计算大小和位置
  • 实现 paint() 进行绘制
  • 通过 Constraints 向下传递约束,通过 Size 向上传递大小
  • 重要子类:
    • RenderBox:2D 盒模型布局(最常用)
    • RenderSliver:滚动布局模型
    • RenderView:渲染树根节点

2.4 三棵树的协作流程

setState() 触发
    ↓
Widget 重建(调用 build 方法)→ 新的 Widget Tree
    ↓
Element 进行 Diff(canUpdate 判断)
    ↓
canUpdate = true → 更新 Element,调用 RenderObject.updateRenderObject()
canUpdate = false → 销毁旧 Element/RenderObject,创建新的
    ↓
标记需要重新布局/绘制的 RenderObject
    ↓
下一帧执行布局和绘制

2.5 canUpdate 判断机制(极其重要)

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}
  • 只比较 runtimeTypekey
  • 不比较 Widget 的其他属性(颜色、大小等都不比较)
  • 这就是为什么 Key 如此重要——当列表项顺序变化时,没有 Key 会导致错误复用

三、Key 的深入理解

3.1 Key 的分类体系

Key
 ├── LocalKey(局部 Key,在同一父节点下唯一)
 │   ├── ValueKey<T>    ← 用值比较(如 ID)
 │   ├── ObjectKey       ← 用对象引用比较
 │   └── UniqueKey       ← 每次都唯一(不可复用)
 └── GlobalKey(全局 Key,整棵树中唯一)
     └── GlobalObjectKey

3.2 各种 Key 的使用场景

Key 类型 适用场景 原理
ValueKey 列表项有唯一业务 ID 时 用 value 的 == 运算符比较
ObjectKey 组合多个字段作为标识时 identical() 比较对象引用
UniqueKey 强制每次重建时 每个实例都是唯一的
GlobalKey 跨组件访问 State、跨树移动 Widget 通过全局注册表维护 Element 引用

3.3 GlobalKey 的代价与原理

  • GlobalKey 通过全局 HashMap 注册,查找复杂度 O(1)
  • 但维护全局注册表有额外内存开销
  • GlobalKey 可以实现 Widget 在树中跨位置移动而不丢失 State
  • 原理:deactivate 时不销毁,而是暂存,等待 activate 重新挂载
  • 注意:GlobalKey 在整棵树中必须唯一,否则会抛异常

四、Widget 生命周期(StatefulWidget 完整生命周期)

4.1 完整生命周期流程

createState()          → 创建 State 对象(仅一次)
    ↓
initState()            → 初始化状态(仅一次),可访问 context
    ↓
didChangeDependencies() → 依赖变化时调用(首次 initState 之后也调用)
    ↓
build()                → 构建 Widget 树(多次调用)
    ↓
didUpdateWidget()      → 父组件重建导致 Widget 配置变化时
    ↓
setState()             → 手动触发重建
    ↓
deactivate()           → 从树中移除时(可能重新插入)
    ↓
dispose()              → 永久移除时,释放资源(仅一次)

4.2 各生命周期方法的注意事项

方法 调用次数 能否调用 setState 典型用途
createState 1 次 不能 创建 State 实例
initState 1 次 不能(但赋值 OK) 初始化控制器、订阅流
didChangeDependencies 多次 可以 响应 InheritedWidget 变化
build 多次 不能 返回 Widget 树
didUpdateWidget 多次 可以 对比新旧 Widget,更新状态
reassemble 多次(仅 debug) 可以 hot reload 时调用
deactivate 可能多次 不能 临时清理
dispose 1 次 不能 取消订阅、释放控制器

4.3 didChangeDependencies 何时触发?

  • 首次 initState() 之后自动调用一次
  • 当依赖的 InheritedWidget 发生变化时
  • 典型场景:Theme.of(context)MediaQuery.of(context)Provider.of(context) 的数据发生变化
  • 注意:仅当通过 dependOnInheritedWidgetOfExactType 注册了依赖关系才会触发

五、渲染流水线(Rendering Pipeline)

5.1 帧渲染流程(一帧的生命周期)

Vsync 信号到来
    ↓
① Animate 阶段:执行 Ticker 回调(动画)
    ↓
② Build 阶段:执行被标记 dirty 的 Element 的 build 方法
    ↓
③ Layout 阶段:遍历需要重新布局的 RenderObject,执行 performLayout()
    ↓
④ Compositing Bits 阶段:更新合成层标记
    ↓
⑤ Paint 阶段:遍历需要重绘的 RenderObject,执行 paint()
    ↓
⑥ Compositing 阶段:将 Layer Tree 组合成场景
    ↓
⑦ Semantics 阶段:生成无障碍语义树
    ↓
⑧ Finalize 阶段:将场景提交给 GPU

5.2 SchedulerBinding 的调度阶段

阶段 枚举值 说明
idle SchedulerPhase.idle 空闲,等待下一帧
transientCallbacks SchedulerPhase.transientCallbacks 动画回调(Ticker)
midFrameMicrotasks SchedulerPhase.midFrameMicrotasks 动画后的微任务
persistentCallbacks SchedulerPhase.persistentCallbacks build/layout/paint
postFrameCallbacks SchedulerPhase.postFrameCallbacks 帧后回调

5.3 布局约束传递机制(Constraints go down, Sizes go up)

  • 父节点向子节点传递 Constraints(约束)
  • 子节点根据约束计算自己的 Size(大小)
  • 父节点根据子节点的 Size 决定子节点的 Offset(位置)
父 RenderObject
    │ 传递 BoxConstraints(minW, maxW, minH, maxH)
    ↓
子 RenderObject
    │ 根据约束计算 Size
    ↑ 返回 Size(width, height)
    │
父 RenderObject 确定子的 Offset

5.4 RelayoutBoundary 优化

  • 当一个 RenderObject 被标记为 relayout boundary 时,其子树的布局变化不会影响父节点
  • 自动标记条件(满足任一):
    • sizedByParent == true
    • constraints.isTight(紧约束)
    • parentUsesSize == false
  • 这大大减少了布局重算的范围

5.5 RepaintBoundary 优化

  • 创建独立的 Layer,使得该子树的重绘不影响其他区域
  • 适用场景:频繁变化的局部区域(如动画区域、时钟、进度条)
  • 不宜过度使用:每个 Layer 有内存开销,过多 Layer 反而降低合成效率

六、Dart 语言核心机制

6.1 Dart 的事件循环模型(Event Loop)

Dart 是单线程模型

main() 函数执行
    ↓
进入事件循环 Event Loop
    ↓
┌─────────────────────────────┐
│   检查 MicroTask Queue      │ ← 优先级高
│   (全部执行完才处理 Event)   │
├─────────────────────────────┤
│   检查 Event Queue          │ ← I/O、Timer、点击等
│   (取一个事件处理)          │
└─────────────────────────────┘
    ↓ 循环

6.2 MicroTask 与 Event 的区别

特性 MicroTask Event
优先级
来源 scheduleMicrotask()Future.microtask()、Completer Timer、I/O、手势事件、Future()Future.delayed()
执行时机 在当前 Event 处理完之后、下一个 Event 之前 按顺序从队列取出
风险 过多会阻塞 UI(卡帧) 正常调度

6.3 Future 和 async/await 的本质

  • Future 是对异步操作结果的封装
  • async 函数总是返回 Future
  • await 暂停当前异步函数执行,但不阻塞线程
  • await 本质上是注册一个回调到 Future 的 then 链上
  • Future() 构造函数将任务放入 Event Queue
  • Future.microtask() 将任务放入 MicroTask Queue
  • Future.value() 如果值已就绪,回调仍然异步执行(下一个 microtask)

6.4 Isolate 机制

  • Dart 的线程模型是 Isolate(隔离区)
  • 每个 Isolate 有独立的内存堆和事件循环
  • Isolate 之间不共享内存,通过 SendPort/ReceivePort 消息传递通信
  • compute() 函数是对 Isolate 的高层封装
  • Flutter 3.x 引入 Isolate.run(),更简洁
  • 适用场景:JSON 解析、图片处理、加密等 CPU 密集型任务

6.5 Dart 的内存管理与 GC

  • Dart 使用分代垃圾回收(Generational GC)
  • 新生代(Young Generation)
    • 采用**半空间(Semi-space)**算法
    • 分为 From 空间和 To 空间
    • 对象先分配在 From 空间
    • GC 时将存活对象复制到 To 空间,然后交换
    • 速度极快(毫秒级)
  • 老年代(Old Generation)
    • 采用**标记-清除(Mark-Sweep)**算法
    • 存活多次 GC 的对象会晋升到老年代
    • GC 时间较长,但触发频率低
  • Flutter 中 Widget 频繁创建销毁,大部分在新生代被回收,性能影响很小

6.6 Dart 编译模式

模式 全称 场景 特点
JIT Just-In-Time Debug/开发 支持 Hot Reload、增量编译、反射
AOT Ahead-Of-Time Release/生产 预编译为机器码,启动快、性能高
Kernel Snapshot - 测试/CI 编译为中间表示

6.7 Dart 的空安全(Null Safety)

  • 从 Dart 2.12 开始支持 Sound Null Safety
  • 类型默认不可为空String name 不能为 null
  • 可空类型需显式声明:String? name
  • late 关键字:延迟初始化,使用前必须赋值,否则运行时报错
  • required 关键字:命名参数必须传值
  • 空安全运算符:?.(安全调用)、??(空值合并)、!(强制非空)
  • 类型提升(Type Promotion):if (x != null) 后 x 自动提升为非空类型

6.8 Dart 的 mixin 机制

  • mixin 是代码复用机制,区别于继承
  • 使用 with 关键字混入
  • mixin 不能有构造函数
  • mixin 可以用 on 限制只能混入特定类的子类
  • 多个 mixin 的方法冲突时,最后混入的优先(线性化 Linearization)
  • mixin 的方法查找是通过C3 线性化算法

6.9 Extension 扩展方法

  • Dart 2.7 引入,为已有类添加方法,不修改原类
  • 编译时静态解析,不是运行时动态分派
  • 不能覆盖已有方法,当扩展方法和类方法同名时,类方法优先

七、状态管理深入理解

7.1 InheritedWidget 原理

  • 数据共享的基石,Provider/Bloc 等底层都依赖它
  • 通过 dependOnInheritedWidgetOfExactType<T>() 注册依赖
  • 当 InheritedWidget 更新时,所有注册了依赖的 Element 会调用 didChangeDependencies()
  • 原理:InheritedElement 维护一个 _dependents 集合,保存所有依赖它的 Element
  • updateShouldNotify() 方法决定是否通知依赖者

7.2 setState 的底层过程

setState(() { /* 修改状态 */ })
    ↓
_element!.markNeedsBuild()  → 将 Element 标记为 dirty
    ↓
SchedulerBinding.instance.scheduleFrame()  → 请求新帧
    ↓
下一帧时 BuildOwner.buildScope()
    ↓
遍历 dirty Elements,调用 element.rebuild()
    ↓
调用 State.build() 获取新 Widget
    ↓
Element.updateChild() 进行 Diff 更新

7.3 ValueNotifier / ChangeNotifier 原理

  • ChangeNotifier 维护一个 _listeners 列表
  • notifyListeners() 遍历列表调用所有监听器
  • ValueNotifier<T> 继承自 ChangeNotifier,当 value 变化时自动 notifyListeners()
  • Flutter 3.x 优化:_listeners 使用 _count 跟踪,支持在遍历时添加/移除监听器

八、手势系统(GestureArena 竞技场机制)

8.1 事件分发流程

平台原始事件(PointerEvent)
    ↓
GestureBinding.handlePointerEvent()
    ↓
HitTest(命中测试):从根节点向叶子节点遍历
    ↓
生成 HitTestResult(命中路径)
    ↓
按命中路径分发 PointerEvent 给各 RenderObject
    ↓
GestureRecognizer 加入竞技场(GestureArena)
    ↓
竞技场裁决(Arena Resolution)→ 只有一个胜出

8.2 竞技场裁决规则

  • 每个指针事件创建一个竞技场
  • 多个 GestureRecognizer 参与竞争
  • 裁决方式:
    • 接受(accept):手势确认,如长按超过阈值
    • 拒绝(reject):手势放弃
    • 当只剩一个参与者时,自动胜出
    • 当 PointerUp 时强制裁决,最后一个未拒绝的胜出
  • 手势冲突解决:使用 RawGestureDetectorGestureRecognizer.resolve()Listener 绕过竞技场

8.3 命中测试(HitTest)深入

  • 从 RenderView(根)开始,调用 hitTest()
  • 遍历子节点时采用逆序(从最上层视觉元素开始)
  • 命中判断通过 hitTestSelf()hitTestChildren()
  • HitTestBehavior
    • deferToChild:只有子节点命中时才命中(默认)
    • opaque:自身命中(即使子节点没命中)
    • translucent:自身也命中,但不阻止后续命中测试

九、平台通信机制(Platform Channel)

9.1 三种 Channel 类型

Channel 类型 编解码 通信模式 典型用途
BasicMessageChannel 标准消息编解码器 双向消息传递 简单数据传递(字符串、JSON)
MethodChannel StandardMethodCodec 方法调用(请求-响应) 调用原生方法并获取返回值
EventChannel StandardMethodCodec 单向事件流(原生→Flutter) 传感器数据、电池状态等持续性事件

9.2 消息编解码器(Codec)

编解码器 支持类型 适用场景
StringCodec String 纯文本
JSONMessageCodec JSON 兼容类型 JSON 数据
BinaryCodec ByteData 二进制数据
StandardMessageCodec null, bool, int, double, String, List, Map, Uint8List 默认,最常用

9.3 通信原理

Flutter (Dart)                      Platform (Native)
     │                                    │
     │  MethodChannel.invokeMethod()      │
     ├────────────────────────────────────→│
     │      BinaryMessenger              │
     │      (BinaryCodec编码)             │
     │                                    │ MethodCallHandler 处理
     │←────────────────────────────────────┤
     │      返回 Result                   │
     │      (BinaryCodec解码)             │
  • 底层通过 BinaryMessenger 传输 ByteData
  • 通信是异步的(返回 Future)
  • 线程模型:
    • Dart 侧:在 UI Isolate(主线程)处理
    • Android:默认在主线程(可切换到后台线程)
    • iOS:默认在主线程

9.4 FFI(Foreign Function Interface)

  • 直接调用 C/C++ 函数,无需经过 Channel
  • 性能远高于 MethodChannel(无序列化/反序列化开销)
  • 适合高频调用、大数据传输
  • 通过 dart:ffi 包使用
  • 支持同步调用(Channel 只支持异步)

十、路由与导航机制

10.1 Navigator 1.0(命令式路由)

  • 基于栈模型(Stack),push/pop 操作
  • Navigator.push() / Navigator.pop()
  • Navigator.pushNamed() / onGenerateRoute
  • 路由栈通过 Overlay + OverlayEntry 实现,每个页面是一个 OverlayEntry

10.2 Navigator 2.0(声明式路由)

  • 引入 RouterRouteInformationParserRouterDelegate
  • 声明式:通过修改状态来控制路由栈
  • 更适合 Web、Deep Link 场景
  • 三大核心组件:
    • RouteInformationProvider:提供路由信息(URL)
    • RouteInformationParser:解析路由信息为应用状态
    • RouterDelegate:根据状态构建 Navigator 的页面栈

10.3 路由传参与返回值

  • push 返回 Future<T?>pop 传回结果
  • 命名路由通过 arguments 传参
  • onGenerateRoute 中解析 RouteSettings 获取参数
  • 返回值本质:Navigator 内部用 Completer<T> 管理,pop 时 complete

十一、动画系统

11.1 动画的核心组成

组件 作用
Animation 动画值的抽象,持有当前值和状态
AnimationController 控制动画的播放、暂停、反向,产生 0.0~1.0 的线性值
Tween 将 0.0~1.0 映射到任意范围(如颜色、大小)
Curve 定义动画的速度曲线(如 easeIn、bounceOut)
AnimatedBuilder 监听动画值变化,触发重建
Ticker 与 Vsync 同步的时钟,驱动 AnimationController

11.2 隐式动画 vs 显式动画

特性 隐式动画(AnimatedXxx) 显式动画(XxxTransition)
复杂度
控制力 低(只需改属性值) 高(完全控制播放)
实现 内部自动管理 Controller 手动创建 Controller
典型组件 AnimatedContainer、AnimatedOpacity FadeTransition、RotationTransition
适用场景 简单属性变化 复杂动画、组合动画、循环动画

11.3 Ticker 与 SchedulerBinding

  • Ticker 在每一帧 Vsync 信号到来时执行回调
  • TickerProviderStateMixin:为 State 提供 Ticker
  • 当页面不可见时(如切换 Tab),TickerMode 可以禁用 Ticker 节省资源
  • 一个 SingleTickerProviderStateMixin 只能创建一个 AnimationController
  • 多个 Controller 需要用 TickerProviderStateMixin

11.4 Hero 动画原理

  • 在路由切换时,两个页面中相同 tag 的 Hero Widget 会执行飞行动画
  • 原理:
    1. 路由切换开始时,找到新旧页面中匹配的 Hero
    2. 计算起始和结束的位置/大小
    3. 在 Overlay 层创建一个飞行中的 Hero
    4. 通过 Tween 动画从起始位置/大小过渡到结束位置/大小
    5. 动画结束后,飞行 Hero 消失,目标页面的 Hero 显示

十二、Sliver 滚动机制

12.1 滚动模型

  • Flutter 滚动基于 Viewport + Sliver 模型
  • Viewport:可视窗口,持有 ViewportOffset(滚动偏移)
  • Sliver:可滚动的条状区域
  • 与盒模型(BoxConstraints)不同,Sliver 使用 SliverConstraints

12.2 SliverConstraints vs BoxConstraints

特性 BoxConstraints SliverConstraints
约束维度 宽度 + 高度 主轴剩余空间 + 交叉轴大小
布局结果 Size SliverGeometry
适用场景 普通布局 滚动列表
包含信息 min/maxWidth, min/maxHeight scrollOffset, remainingPaintExtent, overlap 等

12.3 SliverGeometry 关键字段

字段 含义
scrollExtent 沿主轴方向的总长度
paintExtent 可绘制的长度
layoutExtent 占用的布局空间
maxPaintExtent 最大可绘制长度
hitTestExtent 可命中测试的长度
hasVisualOverflow 是否有视觉溢出

12.4 CustomScrollView 与 NestedScrollView

  • CustomScrollView:使用 Sliver 协议的自定义滚动视图
  • NestedScrollView:处理嵌套滚动(如 TabBar + TabBarView + ListView)
  • NestedScrollView 通过 _NestedScrollCoordinator 协调内外滚动

十三、BuildContext 深入理解

13.1 BuildContext 的本质

  • BuildContext 实际上就是 Element
  • abstract class Element implements BuildContext
  • 它代表 Widget 在树中的位置
  • 通过 context 可以:
    • 获取 InheritedWidget 数据(Theme.of(context)
    • 获取 RenderObject(context.findRenderObject()
    • 向上遍历祖先(context.findAncestorWidgetOfExactType<T>()
    • 向上遍历状态(context.findAncestorStateOfType<T>()

13.2 Context 的使用陷阱

  • initState 中 context 已可用,但某些操作需要放在 addPostFrameCallback
  • Navigator.of(context) 的 context 必须在 Navigator 之下
  • Scaffold.of(context) 的 context 必须在 Scaffold 之下
  • 异步操作后使用 context 需要先检查 mounted

十四、图片加载与缓存机制

14.1 Image Widget 加载流程

Image Widget
    ↓
ImageProvider.resolve()
    ↓
检查 ImageCache(内存缓存)
    ↓ 未命中
ImageProvider.load()
    ↓
ImageStreamCompleter
    ↓
解码(codec)→ ui.Image
    ↓
放入 ImageCache
    ↓
通知 ImageStream 监听器
    ↓
Image Widget 获取帧数据并绘制

14.2 ImageCache 机制

  • 默认最大缓存 1000 张图片
  • 默认最大缓存 100MB
  • LRU 淘汰策略
  • Key 是 ImageProvider 的实例(需正确实现 ==hashCode
  • 可通过 PaintingBinding.instance.imageCache 配置

十五、国际化(i18n)与本地化(l10n)

15.1 Flutter 国际化架构

  • 基于 Localizations Widget 和 LocalizationsDelegate
  • 三个核心 Delegate:
    • GlobalMaterialLocalizations.delegate:Material 组件文本
    • GlobalWidgetsLocalizations.delegate:文字方向
    • GlobalCupertinoLocalizations.delegate:Cupertino 组件文本
  • 自定义 Delegate 需实现 LocalizationsDelegate<T>,重写 load() 方法

第二部分:第三方常用库原理与八股文

一、Provider

1.1 核心原理

  • 本质是对 InheritedWidget 的封装
  • ChangeNotifierProvider 内部创建 InheritedProvider
  • 依赖注入 + 响应式通知
  • 监听变化通过 ChangeNotifier.addListener() → Element 标记 dirty → 重建

1.2 核心类

作用
Provider<T> 最基础的 Provider,提供值但不监听变化
ChangeNotifierProvider<T> 监听 ChangeNotifier 并自动 rebuild
FutureProvider<T> 提供 Future 的值
StreamProvider<T> 提供 Stream 的值
MultiProvider 嵌套多个 Provider 的语法糖
ProxyProvider 依赖其他 Provider 的值来创建
Consumer<T> 精确控制重建范围
Selector<T, S> 选择特定属性监听,减少重建

1.3 Provider 的读取方式对比

方式 监听变化 使用场景
context.watch<T>() build 方法中,需要响应变化
context.read<T>() 事件回调中,只读取一次
context.select<T, R>() 是(部分) 只监听特定属性
Provider.of<T>(context) 默认是 等价于 watch
Provider.of<T>(context, listen: false) 等价于 read

1.4 Provider 的 dispose 机制

  • ChangeNotifierProvider 默认在 dispose 时调用 ChangeNotifier.dispose()
  • ChangeNotifierProvider.value() 不会自动 dispose(因为不拥有生命周期)
  • 这是一个常见坑:使用 .value() 构造时需要手动管理生命周期

二、Bloc / Cubit

2.1 Bloc 模式核心概念

UI 发出 Event → Bloc 处理 → 产生新 State → UI 根据 State 重建
概念 说明
Event 用户操作或系统事件,输入
State UI 状态,输出
Bloc 业务逻辑容器,Event → State 的转换器
Cubit 简化版 Bloc,直接通过方法调用 emit State(没有 Event)

2.2 Bloc 底层原理

  • Bloc 内部使用 Stream 处理 Event 和 State
  • Event 通过 StreamController 传入
  • mapEventToState(旧版)或 on<Event>()(新版)处理事件
  • State 通过 emit() 发出,本质是向 State Stream 中添加值
  • BlocProvider 底层也是基于 InheritedWidget + Provider 实现
  • BlocBuilder 内部使用 BlocListener + buildWhen 来控制重建

2.3 Bloc vs Cubit 对比

特性 Bloc Cubit
输入方式 Event 类 方法调用
可追溯性 高(Event 可序列化)
复杂度
测试性 优秀(可 mock Event) 良好
适用场景 复杂业务逻辑、需要 Event Transform 简单状态管理
调试 BlocObserver 可监控所有事件 同样支持

三、GetX

3.1 核心模块

模块 功能
状态管理 GetBuilder(简单)、Obx(响应式)
路由管理 Get.to()Get.toNamed() 无需 context
依赖注入 Get.put()Get.lazyPut()Get.find()
工具类 Snackbar、Dialog、BottomSheet 无需 context

3.2 响应式原理(Obx)

  • .obs 将值包装成 RxT(如 RxIntRxString
  • Obx 内部创建 RxNotifier,通过 Stream 监听变化
  • 自动追踪依赖:Obx build 时记录访问的 Rx 变量
  • 当 Rx 变量变化时,自动重建对应的 Obx

3.3 GetX 的争议

  • 优点:简单、快速开发、不依赖 context
  • 缺点:过度封装、黑盒行为多、测试困难、不遵循 Flutter 惯用模式

四、Riverpod

4.1 核心设计

  • 不依赖 BuildContext(区别于 Provider)
  • 编译时安全(不会出现 ProviderNotFound 异常)
  • 通过 ProviderContainer 管理状态,而非 Widget Tree
  • 支持自动 dispose、按需加载

4.2 Provider 类型

类型 用途
Provider 只读值
StateProvider 简单可变状态
StateNotifierProvider 复杂状态逻辑
FutureProvider 异步计算
StreamProvider 流数据
NotifierProvider 2.0 新式状态管理
AsyncNotifierProvider 2.0 异步状态管理

4.3 Riverpod vs Provider 对比

特性 Provider Riverpod
依赖 BuildContext
编译时安全 否(运行时异常)
多同类型 Provider 困难 通过 family 支持
测试性 中等 优秀
生命周期 跟随 Widget 独立管理
学习曲线 中等

五、Dio(网络请求库)

5.1 核心架构

  • 基于**拦截器链(Interceptor Chain)**模式
  • 请求流程:Request → Interceptors(onRequest) → HttpClientAdapter → Response → Interceptors(onResponse)
  • 底层使用 dart:ioHttpClient(可替换为其他 Adapter)

5.2 拦截器机制

请求发出
  ↓
Interceptor1.onRequest → Interceptor2.onRequest → ... → InterceptorN.onRequest
  ↓
实际网络请求(HttpClientAdapter)
  ↓
InterceptorN.onResponse → ... → Interceptor2.onResponse → Interceptor1.onResponse
  ↓
返回结果
  • 拦截器可以短路请求(resolve/reject 直接返回)
  • 典型拦截器:Token 刷新、日志、缓存、重试

5.3 关键特性

特性 说明
拦截器 请求/响应/错误拦截
FormData 文件上传
取消请求 CancelToken
超时控制 connectTimeout/receiveTimeout/sendTimeout
转换器 Transformer(JSON 解析可在 Isolate 中进行)
适配器 HttpClientAdapter(可替换底层实现)

六、go_router

6.1 核心原理

  • 基于 Navigator 2.0 的声明式路由封装
  • 通过 GoRouterState 管理路由状态
  • 支持嵌套路由、重定向、守卫

6.2 关键特性

特性 说明
声明式路由 通过配置定义路由表
Deep Link 自动处理 URL 解析
路由重定向 redirect 回调
ShellRoute 保持底部导航栏等布局
类型安全路由 通过 code generation 实现
Web 友好 URL 自动同步

七、freezed / json_serializable

7.1 freezed 原理

  • 基于 build_runner 的代码生成
  • 自动生成 ==hashCodetoStringcopyWith
  • 支持联合类型(Union Types)密封类(Sealed Classes)
  • 生成的代码是不可变的(Immutable)

7.2 json_serializable 原理

  • 通过注解 @JsonSerializable() 标记类
  • build_runner 生成 _$XxxFromJson_$XxxToJson 方法
  • 编译时生成代码,零反射,性能优于运行时反射的序列化方案

八、cached_network_image

8.1 缓存架构

请求图片 URL
    ↓
检查内存缓存(ImageCache)
    ↓ 未命中
检查磁盘缓存(flutter_cache_manager)
    ↓ 未命中
网络下载
    ↓
存入磁盘缓存
    ↓
解码并存入内存缓存
    ↓
显示

8.2 flutter_cache_manager 策略

  • 基于 SQLite 存储缓存元数据
  • 默认缓存有效期 30 天
  • 支持自定义缓存策略、最大缓存大小
  • 支持 ETag / Last-Modified 验证缓存

九、auto_route / flutter_hooks / get_it

9.1 auto_route

  • 代码生成式路由管理
  • 类型安全:编译时检查路由参数
  • 支持嵌套路由、Tab 路由、守卫
  • 底层使用 Navigator 2.0

9.2 flutter_hooks

  • 将 React Hooks 概念引入 Flutter
  • useStateuseEffectuseMemoizeduseAnimationController
  • 原理:HookWidget 内部维护 Hook 链表,按顺序调用
  • 优势:减少样板代码,逻辑复用更方便

9.3 get_it(Service Locator)

  • 服务定位器模式,全局依赖注入
  • 非响应式,纯粹的依赖管理
  • 支持单例、懒加载、工厂模式
  • 与 Widget Tree 解耦,可在任何地方使用

第三部分:开发疑难杂症与解决方案

一、列表性能问题

1.1 问题:长列表卡顿

症状:包含大量数据的 ListView 滚动时帧率下降

根因分析

  • 使用 ListView(children: [...]) 一次构建所有子项
  • 子项 Widget 过于复杂
  • 图片未做懒加载和缓存

解决方案

  1. 使用 ListView.builder 按需构建(Lazy Construction)
  2. 使用 const 构造器减少不必要的重建
  3. 对列表项使用 AutomaticKeepAliveClientMixin 保持状态(谨慎使用,会增加内存)
  4. 使用 RepaintBoundary 隔离重绘区域
  5. 图片使用 CachedNetworkImage 并指定合理的 cacheWidth/cacheHeight
  6. 使用 Scrollbar + physics: const ClampingScrollPhysics() 优化滚动感

1.2 问题:列表项动态高度导致跳动

症状:列表项高度不固定,滚动到中间后返回顶部时发生跳动

根因分析

  • Sliver 协议中,已滚过的 Sliver 的精确尺寸未知
  • SliverList 默认使用 estimatedMaxScrollOffset 估算

解决方案

  1. 使用 itemExtent 指定固定高度(最优)
  2. 使用 prototypeItem 提供原型项
  3. 缓存已计算的高度(自定义 ScrollController + IndexedScrollController
  4. 使用 scrollable_positioned_list 等第三方库

二、嵌套滚动冲突

2.1 问题:滚动容器嵌套导致无法正常滚动

症状:PageView 内嵌 ListView,上下滑动和左右滑动冲突

根因分析

  • 手势竞技场中,内层和外层滚动容器同时参与竞争
  • 默认情况下内层会优先获取滚动事件

解决方案

  1. 给内层 ListView 设置 physics: ClampingScrollPhysics()NeverScrollableScrollPhysics()
  2. 使用 NestedScrollView + SliverOverlapAbsorber/SliverOverlapInjector
  3. 使用 CustomScrollView 统一管理 Sliver
  4. 自定义 ScrollPhysics 在边界时转发滚动事件给外层
  5. 使用 NotificationListener<ScrollNotification> 手动协调

2.2 问题:TabBarView + ListView 嵌套滚动不协调

解决方案

  • NestedScrollView 是标准方案
  • body 中的 ListView 使用 SliverOverlapInjector
  • headerSliverBuilder 中使用 SliverOverlapAbsorber
  • floatHeaderSlivers 控制头部是否浮动

三、键盘相关问题

3.1 问题:键盘弹出遮挡输入框

解决方案

  1. 使用 ScaffoldresizeToAvoidBottomInset: true(默认开启)
  2. SingleChildScrollView 包裹表单
  3. 使用 MediaQuery.of(context).viewInsets.bottom 获取键盘高度
  4. 使用 Scrollable.ensureVisible() 滚动到输入框位置

3.2 问题:键盘弹出导致底部布局被挤压

解决方案

  1. 设置 resizeToAvoidBottomInset: false,手动处理布局
  2. 使用 AnimatedPadding 添加键盘高度的底部间距
  3. 底部按钮使用 MediaQuery.of(context).viewInsets.bottom 动态调整位置

四、内存泄漏问题

4.1 问题:页面退出后内存不释放

根因分析

  • AnimationController 未在 dispose() 中释放
  • StreamSubscription 未取消
  • ScrollControllerTextEditingController 未 dispose
  • 闭包持有 State 引用(如 Timer 回调)
  • GlobalKey 使用不当

解决方案

  1. 所有 Controller 在 dispose() 中调用 .dispose()
  2. 所有 Stream 订阅在 dispose().cancel()
  3. Timer 在 dispose().cancel()
  4. 异步回调中检查 mounted 状态
  5. 使用 DevTools Memory 面板检测泄漏
  6. 使用 flutter_leak 包自动检测

4.2 问题:大图片导致 OOM

解决方案

  1. 使用 ResizeImagecacheWidth/cacheHeight 降低解码尺寸
  2. 及时调用 imageCache.clear() 清理缓存
  3. 避免同时加载过多大图
  4. 使用 Image.memory 时注意 Uint8List 的释放
  5. 列表中的图片使用懒加载,离屏时释放

五、Platform Channel 相关问题

5.1 问题:Channel 调用无响应

根因分析

  • 原生端未注册对应的 Handler
  • Channel 名称拼写不一致
  • 原生端在非主线程处理
  • 返回了不支持的数据类型

解决方案

  1. 统一管理 Channel 名称(使用常量)
  2. 确保原生端在主线程注册 Handler
  3. 使用 StandardMethodCodec 支持的类型
  4. 原生端的异步操作完成后再调用 result
  5. 添加错误处理(try-catch + result.error)

5.2 问题:大数据传输性能差

解决方案

  1. 使用 BasicMessageChannel + BinaryCodec 传输二进制数据
  2. 大文件通过文件路径传递,而非文件内容
  3. 考虑使用 FFI 直接调用 C 代码(无序列化开销)
  4. 分批传输,避免一次性传输过大数据

六、状态管理复杂场景

6.1 问题:深层嵌套组件的状态传递

解决方案

  1. 使用 Provider/Riverpod 进行状态提升
  2. 使用 InheritedWidget 进行数据共享
  3. 避免过深的 Widget 嵌套(提取为独立组件)
  4. 使用 context.select() 避免不必要的重建

6.2 问题:多个状态之间的依赖关系

解决方案

  1. Provider 使用 ProxyProvider 处理依赖
  2. Riverpod 使用 ref.watch() 自动追踪依赖
  3. Bloc 使用 BlocListener 监听一个 Bloc 的变化来触发另一个
  4. 避免循环依赖(A 依赖 B,B 依赖 A)

七、混合开发相关问题

7.1 问题:Flutter 页面嵌入原生 App 性能差

根因分析

  • 每个 FlutterEngine 占用大量内存(约 40~50 MB)
  • 首次启动 Flutter 页面需要初始化引擎

解决方案

  1. 使用预热引擎(FlutterEngineCache
  2. 使用 FlutterEngineGroup 共享引擎(Flutter 2.0+)
  3. 使用 FlutterFragment/FlutterViewController 而非 FlutterActivity
  4. 合理管理 FlutterEngine 生命周期

7.2 问题:PlatformView 性能问题

根因分析

  • VirtualDisplay 模式(Android):额外的纹理拷贝
  • HybridComposition 模式(Android):线程同步开销

解决方案

  1. Android 优先使用 Hybrid Composition(性能更好,但有线程同步问题)
  2. iOS 没有这个问题(使用 Composition 方式)
  3. 减少 PlatformView 的数量和大小
  4. 对于简单需求,考虑用 Flutter 原生 Widget 替代

八、文字与字体问题

8.1 问题:不同平台文字显示不一致

根因分析

  • 各平台默认字体不同
  • 文字行高计算方式不同
  • TextPainterstrutStyletextHeightBehavior 差异

解决方案

  1. 使用自定义字体(包入 App 中)
  2. 设置 StrutStyle 统一行高
  3. 使用 TextHeightBehavior 控制首行和末行的行高行为
  4. 通过 height 属性精确控制行高比例

8.2 问题:自定义字体包体积过大

解决方案

  1. 只包含需要的字重(Regular/Bold)
  2. 使用 fontTools 子集化字体(只包含用到的字符)
  3. 中文字体按需加载(Google Fonts 动态下载)
  4. 使用可变字体(Variable Font)减少文件数

九、热更新与动态化

9.1 问题:Flutter 不支持热更新

根因分析

  • Flutter Release 模式使用 AOT 编译,生成机器码
  • 不像 RN/Weex 那样解释执行 JS
  • Apple App Store 禁止动态下载可执行代码

解决方案(有限制)

  1. MXFlutter / Fair / Kraken:DSL 方案,用 JSON/JS 描述 UI
  2. Shorebird(Code Push):Flutter 官方团队成员的方案,支持 Dart 代码热更新
  3. 资源热更新:图片、配置等非代码资源可以动态下载
  4. 服务端驱动 UI(Server-Driven UI):服务端下发 JSON 描述 UI 结构
  5. 混合方案:核心逻辑 Flutter,动态部分 Web/H5

十、国际化与适配问题

10.1 问题:RTL(从右到左)布局适配

解决方案

  1. 使用 Directionality Widget 或 Localizations
  2. 使用 TextDirection.rtl
  3. 使用 start/end 代替 left/rightEdgeInsetsDirectional
  4. 使用 Positioned.directional 代替 Positioned
  5. 测试:flutter run --dart-define=FORCE_RTL=true

10.2 问题:不同屏幕密度适配

解决方案

  1. 使用 MediaQuery.of(context).devicePixelRatio 获取像素密度
  2. 使用 LayoutBuilder 根据可用空间自适应
  3. 使用 FittedBoxAspectRatio 比例适配
  4. 设计稿基于 375 逻辑像素宽度,使用 ScreenUtil 等比缩放
  5. 使用 flutter_screenutil 第三方库辅助适配

第四部分:性能优化八股文与深入细节

一、渲染性能优化

1.1 Widget 重建优化

核心原则:减少不必要的 rebuild

1.1.1 const 构造器
  • const Widget 在编译期创建实例,运行时不重新创建
  • 当父 Widget rebuild 时,const 子 Widget 被跳过
  • 原理:canUpdate 比较时,const 实例是同一个对象,直接跳过 updateChild
  • 适用:所有不依赖运行时数据的 Widget
1.1.2 拆分 Widget
  • 将频繁变化的部分拆分为独立的 StatefulWidget
  • 只有该子树 rebuild,不影响兄弟节点
  • 避免在顶层 setState 导致整棵树重建
1.1.3 Provider 的 Selector / Consumer
  • Selector<T, S> 只监听 T 的某个属性 S
  • 当 S 没变时,即使 T 变了也不 rebuild
  • Consumer 将 rebuild 范围限制在 Consumer 的 builder 内
1.1.4 shouldRebuild 控制
  • SelectorshouldRebuild:自定义比较逻辑
  • BlocBuilderbuildWhen:控制何时重建
  • 自定义 Widget 中重写 shouldRebuild / operator ==

1.2 布局优化

1.2.1 避免深层嵌套
  • 过深的 Widget 树增加 build 和 layout 时间
  • 提取复杂布局为独立 Widget
  • 使用 CustomMultiChildLayoutCustomPaint 处理复杂布局
1.2.2 使用 RepaintBoundary
  • 在频繁变化的区域添加 RepaintBoundary
  • 使 Flutter 为该子树创建独立的 Layer
  • 重绘时只更新该 Layer,不影响其他区域
  • 适用:动画、倒计时、视频播放器上层
1.2.3 RelayoutBoundary 理解
  • Flutter 自动在满足条件时创建 RelayoutBoundary
  • 当一个 RenderObject 是 relayout boundary 时,其子树布局变化不传播到父节点
  • 可通过 sizedByParent 等手段触发
1.2.4 Intrinsic 尺寸计算的代价
  • IntrinsicHeight / IntrinsicWidth 会触发两次布局(一次计算 intrinsic,一次正式布局)
  • 嵌套使用会导致指数级性能下降(O(2^n))
  • 尽量避免使用,改用固定尺寸或 LayoutBuilder

1.3 绘制优化

1.3.1 saveLayer 的代价
  • saveLayer 会创建离屏缓冲区(OffscreenBuffer)
  • 开销包括:分配纹理、额外的绘制 pass、合成
  • 触发 saveLayer 的 Widget:Opacity(< 1.0 时)、ShaderMaskColorFilterClip.antiAliasWithSaveLayer
  • 优化:使用 AnimatedOpacity 代替 Opacity,使用 FadeTransition
1.3.2 Clip 行为选择
ClipBehavior 性能 质量
Clip.none 最好 无裁剪
Clip.hardEdge 锯齿
Clip.antiAlias 抗锯齿
Clip.antiAliasWithSaveLayer 差(触发 saveLayer) 最好
  • 大多数场景 Clip.hardEdgeClip.antiAlias 即可
  • Flutter 3.x 默认很多 Widget 的 clipBehavior 改为 Clip.none
1.3.3 图片渲染优化
  • 指定 cacheWidth / cacheHeight:告诉解码器以较小尺寸解码
  • 避免在 build 中创建 ImageProvider(会重复触发加载)
  • 使用 precacheImage() 预加载
  • 使用 ResizeImage 包装 Provider

1.4 Shader 编译卡顿(Jank)

1.4.1 问题本质
  • Skia 在首次使用某个 Shader 时需要编译
  • 编译发生在 GPU 线程,导致该帧耗时增加
  • 表现为首次执行某个动画/效果时卡顿,后续流畅
1.4.2 解决方案
  1. SkSL 预热:收集 Shader 并预编译(flutter run --cache-sksl
  2. Impeller 引擎:预编译所有 Shader,彻底解决该问题(Flutter 3.16+ iOS 默认启用)
  3. 避免在首帧使用复杂效果:延迟执行复杂动画
  4. 减少 saveLayer 使用:saveLayer 会触发额外的 Shader

二、内存优化

2.1 图片内存优化

策略 效果 实现方式
降低解码分辨率 显著 cacheWidth / cacheHeight
调整缓存大小 中等 imageCache.maximumSize / maximumSizeBytes
及时清理缓存 中等 imageCache.clear() / evict()
使用占位图 间接 placeholder / FadeInImage
列表离屏回收 显著 ListView.builder 的自动回收机制

2.2 大列表内存优化

  • ListView.builder:自动回收离屏 Widget 和 Element
  • addAutomaticKeepAlives: false:禁止保持状态,释放离屏资源
  • addRepaintBoundaries: false:在确定不需要时禁用(每项都有 RepaintBoundary 也有开销)
  • 使用 findChildIndexCallback 优化长列表 Key 查找

2.3 内存泄漏排查

DevTools Memory 面板
  1. 点击 "Take Heap Snapshot" 获取堆快照
  2. 对比两个快照的差异
  3. 查找不应存在的对象(如已 pop 的页面的 State)
  4. 分析引用链,找到 GC Root
常见泄漏模式
泄漏模式 原因 修复
Controller 未释放 dispose 未调用 controller.dispose() 在 dispose 中释放
Stream 未取消 StreamSubscription 未 cancel 在 dispose 中 cancel
Timer 未取消 Timer 回调持有 State 引用 在 dispose 中 cancel
闭包引用 匿名函数持有 context/state 使用弱引用或检查 mounted
GlobalKey 滥用 GlobalKey 持有 Element 引用 减少使用,及时释放
Static 变量持有 静态变量引用了 Widget/State 避免在 static 中存储 UI 相关对象

三、启动性能优化

3.1 启动阶段分析

原生初始化                           Flutter 引擎初始化
┌──────────┐     ┌─────────────────────────────┐     ┌──────────────┐
│ App Start │ →→→ │ Engine Init + Dart VM Init  │ →→→ │ First Frame  │
│ (Native)  │     │ + Framework Init            │     │  Rendered    │
└──────────┘     └─────────────────────────────┘     └──────────────┘

3.2 优化策略

阶段 优化措施
原生阶段 使用 FlutterSplashScreen,减少原生初始化逻辑
引擎初始化 预热引擎(FlutterEngineCache)、FlutterEngineGroup
Dart 初始化 延迟非必要初始化、懒加载服务
首帧渲染 简化首屏 UI、减少首屏网络请求、使用骨架屏
AOT 编译 确保 Release 模式使用 AOT
Tree Shaking 移除未使用代码和资源
延迟加载 deferred as 延迟导入库

3.3 Deferred Components(延迟组件)

  • Android 支持 deferred-components(基于 Play Feature Delivery)
  • 将不常用的模块延迟下载
  • 减少初始安装包大小和启动负载

四、包体积优化

4.1 Flutter App 包组成

组成部分 占比 说明
Dart AOT 代码 ~30% 编译后的机器码
Flutter Engine ~40% libflutter.so / Flutter.framework
资源文件 ~20% 图片、字体、音频等
原生代码 ~10% 第三方 SDK、Channel 实现

4.2 优化措施

措施 效果
--split-debug-info 分离调试信息,减少 ~30%
--obfuscate 代码混淆,略微减少
移除未使用资源 手动或使用工具检测
压缩图片 WebP 格式、TinyPNG
字体子集化 减少中文字体体积
--tree-shake-icons 移除未使用的 Material Icons
deferred-components 延迟加载非核心模块
移除未使用的插件 pubspec.yaml 清理

五、列表与滚动性能优化

5.1 列表构建优化

策略 说明
使用 itemExtent 跳过子项布局计算,直接使用固定高度
使用 prototypeItem 用原型项推导高度
findChildIndexCallback 优化长列表的 Key 查找复杂度
addAutomaticKeepAlives: false 减少内存占用
缩小 cacheExtent 减少预渲染范围(默认 250 逻辑像素)

5.2 列表项优化

  • 使用 const Widget
  • 避免在列表项中使用 OpacityClipPath 等高开销 Widget
  • 使用 RepaintBoundary 隔离
  • 图片指定 cacheWidth/cacheHeight
  • 使用 CachedNetworkImage 避免重复加载

六、动画性能优化

6.1 减少动画引起的重建

  • 使用 AnimatedBuilder / XXXTransition 而非在 setState 中直接更新
  • AnimatedBuilderchild 参数:不受动画影响的子树只构建一次
  • 使用 RepaintBoundary 隔离动画区域

6.2 物理动画与复合动画

  • 使用 Transform 而非改变 Widget 的实际属性
  • Transform 只影响绘制阶段,不触发布局
  • 避免动画中触发布局重算(不要在动画中改变 width/height/padding 等布局属性)

6.3 Impeller 对动画的提升

  • 预编译 Shader,消除首次动画卡顿
  • 更高效的 tessellation
  • iOS 默认启用(Flutter 3.16+),Android 实验中

七、网络性能优化

7.1 请求优化

策略 说明
请求缓存 Dio Interceptor 实现 HTTP 缓存
请求合并 相同 URL 的并发请求合并为一个
请求取消 页面退出时取消未完成请求(CancelToken)
连接复用 HTTP/2 多路复用
数据压缩 开启 gzip 响应
分页加载 避免一次加载全部数据

7.2 JSON 解析优化

  • 大 JSON 使用 compute() 在 Isolate 中解析
  • Dio 的 Transformer 可配置在后台线程处理
  • 使用 json_serializable 代码生成而非手写

八、DevTools 性能调试工具

8.1 Performance Overlay

  • 顶部条:GPU 线程耗时(光栅化)
  • 底部条:UI 线程耗时(Dart 代码执行)
  • 绿色条 < 16ms = 60fps
  • 红色条 > 16ms = 掉帧

8.2 Timeline 分析

  • 按帧查看 Build、Layout、Paint 各阶段耗时
  • 识别耗时操作和卡顿原因
  • 按树结构查看各 Widget 的 build 耗时

8.3 Widget Inspector

  • 查看 Widget Tree 和 RenderObject Tree
  • 高亮 RepaintBoundary 区域
  • 显示布局约束信息(Constraints、Size)
  • Debug Paint:可视化布局边界和 Padding

8.4 检测方法

工具/标志 用途
debugProfileBuildsEnabled 跟踪 build 调用
debugProfileLayoutsEnabled 跟踪 layout 调用
debugProfilePaintsEnabled 跟踪 paint 调用
debugPrintRebuildDirtyWidgets 打印 dirty Widget
debugRepaintRainbowEnabled 彩虹色显示重绘区域
debugPrintLayouts 打印布局过程

第五部分:全面横向纵向对比

一、状态管理方案对比

1.1 六大状态管理方案全面对比

维度 setState InheritedWidget Provider Bloc GetX Riverpod
学习成本 极低 中高
代码量
可测试性 优秀 优秀
可维护性 差(项目大时) 优秀 优秀
性能 低(全量重建)
依赖 context
编译安全 -
适合项目规模 小型 中型 中型 大型 小中型 大型
社区活跃度 - -
响应式模式 手动 手动 自动 自动 自动 自动
DevTools 支持 - - 优秀 有限
原理 Element dirty InheritedElement InheritedWidget封装 Stream GetxController+Rx ProviderContainer

1.2 何时选择哪个?

场景 推荐方案 原因
原型 / Demo setState / GetX 最快出结果
中型项目 Provider 简单够用,社区支持好
大型企业项目 Bloc / Riverpod 可测试性强,架构清晰
需要脱离 Widget 树 Riverpod / GetX 不依赖 BuildContext
团队不熟悉 Flutter Provider 最容易上手
重视可追溯性 Bloc Event 日志、Time Travel

二、Widget 生命周期各方法对比

2.1 StatefulWidget 生命周期方法对比

方法 调用时机 调用次数 可否 setState 有 oldWidget 典型操作
createState Widget 创建时 1 创建 State
initState State 初始化 1 否(可赋值) 初始化变量、订阅
didChangeDependencies 依赖变化 ≥1 可以 读取 InheritedWidget
build 每次重建 多次 返回 Widget 树
didUpdateWidget 父 Widget 重建 多次 可以 对比新旧配置
reassemble Hot Reload 多次(Debug only) 可以 调试
deactivate 从树移除 可能多次 清理临时状态
dispose 永久移除 1 释放资源

2.2 App 生命周期(AppLifecycleState)

状态 含义 iOS 对应 Android 对应
resumed 前台可见可交互 viewDidAppear onResume
inactive 前台可见不可交互 viewWillDisappear onPause(部分)
paused 后台不可见 进入后台 onStop
detached 分离(即将销毁) 应用终止 onDestroy
hidden Flutter 3.13+ 新增 过渡态 过渡态

2.3 didChangeDependencies vs didUpdateWidget 对比

特性 didChangeDependencies didUpdateWidget
触发条件 InheritedWidget 变化 父 Widget rebuild
参数 covariant oldWidget
首次调用 initState 之后调用一次 首次不调用
典型用途 获取 Theme/MediaQuery/Provider 对比新旧 Widget 属性
发生频率 较低 较高

三、三种 Channel 全面对比

3.1 BasicMessageChannel vs MethodChannel vs EventChannel

维度 BasicMessageChannel MethodChannel EventChannel
通信方向 双向 双向(请求-响应) 单向(Native → Flutter)
通信模式 消息传递 方法调用 事件流
返回值 消息回复 Future<T?> Stream
编解码 MessageCodec MethodCodec MethodCodec
适用场景 简单数据传递 调用原生功能 持续性事件监听
典型用例 传递配置、简单消息 获取电量、打开相机 传感器数据、位置更新、网络状态
原生端 API setMessageHandler setMethodCallHandler EventChannel.StreamHandler
调用方式 send(message) invokeMethod(method, args) receiveBroadcastStream()

3.2 Channel vs FFI 对比

维度 Platform Channel Dart FFI
通信方式 异步消息传递 直接函数调用
性能 中(序列化开销) 高(无序列化)
支持同步
支持的语言 Java/Kotlin/ObjC/Swift C/C++
复杂度
线程模型 主线程间通信 可在任意 Isolate 调用
适用场景 一般原生交互 高频调用、大数据、音视频

四、布局 Widget 对比

4.1 Row / Column / Stack / Wrap / Flow 对比

Widget 布局方向 超出处理 子项数量 性能 适用场景
Row 水平 溢出警告 少量 水平排列
Column 垂直 溢出警告 少量 垂直排列
Stack 层叠 可溢出 少量 重叠布局
Wrap 自动换行 换行 中等 标签流
Flow 自定义 自定义 大量 高(自定义布局) 复杂流式布局
ListView 单轴滚动 滚动 大量 高(懒加载) 长列表
GridView 二维网格 滚动 大量 高(懒加载) 网格布局
CustomScrollView 自定义 滚动 大量 混合滚动

4.2 Flexible / Expanded / Spacer 对比

Widget flex 默认值 fit 默认值 行为
Flexible 1 FlexFit.loose 子 Widget 可以小于分配空间
Expanded 1 FlexFit.tight 子 Widget 必须填满分配空间
Spacer 1 FlexFit.tight 纯空白占位

关系Expanded = Flexible(fit: FlexFit.tight)Spacer = Expanded(child: SizedBox.shrink())

4.3 SizedBox / Container / ConstrainedBox / LimitedBox / UnconstrainedBox 对比

Widget 功能 约束行为 性能
SizedBox 指定固定大小 传递紧约束 最高
Container 多功能容器 取决于属性组合 中(功能多)
ConstrainedBox 添加额外约束 合并约束
LimitedBox 在无限约束时限制大小 仅在无界时生效
UnconstrainedBox 去除父约束 让子 Widget 自由布局
FractionallySizedBox 按比例设置大小 按父空间百分比

五、异步编程对比

5.1 Future vs Stream

维度 Future Stream
值的数量 单个值 多个值(序列)
完成时机 产生值后完成 可持续发出值
订阅方式 then / await listen / await for
错误处理 catchError / try-catch onError / handleError
取消 不可取消 StreamSubscription.cancel()
典型场景 网络请求、文件读写 WebSocket、传感器、事件流

5.2 Stream 的类型对比

维度 单订阅 Stream 广播 Stream
监听者数量 仅 1 个 多个
数据缓存 未监听时缓存 未监听时丢弃
创建方式 StreamController() StreamController.broadcast()
适用场景 文件读取、HTTP 响应 事件总线、UI 事件

5.3 compute() vs Isolate.spawn() vs Isolate.run()

维度 compute() Isolate.spawn() Isolate.run()
API 级别
返回值 Future 无(需 SendPort) Future
通信方式 封装好 手动 SendPort/ReceivePort 封装好
多次通信 不支持 支持 不支持
适用场景 简单单次计算 复杂长期任务 简单单次计算(推荐)
版本 所有版本 所有版本 Dart 2.19+

六、导航与路由方案对比

6.1 Navigator 1.0 vs Navigator 2.0

维度 Navigator 1.0 Navigator 2.0
编程范式 命令式 声明式
API 复杂度
URL 同步 需手动 自动
Deep Link 不完善 完善
Web 友好
路由栈控制 受限 完全控制
适用场景 移动端简单导航 Web、深度链接、复杂导航

6.2 路由库对比

维度 go_router auto_route beamer GetX Router
基于 Navigator 2.0 Navigator 2.0 Navigator 2.0 自定义
代码生成 可选
类型安全 可选 部分
嵌套路由 ShellRoute 支持 BeamLocation 支持
守卫 redirect AutoRouteGuard BeamGuard 中间件
官方维护 社区 社区 社区
学习成本 中高

七、动画方案对比

7.1 隐式动画 vs 显式动画 vs 物理动画 vs Rive/Lottie

维度 隐式动画 显式动画 物理动画 Rive/Lottie
复杂度 中高 低(但需设计工具)
控制力
性能 取决于复杂度
典型用途 属性过渡 自定义动画 弹性/惯性效果 复杂矢量动画
代码量
适合场景 简单过渡 精确控制 自然效果 品牌动画

7.2 AnimatedBuilder vs AnimatedWidget

维度 AnimatedBuilder AnimatedWidget
使用方式 通过 builder 回调 继承后重写 build
child 优化 支持(child 参数不重建) 不直接支持
复用性 高(不需要创建新类) 需要为每种动画创建类
适用场景 简单动画、一次性使用 可复用的动画 Widget

7.3 Tween vs CurveTween vs TweenSequence

维度 Tween CurveTween TweenSequence
功能 线性映射 begin→end 添加曲线 多段动画序列
输入 Animation Animation Animation
输出 Animation Animation Animation
用法 tween.animate(controller) CurveTween(curve: ...) 定义多段 TweenSequenceItem

八、跨平台方案对比

8.1 Flutter vs React Native vs Native

维度 Flutter React Native Native
语言 Dart JavaScript Swift/Kotlin
渲染方式 自绘引擎(Skia/Impeller) 原生控件桥接 原生控件
性能 接近原生 低于原生(桥接开销) 原生
UI 一致性 跨平台完全一致 平台差异 仅单平台
热重载 支持 支持 Xcode Preview
生态 增长中 成熟 最成熟
包大小 较大(含引擎) 中等 最小
调试体验 DevTools Chrome DevTools Xcode/AS
适合场景 UI 密集型、跨端一致 已有 RN 团队 极致性能/平台特性

8.2 Flutter Web vs Flutter Mobile vs Flutter Desktop

维度 Web Mobile Desktop
渲染后端 CanvasKit / HTML Skia / Impeller Skia / Impeller
性能 中(取决于浏览器)
包大小 CanvasKit ~2MB 取决于代码 取决于代码
SEO 差(CanvasKit)/ 中(HTML) 不适用 不适用
成熟度 中等 成熟 中等
特殊考虑 字体加载、URL 路由 平台权限 窗口管理

九、构建模式对比

9.1 Debug vs Profile vs Release

维度 Debug Profile Release
编译方式 JIT AOT AOT
热重载 支持 不支持 不支持
性能 接近 Release 最高
包大小 最小
断言 启用 禁用 禁用
DevTools 全功能 性能分析 不可用
Observatory 可用 可用 不可用
用途 开发调试 性能分析 发布上线

十、滚动 Widget 对比

10.1 ListView vs GridView vs CustomScrollView vs SingleChildScrollView

维度 ListView GridView CustomScrollView SingleChildScrollView
布局方式 线性列表 网格 自定义 Sliver 组合 单个子 Widget 滚动
懒加载 .builder 支持 .builder 支持 取决于 Sliver 类型 不支持
性能(大量子项) 高(builder) 高(builder) 差(全量渲染)
灵活性 最高
适用场景 普通列表 图片墙 混合滚动布局 内容少但需滚动

10.2 ScrollPhysics 对比

Physics 效果 平台
BouncingScrollPhysics iOS 弹性效果 iOS 默认
ClampingScrollPhysics Android 边缘效果 Android 默认
NeverScrollableScrollPhysics 禁止滚动 嵌套时使用
AlwaysScrollableScrollPhysics 总是可滚动 下拉刷新
PageScrollPhysics 翻页效果 PageView
FixedExtentScrollPhysics 对齐到固定高度项 ListWheelScrollView

十一、Key 类型对比

Key 类型 唯一性范围 比较方式 内存开销 适用场景
ValueKey<T> 同级 value 的 == 列表项有唯一 ID
ObjectKey 同级 identical() 用对象作为标识
UniqueKey 同级 每个实例唯一 强制重建
GlobalKey 全局 同一实例 高(全局注册) 跨组件访问 State
PageStorageKey 存储范围 value 的 == 保存滚动位置

十二、State 存储与恢复对比

12.1 数据持久化方案对比

方案 数据类型 性能 容量 适用场景
SharedPreferences K-V(基本类型) 配置项、简单设置
sqflite 结构化数据 复杂查询、关系数据
hive K-V / 对象 极高 NoSQL、高性能
drift(moor) 结构化数据 类型安全 ORM
isar 对象数据库 极高 全文搜索、高性能
文件存储 任意 日志、缓存
secure_storage K-V(加密) 敏感数据(Token)

十三、BuildContext 获取方式对比

方式 作用 返回值 性能影响
context.dependOnInheritedWidgetOfExactType<T>() 获取+注册依赖 T? 会触发 didChangeDependencies
context.getInheritedWidgetOfExactType<T>() 仅获取,不注册依赖 T? 无重建影响
context.findAncestorWidgetOfExactType<T>() 向上查找 Widget T? O(n) 遍历
context.findAncestorStateOfType<T>() 向上查找 State T? O(n) 遍历
context.findRenderObject() 获取 RenderObject RenderObject? 直接获取
context.findAncestorRenderObjectOfExactType<T>() 向上查找 RenderObject T? O(n) 遍历

十四、错误处理对比

14.1 Flutter 错误类型

错误类型 触发场景 处理方式
Dart 异常 代码逻辑错误 try-catch
Widget 构建异常 build 方法中抛出 ErrorWidget.builder 自定义
Framework 异常 布局溢出、约束冲突 FlutterError.onError
异步异常 未捕获的 Future 错误 runZonedGuarded
Platform 异常 原生代码异常 PlatformDispatcher.onError
Isolate 异常 计算 Isolate 中的错误 Isolate.errors / compute catch

14.2 全局错误捕获最佳实践

void main() {
  // 1. Flutter Framework 错误
  FlutterError.onError = (details) {
    // 上报
  };
  
  // 2. 平台错误
  PlatformDispatcher.instance.onError = (error, stack) {
    // 上报
    return true;
  };
  
  // 3. Zone 内异步错误
  runZonedGuarded(() {
    runApp(MyApp());
  }, (error, stack) {
    // 上报
  });
}

十五、测试方案对比

维度 单元测试 Widget 测试 集成测试
速度 最快
信心
依赖 部分 完整 App
环境 Dart VM 模拟 Framework 真机/模拟器
测试对象 函数、类 Widget、交互 完整用户流程
工具 test flutter_test integration_test
Mock mockito mockito + pump -
维护成本

十六、Impeller vs Skia 渲染引擎对比

维度 Skia Impeller
类型 通用 2D 渲染 Flutter 专用渲染
Shader 编译 运行时编译(卡顿) 预编译(无卡顿)
API 后端 OpenGL / Vulkan / Metal Metal / Vulkan
性能一致性 首次卡顿后流畅 始终流畅
成熟度 非常成熟 发展中
iOS 状态 已弃用 默认启用(3.16+)
Android 状态 默认 实验中(可选启用)
文字渲染 成熟 持续改进

十七、不同约束类型对比

17.1 BoxConstraints 的四种情况

约束类型 条件 含义 例子
紧约束 (Tight) minW==maxW && minH==maxH 大小完全确定 SizedBox(w:100, h:100)
松约束 (Loose) minW==0 && minH==0 只有上限 Center 传给子节点
有界约束 (Bounded) maxW < ∞ && maxH < ∞ 有限空间 普通容器
无界约束 (Unbounded) maxW == ∞ 或 maxH == ∞ 无限空间 ListView 主轴方向

17.2 约束传递的常见问题

问题 原因 解决
"RenderFlex overflowed" 子项总大小超过约束 Flexible/Expanded/滚动
"unbounded height" 在无界约束中使用需要有界的 Widget 给定明确高度/用 Expanded
"A RenderFlex overflowed by X pixels" Row/Column 子项过多 使用 Wrap、ListView
子 Widget 撑满父容器 紧约束传递 用 Center/Align 包裹

十八、编译产物对比

18.1 Android 编译产物

产物 说明 位置
libflutter.so Flutter Engine lib/armeabi-v7a & arm64-v8a
libapp.so Dart AOT 代码 lib/armeabi-v7a & arm64-v8a
flutter_assets/ 资源文件 assets/
isolate_snapshot_data Isolate 快照 Debug 模式
vm_snapshot_data VM 快照 Debug 模式

18.2 iOS 编译产物

产物 说明
App.framework Dart AOT 代码
Flutter.framework Flutter Engine
flutter_assets/ 资源文件

十九、混入方式对比(Mixin / Extends / Implements)

维度 extends(继承) implements(实现) with(混入)
关系 is-a can-do has-ability
数量 单继承 多实现 多混入
方法实现 继承父类实现 必须全部实现 获得 mixin 实现
构造函数 继承 不继承 mixin 不能有构造函数
字段 继承 需要重新声明 获得 mixin 字段
适用场景 核心继承关系 接口协议 横向能力扩展

二十、typedef / Function / Callback 对比

概念 说明 示例
typedef 函数类型别名 typedef VoidCallback = void Function();
Function 通用函数类型 Function? callback;(不推荐,无类型)
ValueChanged<T> 接收一个值的回调 ValueChanged<String> = void Function(String)
ValueGetter<T> 无参返回值 ValueGetter<int> = int Function()
ValueSetter<T> 接收一个值无返回 ValueSetter<int> = void Function(int)
VoidCallback 无参无返回 void Function()

二十一、final / const / late / static 对比

关键字 赋值次数 初始化时机 作用域 典型用途
final 一次 运行时 实例 运行时确定的不可变值
const 一次 编译时 实例/类 编译时确定的常量
late 延迟一次 首次访问时 实例 延迟初始化、不可空但无法立即初始化
static 多次 首次访问时 类级别共享变量
static final 一次 首次访问时 类级别常量(运行时)
static const 一次 编译时 类级别常量(编译时)

二十二、集合类型对比

集合 有序 唯一 索引访问 查找复杂度 适用场景
List<T> O(1) O(n) 有序数据
Set<T> 否(LinkedHashSet 有序) 不支持 O(1) 去重
Map<K,V> 否(LinkedHashMap 有序) Key 唯一 O(1) O(1) 键值对
Queue<T> 不支持 O(n) 队列操作
SplayTreeSet<T> 排序 不支持 O(log n) 有序集合
SplayTreeMap<K,V> 排序 Key 唯一 O(log n) O(log n) 有序映射

二十三、常用 Sliver 组件对比

Sliver 功能 对应普通 Widget
SliverList 列表 ListView
SliverGrid 网格 GridView
SliverFixedExtentList 固定高度列表 ListView(itemExtent)
SliverAppBar 可折叠 AppBar AppBar
SliverToBoxAdapter 包装普通 Widget -
SliverFillRemaining 填充剩余空间 -
SliverPersistentHeader 吸顶/固定头部 -
SliverPadding 内边距 Padding
SliverOpacity 透明度 Opacity
SliverAnimatedList 动画列表 AnimatedList

二十四、线程模型对比

24.1 Flutter 的四个 Runner(线程)

Runner 职责 阻塞影响
UI Runner Dart 代码执行、Widget build、Layout 界面卡顿
GPU Runner(Raster) 图层合成、GPU 指令提交 渲染延迟
IO Runner 图片解码、文件读写 资源加载慢
Platform Runner 平台消息处理、插件交互 原生交互延迟

24.2 线程 vs Isolate vs Zone

概念 内存共享 通信方式 用途
线程(Runner) 共享 直接访问 引擎内部
Isolate 不共享 SendPort/ReceivePort Dart 并行计算
Zone 同一 Isolate 直接 错误处理、异步追踪

二十五、打包与发布对比

25.1 Android 打包格式

格式 全称 大小 适用渠道
APK Android Package 较大(含所有架构) 直接安装
AAB Android App Bundle 较小(按需分发) Google Play
Split APK 按架构/语言分包 最小 需要工具分发

25.2 iOS 打包格式

格式 用途
.ipa 发布到 App Store / TestFlight
.app 模拟器运行
.xcarchive Xcode 归档

二十六、补充:Flutter 3.x 重要更新对比

版本 重要特性
Flutter 3.0 稳定支持 macOS/Linux、Material 3、Casual Games Toolkit
Flutter 3.3 文字处理改进、SelectionArea、触控板手势
Flutter 3.7 Material 3 完善、iOS 发布检查、Impeller preview
Flutter 3.10 Impeller iOS 默认、SLSA 合规、无缝 Web 集成
Flutter 3.13 Impeller 改进、AppLifecycleListener、2D Fragment Shaders
Flutter 3.16 Material 3 默认、Impeller iOS 完全启用、Gemini API
Flutter 3.19 Impeller Android preview、滚动优化、Windows ARM64
Flutter 3.22 Wasm 稳定、Impeller Android 改进
Flutter 3.24 Flutter GPU API preview、Impeller Android 更稳定

本文档力求全面、深入、细致地覆盖 Flutter 面试和实战开发中的各个知识点。建议结合实际项目经验理解,理论+实践相结合才能真正融会贯通。

❌