阅读视图

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

12-主题|内存管理@iOS-Option与内存优化技术

本文介绍与内存相关的几类优化与极限管理Option/位运算共用内存(多选项共用一个整数)、内存的极限管理(低内存策略与约束)、Copy-on-Write(写时拷贝)Tagged Pointer。与 01-内存五大分区11-深浅拷贝与内存07-实践与常见问题 配合阅读。


一、Option 与位运算共用内存

1.1 概念

  • 将多个布尔或选项压缩到一个整数的不同上,通过位运算读写,实现「多个开关/状态共占一块内存」;在 C/OC 中常用 NS_OPTIONS位域(bitfield),在 Swift 中对应 OptionSet
  • 内存:N 个独立 BOOLbool 可能占 N 个字节(甚至对齐后更多);用一个整型的若干位表示,只需 1 个整型(如 4 或 8 字节),在选项较多或实例数量巨大时显著节省内存并利于缓存。

1.2 NS_OPTIONS / 位运算示例(Objective-C)

// 多个「选项」共用一个整型,每位表示一种开关
typedef NS_OPTIONS(NSUInteger, ViewOptions) {
    ViewOptionsNone     = 0,
    ViewOptionsHidden   = 1 << 0,   // 1
    ViewOptionsDisabled = 1 << 1,   // 2
    ViewOptionsSelected = 1 << 2,   // 4
    ViewOptionsLoading  = 1 << 3,   // 8
};

// 使用:一个 NSUInteger 存下所有选项
ViewOptions opts = ViewOptionsHidden | ViewOptionsSelected;

// 判断
BOOL isHidden = (opts & ViewOptionsHidden) != 0;

// 置位 / 清位
opts |= ViewOptionsLoading;
opts &= ~ViewOptionsDisabled;
  • 上述 ViewOptions 只占 一个 NSUInteger(8 字节),即可表示 64 个独立布尔;若用 64 个 BOOL 属性,会占用更多内存且不利于缓存。

1.3 位域(bitfield)共用内存

// 结构体内用位域:多个成员共占一个或多个整型
struct PackedFlags {
    unsigned int visible  : 1;  // 1 bit
    unsigned int enabled  : 1;
    unsigned int selected : 1;
    unsigned int loading  : 1;
    unsigned int reserved : 4;  // 预留
};  // 整体可仅占 4 字节(一个 unsigned int)
  • 多个成员共享同一整型内存,适合配置、状态、权限等密集布尔/小范围枚举,在大量实例(如 Cell、配置项)时减少内存占用。

1.4 典型场景

场景 说明
UI 状态 如 hidden、enabled、selected、loading 等用 NS_OPTIONS 或 OptionSet 存为一个整数
权限/能力 读、写、执行等用位表示,一个整数表示一组权限
配置/特性开关 大量配置项用位域或 OptionSet,减少结构体/对象体积
网络/解析标志 协议中的 flags 字段,多位表示多种含义,共用内存

二、内存的极限管理

2.1 目标与场景

  • 内存紧张(低内存设备、后台、系统压力大)时,通过主动释放、限制缓存、延迟加载等手段,把占用控制在系统允许范围内,避免被系统杀掉或 OOM。
  • 07-实践与常见问题 中的「内存警告」「音视频/图层场景」配合使用。

2.2 策略要点

策略 说明
响应 didReceiveMemoryWarning 释放可重建的缓存(图片、数据)、释放非当前页大对象;主线程不阻塞,异步释放。
缓存上限与淘汰 图片/数据缓存设置 maxCount / maxCost,LRU 等淘汰;避免无界增长。
后台释放 进入后台时释放非必要资源(解码器、大缓冲、预览图),回到前台再按需加载。
按需加载 / 流式 大列表、大文件不一次性进内存;分页、流式读取、大图降采样。
@autoreleasepool 循环中大量临时对象用 @autoreleasepool {} 控制峰值,见 [05-AutoreleasePool与RunLoop](05-主题 内存管理@iOS-AutoreleasePool与RunLoop.md)。
内存映射 大文件用 mmap 等映射访问,减少常驻 RSS;注意映射大小与释放时机。

2.3 极限下的注意

  • 不保留可重建数据:能重新下载、重新计算的就不要在内存里常驻。
  • 控制单页/单模块占用:列表、相册、音视频播放等设定上限,避免单场景吃满内存。
  • Instruments:用 Allocations、VM Tracker、Leaks 做「极限场景」压测(反复进入退出、后台、低内存模拟),观察峰值与泄漏。

三、Copy-on-Write(写时拷贝)

3.1 概念

  • Copy-on-Write(COW):多个逻辑上的「副本」在未修改前共享同一份底层存储;仅在某一方发生写操作时才为该方复制出一份新存储,再修改,从而避免「一赋值就整块拷贝」的开销。
  • 与深浅拷贝的关系:浅拷贝是「多引用、共享子对象」;COW 是「多引用、共享存储,写时才真正拷贝」,在保证值语义的前提下减少内存与 CPU 消耗。详见 11-深浅拷贝与内存

3.2 Swift 中的 COW

  • Array、Dictionary、Set、String 等值类型在 Swift 标准库中实现了 COW:赋值时不立即复制底层 buffer,而是共享;首次发生写操作时,若检测到 buffer 被多处引用(非唯一引用),则先复制 buffer 再写。
  • 实现要点:内部持有一个引用类型的 buffer;写前通过 isKnownUniquelyReferenced(或等价机制)判断是否唯一引用,若不唯一则 copy buffer 再写。
  • 效果:大量「只读共享」的赋值与传参几乎零拷贝成本;只有写时才付出拷贝代价,适合读多写少的集合与字符串。

3.3 与内存的关系

  • 省内存:未修改的「副本」不占额外存储,仅多一个指向同一 buffer 的引用。
  • 写时峰值:在共享的 buffer 上首次写入会触发一次拷贝,此时有短暂的内存与 CPU 开销;若写非常频繁且共享多,需注意是否适合用 COW 结构。
  • 自定义值类型:Swift 不会自动为自定义 struct 实现 COW,若需要需自己维护「内部引用 + 写时复制」逻辑。

3.4 流程图(概念)

flowchart LR
    A[赋值/传参] --> B{写操作?}
    B -->|否| C[继续共享 buffer]
    B -->|是| D{唯一引用?}
    D -->|是| E[直接写]
    D -->|否| F[复制 buffer 再写]

四、Tagged Pointer

4.1 概念

  • Tagged Pointer 是 Apple 在 64 位 架构下的一种优化:把「小对象」的数据与类型信息直接编码进指针值本身,而不在堆上分配对象;该「指针」并不是指向堆地址,而是即是指针也是数据
  • 内存:不占用,不参与引用计数(retain/release 对 Tagged Pointer 为 no-op);仅占一个指针宽度(8 字节),无额外分配、无 isa、无引用计数块,极限节省小对象的内存与调用开销。

4.2 原理(64 位简要)

  • 64 位下对象指针通常 16 字节对齐,低 4 位恒为 0;系统用最高位或最低位(依平台而定,如 ARM64 常用最高位)作为 tag,表示「这是 Tagged Pointer」。
  • 其余位中:若干位表示类型(如 NSNumber、NSString、NSDate 等),其余位存数据(如小整数、短字符串的编码)。
  • 运行时通过「解 tag + 类型 + 数据位」还原出逻辑上的「对象」,不访问堆,不触发 retain/release。

4.3 典型类型与约束

类型 说明
NSNumber 小整数、部分浮点数可直接存进指针,不分配堆对象。
NSString 较短字符串(如 ASCII 或少量字符)在较新系统上可能用 Tagged Pointer;更长则仍为堆上分配。
NSDate 部分小对象类型在系统实现中可能使用 Tagged Pointer。
  • 约束:能编码进指针的数据量有限(几十 bit),仅适用于「小」数据;大数、长字符串、复杂对象仍走普通堆分配。

4.4 对内存管理的影响

  • 无堆分配:Tagged Pointer 不占堆,不增加 Allocations 中的对象数。
  • 无引用计数:对 Tagged Pointer 发 retain/release 会被识别并忽略,不会造成过度释放或泄漏(从引用计数角度)。
  • 不可假设地址:不能把 Tagged Pointer 当普通指针做指针运算或与 C 内存接口混用;判断是否 Tagged Pointer 可用运行时 API(如 objc_isTaggedPointer)。

4.5 小结对比

维度 普通堆对象 Tagged Pointer
存储位置 指针值本身(无堆)
引用计数 无(no-op)
内存占用 对象头 + 实例 + 指针 仅 8 字节指针
适用 任意对象 小数据(小整数、短字符串等)

五、思维导图

mindmap
  root((Option 与内存优化))
    Option 位运算
      NS_OPTIONS OptionSet
      位域 共用整型
    内存极限管理
      内存警告 缓存上限
      后台释放 按需加载
    CopyOnWrite
      写时复制 共享 buffer
      Swift 集合 isKnownUniquelyReferenced
    Tagged Pointer
      小对象编码进指针
      无堆 无引用计数

参考文献

10-主题|内存管理@iOS-Block内存管理

本文专门介绍 Objective-C BlockSwift 闭包内存管理:Block 的三种类型(全局/栈/堆)、捕获变量与内存、copy 语义、循环引用 与破除,以及作为属性/参数时的注意点。前置知识见 04-ARC详解06-weak与循环引用


一、Block 是什么(与内存的关系)

  • Block 是 Apple 对 C 语言扩展的闭包:可捕获外部变量、作为对象参与引用计数;在内存上既包含代码(函数指针),也包含捕获的变量(结构体形式),因此既有「存在位置」(栈/堆/全局)也有「对捕获对象的持有关系」。
  • 内存管理 需关注两点:Block 对象本身 的分配与释放(栈 block / 堆 block / 全局 block),以及 Block 对捕获变量(尤其是 OC 对象) 的强引用/弱引用,避免循环引用与泄漏。

二、Block 的三种类型与内存位置

2.1 类型与存储位置

类型(运行时 isa) 存储位置 产生条件(典型)
NSGlobalBlock 全局区(.data/.text) 未捕获任何外部变量(或仅捕获全局/静态变量)
NSStackBlock 捕获了自动变量(局部变量),且未 copy 到堆(MRC 下常见)
NSMallocBlock 对栈 block 执行 copy,或 ARC 下多数「需要逃逸」的 block 被编译器自动 copy 到堆
  • 全局 Block:不依赖栈帧,无需 copy,可当作单例使用。
  • 栈 Block:随栈帧销毁而失效,若要在作用域外使用(如存为属性、异步回调),必须先 copy 到堆;ARC 下编译器会在赋值给 strong/copy 属性、跨函数传递等场景自动插入 copy。
  • 堆 Block:参与引用计数,由 ARC/MRC 管理;copy 时引用计数 +1,release 时 -1。

2.2 简单判断示例(ARC)

// 无捕获 → 全局 Block(__NSGlobalBlock__)
void (^gBlock)(void) = ^{ NSLog(@"no capture"); };

// 捕获局部变量 → 栈 Block(__NSStackBlock__),若赋给 strong/copy 属性则会被 copy 成堆 Block
int a = 1;
void (^sBlock)(void) = ^{ NSLog(@"%d", a); };
// 赋值给 copy/strong 属性或作为参数传给需要「持有」的 API 时,会变成 __NSMallocBlock__

2.3 MRC 下 Block 的 copy 必要性

  • MRC 下,栈上的 Block 在函数返回后栈帧被回收,若此时 block 已被传给调用方或存到堆对象(如属性),再执行会野指针/未定义行为
  • 因此 MRC 下:凡是需要跨作用域保留的 block,必须对其执行一次 copy,将栈 block 拷贝到堆上,得到 NSMallocBlock,之后按普通 OC 对象做 retain/release;用完后要对堆 block 做 release(或 autorelease)。
  • ARC 下:编译器在「赋值给 strong/copy 属性、作为参数传给会保留 block 的 API」等场景自动插入 copy,一般无需手写 [block copy]

三、Block 捕获变量与内存

3.1 捕获方式概览

捕获对象/变量 默认行为(OC 对象) 对引用计数的影响
局部 OC 对象(自动变量) 强引用(strong) Block 被 copy 到堆时,会 retain 被捕获的对象;block 释放时 release
局部标量(int、结构体等) 值拷贝 不涉及引用计数
__block 修饰的变量 生成结构体,block 与外部共享 若 __block 变量指向 OC 对象,需注意 MRC/ARC 下 retain 行为;__block 可改写
__weak 修饰的对象 弱引用 Block 不持有该对象,不增加引用计数,可避免循环引用

3.2 对象捕获与循环引用

  • Block 若强引用了某个对象 A(如直接使用 self),而 A 又强引用了该 block(如 block 被 A 的 strong/copy 属性持有),则形成 self → block → self 的循环,两者都不会释放。
  • 解决:在 block 外使用 __weak typeof(self) wself = self,block 内使用 wself,这样 block 对 self 是弱引用;若在 block 执行过程中担心 self 被释放,可在 block 内再用 __strong typeof(wself) sself = wself 强引用一次(仅限 block 执行期),避免执行到一半 self 被置 nil。详见 06-weak与循环引用

3.3 __block 与内存(简述)

  • __block 使局部变量在 block 内可被修改,编译器会生成一个包装结构,block 捕获的是该结构;若 __block 变量指向 OC 对象,在 ARC 下通常不会造成 block 对对象的强引用(对象存在 __block 结构里),但若在 block 内给该变量赋新值,会涉及旧值 release、新值 retain。MRC 下 __block 不会自动 retain 对象,需自行管理。
  • 历史上用 __block 打破循环(__block self + block 内置 nil)的写法在 ARC 下不推荐,应使用 __weak 打破循环。

四、Block 作为属性、参数与返回值

4.1 属性声明

属性修饰 说明
copy 设值时对 block 执行 copy;MRC 时代推荐,ARC 下 strong 与 copy 对 block 效果类似(都会 copy 到堆),习惯上仍常用 copy 表达「这是 block」的语义。
strong ARC 下与 copy 类似,赋值时也会把栈 block copy 到堆并强引用。
  • Block 属性应避免用 assign(栈 block 离开作用域后失效,会野指针)。

4.2 作为参数与返回值

  • 作为参数:若 API 会保存 block(如延迟执行、存入数组),API 内部应对传入的 block 做 copy(或由 ARC 在传入时保证是堆 block);调用方传栈 block 时,由被调用方 copy 到堆是常见约定。
  • 作为返回值:返回 block 时,若希望调用方在函数返回后仍能使用,应返回堆上的 block(MRC 下 return 前对 block 做 copy/autorelease;ARC 下编译器会根据返回类型自动处理)。

五、ARC 与 MRC 下 Block 内存小结

场景 MRC ARC
栈 block 需跨作用域使用 必须对 block 执行 copy,用完后 release 编译器在赋值给 strong/copy、传参等场景自动 copy
Block 属性 copy,setter 里对 block copy、对旧 block release copystrong 均可,都会导致 copy 到堆并强引用
Block 内引用 self 避免 self→block→self:用 __weak 或 __block+置 nil __weak self,必要时 block 内 __strong 一次
Block 捕获的 OC 对象 copy 到堆时 block 会 retain 捕获的对象;block release 时 release 这些对象 同左,由编译器插入

六、流程图:Block 从创建到释放(概念)

flowchart LR
    A[定义 Block] --> B{是否捕获自动变量?}
    B -->|否| C[__NSGlobalBlock__ 全局]
    B -->|是| D[__NSStackBlock__ 栈]
    D --> E[赋值给 strong/copy 或 传参]
    E --> F[copy 到堆 __NSMallocBlock__]
    F --> G[Block 被 release]
    G --> H[对捕获对象 release]

七、Swift 闭包与内存

  • Swift 闭包 与 OC Block 语义对应:闭包会捕获外部变量,默认对类对象是强引用
  • 循环引用:若对象强引用闭包,闭包内又使用了 self(或捕获了 self),则形成循环;解决方式为在闭包捕获列表中写 [weak self][unowned self](后者在 self 一定不会先于闭包释放时使用,否则会野指针)。
  • @escaping:标记闭包会「逃逸」出当前函数(如异步回调),编译器会按需将闭包拷贝到堆上,与 OC 中「block 被 copy 到堆」对应。

八、思维导图:Block 内存管理知识结构

mindmap
  root((Block 内存管理))
    三种类型
      全局 Block 无捕获
      栈 Block 捕获未 copy
      堆 Block copy 后
    捕获与引用
      对象默认强引用
      __weak 破循环
      __block 可改写
    属性与生命周期
      copy/strong 属性
      MRC 需手写 copy
      ARC 自动 copy
    循环引用
      self → block → self
      weak self strong self

九、参考文献

09-主题|内存管理@iOS-Category与关联对象内存管理

本文介绍 Objective-C Category(分类) 与内存的关系,以及通过 关联对象(Associated Objects) 在 Category 中「挂载」数据时的内存管理:关联策略(policy)、释放时机、循环引用与最佳实践。前置知识见 04-ARC详解06-weak与循环引用


一、Category 与内存的关系

1.1 Category 是什么

  • Category 用于在不修改原类的前提下,为已有类添加方法(以及通过关联对象间接添加「属性」式的存储)。
  • Category 不能直接添加实例变量(ivar),因此不会改变类实例的内存布局sizeof;实例大小由原类及其子类的 ivar 决定。

1.2 对内存管理的影响

维度 说明
实例大小 Category 不增加实例占用,无需从「对象体积」角度做特殊内存管理。
方法实现 Category 中的方法若创建或持有对象,仍遵循 ARC/MRC 规则(谁持有谁释放、避免循环引用)。
「属性」存储 若在 Category 中通过 关联对象 模拟属性,则关联的 value 的持有方式association policy 决定,需正确设置以避免泄漏或野指针。

下文重点说明关联对象的内存语义与使用注意。


二、关联对象(Associated Objects)简述

2.1 作用

  • 不增加 ivar 的前提下,把键值对绑在某个对象上:主对象被释放时,运行时会自动释放其关联的 value(按 policy 做 release 等)。
  • 常用于在 Category 中为已有类添加「存储型属性」、或为任意对象挂载扩展数据。

2.2 API(Objective-C 运行时)

// 设置:object 为主对象,key 为键,value 为值,policy 为关联策略
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

// 获取
id objc_getAssociatedObject(id object, const void *key);

// 移除(将 value 设为 nil 即可,会按 policy 释放原 value)
objc_setAssociatedObject(object, key, nil, policy);

三、关联策略(policy)与内存管理

3.1 常用策略对照表

策略常量 语义(对 value 的持有方式) 适用场景
OBJC_ASSOCIATION_RETAIN 强引用(retain),主对象释放时对 value release 普通 OC 对象属性(类似 strong)
OBJC_ASSOCIATION_COPY 拷贝后强引用(copy),主对象释放时对拷贝 release 字符串、block 等需拷贝的类型
OBJC_ASSOCIATION_ASSIGN 不持有(assign),主对象释放时不对 value 做 release 基本类型、或「弱引用」场景(注意野指针)
OBJC_ASSOCIATION_RETAIN_NONATOMIC 同 RETAIN,非原子 性能敏感、不需原子性时
OBJC_ASSOCIATION_COPY_NONATOMIC 同 COPY,非原子 同上

3.2 与 ARC 属性修饰符的对应

若属性声明为 关联时建议 policy
strong(对象) OBJC_ASSOCIATION_RETAIN
copy(block/NSString) OBJC_ASSOCIATION_COPY
assign / weak OBJC_ASSOCIATION_ASSIGN(assign 不保证置 nil,若为对象有野指针风险;true weak 需运行时支持,关联对象常用 ASSIGN 存 weak 包装或非持有)

3.3 释放时机

  • 主对象 dealloc 时,运行时会自动对所有关联的 value 按各自 policy 执行 release(或等效操作),无需在 dealloc 里手动 objc_setAssociatedObject(..., nil, ...) 或单独 release。
  • 若在业务上希望提前解除某条关联,可主动 objc_setAssociatedObject(object, key, nil, policy),原 value 会按 policy 被释放。

四、Category 中「属性」的常见写法与内存

4.1 强引用存储(RETAIN)

// Category 中为 NSObject 添加一个“强引用”属性
static const void *kMyKey = &kMyKey;

- (void)setMyProperty:(id)obj {
    objc_setAssociatedObject(self, kMyKey, obj, OBJC_ASSOCIATION_RETAIN);
}
- (id)myProperty {
    return objc_getAssociatedObject(self, kMyKey);
}
  • 内存:set 时对新 value retain、对旧 value release;主对象 dealloc 时自动 release 当前 value,无泄漏。
  • 注意:若 myProperty 内部又强引用主对象(如 block 捕获 self),会循环引用,需用 weak 打破(见下)。

4.2 拷贝存储(COPY,如 block)

- (void)setMyBlock:(void (^)(void))block {
    objc_setAssociatedObject(self, kBlockKey, block, OBJC_ASSOCIATION_COPY);
}
  • Block 常用 COPY,与属性 copy 一致;主对象释放时会对拷贝的 block release。

4.3 弱引用 / 不持有(ASSIGN)与循环引用

  • 若用 OBJC_ASSOCIATION_ASSIGN 存一个对象指针,主对象不会持有该对象;但主对象 dealloc 时不会把该指针置 nil,若外部未持有,可能产生野指针
  • 循环引用:主对象 A 通过 RETAIN 关联了对象 B,B 又强引用了 A → 双方都不释放。解决办法:让 B 对 A 使用 weak(若 B 是自定义类可改);或 A 不通过 RETAIN 关联 B,改用 ASSIGN + 弱引用包装(需注意生命周期与野指针)。
  • Category 中若「属性」是 delegate 或会反向引用 self 的对象,应避免用 RETAIN 持有该对象,可考虑 ASSIGN 存 weak 包装或不在 Category 里存该引用。

五、流程图:关联对象生命周期

flowchart LR
    A[主对象存在] --> B[setAssociatedObject value policy]
    B --> C[value 被 retain/copy 等]
    A --> D[主对象 dealloc]
    D --> E[运行时按 policy 释放所有关联 value]
    E --> F[value 引用计数减一 或 置空]

六、小结与最佳实践

场景 建议
Category 中存普通 OC 对象 使用 OBJC_ASSOCIATION_RETAIN(或 RETAIN_NONATOMIC)。
Category 中存 block / 需拷贝类型 使用 OBJC_ASSOCIATION_COPY
不持有、仅赋值指针(如 delegate) 可用 OBJC_ASSOCIATION_ASSIGN,注意主对象释放后不置 nil,避免野指针。
避免循环引用 不在 Category 中用 RETAIN 关联「会强引用主对象」的对象;或对方对主对象使用 weak。
释放 主对象 dealloc 时关联会自动清理,一般无需在 dealloc 里手动移除。

参考文献

05-主题|内存管理@iOS-AutoreleasePool与RunLoop

本文介绍 自动释放池(AutoreleasePool) 的原理、底层结构(AutoreleasePoolPage)、与 RunLoop 的协作关系,以及对象何时被批量 release。引用计数基础见 03-引用计数与MRC详解


自动释放池是什么(简要介绍)

自动释放池(AutoreleasePool) 是用于延迟释放对象的机制:当对象收到 autorelease 时,不会立即让引用计数 -1,而是被加入当前线程的自动释放池;当池被 pop/drain 时,池会对其中所有对象统一发送 release,从而在「某一时刻」批量 -1。在 MRC 下需手写 autorelease;在 ARC 下由编译器在需要时自动插入。主线程的 RunLoop 在每次循环开始会 push 一个池、在休眠或退出前 pop 该池,因此主线程上的 autorelease 对象多在「本次事件处理结束」时被释放。子线程若无 RunLoop,应显式使用 @autoreleasepool { } 控制释放时机,避免临时对象堆积。


一、AutoreleasePool 的作用

1.1 为什么需要

  • autorelease 表示「稍后再 release」:不立刻 -1,而是把对象交给当前自动释放池,由池在某一时刻统一对池内对象发送 release。
  • 作用:延迟释放,避免在密集创建临时对象的场景下频繁立刻 release,可将多次 release 合并到池 drain 时执行,有利于性能与局部性。

1.2 与 RunLoop 的关系(主线程)

  • 主线程 RunLoop 在一次循环中会:
    • 进入时:push 一个 AutoreleasePool;
    • 休眠/退出前:pop 该池,即对池内所有对象执行 release(drain)。
  • 因此,主线程上没有显式 @autoreleasepool 时,当前 RunLoop 迭代结束前创建的 autorelease 对象,会在本次迭代末尾被批量释放。

二、@autoreleasepool 语法与底层

2.1 语法

@autoreleasepool {
    // 池内创建的 autorelease 对象,在 } 时统一 release
    id obj = [SomeObject createObject]; // 若返回 autorelease 对象
}
// 池 pop,obj 收到 release

2.2 底层对应(伪代码)

  • @autoreleasepool { ... } 编译后等价于:
    • 入口:objc_autoreleasePoolPush()(入栈一个哨兵/边界);
    • 出口:objc_autoreleasePoolPop()(pop 到该边界,对之间加入的对象依次 release)。

2.3 AutoreleasePoolPage(简述)

  • 自动释放池由 AutoreleasePoolPage 组成的栈结构实现;每页约 4KB,存若干对象指针。
  • push 时可能新开一页或复用当前页;pop 时从栈顶向栈底对每个对象 release,直到遇到对应 push 的边界。

三、释放时机小结

场景 释放时机
主线程、无显式 @autoreleasepool 当前 RunLoop 迭代结束前(休眠/退出时 pop 顶层池)
显式 @autoreleasepool { } 离开 } 时 pop,池内对象立即被 release
子线程 若没有 RunLoop 或未手动加池,需在线程中显式 @autoreleasepool,否则 autorelease 对象可能堆积到线程退出

四、流程图:RunLoop 与 AutoreleasePool 协作(主线程)

flowchart LR
    subgraph RunLoop 一次迭代
        A[进入] --> B[Push Pool]
        B --> C[处理事件]
        C --> D[休眠/退出前]
        D --> E[Pop Pool]
        E --> F[池内对象 release]
    end

五、应用场景

  • 循环中大量创建临时对象:在循环内层包一层 @autoreleasepool { },每轮迭代结束即释放,避免峰值过高。
  • 子线程中创建大量 autorelease 对象:在线程入口或循环内使用 @autoreleasepool,避免只依赖线程退出才释放。

参考文献

04-主题|内存管理@iOS-ARC详解

本文介绍 ARC(Automatic Reference Counting,自动引用计数) 的机制、strong/weak/unowned 等所有权修饰符、编译器如何插入引用计数代码,以及常见应用场景与注意事项。前置知识见 03-引用计数与MRC详解


ARC 是什么(简要介绍)

ARCAutomatic Reference Counting:在编译期由编译器根据代码中的所有权修饰符(如 strong、weak)和代码结构,自动插入 retain、release、autorelease 等调用,开发者不再手写这些方法。底层仍然使用与 MRC 相同的引用计数规则,只是「谁在何时 +1/-1」由编译器决定。ARC 自 iOS 5 / WWDC 2011 引入,现为 Objective-C 与 Swift 的推荐方式;Swift 仅支持 ARC。使用 ARC 时仍需理解强引用与弱引用循环引用自动释放池 的释放时机,见 05-AutoreleasePool与RunLoop06-weak与循环引用


一、ARC 是什么

1.1 定义

  • ARC 是编译期特性:编译器根据所有权修饰符代码结构,在合适位置自动插入 retain、release、autorelease 等调用。
  • 与 MRC 使用同一套引用计数规则,对象生命周期语义一致;开发者不再手写 retain/release,减少遗漏与错误。

1.2 与 MRC 对比

维度 MRC ARC
谁写 retain/release 开发者手写 编译器自动插入
所有权表达 通过方法名约定 + 手写调用 通过变量/属性修饰符(strong/weak 等)
autorelease 手写 autorelease 编译器在需要时插入
循环引用 需手写 weak 或打破引用 同样需用 weak/unowned 打破

二、所有权修饰符(Objective-C)

2.1 常见修饰符

修饰符 含义 引用计数影响
__strong(默认) 强引用,拥有对象 赋值时 retain,离开作用域或置 nil 时 release
__weak 弱引用,不拥有对象 不增加引用计数;对象释放时自动置为 nil
__unsafe_unretained 不保留引用,不拥有 不增加引用计数;对象释放后不置 nil,可能野指针
__autoreleasing 通过引用传入并在 autorelease 池中释放 用于 out 参数等场景

2.2 属性与修饰符对应

属性声明 默认修饰符 说明
strong __strong 强引用,常用
weak __weak 弱引用,打破循环或非拥有关系
copy __strong(拷贝语义) 设值时 copy,用于 block、NSString 等;深浅拷贝与 copy 语义见 [11-深浅拷贝与内存](11-主题 内存管理@iOS-深浅拷贝与内存.md)
assign __unsafe_unretained 不持有,多用于基本类型或需避免循环时(非对象慎用)

三、ARC 下的典型场景

3.1 强引用与释放时机

// 局部变量:离开作用域时自动 release
- (void)foo {
    NSObject *obj = [[NSObject alloc] init]; // 强引用,rc=1
    // 使用 obj
} // 作用域结束,编译器插入 release,obj 可能 dealloc

3.2 弱引用与循环引用

// 两个对象互相强引用 → 循环引用,都无法释放
// 解决:一方改为 weak
@interface Child : NSObject
@property (nonatomic, weak) Parent *parent; // 弱引用父类
@end

3.3 Block 中的循环引用

// self → block → self,形成循环
__weak typeof(self) wself = self;
self.block = ^{
    __strong typeof(wself) sself = wself;
    [sself doSomething];
};

详见 06-weak与循环引用。Block 的三种类型(全局/栈/堆)、copy 语义与 MRC/ARC 差异见 10-Block内存管理


四、流程图:ARC 编译期插入示意

flowchart TB
    subgraph 源码
        A[strong 赋值]
        B[变量离开作用域]
    end
    subgraph 编译器插入
        A --> C[插入 retain]
        B --> D[插入 release]
    end

五、Swift 中的 ARC

  • Swift 仅支持 ARC,无 MRC。
  • strong(默认)、weakunowned 与 OC 语义对应;闭包捕获列表 [weak self] / [unowned self] 用于避免循环引用。
  • 详见 Swift 官方 - Automatic Reference Counting

参考文献

03-主题|内存管理@iOS-引用计数与MRC详解

本文介绍 引用计数(Reference Counting) 的原理、MRC(Manual Reference Counting,手动引用计数) 下的规则与配对原则,以及 retain/release/autorelease 的语义与典型用法。ARC 在此基础上由编译器自动插入,见 04-ARC详解


MRC 是什么(简要介绍)

MRCManual Reference Counting:由开发者手动调用 retainreleaseautorelease 来增加或减少对象的引用计数,从而决定对象何时被释放。在 iOS 5 之前是 Objective-C 的主流方式;理解 MRC 的规则与配对原则,是理解 ARC 和自动释放池的基础。下文的「引用计数原理」与「加一/减一规则」同时适用于 MRC 与 ARC,ARC 只是把这些调用交给编译器自动插入。


一、引用计数原理

1.1 基本思想

  • 每个堆上对象维护一个引用计数(retain count):表示当前有多少处「引用」正在使用该对象。
  • 引用计数为 0 时,对象不再被任何引用使用,系统销毁对象并回收内存
  • +1:新增加一处引用(如持有、拷贝得到新指针);-1:减少一处引用(如不再持有、释放)。

1.2 规则小结

事件 引用计数变化
创建对象(alloc/new/copy/mutableCopy) +1,创建者持有
retain +1
release -1
autorelease 将「稍后 -1」交给当前 AutoreleasePool 处理

1.3 流程图:对象生命周期

flowchart LR
    A[alloc/new 等] --> B[rc=1]
    B --> C{有 retain?}
    C -->|是| D[rc+=1]
    D --> C
    C -->|否| E{有 release/autorelease?}
    E -->|是| F[rc-=1]
    F --> G{rc==0?}
    G -->|是| H[dealloc 回收]
    G -->|否| E

二、MRC 下的「加一」与「减一」

2.1 引用计数 +1 的操作

方法/操作 说明
alloc 分配内存并返回对象,调用者拥有,rc 初始为 1
new 等价于 alloc + init,调用者拥有
copy / mutableCopy 得到新对象,调用者拥有新对象(rc=1)
retain 对已有对象调用,表示多一处引用,rc+1

2.2 引用计数 -1 的操作

方法/操作 说明
release 立即减少引用计数,rc-1;若 rc 变为 0 则 dealloc
autorelease 将对象加入当前 AutoreleasePool,在池 drain 时对该对象 release(延迟 -1)

2.3 配对原则(MRC 核心)

  • 谁让引用计数 +1,谁就要在合适时机让引用计数 -1(自己 release 或 autorelease)。
  • 只 +1 不 -1 → 泄漏;若多 -1 或对已释放对象再访问 → 野指针/崩溃。

三、伪代码与算法说明

3.1 retain / release 语义(伪代码)

函数 retain(obj):
    若 obj == nil: 返回 nil
    obj.retainCount += 1
    返回 obj

函数 release(obj):
    若 obj == nil: 返回
    obj.retainCount -= 1
    若 obj.retainCount == 0:
        调用 obj.dealloc
        释放对象内存

3.2 autorelease 语义(伪代码)

函数 autorelease(obj):
    若 obj == nil: 返回 nil
    将 obj 加入当前线程的 AutoreleasePool 栈顶
    返回 obj
// 当 AutoreleasePool pop/drain 时,对池内每个对象调用 release

3.3 MRC 下典型写法示例(Objective-C)

// 创建并持有:alloc 后 rc=1,需要在不使用时 release
NSObject *obj = [[NSObject alloc] init];
// ... 使用 obj ...
[obj release];

// 通过方法返回「已 autorelease」的对象:调用者不拥有,不需 release
- (NSString *)name {
    return [[[NSString alloc] initWithFormat:@"name"] autorelease];
}

四、所有权与命名约定(MRC 时代)

  • 方法名以 alloc / new / copy / mutableCopy 开头:返回的对象调用者拥有,需负责 release 或 autorelease。
  • 其他返回对象的方法:默认约定返回 autorelease 对象,调用者不拥有,不应 release(除非先 retain)。

五、与 ARC 的衔接

ARC 仍基于同一套引用计数规则,只是 retain/release/autorelease编译器在编译期自动插入;开发者通过 strong/weak 等修饰符表达所有权,编译器据此生成对应的 retain/release。详见 04-ARC详解


参考文献

01-研究系统框架@Web@iOS | JavaScriptCore 框架:从使用到原理解析

JavaScriptCore 框架:从使用到原理解析

JavaScript 越来越多地出现在我们客户端开发的视野中,从 React Native 到 JSPatch,JavaScript 与客户端相结合的技术开始变得魅力无穷。本文主要讲解 iOS 中的 JavaScriptCore 框架,正是它为 iOS 提供了执行 JavaScript 代码的能力。未来的技术日新月异,JavaScript 与 iOS 正在碰撞出新的激情。

JavaScriptCoreJavaScript虚拟机,为 JavaScript 的执行提供底层资源。


📋 目录


一、JavaScript

在讨论JavaScriptCore之前,我们首先必须对JavaScript有所了解。

1. JavaScript干啥的?

  • 说的高大上一点:一门基于原型、函数先行的高级编程语言,通过解释执行,是动态类型的直译语言。是一门多范式的语言,它支持面向对象编程,命令式编程,以及函数式编程。
  • 说的通俗一点:主要用于网页,为其提供动态交互的能力。可嵌入动态文本于HTML页面,对浏览器事件作出响应,读写HTML元素,控制cookies等。
  • 再通俗一点:抢月饼,button.click()。(PS:请谨慎使用while循环)

img

2. JavaScript起源与历史

  • 1990年底,欧洲核能研究组织(CERN)科学家Tim Berners-Lee,在互联网的基础上,发明了万维网(World Wide Web),从此可以在网上浏览网页文件。
  • 1994年12月,Netscape 发布了一款面向普通用户的新一代的浏览器Navigator 1.0版,市场份额一举超过90%。
  • 1995年,Netscape公司雇佣了程序员Brendan Eich开发这种嵌入网页的脚本语言。最初名字叫做Mocha,1995年9月改为LiveScript。
  • 1995年12月,Netscape公司与Sun公司达成协议,后者允许将这种语言叫做JavaScript。

3. JavaScript与ECMAScript

  • “JavaScript”是Sun公司的注册商标,用来特制网景(现在的Mozilla)对于这门语言的实现。网景将这门语言作为标准提交给了ECMA——欧洲计算机制造协会。由于商标上的冲突,这门语言的标准版本改了一个丑陋的名字“ECMAScript”。同样由于商标的冲突,微软对这门语言的实现版本取了一个广为人知的名字“Jscript”。
  • ECMAScript作为JavaScript的标准,一般认为后者是前者的实现。

4. Java和JavaScript

img

《雷锋和雷峰塔》

Java 和 JavaScript 是两门不同的编程语言 一般认为,当时 Netscape 之所以将 LiveScript 命名为 JavaScript,是因为 Java 是当时最流行的编程语言,带有 “Java” 的名字有助于这门新生语言的传播。

二、 JavaScriptCore

1. 浏览器演进

  • 演进完整图

upload.wikimedia.org/wikipedia/c…

  • WebKit分支

现在使用WebKit的主要两个浏览器Sfari和Chromium(Chorme的开源项目)。WebKit起源于KDE的开源项目Konqueror的分支,由苹果公司用于Sfari浏览器。其一条分支发展成为Chorme的内核,2013年Google在此基础上开发了新的Blink内核。

img

2. WebKit排版引擎

webkit是sfari、chrome等浏览器的排版引擎,各部分架构图如下

img

  • webkit Embedding API是browser UI与webpage进行交互的api接口;
  • platformAPI提供与底层驱动的交互, 如网络, 字体渲染, 影音文件解码, 渲染引擎等;
  • WebCore它实现了对文档的模型化,包括了CSS, DOM, Render等的实现;
  • JSCore是专门处理JavaScript脚本的引擎;

3. JavaScript引擎

  • JavaScript引擎是专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中。第一个JavaScript引擎由布兰登·艾克在网景公司开发,用于Netscape Navigator网页浏览器中。JavaScriptCore就是一个JavaScript引擎。
  • 下图是当前主要的还在开发中的JavaScript引擎

img

4. JavaScriptCore组成

JavaScriptCore主要由以下模块组成:

  • Lexer 词法分析器,将脚本源码分解成一系列的Token
  • Parser 语法分析器,处理Token并生成相应的语法树
  • LLInt 低级解释器,执行Parser生成的二进制代码
  • Baseline JIT 基线JIT(just in time 实施编译)
  • DFG 低延迟优化的JIT
  • FTL 高通量优化的JIT

关于更多JavaScriptCore的实现细节,参考 trac.webkit.org/wiki/JavaSc…

5. JavaScriptCore 框架与历史

JavaScriptCore 是一个 C++ 实现的开源项目(WebKit 的一部分)。历史上,JSC 长期作为 Safari / WebKit 的内置 JS 引擎;自 iOS 7.0 / OS X 10.9 起,Apple 将 JavaScriptCore 以系统框架 JavaScriptCore.framework 的形式开放给开发者,使其可在 Objective-C 或基于 C 的程序中执行 JavaScript 代码,并向 JS 环境中插入自定义对象,而无需依赖 UIWebView。这为 Hybrid 应用、热更新、脚本引擎等场景提供了统一的底层能力。

JavaScriptCore.h 中,我们可以看到:

#ifndef JavaScriptCore_h
#define JavaScriptCore_h

#include <JavaScriptCore/JavaScript.h>
#include <JavaScriptCore/JSStringRefCF.h>

#if defined(__OBJC__) && JSC_OBJC_API_ENABLED

#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"

#endif

#endif /* JavaScriptCore_h */

这里已经很清晰地列出了JavaScriptCore的主要几个类:

  • JSContext
  • JSValue
  • JSManagedValue
  • JSVirtualMachine
  • JSExport

接下来我们会依次讲解这几个类的用法。

6. Hello World!

这段代码展示了如何在 Objective-C 中执行一段 JavaScript 代码,并且获取返回值并转换成 OC 数据打印:

// 创建虚拟机
JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];

//创建上下文
JSContext *context = [[JSContext alloc] initWithVirtualMachine:vm];

//执行JavaScript代码并获取返回值
JSValue *value = [context evaluateScript:@"1+2*3"];

// 转换成 OC 数据并打印
NSLog(@"value = %d", [value toInt32]);
// Output: value = 7

Swift 等价写法:

import JavaScriptCore

let vm = JSVirtualMachine()!
let context = JSContext(virtualMachine: vm)!
let value = context.evaluateScript("1 + 2 * 3")!
print("value =", value.toInt32())  // value = 7

三、 JSVirtualMachine

一个JSVirtualMachine的实例就是一个完整独立的JavaScript的执行环境,为JavaScript的执行提供底层资源。

这个类主要用来做两件事情:

  1. 实现并发的 JavaScript 执行
  2. JavaScript 和 Objective-C 桥接对象的内存管理

看下头文件 JSVirtualMachine.h 里有什么:

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSVirtualMachine : NSObject

/* 创建一个新的完全独立的虚拟机 */
(instancetype)init;

/* 对桥接对象进行内存管理 */
- (void)addManagedReference:(id)object withOwner:(id)owner;

/* 取消对桥接对象的内存管理 */
- (void)removeManagedReference:(id)object withOwner:(id)owner;

@end

每一个JavaScript上下文(JSContext对象)都归属于一个虚拟机(JSVirtualMachine)。每个虚拟机可以包含多个不同的上下文,并允许在这些不同的上下文之间传值(JSValue对象)。

然而,每个虚拟机都是完整且独立的,有其独立的堆空间和垃圾回收器(garbage collector ),GC无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。

img

线程和JavaScript的并发执行

JavaScriptCore API都是线程安全的。你可以在任意线程创建JSValue或者执行JS代码,然而,所有其他想要使用该虚拟机的线程都要等待。

  • 如果想并发执行JS,需要使用多个不同的虚拟机来实现。
  • 可以在子线程中执行JS代码。

通过下面这个 demo 来理解这个并发机制:

JSContext *context = [[CustomJSContext alloc] init];
JSContext *context1 = [[CustomJSContext alloc] init];
JSContext *context2 = [[CustomJSContext alloc] initWithVirtualMachine:[context virtualMachine]];
NSLog(@"start");
dispatch_async(queue, ^{
    while (true) {
        sleep(1);
        [context evaluateScript:@"log('tick')"];
    }
});
dispatch_async(queue1, ^{
    while (true) {
        sleep(1);
        [context1 evaluateScript:@"log('tick_1')"];
    }
});
dispatch_async(queue2, ^{
    while (true) {
        sleep(1);
        [context2 evaluateScript:@"log('tick_2')"];
    }
});
[context evaluateScript:@"sleep(5)"];
NSLog(@"end");

context和context2属于同一个虚拟机。

context1属于另一个虚拟机。

三个线程分别异步执行每秒1次的js log,首先会休眠1秒。

在context上执行一个休眠5秒的JS函数。

首先执行的应该是休眠5秒的JS函数,在此期间,context所处的虚拟机上的其他调用都会处于等待状态,因此tick和tick_2在前5秒都不会有执行。

而context1所处的虚拟机仍然可以正常执行tick_1

休眠5秒结束后,tick和tick_2才会开始执行(不保证先后顺序)。

实际运行输出的 log 是:

start
tick_1
tick_1
tick_1
tick_1
end
tick
tick_2

四、 JSContext

一个JSContext对象代表一个JavaScript执行环境。在native代码中,使用JSContext去执行JS代码,访问JS中定义或者计算的值,并使JavaScript可以访问native的对象、方法、函数。

img

1. JSContext执行JS代码

  • 调用evaluateScript函数可以执行一段top-level 的JS代码,并可向global对象添加函数和对象定义
  • 其返回值是JavaScript代码中最后一个生成的值

API Reference

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSContext : NSObject

/* 创建一个JSContext,同时会创建一个新的JSVirtualMachine */
(instancetype)init;

/* 在指定虚拟机上创建一个JSContext */
(instancetype)initWithVirtualMachine:
        (JSVirtualMachine*)virtualMachine;

/* 执行一段JS代码,返回最后生成的一个值 */
(JSValue *)evaluateScript:(NSString *)script;

/* 执行一段JS代码,并将sourceURL认作其源码URL(仅作标记用) */
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL*)sourceURL     NS_AVAILABLE(10_10, 8_0);

/* 获取当前执行的JavaScript代码的context */
+ (JSContext *)currentContext;

/* 获取当前执行的JavaScript function*/
+ (JSValue *)currentCallee NS_AVAILABLE(10_10, 8_0);

/* 获取当前执行的JavaScript代码的this */
+ (JSValue *)currentThis;

/* Returns the arguments to the current native callback from JavaScript code.*/
+ (NSArray *)currentArguments;

/* 获取当前context的全局对象。WebKit中的context返回的便是WindowProxy对象*/
@property (readonly, strong) JSValue *globalObject;

@property (strong) JSValue *exception;
@property (copy) void(^exceptionHandler)(JSContext *context, JSValue
    *exception);

@property (readonly, strong) JSVirtualMachine *virtualMachine;

@property (copy) NSString *name NS_AVAILABLE(10_10, 8_0);


@end

2. JSContext访问JS对象

一个JSContext对象对应了一个全局对象(global object)。例如web浏览器中中的JSContext,其全局对象就是window对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的JavaScript context的作用域。全局变量是全局对象的属性,可以通过JSValue对象或者context下标的方式来访问。

示例代码:

JSValue *value = [context evaluateScript:@"var a = 1+2*3;"];

NSLog(@"a = %@", [context objectForKeyedSubscript:@"a"]);
NSLog(@"a = %@", [context.globalObject objectForKeyedSubscript:@"a"]);
NSLog(@"a = %@", context[@"a"]);
// Output: a = 7, a = 7, a = 7

这里列出了三种访问JavaScript对象的方法

  • 通过context的实例方法objectForKeyedSubscript
  • 通过context.globalObject的objectForKeyedSubscript实例方法
  • 通过下标方式

设置属性也是对应的。

API Reference

/* 为 JSContext 提供下标访问元素的方式 */
@interface JSContext (SubscriptSupport)

/* 首先将key转为JSValue对象,然后使用这个值在JavaScript context的全局对象中查找这个名字的属性并返回 */
(JSValue *)objectForKeyedSubscript:(id)key;

/* 首先将key转为JSValue对象,然后用这个值在JavaScript context的全局对象中设置这个属性。
可使用这个方法将native中的对象或者方法桥接给JavaScript调用 */
(void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying>*)key;

@end



/* 例如:以下代码在JavaScript中创建了一个实现是Objective-C block的function */
context[@"makeNSColor"] = ^(NSDictionary *rgb){
    float r = [rgb[@"red"] floatValue];
    float g = [rgb[@"green"] floatValue];
    float b = [rgb[@"blue"] floatValue];
    return [NSColor colorWithRed:(r / 255.f) green:(g / 255.f) blue:(b / 255.f)         alpha:1.0];
};
JSValue *value = [context evaluateScript:@"makeNSColor({red:12, green:23, blue:67})"];

五、 JSValue

一个JSValue实例就是一个JavaScript值的引用。使用JSValue类在JavaScript和native代码之间转换一些基本类型的数据(比如数值和字符串)。你也可以使用这个类去创建包装了自定义类的native对象的JavaScript对象,或者创建由native方法或者block实现的JavaScript函数。

每个JSValue实例都来源于一个代表JavaScript执行环境的JSContext对象,这个执行环境就包含了这个JSValue对应的值。每个JSValue对象都持有其JSContext对象的强引用,只要有任何一个与特定JSContext关联的JSValue被持有(retain),这个JSContext就会一直存活。通过调用JSValue的实例方法返回的其他的JSValue对象都属于与最始的JSValue相同的JSContext。

img

每个JSValue都通过其JSContext间接关联了一个特定的代表执行资源基础的JSVirtualMachine对象。你只能将一个JSValue对象传给由相同虚拟机管理(host)的JSValue或者JSContext的实例方法。如果尝试把一个虚拟机的JSValue传给另一个虚拟机,将会触发一个Objective-C异常。

img

1. JSValue类型转换

JSValue提供了一系列的方法将native与JavaScript的数据类型进行相互转换:

img

2. NSDictionary与JS对象

NSDictionary 对象以及其包含的 keys 与 JavaScript 中的对应名称的属性相互转换。key 所对应的值也会递归地进行拷贝和转换。

[context evaluateScript:@"var color = {red:230, green:90, blue:100}"];

//js->native 给你看我的颜色
JSValue *colorValue = context[@"color"];
NSLog(@"r=%@, g=%@, b=%@", colorValue[@"red"], colorValue[@"green"], colorValue[@"blue"]);
NSDictionary *colorDic = [colorValue toDictionary];
NSLog(@"r=%@, g=%@, b=%@", colorDic[@"red"], colorDic[@"green"], colorDic[@"blue"]);

//native->js 给你点颜色看看
context[@"color"] = @{@"red":@(0), @"green":@(0), @"blue":@(0)};
[context evaluateScript:@"log('r:'+color.red+'g:'+color.green+' b:'+color.blue)"];
// Output:
// r=230, g=90, b=100
// r=230, g=90, b=100
// r:0 g:0 b:0

可见,JS中的对象可以直接转换成Objective-C中的NSDictionary,NSDictionary传入JavaScript也可以直接当作对象被使用。

3. NSArray与JS数组

NSArray 对象与 JavaScript 中的 array 相互转换。其子元素也会递归地进行拷贝和转换。

[context evaluateScript:@"var friends = ['Alice','Jenny','XiaoMing']"];

//js->native 你说哪个是真爱?
JSValue *friendsValue = context[@"friends"];
NSLog(@"%@, %@, %@", friendsValue[0], friendsValue[1], friendsValue[2]);
NSArray *friendsArray = [friendsValue toArray];
NSLog(@"%@, %@, %@", friendsArray[0], friendsArray[1], friendsArray[2]);

//native->js 我觉得 XiaoMing 不错,给你再推荐个 Jimmy
context[@"girlFriends"] = @[friendsArray[2], @"Jimmy"];
[context evaluateScript:@"log('girlFriends :'+girlFriends[0]+' '+girlFriends[1])"];
// Output: Alice, Jenny, XiaoMing / girlFriends : XiaoMing Jimmy

4. Block/函数和JS function

Objective-C中的block转换成JavaScript中的function对象。参数以及返回类型使用相同的规则转换。

将一个代表native的block或者方法的JavaScript function进行转换将会得到那个block或方法。

其他的JavaScript函数将会被转换为一个空的dictionary。因为JavaScript函数也是一个对象。

5. OC对象和JS对象

对于所有其他 native 的对象类型,JavaScriptCore 都会创建一个拥有 constructor 原型链的 wrapper 对象,用来反映 native 类型的继承关系。默认情况下,native 对象的属性和方法并不会导出给其对应的 JavaScript wrapper 对象。通过 JSExport 协议可选择性地导出属性和方法。下面第六节对 JSExport 与原生对象导出做详细讲解。


六、JSExport 与原生对象导出

JSExport 是 JavaScriptCore 框架中的协议,用于将 Objective-C/Swift 的类(属性与方法)选择性导出给 JavaScript,使 JS 代码可以像调用普通对象一样调用原生对象 [1][2]。

6.1 作用与机制

  • 遵循 JSExport 的协议中声明的属性和方法,会在将 native 对象注入到 JSContext(如 context[@"bridge"] = nativeObject)时,自动暴露为 JS 侧的属性和函数。
  • 若类未实现 JSExport 或未在协议中声明,则对应属性/方法不会出现在 JS 中;这样可控制「桥接面」,避免暴露内部实现 [1][2]。

6.2 使用示例(概念)

@protocol MyPointExport <JSExport>
@property (nonatomic, assign) double x;
@property (nonatomic, assign) double y;
- (NSString *)description;
@end

@interface MyPoint : NSObject <MyPointExport>
@property (nonatomic, assign) double x;
@property (nonatomic, assign) double y;
@end

MyPoint 实例赋给 context[@"point"] 后,在 JS 中可访问 point.xpoint.y 并调用 point.description()
注意:若在 Block 或导出方法中再次使用 JSValueJSContext,需注意线程与内存管理(见第七节 JSManagedValue)[1][2]。

Swift 中的等价写法(通过 JSContext 注入遵循 JSExport 的类):

import JavaScriptCore

@objc protocol PointExport: JSExport {
    var x: Double { get set }
    var y: Double { get set }
    func description() -> String
}

class Point: NSObject, PointExport {
    @objc var x: Double
    @objc var y: Double
    init(x: Double, y: Double) { self.x = x; self.y = y }
    func description() -> String { "Point(\(x), \(y))" }
}

// 注入到 context
let context = JSContext()!
context.setObject(Point(x: 1, y: 2), forKeyedSubscript: "point" as NSString)
context.evaluateScript("point.x; point.description()")

6.3 与 Block 注入的对比

方式 适用场景
context[@"fn"] = ^(id arg){ ... } 单次或简单逻辑,直接暴露为 JS 函数
JSExport 协议 + 原生对象 需要暴露多个方法/属性、保持对象身份与状态的「桥接对象」

七、JSManagedValue 与内存管理

7.1 为何需要 JSManagedValue

  • JSValueJSContext强引用JSContext 又挂在 JSVirtualMachine 上。
  • 若在 堆上的 OC 对象(如某 ViewController 的 property)中直接强引用 JSValue,而该 JSValue 通过某种方式(例如被注入到 context 的全局对象)又引用回该 OC 对象,会形成 OC ↔ JS 的循环引用,导致 Context 与 OC 对象均无法释放 [1][2]。

7.2 JSManagedValue 的职责

JSManagedValueJSValue 的包装类,用于在「被 OC 堆对象持有」的场景下,以条件保留的方式引用 JS 值,并可与 JSVirtualMachineaddManagedReference:withOwner: / removeManagedReference:withOwner: 配合,让虚拟机在合适的时机断开或保留对 native 对象的引用,从而打破循环、避免 JSContext 无法释放 [1][2]。

7.3 使用要点(概念)

  • 当需要把 JSValue(或从 JS 传回的函数/对象)存为 OC 对象的成员变量时,应使用 JSManagedValue 包装,并以 owner 注册到 JSVirtualMachine;在 owner 析构或不再需要时调用 removeManagedReference:withOwner: [1][2]。
  • 仅临时在栈上使用 JSValue(如 evaluateScript 的返回值在方法内使用后不再持有)时,一般无需 JSManagedValue。

八、关键概念图示与流程

8.1 VM、Context、Value 关系

flowchart TB
  subgraph VM1[JSVirtualMachine 1]
    C1[JSContext 1]
    C2[JSContext 2]
  end
  subgraph VM2[JSVirtualMachine 2]
    C3[JSContext 3]
  end
  C1 --> V1[JSValue]
  C2 --> V2[JSValue]
  C1 -.->|可传值| C2
  C1 -.->|不可跨 VM| C3

同一 JSVirtualMachine 下多个 JSContext 可共享、传递 JSValue;不同 VM 之间不能传递 JSValue [3]。

8.2 JavaScriptCore 引擎执行层级(概念)

源码经 Lexer → Parser 得到语法树并生成字节码后,由下至上的执行/编译层级可概括为:

flowchart LR
  A[源码] --> B[Lexer]
  B --> C[Parser / AST]
  C --> D[字节码]
  D --> E[LLInt 解释器]
  E --> F[Baseline JIT]
  F --> G[DFG JIT]
  G --> H[FTL JIT]
  • LLInt:低级解释器,低延迟启动。
  • Baseline JIT:首次 JIT,兼顾分析与回退。
  • DFG:基于数据流的优化 JIT。
  • FTL:更高优化层(历史上曾用 LLVM/B3 后端)[4][5]。

更多实现细节见 WebKit JavaScriptCore Wiki


九、应用场景与最佳实践

9.1 典型应用场景

场景 说明
Hybrid 应用 在 App 内执行 JS 脚本、调用原生能力(如弹窗、定位、支付),JavaScriptCore 提供 OC/Swift 与 JS 的双向桥接 [1][2]
React Native / 类 RN 方案 早期 RN 等方案在 iOS 上依赖 JSC 执行 JS bundle;JSC 提供 VM、Context、Value 等能力 [3]
JSPatch 等热修复 通过下发 JS 脚本并在 JSC 中执行,动态调用原生类与方法,实现热更新(需注意安全与审核政策)[3]
WKWebView 与 Web 页面 WKWebView 内部使用系统 WebKit,其 JS 引擎与 Safari 一致;独立使用 JSC 时无需 WebView 即可执行 JS [1][2]
规则引擎 / 脚本配置 将业务规则或配置写成 JS,由原生在 JSC 中执行并取结果,便于迭代与 A/B 测试

9.2 最佳实践要点

  • 线程:同一 VM 下多线程会串行等待;需并发执行 JS 时使用多个 JSVirtualMachine [3]。
  • 异常:设置 context.exceptionHandler,在 JS 抛错时记录或上报,避免静默失败 [3]。
  • 内存:在 OC 堆对象中持有 JS 值时使用 JSManagedValue + add/removeManagedReference,避免循环引用 [1][2]。
  • 安全:执行来自网络或不可信来源的 JS 时,需做沙箱与权限控制;避免将敏感 API 无限制暴露给 JS [3]。

十、伪代码与算法说明

10.1 执行脚本并取返回值(概念)

function evaluateScript(script: String) -> JSValue:
  parse script -> AST
  generate bytecode from AST
  execute bytecode (via LLInt or JIT tier)
  return last expression value as JSValue

10.2 将 Native 对象注入 Context(概念)

function setObject(object: Any, forKey key: String):
  if object is Block or conforms to JSExport:
    create JS wrapper (function or object with exported properties/methods)
  else:
    create generic wrapper preserving native type hierarchy
  set wrapper on context.globalObject[key]

10.3 JS 调用 Native Block 时(概念)

JavaScript 侧,调用通过 context[@"key"] 注入的 Block,与调用普通函数一致:

// 假设 Native 已注入:context["makeColor"] = ^(NSDictionary *rgb) { ... }
var color = makeColor({ red: 12, green: 23, blue: 67 });

底层流程(伪代码):

当 JS 调用 context 中注册的 Block 时:
  1. JSC 将 JS 参数按类型转换为 OC 对象(NSNumber/NSString/NSDictionary/NSArray 等)
  2. 调用 Block,传入转换后的参数
  3. 将 Block 返回值按类型转换为 JSValue 并返回给 JS

参考文献

[1] Apple. JavaScriptCore Framework. iOS / macOS Developer Documentation.
[2] 掘金 / 博客. iOS 与 JS 交互开发知识总结JavaScriptCore 初探 等.
[3] 本文原稿与常见 JSC 教程(JSVirtualMachine、JSContext、JSValue、并发与内存).
[4] WebKit. Introducing the WebKit FTL JIT. webkit.org/blog/3362/i…
[5] WebKit. JavaScriptCore - Deep Dive. docs.webkit.org/Deep%20Dive…
[6] trac.webkit.org. JavaScriptCore. trac.webkit.org/wiki/JavaSc…
[7] 美团技术团队. 深入理解 JSCore. blog.csdn.net/MeituanTech…

02-编程范式和编程思想学习@iOS |【Effective Objective-C】精华导读

image.png

📋 目录


一、概述与定位

《Effective Objective-C 2.0: 52 Specific Ways to Write Better iOS and OS X Programs》(以下简称「本书」)由 Matt Galloway 撰写,2013 年由 Addison-Wesley Professional 出版,隶属 Effective Software Development Series(Scott Meyers 主编),与《Effective C++》《Effective Java》等同属「以条目化、可操作建议」提升代码质量的经典技术书 [1][2]。

1.1 目标读者与写作方式

  • 目标读者:具备 Objective-C 与 Cocoa/Cocoa Touch 基础的中高级开发者,不侧重语法入门,而侧重在既有知识基础上写出更安全、可维护、符合范式的代码 [2]。
  • 写作方式:全书分为 52 条(Item) 独立建议,每条聚焦一个具体问题或原则,可单独阅读;条目间有交叉引用,便于形成体系 [1]。

1.2 与「编程范式」的关系

本书所涉的编程范式涵盖:

  • 面向对象范式:对象、消息传递、继承与多态在 Objective-C 中的实现方式(动态类型、运行时)。
  • 内存管理范式:从手动引用计数(MRR)到自动引用计数(ARC)的演进,以及所有权与对象图思维。
  • 并发与异步范式:Block 闭包与 Grand Central Dispatch(GCD)所代表的「任务 + 队列」模型。
  • 接口与 API 设计范式:命名、不可变性、委托与协议、分类与扩展等 Cocoa 惯例。

下文从历史演进核心原理图示与算法应用场景四方面系统梳理本书内容,并引用权威文献与业界实践。本文档同时参考了掘金上的「《Effective Objective-C》干货三部曲」(概念篇、规范篇、技巧篇)[13][14][15],对部分条目的示例与归类做了补充。文档兼顾学术严谨性(概念定义、引用来源)与可读性(结构分条、图示与伪代码),便于既作速查又作体系化学习。


二、技术演进与历史脉络

2.1 Objective-C 与 Cocoa 的渊源

Objective-C 在 C 语言之上增加了单继承的面向对象动态消息传递(dynamic messaging)。对象收到「消息」后,由运行时根据**选择子(selector)**查找并执行对应方法实现;这种「发消息」而非「调函数」的模型,使得方法解析、转发、替换(如 method swizzling)均在运行时完成,构成本书所述「对象、消息与运行时」的基础 [3][4]。

2.2 内存管理范式的演进

阶段 时期 范式 说明
MRR 早期至 iOS 4 / Mac OS X 10.6 手动引用计数 开发者显式调用 retain / release / autorelease,所有权由命名约定(如 allocnewcopy 返回持有)约定 [5][6]
ARC iOS 5 / Mac OS X 10.7 起 自动引用计数 编译器在编译期插入合适的 retain/release,仍为引用计数语义,非追踪式 GC;循环引用需开发者用 __weak 等打破 [5][7]
GC 弃用 OS X Mountain Lion 起 垃圾回收在 OS X 上被弃用,macOS Sierra 后从运行时移除,ARC 成为官方推荐 [7]

要点:ARC 与 MRR 共享同一套所有权与引用计数概念;理解「谁拥有对象、何时释放」有助于写出 ARC 下仍正确的代码(尤其 Block、delegate、timer 等易产生循环引用的场景)[5][8]。

2.3 Block 与 GCD 的引入

  • Block:Apple 在 C、Objective-C、C++ 中引入的闭包语法,可捕获上下文变量并延迟执行,是回调、动画、GCD 任务的基础。本书强调 Block 的循环引用(block 捕获 self、self 又持有 block)及 weak–strong dance 的规范写法 [9][10]。
  • Grand Central Dispatch (GCD):基于队列的并发抽象,将任务(block)派发到串行/并发队列,由系统管理线程。与 performSelector: 相比,GCD 支持异步、取消语义与队列层次,成为 iOS/macOS 并发编程的主流范式 [2][11]。

2.4 内存管理范式演进(时间线)

时期 范式/事件
早期 Cocoa MRR:手动 retain / release / autorelease
iOS 5 / Mac OS X 10.7 (Xcode 4.2) ARC 完整支持,编译期插入引用计数调用
OS X Mountain Lion 起 垃圾回收(GC)弃用
macOS Sierra 起 GC 从运行时移除,ARC 为唯一推荐方式

三、全书结构与 52 条建议总览

本书共 7 章、52 条,下表给出每章主题与条目范围,便于按需查阅 [1][2]。

主题 条目 核心内容概要
1 熟悉 Objective-C 1–5 语言根源、头文件与导入、字面量语法、类型常量与枚举
2 对象、消息与运行时 6–14 属性与实例变量、相等性、类簇、关联对象、消息机制、方法转发、method swizzling、类对象
3 接口与 API 设计 15–22 命名、指定初始化器、description、不可变优先、命名一致性、私有方法、错误处理、NSCopying
4 协议与分类 23–28 委托模式、分段实现、分类前缀、分类中避免属性、类扩展、匿名对象
5 内存管理 29–36 引用计数、ARC、dealloc、异常安全、弱引用、autorelease 池、僵尸对象、retainCount
6 Block 与 GCD 37–46 Block 语法与 typedef、handler block、循环引用、dispatch 队列、GCD 与 performSelector、NSOperation、dispatch group、dispatch_once、当前队列
7 系统框架 47–52 框架使用、块枚举、桥接、NSCache、+load/+initialize、NSTimer

四、核心原理与精华条目

4.1 第一章:熟悉 Objective-C

  • 语言根源与运行期组件:Objective-C 采用消息结构,运行时才查找要执行的方法;运行期组件是与开发者代码链接的动态库,包含面向对象所需的数据结构与函数,更新运行期组件即可提升应用性能。对象分配在、指针在;不含 * 的变量可能用栈,结构体保存非对象类型 [13]。
  • 头文件与向前声明:在类的头文件中尽量少引用其他头文件;若仅需声明某类型为属性,使用 向前声明@class EOCEmployer;),在 .m 中再 #import,可减少编译时间并避免循环引用。继承或遵从协议时必须在头文件中引入对应头文件 [14]。
  • 字面量与装箱:使用 @""@[]@{}@() 等字面量可减少冗长代码并降低错误;字面量若含 nil 会立即抛异常,而 arrayWithObjects:nil 会截断,易埋坑。字面量创建的集合为不可变 [2][14]。
  • 常量与枚举:用 static const(编译单元内可见)或 extern const(对外公开)定义常量,避免 #define(无类型、易被改)。对外常量命名建议带类名前缀;枚举用 NS_ENUM / NS_OPTIONSswitch 中不要写 default,以便新增枚举成员时编译器提示未处理 [2][14][15]。

4.2 第二章:对象、消息与运行时

4.2.1 属性与实例变量

属性(@property)是编译器自动生成存取器与(可选)实例变量的语法糖。要点 [2][4]:

  • 读写语义strong(默认对象)、copy(如 NSString/Block 防外部修改)、weak(避免循环引用)、assign(非对象类型)。
  • 原子性atomic(默认)在存取时加锁,多数场景下 nonatomic 更高效且足够;若需线程安全,应结合更高级的同步手段。
  • 属性关键字小结copy 用于 NSString/Block 等需拷贝语义的类型;unsafe_unretained 类似 assign 但用于对象,对象释放后不会清空;在非 setter 中给属性赋值时也需遵循其语义(如 copy 属性在 init 里应对传入值 copy)[13]。

4.2.2 对象相等性与 isEqual / hash

  • 相等性:若逻辑上「相等」需自定义,应实现 isEqual:hashhash 在对象被放入集合(如 NSSet、NSDictionary key)时使用,相等对象必须有相同 hash,反之不要求;hash 应稳定、计算量小 [2]。
  • 类簇(Class Cluster):公开接口是抽象基类(如 NSString、NSArray),实际返回私有子类实例。自定义子类需继承簇的「抽象基类」并实现其工厂方法所依赖的初始器;直接比较类时要注意类簇的多种子类 [2][4]。

4.2.3 关联对象(Associated Objects)

运行时允许在不修改类定义的前提下,给对象关联键值对。常用于:给分类「添加」存储、给系统类绑定上下文数据。需注意键的唯一性与内存语义(如 OBJC_ASSOCIATION_RETAIN_NONATOMIC)[2][4]。

4.2.4 objc_msgSend 与消息查找

[someObject messageName:parameter] 在底层转为 C 函数调用:objc_msgSend(someObject, @selector(messageName:), parameter)。该函数在接收者所属类及父类链的方法列表中查找与选择子相符的 IMP;找到则执行并缓存到类的快速映射表,下次同消息更快;找不到则进入消息转发 [3][13]。

4.2.5 消息转发(Message Forwarding)

当对象收到无法识别的消息时,运行时在报错前会给予二次机会 [3][12][13]:

  1. 动态方法解析+resolveInstanceMethod: / +resolveClassMethod:,可为该类动态添加方法实现(如 class_addMethod);典型应用是 @dynamic 属性 + 内部字典存储(EOCAutoDictionary 模式)。
  2. 快速转发-forwardingTargetForSelector:,返回备援接收者,运行期将消息转给该对象。
  3. 完整转发-methodSignatureForSelector:-forwardInvocation:,将消息封装为 NSInvocation,可修改目标、参数或返回值,实现代理、多继承等。

应用:代理对象、惰性加载大型对象、将未识别消息转发到后备对象等 [12]。

4.2.6 类对象与类型查询

运行期用 objc_class 结构描述类(含 isa、super_class、methodLists、cache 等)。isMemberOfClass: 判断是否为某特定类的实例;isKindOfClass: 判断是否为某类或其派生类的实例。从集合取出对象后往往需做类型判断再调用方法,避免向错误类型发消息 [13]。

4.2.7 Method Swizzling

通过运行时交换两个方法的实现(IMP),从而在不修改原类源码的情况下「注入」或「替换」行为;常用于 AOP、调试、无埋点统计。注意:交换应在 +load 等单次执行路径执行,并考虑继承与多线程安全 [2][4]。

4.3 第三章:接口与 API 设计

  • 命名:方法名应语义清晰、读起来像句子,如 initWithWidth:height: 优于 initWithSize::;布尔 getter 用 is/has 前缀(如 isEqualToString:hasPrefix:)。每个冒号左侧的方法部分最好与右侧参数名对应 [14]。
  • 指定初始化器(Designated Initializer):选定全能初始化方法(参数最多的那个),其他 init 及子类 init 均委托到它;子类若有自己的全能初始化器,需覆写父类的全能初始化器并转调自己的,避免用父类 init 产生非法状态(如 Square 覆写 initWithWidth:andHeight: 转调 initWithDimension:)。实现 initWithCoder: 时也应调用超类对应方法 [2][15]。
  • description:覆写 description 返回类名、地址与关键属性(或字典形式),便于调试时在控制台看到有意义信息 [15]。
  • 不可变优先:对外属性设为 readonly,在类扩展中改为 readwrite;集合对外暴露不可变类型(如 NSSet *friends),内部用 NSMutableSet,通过定制 addFriend:/removeFriend: 等接口修改,getter 返回 [_internalFriends copy],避免外部直接改底层数据 [14]。
  • 私有方法前缀:实现文件中的私有方法加前缀(如 p_privateMethod),便于与公共方法区分;不要用单下划线(与 Apple API 冲突)[14]。
  • NSError:用 NSError 封装错误域(domain)、错误码(code)、用户信息(userInfo);作为「输出参数」传递时用 (NSError **)error,调用方检查 *error;可定义 extern NSString *const EOCErrorDomainNS_ENUM 错误码 [13]。
  • NSCopying:实现 copyWithZone:(及可变版的 mutableCopyWithZone:);Foundation 集合默认浅拷贝,深拷贝需自己遍历并 copyItems:YES 或实现 deepCopy [13][15]。

4.4 第四章:协议与分类

  • 委托(Delegate):用 @protocol 定义回调接口,属性用 weak 避免循环引用;delegate 可选方法用 @optional,调用前先判断 delegate 是否存在再 respondsToSelector:,例如 if (_delegate && [_delegate respondsToSelector:@selector(...)]) { ... } [14]。委托模式与数据源模式:信息从类流向委托者 vs 从数据源流向类。
  • 分类(Category):按逻辑将类方法分散到多个分类(如 Friendship、Work、Play),便于管理;可为「私有方法」建 Private 分类。勿在分类中声明属性(仅 class-continuation 可增加实例变量);为第三方或系统类加分类时,分类名与方法名均加前缀(如 ABC_HTTP),避免覆盖原实现 [2][14]。
  • 类扩展(Class Continuation):在 .m 中的匿名分类,可遵循协议而不暴露、将只读属性改为读写、增加实例变量 [14]。
  • 匿名对象id<EOCDelegate> 表示「遵从某协议的对象」而非「某类的实例」,用作 delegate 属性或方法参数(如 setObject:forKey:(id<NSCopying>)key),强调协议契约 [15]。

4.5 第五章:内存管理

4.5.1 引用计数与 ARC

  • 所有权:谁创建(alloc/new/copy/mutableCopy)、谁持有;谁不再需要,谁释放(在 ARC 下由编译器插入)[5][6]。
  • ARC 规则:不能显式调用 retain/release/autorelease;不能使用 retainCount(仅调试用且不可靠);Core Foundation 与 Objective-C 对象混用需注意桥接(__bridge / __bridge_retained / __bridge_transfer)[5][7]。

4.5.2 循环引用与 weak

典型循环:对象 A 强引用 B,B 强引用 A(或通过 block/delegate 形成环)。解决:将其中一侧改为 weak(如 delegate、block 内对 self 的引用)[8][10]。

4.5.3 其他要点

  • dealloc:在 ARC 下仅用于释放 Core Foundation 对象(如 CFRelease)、移除 KVO/通知(如 removeObserver:self)等;不要在 dealloc 中调用其他方法或属性存取器,可能触发异步回调或 KVO 导致使用已释放对象 [2][5][14]。
  • autorelease 池:对象 autorelease 后在下一次事件循环清空池时才会 release;在循环中创建大量临时对象时,在循环内使用 @autoreleasepool { ... }降低内存峰值 [5][8][15]。
  • 僵尸对象(Zombie):开启后,已释放对象的 isa 被改为指向特殊僵尸类,不回收内存、不覆写;再次向该对象发消息会抛出异常并描述原对象与消息,便于排查野指针 [2][6][15]。
  • retainCount:不应使用;ARC 下已废弃,且其返回值只能反映某一时刻的计数,无法反映自动释放池等后续变化 [14]。
  • 异常安全:MRC 下 try 中 retain 的对象若在 release 前抛异常会泄漏,应在 @finally 中 release;ARC 下需 -fobjc-arc-exceptions 才会在异常路径插入清理代码,会增大体积并影响性能 [15]。

4.6 第六章:Block 与 GCD

4.6.1 Block 类型与循环引用

  • Block 三种类型栈 block(定义时在栈上,离开作用域可能失效);堆 block(对栈 block 发 copy 后拷贝到堆,带引用计数);全局 block(不捕获外部变量时可为全局块)。需长期持有的 block 应 copy 到堆 [13][15]。
  • Block 会捕获其使用的局部变量;对对象默认是强引用。若 block 被当前对象持有(如属性、成员变量),且 block 内又使用了 self_ivar(等价于 self),则形成循环引用 [9][10]。
  • 规范写法:在 block 外先 __weak typeof(self) weakSelf = self;,在 block 内使用 weakSelf;若需在 block 执行过程中保证 self 存活,可在 block 内再 __strong typeof(weakSelf) strongSelf = weakSelf; 后使用 strongSelf(weak–strong dance)。也可在 block 末尾将持有 block 的成员置为 nil 以打破环(如 completion 内 _networkFetcher = nil)[10][15]。
  • handler block 与 typedef:用 completion handler 块替代 delegate 回调可让「发起请求」与「处理结果」写在一起;对常用块签名使用 typedef void(^EOCCompletionHandler)(NSData *data, NSError *error); 便于复用与修改 [15]。

4.6.2 GCD 队列与任务

  • 队列类型:串行队列(同一时间只执行一个任务)、并发队列(可多任务并发);主队列(main queue)为串行,用于 UI 更新。不要使用 dispatch_get_current_queue 判断「当前队列」,因队列有层级关系,结果不可靠 [11][14]。
  • 常用 APIdispatch_asyncdispatch_syncdispatch_afterdispatch_once(单例等)、dispatch_group_async + dispatch_group_notify(多任务完成后汇总)[2][11][15]。
  • 同步与锁:可用串行队列统一读写(读写都 dispatch_sync 到同一队列);或并发队列 + dispatch_barrier_async 写、普通 async/sync 读,保证写互斥、读可并发 [15]。
  • 与 performSelector 对比:GCD 不依赖 selector、可传多参数与返回值;延后执行用 dispatch_after,回主线程用 dispatch_async(main_queue, ^{ ... }),替代 performSelector:withObject:afterDelay:performSelectorOnMainThread: [2][15]。
  • NSOperation 适用场景:需取消任务、设置依赖、指定优先级或 KVO 监听 isFinished/isCancelled 时,用 NSOperationQueue 更合适;GCD 为「fire and forget」[15]。

4.7 第七章:系统框架

  • 块枚举:使用 enumerateObjectsUsingBlock: 可获下标、键值对及 *stop 提前终止;比 for 循环简洁,且可修改块签名以做类型检查。遍历 Dictionary/Set 时无需先 allKeys/allObjects 再遍历,减少临时数组 [2][14]。
  • NSCache:线程安全、不拷贝 key(保留)、内存紧张时自动删减(含「最久未用」策略);可设置 countLimittotalCostLimit。与 NSPurgeableData 配合时,访问前 beginContentAccess、用毕 endContentAccess,便于系统回收内存 [2][14]。
  • +load 与 +initialize+load 在类/分类加入运行期时各调用一次,尽量不要在 load 里调用其他类(加载顺序未定义)。+initialize 在类首次收到消息前调用,子类未实现会调用父类,因此需判断 if (self == [EOCBaseClass class]) 再执行逻辑,避免子类触发父类 initialize [2][4][14]。
  • NSTimer:会强引用 target,若 target 是 self 且 self 又持有 timer,则形成保留环;dealloc 中 invalidate 可能无法执行(因环未打破)。推荐:用 NSTimer 的 block 封装(Category 提供 eoc_scheduledTimerWithTimeInterval:block:repeats:,timer 的 target 为类对象,userInfo 存 [block copy]),在 block 内用 weakSelf/strongSelf 调用业务逻辑,这样 self 释放后 block 中 weakSelf 为 nil,或 dealloc 中 invalidate 即可打破环 [2][14]。
  • 无缝桥接:Foundation 与 Core Foundation 间用 __bridge(不转移所有权)、__bridge_retained__bridge_transfer 转换;创建 CF 集合时可指定回调以自定义内存管理语义,再桥接到 OC 使用 [15]。

五、关键概念图示与流程

5.1 消息发送与查找流程

Objective-C 中 [obj message] 在运行时转化为 objc_msgSend(obj, selector, ...),随后在类的方法表及父类链中查找 IMP;若未找到,进入消息转发 [3][4]。

flowchart TD
    A[obj 收到消息] --> B{在类及父类中查找 IMP}
    B -->|找到| C[调用 IMP]
    B -->|未找到| D[动态方法解析 resolveInstanceMethod:]
    D --> E{添加方法?}
    E -->|是| C
    E -->|否| F[forwardingTargetForSelector:]
    F --> G{返回非 nil 目标?}
    G -->|是| H[向目标转发消息]
    G -->|否| I[methodSignatureForSelector: + forwardInvocation:]
    I --> J[开发者可转发到其他对象或处理]

5.2 消息转发(forwardInvocation)概念

当使用 forwardInvocation: 时,运行时将原始消息封装为 NSInvocation,传给接收者;接收者可修改目标、参数或返回值,实现「代理」「多继承」等 [12]。

sequenceDiagram
    participant C as 调用方
    participant R as 接收者
    participant T as 转发目标

    C->>R: 发送未知消息
    R->>R: methodSignatureForSelector:
    R->>R: forwardInvocation:(invocation)
    R->>T: [invocation invokeWithTarget:T]
    T-->>R: 返回值
    R-->>C: 返回

5.3 引用计数与所有权(概念)

flowchart LR
    subgraph 创建
        A[alloc/new/copy] --> B[引用计数 = 1]
    end
    subgraph 持有
        B --> C[retain +1]
        C --> D[release -1]
    end
    subgraph 释放
        D --> E{计数 = 0?}
        E -->|是| F[dealloc 释放对象]
        E -->|否| G[仍存活]
    end

5.4 Block 循环引用

flowchart LR
    subgraph 循环
        S[self] --> B[block]
        B --> S
    end
    subgraph 打破
        W[weakSelf] --> B2[block]
        S2[self] -.->|弱引用| W
        B2 -.->|捕获 weakSelf| W
    end

5.5 GCD 队列层次(概念)

flowchart TB
    subgraph 主队列
        M[Main Queue - UI]
    end
    subgraph 全局并发队列
        G[Global Concurrent Queue]
    end
    subgraph 自定义
        Q1[Serial Queue]
        Q2[Concurrent Queue]
    end
    M --> G
    G --> Q1
    G --> Q2

六、伪代码与算法说明

6.1 对象相等性与 hash(约定)

约定(Effective Objective-C 与 Cocoa 惯例):
1. 若 [a isEqual:b] 为 YES,则 [a hash] == [b hash] 必须成立。
2. hash 在对象生命周期内应稳定(不变)。
3. hash 不必唯一,但应尽量均匀以减少冲突。

算法(示例,仅说明思路):
- 对关键属性分别求 hash(如 NSString 的 hash、数值的 hash),再组合(如异或、乘质数相加)。
- 避免在 hash 中做重计算或依赖可变状态。

6.2 weak–strong 避免 Block 循环引用(伪代码)

// 错误:block 被 self 持有,block 内又强引用 self
self.block = ^{ [self doSomething]; };  // 循环引用

// 正确:block 外 weak,block 内 strong(可选,防止执行过程中 self 被释放)
__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) { [strongSelf doSomething]; }
};

6.3 dispatch_once 单例(典型写法)

+ (instancetype)sharedInstance {
    static MyClass *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[MyClass alloc] init];
    });
    return instance;
}
// dispatch_once 保证块只执行一次,且线程安全。

6.4 forwardInvocation 转发到后备对象(伪代码)

// 根据 Apple 文档 [12],简化实现思路:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    NSMethodSignature *sig = [super methodSignatureForSelector:selector];
    if (!sig) sig = [backupObject methodSignatureForSelector:selector];
    return sig;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    if ([backupObject respondsToSelector:[invocation selector]])
        [invocation invokeWithTarget:backupObject];
    else
        [super forwardInvocation:invocation];
}

七、应用场景与最佳实践

7.1 场景与条目对照

场景 本书建议概要 典型条目
网络/异步回调 使用 Block + GCD,避免 performSelector;在 block 内用 weak–strong 避免循环引用 37–40, 42–43
自定义集合元素 实现 isEqual: 与 hash;若需复制实现 NSCopying 8, 22
为系统类添加方法 用 Category,方法名加前缀;需存储用关联对象或类扩展 25–26
单例或一次性初始化 dispatch_once 45
缓存图片/数据 NSCache,不手写 NSDictionary + 淘汰 50
定时任务 NSTimer 注意与 target 的循环引用,及时 invalidate 或拆开 52
调试内存/野指针 僵尸对象、Instruments、静态分析 35–36
多任务完成后统一处理 dispatch_group + dispatch_group_notify 44

7.2 高级应用场景简述

  • AOP / 无埋点:通过 Method Swizzling 在系统或业务方法前后插入逻辑(如统计、日志),需注意交换时机与线程安全;可与 +load 配合 [2][4]。
  • 跨框架混用(Core Foundation ↔ Objective-C):使用 __bridge(不转移所有权)、__bridge_retained(CF 侧持有)、__bridge_transfer(OC 侧持有)正确管理生命周期,避免重复释放或泄漏 [5][7]。
  • 大循环中的临时对象:在循环内使用 @autoreleasepool { ... } 及时排空自动释放池,降低内存峰值 [5][8]。
  • 委托与数据源:delegate 属性声明为 weak,在 dealloc 中无需显式置 nil(weak 会自动清空);调用可选方法前用 respondsToSelector: 判断 [2]。

八、其它补充

第2条: 在类的头文件中尽量少引用其他头文件

有时,类A需要将类B的实例变量作为它公共API的属性。这个时候,我们不应该引入类B的头文件,而应该使用向前声明(forward declaring)使用class关键字,并且在A的实现文件引用B的头文件。

// EOCPerson.h
#import <Foundation/Foundation.h>

@class EOCEmployer;

@interface EOCPerson : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;//将EOCEmployer作为属性

@end

// EOCPerson.m
#import "EOCEmployer.h"

这样做有什么优点呢:

  • 不在A的头文件中引入B的头文件,就不会一并引入B的全部内容,这样就减少了编译时间。
  • 可以避免循环引用:因为如果两个类在自己的头文件中都引入了对方的头文件,那么就会导致其中一个类无法被正确编译。

但是个别的时候,必须在头文件中引入其他类的头文件:

主要有两种情况:

  1. 该类继承于某个类,则应该引入父类的头文件。
  2. 该类遵从某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中。

第3条:多用字面量语法,少用与之等价的方法

1. 声明时的字面量语法:

在声明NSNumber,NSArray,NSDictionary时,应该尽量使用简洁字面量语法。

NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSArray *animals =[NSArray arrayWithObjects:@"cat", @"dog",@"mouse", @"badger", nil];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};

2. 集合类取下标的字面量语法:

NSArray,NSDictionary,NSMutableArray,NSMutableDictionary 的取下标操作也应该尽量使用字面量语法。

NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];

使用字面量语法的优点:

  1. 代码看起来更加简洁。
  2. 如果存在nil值,则会立即抛出异常。如果在不用字面量语法定义数组的情况下,如果数组内部存在nil,则系统会将其设为数组最后一个元素并终止。所以当这个nil不是最后一个元素的话,就会出现难以排查的错误。

注意: 字面量语法创建出来的字符串,数组,字典对象都是不可变的。

第4条:多用类型常量,少用#define预处理命令

在OC中,定义常量通常使用预处理命令,但是并不建议使用它,而是使用类型常量的方法。 首先比较一下这两种方法的区别:

  • 预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改。
  • 类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。

我们可以看出来,使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:不具备类型 + 可以被任意修改,总之给人一种不安全的感觉。

知道了它们的长短处,我们再来简单看一下它们的具体使用方法:

预处理命令:

#define W_LABEL (W_SCREEN - 2*GAP)

这里,(W_SCREEN - 2*GAP)替换了W_LABEL,它不具备W_LABEL的类型信息。而且要注意一下:如果替换式中存在运算符号,以笔者的经验最好用括号括起来,不然容易出现错误(有体会)。

类型常量:

static const NSTimeIntervalDuration = 0.3;

这里: const 将其设置为常量,不可更改。 static意味着该变量仅仅在定义此变量的编译单元中可见。如果不声明static,编译器会为它创建一个外部符号(external symbol)。我们来看一下对外公开的常量的声明方法:

对外公开某个常量:

如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串,那么显然这个字符串是不能被轻易更改,而且可以在不同的地方获取。这个时候就需要定义一个外界可见的字符串常量。

//header file
extern NSString *const NotificationString;

//implementation file
NSString *const  NotificationString = @"Finish Download";

这里NSString *const NotificationString是指针常量。 extern关键字告诉编译器,在全局符号表中将会有一个名叫NotificationString的符号。

我们通常在头文件声明常量,在其实现文件里定义该常量。由实现文件生成目标文件时,编译器会在“数据段”为字符串分配存储空间。

最后注意一下公开和非公开的常量的命名规范:

公开的常量:常量的名字最好用与之相关的类名做前缀。 非公开的常量:局限于某个编译单元(tanslation unit,实现文件 implementation file)内,在签名加上字母k。

第5条:用枚举表示状态,选项,状态码

我们经常需要给类定义几个状态,这些状态码可以用枚举来管理。下面是关于网络连接状态的状态码枚举:

typedef NS_ENUM(NSUInteger, EOCConnectionState) {
  EOCConnectionStateDisconnected,
  EOCConnectionStateConnecting,
  EOCConnectionStateConnected,
};

需要注意的一点是: 在枚举类型的switch语句中不要实现default分支。它的好处是,当我们给枚举增加成员时,编译器就会提示开发者:switch语句并未处理所有的枚举。对此,笔者有个教训,又一次在switch语句中将“默认分支”设置为枚举中的第一项,自以为这样写可以让程序更健壮,结果后来导致了严重的崩溃。

第21条:理解Objective-C错误类型

在OC中,我们可以用NSError描述错误。 使用NSError可以封装三种信息:

  • Error domain:错误范围,类型是字符串
  • Error code :错误码,类型是整数
  • User info:用户信息,类型是字典

1. NSError的使用

用法:

1.通过委托协议来传递NSError,告诉代理错误类型。

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

2.作为方法的“输出参数”返回给调用者

- (BOOL)doSomething:(NSError**)error

使用范例:


NSError *error = nil;
BOOL ret = [object doSomething:&error];

if (error) {
    // There was an error
}

2. 自定义NSError

我们可以设置属于我们自己程序的错误范围和错误码

  • 错误范围可以用全局常量字符串来定义。
  • 错误码可以用枚举来定义。

// EOCErrors.h
extern NSString *const EOCErrorDomain;

//定义错误码
typedef NS_ENUM(NSUInteger, EOCError) {

    EOCErrorUnknown = –1,
    EOCErrorInternalInconsistency = 100,
    EOCErrorGeneralFault = 105,
    EOCErrorBadInput = 500,
};



// EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain"; //定义错误范围

第22条:理解NSCopying协议

如果我们想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法:

- (id)copyWithZone:(NSZone*)zone

作者举了个:


- (id)copyWithZone:(NSZone*)zone {

     EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName  andLastName:_lastName];
    copy->_friends = [_friends mutableCopy];
     return copy;
}

之所以是copy->_friends,而不是copy.friends是因为friends并不是属性,而是一个内部使用的实例变量。

1. 复制可变的版本:

遵从协议

而且要执行:

- (id)mutableCopyWithZone:(NSZone*)zone;

注意:拷贝可变型和不可变型发送的是copymutableCopy消息,而我们实现的却是- (id)copyWithZone:(NSZone*)zone- (id)mutableCopyWithZone:(NSZone*)zone 方法。

而且,如果我们想获得某对象的不可变型,统一调用copy方法;获得某对象的可变型,统一调用mutableCopy方法。

例如数组的拷贝:

-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray

2. 浅拷贝和深拷贝

Foundation框架中的集合类默认都执行浅拷贝:只拷贝容器对象本身,而不复制其中的数据。 而深拷贝的意思是连同对象本身和它的底层数据都要拷贝。

作者用一个图很形象地体现了浅拷贝和深拷贝的区别:

图片来自:《Effective Objective-C》

浅拷贝后的内容和原始内容指向同一个对象 深拷贝后的内容所指的对象是原始内容对应对象的拷贝

3. 如何深拷贝?

我们需要自己编写深拷贝的方法:遍历每个元素并复制,然后将复制后的所有元素重新组成一个新的集合。

- (id)initWithSet:(NSArray*)array copyItems:(BOOL)copyItems;

在这里,我们自己提供了一个深拷贝的方法:该方法需要传入两个参数:需要拷贝的数组和是否拷贝元素(是否深拷贝)


- (id)deepCopy {
       EOCPerson *copy = [[[self class] alloc] initWithFirstName:_firstName andLastName:_lastName];
        copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES];
        return copy;
}

第47条:熟悉系统框架

如果我们使用了系统提供的现成的框架,那么用户在升级系统后,就可以直接享受系统升级所带来的改进。

主要的系统框架:

  • Foundation:NSObject,NSArray,NSDictionary等
  • CFoundation框架:C语言API,Foundation框架中的许多功能,都可以在这里找到对应的C语言API
  • CFNetwork框架:C语言API,提供了C语言级别的网络通信能力
  • CoreAudio:C语言API,操作设备上的音频硬件
  • AVFoundation框架:提供的OC对象可以回放并录制音频和视频
  • CoreData框架:OC的API,将对象写入数据库
  • CoreText框架:C语言API,高效执行文字排版和渲染操作

用C语言来实现API的好处:可以绕过OC的运行期系统,从而提升执行速度

第7条: 在对象内部尽量直接访问实例变量

关于实例变量的访问,可以直接访问,也可以通过属性的方式(点语法)来访问。书中作者建议在读取实例变量时采用直接访问的形式,而在设置实例变量的时候通过属性来做。

1. 直接访问属性的特点:

  • 绕过set,get语义,速度快;

2. 通过属性访问属性的特点:

  • 不会绕过属性定义的内存管理语义
  • 有助于打断点排查错误
  • 可以触发KVO

因此,有个关于折中的方案:

设置属性:通过属性 读取属性:直接访问

不过有两个特例:

  1. 初始化方法和dealloc方法中,需要直接访问实例变量来进行设置属性操作。因为如果在这里没有绕过set方法,就有可能触发其他不必要的操作。
  2. 惰性初始化(lazy initialization)的属性,必须通过属性来读取数据。因为惰性初始化是通过重写get方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。

第15条:用前缀 避免命名空间冲突

Apple宣称其保留使用所有"两字母前缀"的权利,所以我们选用的前缀应该是三个字母的。 而且,如果自己开发的程序使用到了第三方库,也应该加上前缀。

第18条:尽量使用不可变对象

书中作者建议尽量把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体做法是:

在头文件中,设置对象属性为readonly,在实现文件中设置为readwrite。这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。

而且,对于集合类的对象,更应该仔细考虑是否可以将其设为可变的。

如果在公开部分只能设置其为只读属性,那么就在非公开部分存储一个可变型。这样一来,当在外部获取这个属性时,获取的只是内部可变型的一个不可变版本,例如:

在公共API中:

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends //向外公开的不可变集合

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;

@end

在这里,我们将friends属性设置为不可变的set。然后,提供了来增加和删除这个set里的元素的公共接口。

在实现文件里:

@interface EOCPerson ()

@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

@end

@implementation EOCPerson {
     NSMutableSet *_internalFriends;  //实现文件里的可变集合
}

- (NSSet*)friends {
     return [_internalFriends copy]; //get方法返回的永远是可变set的不可变型
}

- (void)addFriend:(EOCPerson*)person {
    [_internalFriends addObject:person]; //在外部增加集合元素的操作
    //do something when add element
}

- (void)removeFriend:(EOCPerson*)person {
    [_internalFriends removeObject:person]; //在外部移除元素的操作
    //do something when remove element
}

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName {

     if ((self = [super init])) {
        _firstName = firstName;
        _lastName = lastName;
        _internalFriends = [NSMutableSet new];
    }
 return self;
}

我们可以看到,在实现文件里,保存一个可变set来记录外部的增删操作。

这里最重要的代码是:

- (NSSet*)friends {
 return [_internalFriends copy];
}

这个是friends属性的获取方法:它将当前保存的可变set复制了一不可变的set并返回。因此,外部读取到的set都将是不可变的版本。

等一下,有个疑问:

在公共接口设置不可变set 和 将增删的代码放在公共接口中是否矛盾的?

答案:并不矛盾!

因为如果将friends属性设置为可变的,那么外部就可以随便更改set集合里的数据,这里的更改,仅仅是底层数据的更改,并不伴随其他任何操作。 然而有时,我们需要在更改set数据的同时要执行隐秘在实现文件里的其他工作,那么如果在外部随意更改这个属性的话,显然是达不到这种需求的。

因此,我们需要提供给外界我们定制的增删的方法,并不让外部”自行“增删。

第19条:使用清晰而协调的命名方式

在给OC的方法取名字的时候要充分利用OC方法的命名优势,取一个语义清晰的方法名!什么叫语义清晰呢?就是说读起来像是一句话一样。

我们看一个例子:

先看名字取得不好的:

//方法定义
- (id)initWithSize:(float)width :(float)height;

//方法调用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithSize:5.0f :10.0f];

这里定义了Rectangle的初始化方法。虽然直观上可以知道这个方法通过传入的两个参数来组成矩形的size,但是我们并不知道哪个是矩形的宽,哪个是矩形的高。 来看一下正确的🌰 :

//方法定义
- (id)initWithWidth:(float)width height:(float)height;

//方法调用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithWidth:5.0f height:10.0f];

这个方法名就很好的诠释了该方法的意图:这个类的初始化是需要宽度和高度的。而且,哪个参数是高度,哪个参数是宽度,看得人一清二楚。永远要记得:代码是给人看的

笔者自己总结的方法命名规则:

每个冒号左边的方法部分最好与右边的参数名一致。

对于返回值是布尔值的方法,我们也要注意命名的规范:

  • 获取”是否“的布尔值,应该增加“is”前缀:

- isEqualToString:

获取“是否有”的布尔值,应该增加“has”前缀:

- hasPrefix:

第20条:为私有方法名加前缀

建议在实现文件里将非公开的方法都加上前缀,便于调试,而且这样一来也很容易区分哪些是公共方法,哪些是私有方法。因为往往公共方法是不便于任意修改的。

在这里,作者举了个例子:

#import <Foundation/Foundation.h>

@interface EOCObject : NSObject

- (void)publicMethod;

@end


@implementation EOCObject

- (void)publicMethod {
 /* ... */
}

- (void)p_privateMethod {
 /* ... */
}

@end

注意: 不要用下划线来区分私有方法和公共方法,因为会和苹果公司的API重复。

第23条:通过委托与数据源协议进行对象间通信

如果给委托对象发送消息,那么必须提前判断该委托对象是否实现了该消息:

NSData *data = /* data obtained from network */;

if ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)])
{
        [_delegate networkFetcher:self didReceiveData:data];
}

而且,最好再加上一个判断:判断委托对象是否存在


NSData *data = /* data obtained from network */;

if ( (_delegate) && ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)]))
{
        [_delegate networkFetcher:self didReceiveData:data];
}

对于代理模式,在iOS中分为两种:

  • 普通的委托模式:信息从类流向委托者
  • 信息源模式:信息从数据源流向类

普通的委托 | 信息源

就好比tableview告诉它的代理(delegate)“我被点击了”;而它的数据源(data Source)告诉它“你有这些数据”。仔细回味一下,这两个信息的传递方向是相反的。

第24条:将类的实现代码分散到便于管理的数个分类中

通常一个类会有很多方法,而这些方法往往可以用某种特有的逻辑来分组。我们可以利用OC的分类机制,将类的这些方法按一定的逻辑划入几个分区中。

例子:

无分类的类:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;

/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;


/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;


/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;


@end

分类之后:

#import <Foundation/Foundation.h>


@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;



- (id)initWithFirstName:(NSString*)firstName

lastName:(NSString*)lastName;

@end



@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end



@interface EOCPerson (Work)

- (void)performDaysWork;
- (void)takeVacationFromWork;

@end



@interface EOCPerson (Play)

- (void)goToTheCinema;
- (void)goToSportsGame;

@end

其中,FriendShip分类的实现代码可以这么写:


// EOCPerson+Friendship.h
#import "EOCPerson.h"


@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end


// EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"


@implementation EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person {
 /* ... */
}

- (void)removeFriend:(EOCPerson*)person {
 /* ... */
}

- (BOOL)isFriendsWith:(EOCPerson*)person {
 /* ... */
}

@end

注意:在新建分类文件时,一定要引入被分类的类文件。

通过分类机制,可以把类代码分成很多个易于管理的功能区,同时也便于调试。因为分类的方法名称会包含分类的名称,可以马上看到该方法属于哪个分类中。

利用这一点,我们可以创建名为Private的分类,将所有私有方法都放在该类里。这样一来,我们就可以根据private一词的出现位置来判断调用的合理性,这也是一种编写“自我描述式代码(self-documenting)”的办法。

第25条:总是为第三方类的分类名称加前缀

分类机制虽然强大,但是如果分类里的方法与原来的方法名称一致,那么分类的方法就会覆盖掉原来的方法,而且总是以最后一次被覆盖为基准。

因此,我们应该以命名空间来区别各个分类的名称与其中定义的方法。在OC里的做法就是给这些方法加上某个共用的前缀。例如:

@interface NSString (ABC_HTTP)

// Encode a string with URL encoding
- (NSString*)abc_urlEncodedString;

// Decode a URL encoded string
- (NSString*)abc_urlDecodedString;

@end

因此,如果我们想给第三方库或者iOS框架里的类添加分类时,最好将分类名和方法名加上前缀。

第26条:勿在分类中声明属性

除了实现文件里的class-continuation分类中可以声明属性外,其他分类无法向类中新增实例变量。

因此,类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量的地方。

关于分类,需要强调一点:

分类机制,目标在于扩展类的功能,而不是封装数据。

第27条:使用class-continuation分类 隐藏实现细节

通常,我们需要减少在公共接口中向外暴露的部分(包括属性和方法),而因此带给我们的局限性可以利用class-continuation分类的特性来补偿:

  • 可以在class-continuation分类中增加实例变量。
  • 可以在class-continuation分类中将公共接口的只读属性设置为读写。
  • 可以在class-continuation分类中遵循协议,使其不为人知。

第31条:在dealloc方法中只释放引用并解除监听

永远不要自己调用dealloc方法,运行期系统会在适当的时候调用它。根据性能需求我们有时需要在dealloc方法中做一些操作。那么我们可以在dealloc方法里做什么呢?

  • 释放对象所拥有的所有引用,不过ARC会自动添加这些释放代码,可以不必操心。
  • 而且对象拥有的其他非OC对象也要释放(CoreFoundation对象就必须手动释放)
  • 释放原来的观测行为:注销通知。如果没有及时注销,就会向其发送通知,使得程序崩溃。

举个简单的🌰 :


- (void)dealloc {

     CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver:self];

}

尤其注意:在dealloc方法中不应该调用其他的方法,因为如果这些方法是异步的,并且回调中还要使用当前对象,那么很有可能当前对象已经被释放了,会导致崩溃。

并且在dealloc方法中也不能调用属性的存取方法,因为很有可能在这些方法里还有其他操作。而且这个属性还有可能处于键值观察状态,该属性的观察者可能会在属性改变时保留或者使用这个即将回收的对象。

第36条:不要使用retainCount

在非ARC得环境下使用retainCount可以返回当前对象的引用计数,但是在ARC环境下调用会报错,因为该方法已经被废弃了 。

它被废弃的原因是因为它所返回的引用计数只能反映对象某一时刻的引用计数,而无法“预知”对象将来引用计数的变化(比如对象当前处于自动释放池中,那么将来就会自动递减引用计数)。

第46条:不要使用dispatch_get_current_queue

我们无法用某个队列来描述“当前队列”这一属性,因为派发队列是按照层级来组织的。

那么什么是队列的层级呢?

队列的层及分布

安排在某条队列中的快,会在其上层队列中执行,而层级地位最高的那个队列总是全局并发队列。

在这里,B,C中的块会在A里执行。但是D中的块,可能与A里的块并行,因为A和D的目标队列是并发队列。

正因为有了这种层级关系,所以检查当前队列是并发的还是非并发的就不会总是很准确。

第48条:多用块枚举,少用for循环

当遍历集合元素时,建议使用块枚举,因为相对于传统的for循环,它更加高效,而且简洁,还能获取到用传统的for循环无法提供的值:

我们首先看一下传统的遍历:

1. 传统的for遍历

NSArray *anArray = /* ... */;
for (int i = 0; i < anArray.count; i++) {
   id object = anArray[i];
   // Do something with 'object'
}



// Dictionary
NSDictionary *aDictionary = /* ... */;
NSArray *keys = [aDictionary allKeys];
for (int i = 0; i < keys.count; i++) {
   id key = keys[i];
   id value = aDictionary[key];
   // Do something with 'key' and 'value'
}


// Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allObjects];
for (int i = 0; i < objects.count; i++) {
   id object = objects[i];
   // Do something with 'object'

}

我们可以看到,在遍历NSDictionary,和NSet时,我们又新创建了一个数组。虽然遍历的目的达成了,但是却加大了系统的开销。

2. 利用快速遍历:

NSArray *anArray = /* ... */;
for (id object in anArray) {
 // Do something with 'object'
}

// Dictionary
NSDictionary *aDictionary = /* ... */;
for (id key in aDictionary) {
 id value = aDictionary[key];
 // Do something with 'key' and 'value'

}


NSSet *aSet = /* ... */;
for (id object in aSet) {
 // Do something with 'object'
}

这种快速遍历的方法要比传统的遍历方法更加简洁易懂,但是缺点是无法方便获取元素的下标。

3. 利用基于block的遍历:

NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop){

   // Do something with 'object'
   if (shouldStop) {
      *stop = YES; //使迭代停止
  }

}];


“// Dictionary
NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop){
     // Do something with 'key' and 'object'
     if (shouldStop) {
        *stop = YES;
    }
}];


// Set
NSSet *aSet = /* ... */;
[aSet enumerateObjectsUsingBlock:^(id object, BOOL *stop){
     // Do something with 'object'
     if (shouldStop) {
        *stop = YES;
    }
];

我们可以看到,在使用块进行快速枚举的时候,我们可以不创建临时数组。虽然语法上没有快速枚举简洁,但是我们可以获得数组元素对应的序号,字典元素对应的键值,而且,我们还可以随时令遍历终止。

利用快速枚举和块的枚举还有一个优点:能够修改块的方法签名

for (NSString *key in aDictionary) {
         NSString *object = (NSString*)aDictionary[key];
        // Do something with 'key' and 'object'
}

NSDictionary *aDictionary = /* ... */;

    [aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop){

             // Do something with 'key' and 'obj'

}];

第50条:构建缓存时选用NSCache 而非NSDictionary

如果我们缓存使用得当,那么应用程序的响应速度就会提高。只有那种“重新计算起来很费事的数据,才值得放入缓存”,比如那些需要从网络获取或从磁盘读取的数据。

在构建缓存的时候很多人习惯用NSDictionary或者NSMutableDictionary,但是作者建议大家使用NSCache,它作为管理缓存的类,有很多特点要优于字典,因为它本来就是为了管理缓存而设计的。

1. NSCache优于NSDictionary的几点:

  • 当系统资源将要耗尽时,NSCache具备自动删减缓冲的功能。并且还会先删减“最久未使用”的对象。
  • NSCache不拷贝键,而是保留键。因为并不是所有的键都遵从拷贝协议(字典的键是必须要支持拷贝协议的,有局限性)。
  • NSCache是线程安全的:不编写加锁代码的前提下,多个线程可以同时访问NSCache。

2. 关于操控NSCache删减内容的时机

开发者可以通过两个尺度来调整这个时机:

  • 缓存中的对象总数.
  • 将对象加入缓存时,为其指定开销值。

对于开销值,只有在能很快计算出开销值的情况下,才应该考虑采用这个尺度,不然反而会加大系统的开销。

下面我们来看一下缓存的用法:缓存网络下载的数据

// Network fetcher class
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;

@end

// Class that uses the network fetcher and caches results
@interface EOCClass : NSObject
@end

@implementation EOCClass {
     NSCache *_cache;
}

- (id)init {

     if ((self = [super init])) {
    _cache = [NSCache new];

     // Cache a maximum of 100 URLs
    _cache.countLimit = 100;


     /**
     * The size in bytes of data is used as the cost,
     * so this sets a cost limit of 5MB.
     */
    _cache.totalCostLimit = 5 * 1024 * 1024;
    }
 return self;
}



- (void)downloadDataForURL:(NSURL*)url { 

     NSData *cachedData = [_cache objectForKey:url];

     if (cachedData) {

         // Cache hit:存在缓存,读取
        [self useData:cachedData];

    } else {

         // Cache miss:没有缓存,下载
         EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];      

        [fetcher startWithCompletionHandler:^(NSData *data){
         [_cache setObject:data forKey:url cost:data.length];    
        [self useData:data];
        }];
    }
}
@end

在这里,我们使用URL作为缓存的key,将总对象数目设置为100,将开销值设置为5MB。

3. NSPurgeableData

NSPurgeableData是NSMutableData的子类,把它和NSCache配合使用效果很好。

因为当系统资源紧张时,可以把保存NSPurgeableData的那块内存释放掉。

如果需要访问某个NSPurgeableData对象,可以调用beginContentAccess方发,告诉它现在还不应该丢弃自己所占据的内存。

在使用完之后,调用endContentAccess方法,告诉系统在必要时可以丢弃自己所占据的内存。

上面这两个方法类似于“引用计数”递增递减的操作,也就是说,只有当“引用计数”为0的时候,才可以在将来删去它所占的内存。


- (void)downloadDataForURL:(NSURL*)url { 

      NSPurgeableData *cachedData = [_cache objectForKey:url];

      if (cachedData) {         

            // 如果存在缓存,需要调用beginContentAccess方法
            [cacheData beginContentAccess];

             // Use the cached data
            [self useData:cachedData];

             // 使用后,调用endContentAccess
            [cacheData endContentAccess];


        } else {

                 //没有缓存
                 EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];    

                  [fetcher startWithCompletionHandler:^(NSData *data){                         NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
                         [_cache setObject:purgeableData forKey:url cost:purgeableData.length];

                          // Don't need to beginContentAccess as it begins            
                          // with access already marked
                           // Use the retrieved data
                            [self useData:data];

                             // Mark that the data may be purged now
                            [purgeableData endContentAccess];

            }];
      }
}
复制代码

注意:

在我们可以直接拿到purgeableData的情况下需要执行beginContentAccess方法。然而,在创建purgeableData的情况下,是不需要执行beginContentAccess,因为在创建了purgeableData之后,其引用计数会自动+1;

第51条: 精简initialize 与 load的实现代码

1. load方法

+(void)load;

每个类和分类在加入运行期系统时,都会调用load方法,而且仅仅调用一次,可能有些小伙伴习惯在这里调用一些方法,但是作者建议尽量不要在这个方法里调用其他方法,尤其是使用其他的类。原因是每个类载入程序库的时机是不同的,如果该类调用了还未载入程序库的类,就会很危险。

2. initialize方法

+(void)initialize;

这个方法与load方法类似,区别是这个方法会在程序首次调用这个类的时候调用(惰性调用),而且只调用一次(绝对不能主动使用代码调用)。

值得注意的一点是,如果子类没有实现它,它的超类却实现了,那么就会运行超类的代码:这个情况往往很容易让人忽视。

看一下🌰 :

#import <Foundation/Foundation.h>

@interface EOCBaseClass : NSObject
@end

@implementation EOCBaseClass
+ (void)initialize {
 NSLog(@"%@ initialize", self);
}
@end

@interface EOCSubClass : EOCBaseClass
@end

@implementation EOCSubClass
@end

当使用EOCSubClass类时,控制台会输出两次打印方法:

EOCBaseClass initialize
EOCSubClass initialize

因为子类EOCSubClass并没有覆写initialize方法,那么自然会调用其父类EOCBaseClass的方法。 解决方案是通过检测类的类型的方法:

+ (void)initialize {
   if (self == [EOCBaseClass class]) {
       NSLog(@"%@ initialized", self);
    }
}

这样一来,EOCBaseClass的子类EOCSubClass就无法再调用initialize方法了。 我们可以察觉到,如果在这个方法里执行过多的操作的话,会使得程序难以维护,也可能引起其他的bug。因此,在initialize方法里,最好只是设置内部的数据,不要调用其他的方法,因为将来可能会给这些方法添加其它的功能,那么会可能会引起难以排查的bug。

第52条: 别忘了NSTimer会保留其目标对象

在使用NSTimer的时候,NSTimer会生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那么就会生成保留环。

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end


@implementation EOCClass {
     NSTimer *_pollTimer;
}


- (id)init {
     return [super init];
}


- (void)dealloc {
    [_pollTimer invalidate];
}


- (void)stopPolling {

    [_pollTimer invalidate];
    _pollTimer = nil;
}


- (void)startPolling {
   _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                                 target:self
                                               selector:@selector(p_doPoll)
                                               userInfo:nil
                                                repeats:YES];
}

- (void)p_doPoll {
    // Poll the resource
}

@end

在这里,在EOCClass和_pollTimer之间形成了保留环,如果不主动调用stopPolling方法就无法打破这个保留环。像这种通过主动调用方法来打破保留环的设计显然是不好的。

而且,如果通过回收该类的方法来打破此保留环也是行不通的,因为会将该类和NSTimer孤立出来,形成“孤岛”:

孤立了类和它的NSTimer

这可能是一个极其危险的情况,因为NSTimer没有消失,它还有可能持续执行一些任务,不断消耗系统资源。而且,如果任务涉及到下载,那么可能会更糟。。

那么如何解决呢? 通过“块”来解决!

通过给NSTimer增加一个分类就可以解决:

#import <Foundation/Foundation.h>

@interface NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                         repeats:(BOOL)repeats;
@end



@implementation NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                        repeats:(BOOL)repeats
{
             return [self scheduledTimerWithTimeInterval:interval
                                                  target:self
                                                selector:@selector(eoc_blockInvoke:)
                                                userInfo:[block copy]
                                                 repeats:repeats];

}


+ (void)eoc_blockInvoke:(NSTimer*)timer {
     void (^block)() = timer.userInfo;
         if (block) {
             block();
        }
}
@end

我们在NSTimer类里添加了方法,我们来看一下如何使用它:

- (void)startPolling {

         __weak EOCClass *weakSelf = self;    
         _pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{

               EOCClass *strongSelf = weakSelf;
               [strongSelf p_doPoll];
          }

                                                          repeats:YES];
}

在这里,创建了一个self的弱引用,然后让块捕获了这个self变量,让其在执行期间存活。

一旦外界指向EOC类的最后一个引用消失,该类就会被释放,被释放的同时,也会向NSTimer发送invalidate消息(因为在该类的dealloc方法中向NSTimer发送了invalidate消息)。

而且,即使在dealloc方法里没有发送invalidate消息,因为块里的weakSelf会变成nil,所以NSTimer同样会失效。

如果我们可以知道集合里的元素类型,就可以修改签名。这样做的好处是:可以让编译期检查该元素是否可以实现我们想调用的方法,如果不能实现,就做另外的处理。这样一来,程序就能变得更加安全。

九、iOS底层原理精华

书中其它部分和之前研究底层原理的内容有交叉,因此,可以参照 底层原理的精华篇幅和文章:

9.1 前知识

9.2 基于OC语言探索iOS底层原理

9.3 基于Swift语言探索iOS底层原理

关于函数枚举可选项结构体闭包属性方法swift多态原理StringArrayDictionary引用计数MetaData等Swift基本语法和相关的底层原理文章有如下几篇:

9.4底层原理相关专题

9.4 iOS相关专题

9.5 webApp相关专题

9.6 跨平台开发方案相关专题

9.7 阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

9.8 Android、HarmonyOS页面渲染专题

9.9 小程序页面渲染专题


延伸阅读(掘金三部曲)

以下为同一作者(J_Knight_)对《Effective Objective-C》的概念 / 规范 / 技巧三分法总结,与本书 52 条一一对应,配有大量示例代码与图示,可作为按条目深挖的补充阅读。

标题 链接 内容概要
概念篇 《Effective Objective-C》干货三部曲(一):概念篇 掘金 - 概念篇 第 1 条(起源、运行期组件、堆栈)、第 6 条(属性、存取方法、关键字)、第 8 条(等同性、hash)、第 11 条(objc_msgSend)、第 12 条(消息转发、EOCAutoDictionary)、第 14 条(类对象、objc_class、isKindOfClass)、第 21 条(NSError)、第 22 条(NSCopying、浅/深拷贝)、第 29–30 条(引用计数、ARC)、第 37 条(Block 栈/堆/全局)、第 47 条(系统框架)
规范篇 《Effective Objective-C》干货三部曲(二):规范篇 掘金 - 规范篇 第 2 条(向前声明)、第 3–5 条(字面量、类型常量、枚举)、第 7 条(直接访问实例变量)、第 15 条(前缀)、第 18 条(不可变对象、内部可变集合)、第 19–20 条(命名、私有方法前缀)、第 23–27 条(委托、分类分散、分类前缀、勿在分类声明属性、class-continuation)、第 31 条(dealloc)、第 36 条(retainCount)、第 46 条(dispatch_get_current_queue)、第 48 条(块枚举)、第 50 条(NSCache、NSPurgeableData)、第 51 条(load/initialize)、第 52 条(NSTimer 保留环与 block 方案)
技巧篇 《Effective Objective-C》干货三部曲(三):技巧篇 掘金 - 技巧篇 第 9 条(类族模式)、第 10 条(关联对象、UIAlertView+block)、第 13 条(方法调配、lowercaseString 示例)、第 16 条(全能初始化、子类覆写、initWithCoder)、第 17 条(description)、第 28 条(匿名对象)、第 32–35 条(异常安全、弱引用、自动释放池块、僵尸对象)、第 38–45 条(block typedef、handler 块、保留环、串行队列/barrier、GCD vs performSelector、NSOperation、dispatch group、dispatch_once)、第 49 条(无缝桥接)

参考文献

[1] Galloway, M. Effective Objective-C 2.0: 52 Specific Ways to Write Better iOS and OS X Programs. Addison-Wesley Professional, 2013.
[2] O'Reilly. Effective Objective-C 2.0 — Table of Contents and Chapter Summaries. www.oreilly.com/library/vie…
[3] Apple. Objective-C Runtime Programming Guide. Developer Documentation Archive.
[4] Apple. The Objective-C Programming Language (Legacy).
[5] Apple. Advanced Memory Management Programming Guide. developer.apple.com/library/arc…
[6] Apple. About Memory Management. developer.apple.com/library/arc…
[7] Apple. Transitioning to ARC Release Notes. developer.apple.com/library/arc…
[8] Clang. Automatic Reference Counting (ARC). clang.llvm.org/docs/Automa…
[9] Stack Overflow. Block retain cycle, weak-strong dance.
[10] Apple. Working with Blocks. Programming Guide.
[11] Apple. Dispatch (GCD). Concurrency Programming Guide.
[12] Apple. Message Forwarding. Objective-C Runtime Guide. developer.apple.com/library/arc…
[13] J_Knight_. 《Effective Objective-C》干货三部曲(一):概念篇. 掘金,2018-01-08. juejin.cn/post/684490…
[14] J_Knight_. 《Effective Objective-C》干货三部曲(二):规范篇. 掘金,2018-01-10. juejin.cn/post/684490…
[15] J_Knight_. 《Effective Objective-C》干货三部曲(三):技巧篇. 掘金,2018-01-12. juejin.cn/post/684490…

02-研究优秀开源框架@图层处理@iOS | Kingfisher 框架:从使用到原理解析

📋 目录


一、Kingfisher 概述与历史演进

1. 框架简介

Kingfisher 是一款面向 Apple 平台(iOS / macOS / tvOS / watchOS)的纯 Swift 异步图片下载与缓存库,由 onevcat(王巍)维护。其「图层处理」相关能力以 ImageProcessor 为核心:在「从数据到图像」以及「从图像到图像」的管线中,完成解码、缩放、圆角、模糊、着色等处理,并与 ImageCache(内存 + 磁盘)、ImageDownloader 协同,形成「请求 → 缓存查询 → 下载 → 处理 → 缓存 → 展示」的完整流程 [1][2]。

与 SDWebImage(Objective-C 为主)相比,Kingfisher 采用协议导向Options 模式,图层处理通过统一的 ImageProcessor 协议和 ImageProcessItem 双态输入抽象,便于扩展与组合。

2. 技术演进与版本脉络

Kingfisher 的图层处理能力随版本逐步增强,并与缓存、下载模块解耦清晰。

阶段 版本/时期 图层处理与相关能力
早期 3.x 基础下载与缓存,简单图片处理
缓存与处理器 3.10 带 ImageProcessor 的缓存策略:先查已处理图,若无再查原图,避免重复下载 [3]
架构升级 5.0 MemoryStorage / DiskStorage 分离,可缓存原始 Data,完善 KingfisherError,处理管线与缓存键绑定 [4]
下采样修复 5.3 下采样 scale 与内存表现修复:从原图加载下采样结果时的 scale 与内存问题 [5]
动图与序列化 7.8 磁盘缓存取回动图时正确使用请求中的 processor [6]
渐进式 JPEG 8.3 SwiftUI KFImage 支持 progressiveJPEG 修饰符 [7]

5.0 是重要分水岭:处理管线与缓存键(含 processorIdentifier)深度结合,使「同一 URL + 不同 Processor」对应不同缓存条目,原图与处理后图可并存。

3. 图层处理在整体架构中的位置

下图概括从「资源(URL / ImageDataProvider)」到「显示到视图」的流程,并标出 ImageProcessor 所在阶段。

flowchart LR
    subgraph 输入
        A[URL / ImageDataProvider]
    end
    subgraph 获取数据
        B[ImageDownloader / Provider.data]
    end
    subgraph 处理层
        C[Data]
        D[ImageProcessor 管线]
        E[KFCrossPlatformImage]
    end
    subgraph 缓存与输出
        F[ImageCache]
        G[ImageView / KFImage]
    end
    A --> B --> C --> D --> E --> F --> G

要点

  • ImageProcessor 的输入可以是 Data(未解码)或 Image(已解码);输出为 Image。因此它同时覆盖「Data → Image」(如 DefaultImageProcessor、DownsamplingImageProcessor)和「Image → Image」(如 RoundCorner、Blur、Resizing)两类操作。
  • 处理在 KingfisherManager 协调下、通常在后台队列执行,避免阻塞主线程,符合 Apple 图像最佳实践 [8]。

二、图像处理管线(ImageProcessor Pipeline)

1. ImageProcessItem 与双态输入

Kingfisher 用 ImageProcessItem 表示处理器的输入,有两种情况 [9]:

public enum ImageProcessItem: Sendable {
    /// 已解码的图像,处理器在其上做几何/像素变换
    case image(KFCrossPlatformImage)
    /// 原始数据,处理器需负责解码(或解码+变换)
    case data(Data)
}

设计意图

  • 统一接口:同一套管线既可处理「仅解码」(Data → Image),也可处理「仅变换」(Image → Image),或「解码 + 变换」(Data 经多个 Processor 最终得到 Image)。
  • 避免重复解码:当管线中第一个 Processor 已将 Data 转为 Image 后,后续 Processor 收到 .image(...),只需做几何/滤镜等操作,无需再次解码。

数据流概念

flowchart LR
    subgraph 管线输入
        I[Data]
    end
    subgraph P1[Processor 1]
        I --> D1[解码/下采样]
        D1 --> O1[Image]
    end
    subgraph P2[Processor 2]
        O1 --> D2[圆角/缩放等]
        D2 --> O2[Image]
    end
    O2 --> Out[输出]

2. ImageProcessor 协议与标识符

ImageProcessor 协议是 Kingfisher 图层处理的核心抽象 [9][10]:

协议 ImageProcessor:
    属性 identifier: String   // 唯一标识,参与缓存键
    方法 process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
  • identifier:相同功能/参数的 Processor 应返回相同字符串,用于缓存键。官方建议使用反向域名(如 com.onevcat.Kingfisher.RoundCornerImageProcessor(20)),且不要与 DefaultImageProcessor"" 冲突。
  • process:返回 nil 表示处理失败,管线会报错并中止;若输入已是 .image 且当前步骤可透传,可返回原图以继续后续 Processor。

伪代码:管线执行

函数 runPipeline(item: ImageProcessItem, processors: [ImageProcessor], options) -> Image?:
    current = item
    对每个 p in processors:
        若 current 为 .data 且 p 只支持 .image:
            current = .image(DefaultImageProcessor.default.process(current, options))
        若 current 为 nil: 返回 nil
        next = p.process(current, options)
        若 next 为 nil: 返回 nil
        current = .image(next)
    返回 current

许多内置 Processor(如 RoundCorner、Blur)在收到 .data 时,会先通过 DefaultImageProcessor.default |> self 将 Data 解码为 Image,再对 Image 做自身变换,从而复用同一套协议。

3. 下采样(Downsampling)与 Resizing 的区分

Kingfisher 明确区分两种「变小」的方式,对应不同的内存与 CPU 成本 [10][11]。

3.1 DownsamplingImageProcessor

  • 输入:仅 Data(压缩数据)。在解码阶段直接生成小尺寸位图,而不是先解码全图再缩放。
  • 实现:基于 ImageIO 的 CGImageSourceCreateThumbnailAtIndex,通过 kCGImageSourceThumbnailMaxPixelSize 等选项限制最大边长,在解码器内部只生成缩略图级像素缓冲。
  • 优势:内存占用与目标尺寸相关,避免「先全图解码」的峰值;大图列表、头像等场景推荐使用。

下采样算法步骤(与 Kingfisher / ImageIO 语义一致)

函数 Downsample(data: Data, size: CGSize) -> Image?:
    1. maxDimensionInPixels = max(size.width, size.height) * scale
    2. source = CGImageSourceCreateWithData(data, nil)
    3. options = {
         kCGImageSourceCreateThumbnailFromImageAlways: true,
         kCGImageSourceCreateThumbnailWithTransform: true,
         kCGImageSourceShouldCacheImmediately: true,
         kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
       }
    4. cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options)
    5. 由 cgImage 构造 UIImage/NSImage 并返回

注意size 不能为 (0, 0),否则会触发 "Processing image failed. Processor: DownsamplingImageProcessor" [11];在列表 cell 中应使用 cell 或目标视图的 bounds 计算合理 size。

3.2 ResizingImageProcessor

  • 输入:一般为 Image(或通过 DefaultImageProcessor 先解码的 Data)。对已解码的位图做缩放,支持 ContentMode(如 aspectFit、aspectFill)。
  • 实现:在像素缓冲上做几何变换(绘制到目标尺寸),会先占用全图解码的内存,再产生缩放后的新缓冲。
  • 适用:已解码图、或必须对 Image 做精确尺寸/比例控制时使用;若从 Data 缩小,应优先 DownsamplingImageProcessor

对比小结

维度 DownsamplingImageProcessor ResizingImageProcessor
输入 Data Image(或 Data 经 Default 解码)
时机 解码时直接出小图 先解码全图再缩放
内存 与目标尺寸相关 先有全图峰值再缩放
典型场景 列表缩略图、头像 已解码图的尺寸/比例调整

4. 多处理器链式组合

Kingfisher 支持将多个 ImageProcessor 串联成一条管线,按顺序执行:前一个的输出作为后一个的输入(.image(...))[10]。

组合方式:通过 append(another:)|> 运算符(Kingfisher 在 ImageProcessor 扩展中定义 |> 为调用 append(another:)):

// 先模糊,再圆角
let processor = BlurImageProcessor(blurRadius: 4) |> RoundCornerImageProcessor(cornerRadius: 20)
imageView.kf.setImage(with: url, options: [.processor(processor)])

组合后的 identifier"\(p1.identifier)|>\(p2.identifier)",用于缓存键,保证「同一 URL + 同一处理器链」唯一对应一条缓存。

链式执行语义(伪代码)

函数 GeneralProcessor.process(item, options):
    image1 = self.process(item, options)
    若 image1 为 nil: 返回 nil
    返回 another.process(.image(image1), options)

因此,若链中第一个 Processor 能处理 .data(如 DefaultImageProcessor 或 DownsamplingImageProcessor),后续 Processor 将始终收到 .image(...)


三、解码、缓存与处理器的协同

1. 检索流程与缓存键

Kingfisher 的检索顺序可概括为 [2][3]:

  1. 使用 cacheKey + processorIdentifier内存缓存
  2. 若未命中,查磁盘缓存(同样 key + processorIdentifier);
  3. 若仍未命中,通过 ImageDownloaderImageDataProvider 获取 Data;
  4. 对 Data 执行 ImageProcessor 管线,得到 Image;
  5. 将结果写入内存与磁盘缓存,并交给视图或完成回调。

缓存键:缓存的唯一标识是 cacheKey + processorIdentifier(DefaultImageProcessor 的 identifier 为空字符串)。因此:

  • 同一 URL,不同 Processor(或不同链)会得到不同缓存条目
  • 原图(DefaultImageProcessor)与下采样/圆角等版本可并存
  • 判断或读取缓存时若请求中指定了非 Default 的 Processor,需传入相同 processorIdentifier,例如:cache.isCached(forKey: cacheKey, processorIdentifier: processor.identifier)cache.retrieveImage(forKey: cacheKey, options: [.processor(processor)], ...)
flowchart TD
    A[请求: URL + Processor] --> B[构造 cacheKey + processorIdentifier]
    B --> C{内存缓存?}
    C -->|命中| D[返回 Image]
    C -->|未命中| E{磁盘缓存?}
    E -->|命中| F[解码/反序列化]
    F --> D
    E -->|未命中| G[下载 / Provider]
    G --> H[Processor 管线]
    H --> I[写内存+磁盘]
    I --> D

2. CacheSerializer 与磁盘格式

CacheSerializer 负责「Image ↔ Data」在磁盘缓存中的序列化与反序列化 [10]:

  • 存储data(with:original:),将当前要缓存的 Image 转为 Data(可结合 original Data 决定格式);
  • 读取image(with:options:),将磁盘上的 Data 转回 Image。

调用时机(便于理解与扩展):

  • Processor.process:① 网络下载成功或 ImageDataProvider 返回 Data 后,将 Data 加工为 Image;② 从磁盘读取到原始 Data 后,先经 CacheSerializer 反序列化为 Image,再经 Processor 处理(若请求中指定了 Processor)。因此磁盘命中「已处理图」时直接返回,命中「原图」时会再走一次 Processor。
  • CacheSerializer.image:从磁盘读取到 Data 后,用于将 Data 反序列化为 Image。
  • CacheSerializer.data:需要写入磁盘时,将 Image 序列化为 Data 再落盘。

默认行为:尽量保持原始数据格式(如 JPEG 仍存为 JPEG)。但当使用 RoundCornerImageProcessor 等会引入透明通道的处理器时,若原图是 JPEG(无透明通道),直接按 JPEG 存会丢失圆角透明区域。此时可指定 FormatIndicatedCacheSerializer.png,强制以 PNG 缓存处理后的图像:

imageView.kf.setImage(with: url,
    options: [.processor(RoundCornerImageProcessor(cornerRadius: 20)),
              .cacheSerializer(FormatIndicatedCacheSerializer.png)])

3. 内置 Processor 一览

Processor 输入偏好 功能
DefaultImageProcessor Data / Image Data→Image 解码,或 Image 按 scale 缩放
DownsamplingImageProcessor Data 解码时下采样,限制最大尺寸
ResizingImageProcessor Image 按 referenceSize + ContentMode 缩放
RoundCornerImageProcessor Image 圆角(可指定角、背景色、目标尺寸)
CroppingImageProcessor Image 按 size + anchor 裁剪
BlurImageProcessor Image 高斯模糊(Accelerate)
TintImageProcessor / OverlayImageProcessor Image 着色 / 叠色
ColorControlsProcessor / BlackWhiteProcessor Image 亮度对比度饱和度 / 黑白
BorderImageProcessor Image 加边框
BlendImageProcessor (iOS) / CompositingImageProcessor (macOS) Image 混合模式

4. 应用场景与选型

场景 推荐 Processor 说明
列表/表格缩略图 DownsamplingImageProcessor(size:) 从 Data 直接下采样,控制内存;size 取 cell 或目标尺寸
头像/圆角 RoundCornerImageProcessor 可配合 .png serializer 保留透明圆角
占位/毛玻璃 BlurImageProcessor 基于 Accelerate 的高斯模糊
统一尺寸且需等比 ResizingImageProcessor(referenceSize, mode: .aspectFit) 对已解码图做缩放
多步效果 链式:e.g. Blur |> RoundCorner 顺序决定最终效果与缓存键

RoundCornerImageProcessor 指定圆角:除四角统一圆角外,可指定部分角,如仅左上与右下:RoundCornerImageProcessor(cornerRadius: 20, roundingCorners: [.topLeft, .bottomRight])


四、类结构图分析

1. 核心类总览

Kingfisher 的类可按职责分为:入口与协调加载缓存处理管线视图扩展 五类。下表给出核心类/协议及其职责。

模块 核心类 / 协议 职责简述
协调 KingfisherManager 统一入口:协调 ImageDownloader、ImageCache、ImageProcessor 管线,执行「查缓存 → 下载/Provider → 处理 → 写缓存 → 回调」
加载 ImageDataProvider (协议) 定义数据来源接口:根据 URL 或资源标识返回 Data(如 Base64ImageDataProviderLocalFileImageDataProvider
ImageDownloader 默认网络加载:基于 URLSession 下载,支持并发、取消、RequestModifier、SessionDelegate
ImageDownloaderOperation 单次下载任务,封装 URLSessionTask
缓存 ImageCache 内存 + 磁盘二级缓存,提供 retrieve/store/remove,key 含 cacheKey + processorIdentifier
MemoryStorage / DiskStorage 5.0+ 内存层、磁盘层具体实现,可配置 count/cost 限制与过期策略
处理管线 ImageProcessor (协议) 定义 process(item:options:) -> KFCrossPlatformImage?,输入为 ImageProcessItem(.data / .image)
ImageProcessItem (枚举) 双态输入:.data(Data).image(KFCrossPlatformImage),统一「仅解码」「仅变换」「解码+变换」
DefaultImageProcessor / DownsamplingImageProcessor / RoundCornerImageProcessor 内置 Processor 实现,支持 ` >` 链式组合
CacheSerializer (协议) 磁盘格式:Image ↔ Data 序列化/反序列化,如 FormatIndicatedCacheSerializer.png
视图 KingfisherWrapper + ImageView.kf 为 UIImageView/NSImageView 等提供 kf.setImage(with:options:...)kf.cancelDownloadTask()
KFImage (SwiftUI) SwiftUI 图片组件,支持 URL、Processor、progressiveJPEG 等
ImagePrefetcher 预取多张图片,可配合 UICollectionView 的 prefetch

2. 模块划分与依赖关系

下图从「模块」维度表示各层之间的依赖方向:视图扩展与 Prefetcher 依赖 KingfisherManager,Manager 依赖 Downloader/Cache,处理管线在 Manager 内执行(Processor 链与 CacheSerializer 参与缓存键与磁盘格式)。

flowchart TB
    subgraph 视图层
        V1[ImageView.kf / KFImage]
        V2[ImagePrefetcher]
    end
    subgraph 协调层
        M[KingfisherManager]
    end
    subgraph 加载层
        L[ImageDownloader]
        P[ImageDataProvider 实现]
    end
    subgraph 缓存层
        C[ImageCache]
    end
    subgraph 处理管线层
        IP[ImageProcessor 实现]
        CS[CacheSerializer]
    end
    V1 --> M
    V2 --> M
    M --> L
    M --> P
    M --> C
    M --> IP
    M --> CS

3. 加载与缓存类结构

ImageDownloader 负责从网络获取 Data;ImageDataProvider 可提供本地或自定义 Data;ImageCache 负责内存与磁盘的读写。KingfisherManager 持有 cache 与 downloader,在单次请求中先查缓存(key = cacheKey + processorIdentifier),未命中再通过 downloader 或 provider 取数据,经 Processor 管线后写回缓存。

classDiagram
    class KingfisherManager {
        -cache: ImageCache
        -downloader: ImageDownloader
        +retrieveImage(with:options:progressBlock:completionHandler:)
        -loadAndCacheImage(source:options:completionHandler:)
    }
    class ImageCache {
        -memoryStorage: MemoryStorage
        -diskStorage: DiskStorage
        +retrieveImage(forKey:options:callbackQueue:completionHandler:)
        +store(_:forKey:options:toDisk:completionHandler:)
        +removeImage(forKey:fromMemory:fromDisk:completionHandler:)
    }
    class ImageDownloader {
        -session: URLSession
        -downloadQueue: OperationQueue
        +downloadImage(with:options:completionHandler:)
    }
    class ImageDataProvider {
        <<protocol>>
        +data(handler:)
        +cacheKey
    }
    KingfisherManager --> ImageCache : 使用
    KingfisherManager --> ImageDownloader : 使用
    KingfisherManager ..> ImageDataProvider : 支持 Source.provider
  • KingfisherManager:对外通过 retrieveImage(with:...) 接收 Source(.network(URL) 或 .provider(ImageDataProvider)),先查 ImageCache(key 含 processorIdentifier),未命中则调 downloader 或 provider 取 Data,再跑 Processor 管线并写回缓存。
  • ImageCache:5.0+ 将内存与磁盘拆为 MemoryStorage / DiskStorage,可配置 count/cost、过期时间;存储时由 CacheSerializer 决定 Image → Data 的格式(如 PNG 保留圆角透明)。
  • ImageDownloader:基于 URLSession,单次下载封装为 ImageDownloaderOperation,支持并发数、超时、RequestModifier;与 Provider 一起构成「数据来源」的两种方式。

4. 处理管线与 Processor 类结构

ImageProcessor 协议是图层处理的核心:输入为 ImageProcessItem(.data 或 .image),输出为 KFCrossPlatformImage。Manager 在「取得 Data 后」按 options 中的 processor(或链)依次执行;链的 identifier 拼接后参与缓存键,实现「同一 URL + 不同 Processor」对应不同缓存条目。

classDiagram
    class KingfisherManager {
        -runProcessors(_:data:options:)
    }
    class ImageProcessItem {
        <<enumeration>>
        +image(KFCrossPlatformImage)
        +data(Data)
    }
    class ImageProcessor {
        <<protocol>>
        +identifier: String
        +process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo): KFCrossPlatformImage?
    }
    class DefaultImageProcessor {
        +process(item:options:)
    }
    class DownsamplingImageProcessor {
        +size: CGSize
        +process(item:options:)
    }
    class RoundCornerImageProcessor {
        +cornerRadius: CGFloat
        +process(item:options:)
    }
    class ImageProcessorGroup {
        -processors: [ImageProcessor]
        +append(another:)
        +identifier
    }
    class CacheSerializer {
        <<protocol>>
        +data(with:original:)
        +image(with:options:)
    }
    ImageProcessItem --> ImageProcessor : 输入
    ImageProcessor <|.. DefaultImageProcessor : 实现
    ImageProcessor <|.. DownsamplingImageProcessor : 实现
    ImageProcessor <|.. RoundCornerImageProcessor : 实现
    ImageProcessor <|.. ImageProcessorGroup : 链式组合
    KingfisherManager ..> ImageProcessor : 执行管线
    KingfisherManager ..> CacheSerializer : 磁盘序列化
  • ImageProcessItem:双态设计使同一管线既可处理「Data → Image」(解码/下采样),也可处理「Image → Image」(圆角、模糊、缩放),或混合链式处理;收到 .data 的 Processor 可先通过 DefaultImageProcessor.default |> self 解码再变换。
  • ImageProcessor 链:通过 append(another:)|> 组合,链的 identifier 为各子 Processor identifier 用 "|>" 拼接,参与缓存键;执行时前一个输出作为后一个的 .image(...) 输入。
  • CacheSerializer:磁盘存储时由 data(with:original:) 将 Image 转为 Data,读取时由 image(with:options:) 反序列化;圆角等带透明通道的结果可选用 FormatIndicatedCacheSerializer.png 避免 JPEG 丢失透明。

5. View 扩展与调用链

视图扩展(如 ImageView.kf、SwiftUI 的 KFImage)是业务最常接触的入口:内部将 Resource(URL 或 ImageDataProvider)、placeholder、options 交给 KingfisherManager,并把返回的 DownloadTask 与 view 关联,以便在复用时取消。

sequenceDiagram
    participant V as ImageView
    participant KF as ImageView.kf
    participant M as KingfisherManager
    participant C as ImageCache
    participant D as ImageDownloader

    V->>KF: setImage(with: url, options: [.processor(...)])
    KF->>KF: cancelDownloadTask()
    KF->>M: retrieveImage(with: .network(url), options: ...)
    M->>C: retrieveImage(forKey: cacheKey+processorIdentifier)
    alt 缓存命中
        C-->>M: image (memory/disk)
        M-->>KF: completion(image, .memory/.disk)
    else 未命中
        M->>D: downloadImage(with: url, ...)
        D-->>M: data
        M->>M: Processor 管线处理
        M->>C: store(image, forKey: ...)
        M-->>KF: completion(image, .none)
    end
    KF->>V: imageView.image = image
  • kf.setImage(with: placeholder: options: progressBlock: completionHandler:):先对当前 view 取消未完成的 DownloadTask,再调 KingfisherManager.shared.retrieveImage(with: source, options: options, ...);在 completion 中把得到的 image 赋给 imageView.image(并可选执行 transition)。
  • kf.cancelDownloadTask():取消与该 view 绑定的任务,避免 cell 复用时旧请求覆盖新图。
  • KFImage (SwiftUI):通过 KFImage 传入 url、processor、placeholder 等,内部同样走 KingfisherManager,支持 progressiveJPEG(8.3+)等选项。

将上述「核心类总览」「模块依赖」「加载与缓存类图」「Processor 与管线类图」「View 调用链」串联起来,即可形成对 Kingfisher 类结构图 的完整分析:入口在视图扩展(kf / KFImage),核心协调在 KingfisherManager,加载(Downloader/Provider)、缓存(ImageCache + CacheSerializer)、处理(ImageProcessor + ImageProcessItem)均为协议导向的可插拔设计,便于扩展与测试。


五、与系统及业界实践的衔接

1. Apple 图像与图形最佳实践

Apple 在 WWDC 2018「Image and Graphics Best Practices」[8] 中强调:

  • 在后台线程解码与下采样,避免主线程卡顿;
  • 解码时即做下采样,使解码后缓冲与显示尺寸匹配,降低内存峰值;
  • 预取:在列表等场景提前准备即将显示的图像。

Kingfisher 的 DownsamplingImageProcessor 直接对应「解码时下采样」;处理管线在 KingfisherManager 的队列中执行,满足「后台处理」;配合 ImagePrefetcherUICollectionViewDataSourcePrefetching 等可实现预取 [10]。与 SDWebImage 类似,其设计与此类最佳实践一致。

2. 与 SDWebImage 的对比

维度 Kingfisher SDWebImage
语言 纯 Swift Objective-C 为主,Swift 接口
处理抽象 ImageProcessor + ImageProcessItem SDImageTransformer
输入类型 .image / .data 双态 一般为已解码 Image
下采样 DownsamplingImageProcessor(Data→Image) 解码管线内缩略图/limitBytes
链式组合 append / |>,identifier 拼接 SDImagePipelineTransformer 数组
缓存键 cacheKey + processorIdentifier 含 transformer 信息
渐进式 8.3+ KFImage progressiveJPEG Progressive Coder 体系

二者都遵循「解码/下采样 + 变换 + 缓存」的管线思想,Kingfisher 通过 ImageProcessItem 将「解码」与「变换」统一进同一协议,便于从 Data 直接到最终 Image 的一体化处理。

3. 动图加载(GIF)与 AnimatedImageView

Kingfisher 加载 GIF 的两种方式:UIImageViewAnimatedImageView(继承自 UIImageView),调用方式相同,内部行为不同 [12]。

  • UIImageViewshouldPreloadAllAnimation() 扩展返回 true,即 preloadAllAnimationData 被设为 true,GIF 会先解码为所有帧的 UIImage 数组,再通过 UIImage.animatedImage(with:duration:) 展示。适合帧数少的动图。
  • AnimatedImageView:重写 shouldPreloadAllAnimation() 返回 false,不预加载全部帧;通过关联的 CGImageSourceAnimator 按需解码(默认仅预加载前若干帧),用 CADisplayLink 在每帧刷新时更新 layer.contents(重写 display(_ layer:))。更省内存,CPU 略高。

AnimatedImageView 独有runLoopModebackgroundDecodeframePreloadCountautoPlayAnimatedImagerepeatCount 等。若需在列表或详情中播放 GIF 且控制内存,建议使用 AnimatedImageView


六、设计模式与编程思想

1. 设计模式应用

Kingfisher 在架构上大量运用经典设计模式,与纯 Swift、协议导向的风格结合,使扩展与维护成本可控。

模式 在 Kingfisher 中的体现 作用
外观 / 门面(Facade) KingfisherManager 对外提供 retrieveImage(with:options:progressBlock:completionHandler:),内部协调 ImageDownloader、ImageCache、ImageProcessor 管线,调用方无需关心多级缓存与处理顺序 简化使用、隐藏复杂度
策略(Strategy) ImageProcessorCacheSerializerImageDataProvider 均为协议,多种实现可替换(RoundCorner、Downsampling、FormatIndicatedCacheSerializer 等),通过 KingfisherOptionsInfo 传入 算法/行为可插拔,易扩展新处理与存储格式
责任链 / 管道(Chain of Responsibility / Pipeline) ImageProcessor 通过 append(another:)|> 串联成管线;ImageProcessItem 双态(.data / .image)使「解码 → 变换」在同一链中顺序执行 多步处理顺序清晰,便于组合与复用
单例 + 共享依赖(Singleton) KingfisherManager.sharedImageCache.defaultImageDownloader.default 提供默认实例,同时 retrieveImage 等 API 支持传入自定义 cache、downloader,打破单例绑定 全局统一入口,又保留可测试性与多实例能力
观察者 / 回调(Observer / Callback) 通过 progressBlockcompletionHandler 闭包通知进度与结果;Swift 并发下也可用 async/await 异步结果与 UI 解耦
组合 / 装饰(Composite) 多个 ImageProcessor 通过 |> 组合成新 Processor,其 identifier 为子 Processor 的 identifier 拼接,对外仍满足同一 ImageProcessor 协议 链式处理器可当作单一策略使用,参与缓存键一致

类图关系(概念层)

classDiagram
    class KingfisherManager {
        -cache: ImageCache
        -downloader: ImageDownloader
        +retrieveImage(with:options:progressBlock:completionHandler:)
    }
    class ImageCache {
        +retrieveImage(forKey:options:callbackQueue:completionHandler:)
        +store(_:forKey:options:toDisk:completionHandler:)
    }
    class ImageDownloader {
        +downloadImage(with:options:completionHandler:)
    }
    class ImageProcessor {
        <<protocol>>
        +identifier: String
        +process(item: ImageProcessItem, options:): KFCrossPlatformImage?
    }
    class ImageDataProvider {
        <<protocol>>
        +data(handler:)
        +cacheKey
    }
    KingfisherManager --> ImageCache : 使用
    KingfisherManager --> ImageDownloader : 使用
    KingfisherManager ..> ImageProcessor : 处理时选用
    KingfisherManager ..> ImageDataProvider : Source.provider

2. 编程思想精华

Kingfisher 的编程思想可提炼为以下几点,对理解与模仿其设计很有帮助。

2.1 协议导向与「可替换实现」

  • ImageProcessorCacheSerializerImageDataProvider 均以协议呈现,具体实现可替换、可组合。
  • 新增一种图像处理或一种磁盘格式,只需实现对应协议并通过 options 传入(如 .processor(...).cacheSerializer(...)),无需改动 KingfisherManager 核心流程。这体现了开闭原则:对扩展开放,对修改关闭。

2.2 管线化与单一职责

  • 把「从 Source 到屏幕」拆成:获取数据(Downloader/Provider)→ Processor 管线(解码/下采样 + 变换)→ 缓存 → 展示,每一步只做一件事。
  • Processor 只关心 ImageProcessItem → Image,Cache 只关心存储与查找,Downloader 只关心网络 Data。单一职责使每块可独立测试、优化和扩展;管线化则使数据流清晰,便于加日志与监控。

2.3 双态输入与「解码+变换」统一

  • ImageProcessItem.data / .image 双态设计,使同一 ImageProcessor 协议既能表达「Data → Image」(如 Default、Downsampling),也能表达「Image → Image」(如 RoundCorner、Blur),还能通过链式组合在一次管线中完成解码与多步变换。
  • 避免「解码器」与「变换器」两套抽象,降低概念数量,便于链式组合与缓存键一致(整条链一个 identifier 串)。

2.4 缓存键与「同一资源多形态」

  • 通过 cacheKey + processorIdentifier 的设计,同一 URL 可以对应「原图」「下采样图」「圆角图」等多条缓存,避免重复下载,又满足不同场景对尺寸/形态的需求。这体现了用键设计表达业务差异的思想。

2.5 后台处理与主线程回调

  • 下载、Processor 管线、磁盘 I/O 均在后台队列执行,completionHandler 通过 CallbackQueue.mainAsync 等派发到主线程,兼顾性能与 UI 安全。这是移动端异步加载库的通用范式:重活放后台,结果回主线程

2.6 取消与生命周期绑定

  • 视图扩展(如 ImageView.kf)会把「当前正在进行的 DownloadTask」与 view 绑定,当对同一 view 发起新请求时先取消旧任务,避免错位和浪费。这体现了生命周期与请求绑定的思想,在列表 cell 复用时尤为重要。

2.7 配置通过 Options 透传

  • 不通过全局单例属性堆砌配置,而是通过 KingfisherOptionsInfo(如 .processor.cacheSerializer.callbackQueue)在单次请求中传入,使「同一 App 内不同页面/模块」可使用不同 Processor 与缓存策略,且易于单元测试时注入 mock。

Kingfisher 编程思想精华一览

思想 在框架中的体现
协议导向、可替换 ImageProcessor / CacheSerializer / ImageDataProvider 协议化,新处理、新格式仅需实现协议并通过 options 传入
管线化、单一职责 获取数据 → Processor 管线 → 缓存 → 展示,每步职责单一,便于扩展与测试
双态输入、解码+变换统一 ImageProcessItem(.data / .image) + 链式 Processor,一条管线完成解码与多步变换,identifier 参与缓存键
键设计表达多形态 同一 URL 通过 cacheKey + processorIdentifier 支持原图、下采样图、圆角图等多条缓存
后台处理、主线程回调 重 CPU/IO 在后台队列,completion 回主线程(CallbackQueue),兼顾性能与 UI 安全
生命周期绑定取消 View 与 DownloadTask 绑定,新请求自动取消旧请求,避免列表错位
Options 透传配置 单次请求级 options(processor、cacheSerializer、callbackQueue 等),避免全局状态,利于多策略并存与测试注入

七、使用示例与最佳实践

1. 基础加载与圆角

let processor = RoundCornerImageProcessor(cornerRadius: 20)
imageView.kf.setImage(with: url, options: [.processor(processor)])

2. 列表缩略图(下采样)

let size = imageView.bounds.size
let processor = DownsamplingImageProcessor(size: size)
imageView.kf.setImage(with: url, options: [.processor(processor)])
// 注意:size 不可为 .zero

3. 多处理器链与强制 PNG 缓存

let processor = BlurImageProcessor(blurRadius: 4) |> RoundCornerImageProcessor(cornerRadius: 20)
imageView.kf.setImage(with: url, options: [
    .processor(processor),
    .cacheSerializer(FormatIndicatedCacheSerializer.png)
])

4. 自定义 Processor(仅做示意)

struct MyProcessor: ImageProcessor {
    let identifier = "com.example.myprocessor"
    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
        switch item {
        case .image(let image): return image // 或对 image 做变换
        case .data(let data): return DefaultImageProcessor.default.process(item: item, options: options)
        }
    }
}

5. 预取与列表

// 配合 UICollectionViewDataSourcePrefetching
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
    let urls = indexPaths.compactMap { URL(string: model(at: $0).imageURL) }
    ImagePrefetcher(urls: urls).start()
}

6. Cell 完整示例(复用、下采样、进度与完成回调)

列表 Cell 中需在 prepareForReuse 中取消任务并清空,在 configure 中按目标尺寸下采样并可选显示进度。

class PhotoCell: UITableViewCell {
    static let reuseId = "PhotoCell"
    @IBOutlet weak var photoImageView: UIImageView!
    @IBOutlet weak var progressView: UIProgressView!

    override func prepareForReuse() {
        super.prepareForReuse()
        photoImageView.kf.cancelDownloadTask()
        photoImageView.image = nil
        progressView.progress = 0
        progressView.isHidden = true
    }

    func configure(with url: URL) {
        let size = photoImageView.bounds.size
        let processor = DownsamplingImageProcessor(size: size.isEmpty ? CGSize(width: 120, height: 120) : size)
        photoImageView.kf.setImage(
            with: url,
            placeholder: UIImage(named: "placeholder"),
            options: [.processor(processor), .scaleFactor(UIScreen.main.scale)],
            progressBlock: { [weak self] received, total in
                guard let self = self, total > 0 else { return }
                DispatchQueue.main.async {
                    self.progressView.isHidden = false
                    self.progressView.progress = Float(received) / Float(total)
                }
            },
            completionHandler: { [weak self] result in
                DispatchQueue.main.async {
                    self?.progressView.isHidden = true
                    if case .failure = result { /* 可设置失败占位图 */ }
                }
            }
        )
    }
}

7. UIButton 设置网络图片

为 UIButton 的不同 state 设置网络图片,可配合 Processor 与完成回调。

// 设置 normal / highlighted 等状态的图片
button.kf.setImage(with: url, for: .normal, placeholder: UIImage(named: "btn_placeholder"))
button.kf.setImage(with: highlightedURL, for: .highlighted)
button.kf.setBackgroundImage(with: backgroundURL, for: .normal)

// 带圆角与完成回调
let processor = RoundCornerImageProcessor(cornerRadius: 8)
button.kf.setImage(
    with: url,
    for: .normal,
    placeholder: nil,
    options: [.processor(processor), .cacheSerializer(FormatIndicatedCacheSerializer.png)],
    completionHandler: { result in
        if case .failure = result { print("加载失败") }
    }
)

8. 占位图、进度与过渡动画

使用占位图、下载进度条,并在图片加载完成后执行淡入等过渡动画。

imageView.kf.setImage(
    with: url,
    placeholder: UIImage(named: "placeholder"),
    options: [
        .transition(ImageTransition.fade(0.3)),
        .retryFailed
    ],
    progressBlock: { [weak progressView] received, total in
        guard let pv = progressView, total > 0 else { return }
        DispatchQueue.main.async {
            pv.progress = Float(received) / Float(total)
            pv.isHidden = false
        }
    },
    completionHandler: { [weak progressView] result in
        DispatchQueue.main.async {
            progressView?.isHidden = true
            if case .failure = result { /* 可显示失败占位或提示 */ }
        }
    }
)

9. 自定义缓存键与请求修饰(RequestModifier)

同一 URL 在不同业务下需要不同缓存键时,可通过 KingfisherOptionsInfo 传入自定义 cacheKey;需要鉴权或自定义 Header 时使用 ImageDownloadRequestModifier

// 自定义缓存键:列表用 thumb key、详情用原图 key
let listResource = ImageResource(downloadURL: url, cacheKey: "list_\(url.absoluteString)")
let detailResource = ImageResource(downloadURL: url, cacheKey: "detail_\(url.absoluteString)")
listImageView.kf.setImage(with: listResource, options: [.processor(DownsamplingImageProcessor(size: thumbSize))])
detailImageView.kf.setImage(with: detailResource)

// 请求修饰:Header、Token、超时
struct AuthModifier: ImageDownloadRequestModifier {
    let token: String
    func modified(for request: URLRequest) -> URLRequest? {
        var r = request
        r.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        r.setValue("image/webp,image/*,*/*;q=0.8", forHTTPHeaderField: "Accept")
        return r
    }
}
imageView.kf.setImage(with: url, options: [.requestModifier(AuthModifier(token: userToken))])

10. 缓存查询与手动存储

不经过视图加载流程,直接使用 ImageCache 查询、存储或移除缓存。

let cache = ImageCache.default
let key = url.absoluteString  // 或自定义 cacheKey(与 Processor 组合时由框架自动拼接 processorIdentifier)

// 查询是否已缓存
cache.imageCachedType(forKey: key) { result in
    switch result {
    case .success(let cached):
        switch cached {
        case .none:   print("未缓存")
        case .memory: print("在内存")
        case .disk:   print("在磁盘")
        }
    case .failure: break
    }
}

// 从缓存读取(不触发下载)
cache.retrieveImage(forKey: key, options: nil) { result in
    switch result {
    case .success(let value):
        if let image = value.image { imageView.image = image }
    case .failure: break
    }
}

// 手动写入缓存(如本地生成或从相册来的图)
cache.store(image, forKey: key, options: nil, toDisk: true) { _ in }

11. 自定义 Processor 完整示例(加边框)

实现 ImageProcessor 协议,对已解码图像做自定义绘制(如加灰色边框)。

struct GrayBorderProcessor: ImageProcessor {
    let identifier = "com.example.grayborder(\(borderWidth))"
    let borderWidth: CGFloat

    init(borderWidth: CGFloat = 2) { self.borderWidth = borderWidth }

    func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
        switch item {
        case .image(let image):
            let size = image.size
            let renderer = UIGraphicsImageRenderer(size: size)
            return renderer.image { ctx in
                image.draw(at: .zero)
                UIColor.gray.setStroke()
                let rect = CGRect(origin: .zero, size: size).insetBy(dx: borderWidth/2, dy: borderWidth/2)
                ctx.stroke(rect, with: .color(.gray), lineWidth: borderWidth)
            }
        case .data:
            return DefaultImageProcessor.default.process(item: item, options: options)
        }
    }
}

// 使用
imageView.kf.setImage(with: url, options: [.processor(GrayBorderProcessor(borderWidth: 3))])

12. SwiftUI KFImage 与 async/await

在 SwiftUI 中使用 KFImage,并配合渐进式 JPEG、占位与异步加载。

// 基础用法
KFImage(url)
    .placeholder { ProgressView() }
    .fade(duration: 0.25)
    .resizable()
    .aspectRatio(contentMode: .fit)

// 带 Processor 与圆角
KFImage(url)
    .setProcessor(RoundCornerImageProcessor(cornerRadius: 12))
    .placeholder { Color.gray.opacity(0.2) }
    .cacheSerializer(FormatIndicatedCacheSerializer.png)

// 8.3+ 渐进式 JPEG
KFImage(url)
    .progressiveJPEG(ImageProgressive(isBlur: true, isFastestScan: true, scanInterval: 0.1))

// 使用 async/await(Kingfisher 提供的异步 API)
Task {
    let result = await KingfisherManager.shared.retrieveImage(with: url)
    if case .success(let value) = result {
        await MainActor.run { imageView.image = value.image }
    }
}

13. ImageDataProvider(本地与 Base64)用法

不依赖网络 URL 时,可用 ImageDataProvider 从本地文件或 Base64 字符串加载,并同样走缓存与 Processor 管线。

// 本地文件
let fileURL = Bundle.main.url(forResource: "avatar", withExtension: "jpg")!
let provider = LocalFileImageDataProvider(fileURL: fileURL)
imageView.kf.setImage(with: provider)

// Base64 数据(如接口返回的 data URL)
let base64String = "data:image/png;base64,iVBORw0KGgo..."
if let provider = Base64ImageDataProvider(base64String: base64String, cacheKey: "custom_key") {
    imageView.kf.setImage(with: provider, options: [.processor(RoundCornerImageProcessor(cornerRadius: 10))])
}

// 自定义 Provider:从相册、加密存储等获取 Data
struct MyImageDataProvider: ImageDataProvider {
    var cacheKey: String { "my_\(id)" }
    let id: String
    func data(handler: @escaping (Result<Data, Error>) -> Void) {
        // 异步获取 Data 后调用 handler(.success(data)) 或 handler(.failure(...))
    }
}
imageView.kf.setImage(with: MyImageDataProvider(id: "123"))

14. 其他常用选项速览

选项 含义
.forceRefresh 跳过缓存,强制重新下载
.retryFailed 对之前失败的 URL 重试
.onlyFromCache 仅从缓存读取,不发起网络请求
.backgroundDecode 在后台队列解码,减少主线程压力
.callbackQueue(.mainAsync) 指定完成回调的派发队列
.downloadPriority(1.0) 下载任务优先级(iOS)
.scaleFactor(UIScreen.main.scale) 与 @2x/@3x 匹配,避免模糊
.cacheMemoryOnly 仅写内存缓存,不写磁盘
.loadDiskFileSynchronously 从磁盘加载时是否同步(默认异步)
imageView.kf.setImage(with: url, options: [.forceRefresh, .retryFailed, .callbackQueue(.mainAsync)])

15. Options 详解(延伸)

  • targetCache / originalCache:默认为 nil 时使用 ImageCache(name: "default")targetCache 为最终展示图的缓存(含 Processor 处理后的图),originalCache 为原始数据的缓存,可用于「列表用处理图、详情用原图」等分离策略。
  • transition:图片加载完成后的展示动画;forceTransition 为 true 时即使命中缓存也执行 transition,为 false 时仅在不使用缓存(新下载)时执行。
  • callbackQueue / processingQueuecallbackQueue 可选 .mainAsync.mainCurrentOrAsync(当前线程为主线程则直接执行,否则主线程异步)、.untouch.dispatch(DispatchQueue),默认多为 .mainCurrentOrAsyncprocessingQueue 为 Processor 执行所在队列,默认串行子队列。
  • memoryCacheAccessExtendingExpiration:从内存/磁盘取图时是否延长过期时间,可选 .none(不延长)、.cacheTime(当前时间 + 原过期时长)、.expirationTime(StorageExpiration)(延长到指定时长)。

16. 指示器、Placeholder 与 Transition 类型

  • 指示器(Indicator)imageView.kf.indicatorType 可选 .none.activity(UIActivityIndicatorView)、.image(imageData: Data)(GIF 等)、.custom(indicator: Indicator),自定义需实现 Indicator 协议(startAnimatingView / stopAnimatingView)。
  • Placeholder:除 UIImage 外,可实现 Placeholder 协议的自定义 View(如 class MyPlaceholder: UIView, Placeholder {}),设置 imageView.kf.setImage(with: url, placeholder: myPlaceholderView)
  • ImageTransitionnonefade(TimeInterval)flipFromLeft/Right/Top/Bottom(TimeInterval)custom(duration:options:animations:completion:)

17. 缓存配置与清除

内存缓存cache.memoryStorage.config):totalCostLimit(默认约物理内存 1/4)、countLimitexpiration(默认 300 秒)、cleanInterval(清除过期缓存的时间间隔,仅初始化可设)。单张可设 .memoryCacheExpiration(.never);访问时延长策略用 .memoryCacheAccessExtendingExpiration(.cacheTime)

磁盘缓存cache.diskStorage.config):sizeLimitexpiration(默认 7 天)、pathExtensionusesHashedFileName(文件名是否用 key 的 MD5)。超出容量时按最后访问时间排序,删除最旧文件直至低于 sizeLimit 的一半。

清除cache.clearMemoryCache() / cache.cleanExpiredMemoryCache()cache.clearDiskCache() / cache.cleanExpiredDiskCache();删除指定 key 可用 cache.removeImage(forKey:processorIdentifier:fromMemory:fromDisk:completionHandler:)。获取磁盘占用:cache.calculateDiskStorageSize { result in ... }

18. ImagePrefetcher 与请求修饰、重定向

ImagePrefetcher:除 start() 外,提供 completionHandler(参数为 [Resource] 的 skipped/failed/completed)与 completionSourceHandler(参数为 [Source]),分别对应用 URL/Resource 初始化与用 Source 初始化的场景;progressBlock / progressSourceBlock 同理。maxConcurrentDownloads 控制并发数。stop() 会取消当前未完成的下载任务,并将剩余未加载项计入「完成回调」的 skipped;若调用 stop 时已全部完成,则不会再次触发完成回调。

请求修饰:通过 AnyModifier 或实现 ImageDownloadRequestModifier 在请求前添加 Header、Token 等,例如 let modifier = AnyModifier { var r = $0; r.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization"); return r },options 中加 .requestModifier(modifier)超时ImageDownloader.default.downloadTimeout = 60重定向:通过 .redirectHandler(AnyRedirectHandler { ... }) 自定义 302 等重定向后的请求。

19. 扩展 WebP 支持(Processor + CacheSerializer)

Kingfisher 默认不包含 WebP 编解码,可借助 ProcessorCacheSerializer 扩展 [13]。依赖 libwebp 实现 Data ↔ Image 后,定义 WebPProcessor(在 process 中若为 .data 则用 WebP 解码为 Image,若为 .image 则透传)与 WebPCacheSerializerdata(with:original:) 返回 WebP 编码、image(with:options:) 返回 WebP 解码),使用时设置 options: [.processor(WebPProcessor.default), .cacheSerializer(WebPCacheSerializer.default)] 即可对 .webp URL 加载并缓存。


延伸阅读(掘金系列)

以下为同一作者的 Kingfisher 源码解析系列文章,可按需跳转深入阅读(链接与标题保持一致):

主题 链接 内容概要
使用 Kingfisher源码解析之使用 Resource/ImageDataProvider、Placeholder、GIF、Indicator、Transition、Processor 概览、缓存与下载配置、预加载、常用 options
Options 解释 Kingfisher源码解析之Options解释 targetCache/originalCache、downloader、transition/forceTransition、preloadAllAnimationData、callbackQueue/processingQueue、memoryCacheAccessExtendingExpiration
加载流程 Kingfisher源码解析之加载流程 setImage 之后发生了什么、图片加载与缓存查找流程
ImageCache Kingfisher源码解析之ImageCache MemoryStorage(NSCache、StorageObject、Config)、DiskStorage(FileMeta、removeExpiredValues、removeSizeExceededValues)、缓存读写与清理
加载动图 Kingfisher源码解析之加载动图 UIImageView 与 AnimatedImageView 加载 GIF 的差异、preloadAllAnimationData、CGImageSource、Animator、CADisplayLink、display(_ layer:)
Processor 和 CacheSerializer Kingfisher源码解析之Processor和CacheSerializer Processor/ImageProcessItem 定义与调用时机、CacheSerializer 调用时机、使用 Processor+CacheSerializer 扩展 WebP
ImagePrefetcher Kingfisher源码解析之ImagePrefetcher 预加载功能、completionHandler/completionSourceHandler、progressBlock/progressSourceBlock、stop() 行为、Resource 与 Source 两套回调

参考文献

[1] Kingfisher. Cheat Sheet. GitHub Wiki.
[2] Kingfisher. Image Manager Structure. studyraid.com / Agent Docs.
[3] Kingfisher. CHANGELOG / Releases — 3.10.0 cache retrieval with ImageProcessor.
[4] Kingfisher. Release 5.0.0. GitHub.
[5] Kingfisher. Release 5.3.0 — Downsampling scale/memory fix.
[6] Kingfisher. Release 7.8.1 — Animated image from disk cache with processor.
[7] Kingfisher. Release 8.3.0 — Progressive JPEG for KFImage.
[8] Apple. Image and Graphics Best Practices. WWDC 2018, Session 219.
[9] Kingfisher. ImageProcessor.swift. GitHub (onevcat/Kingfisher).
[10] Kingfisher. Cheat Sheet — Processor, Cache, Downloader. GitHub Wiki.
[11] Stack Overflow / Kingfisher Issues. DownsamplingImageProcessor size (0,0) and processing failure.

01-研究优秀开源框架@图层处理@iOS | SDWebImage 框架:从使用到原理解析

📋 目录


一、SDWebImage 概述与历史演进

0. 框架结构概览与功能简介

SDWebImage 的框架结构

SDWebImage的框架结构

SDWebImage 的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。

功能简介

  1. 一个添加了 web 图片加载和缓存管理的 UIImageView 分类
  2. 一个异步图片下载器
  3. 一个异步的内存加磁盘综合存储图片并且自动处理过期图片
  4. 支持动态 gif 图
    • 4.0 之前的动图效果并不是太好
    • 4.0 以后基于 FLAnimatedImage 加载动图
  5. 支持 webP 格式的图片
  6. 后台图片解压处理
  7. 确保同样的图片 url 不会下载多次
  8. 确保伪造的图片 url 不会重复尝试下载
  9. 确保主线程不会阻塞

1. 框架简介

SDWebImage 是 Apple 平台(iOS / macOS / watchOS / visionOS)上广泛使用的异步图片下载与缓存库,提供从网络(或自定义 Loader)加载图片、解码、变换、缓存到展示的完整管线。其「图层处理」相关能力主要体现在:解码管线(将压缩数据解码为可渲染的位图)与变换管线(在解码后对位图做缩放、裁剪、滤镜等处理),二者共同构成「从数据到屏幕」的中间处理层。

2. 技术演进与版本脉络

SDWebImage 的图层处理能力并非一蹴而就,而是随版本逐步完善,与系统 API 和业界实践同步演进。

阶段 版本/时期 解码与图层处理相关能力
早期 3.x 及以前 以网络下载 + 简单缓存为主,解码依赖系统默认行为
规范化 4.0 引入 Custom Download Operation、更清晰的职责划分
编解码扩展 4.2 Custom Coder:支持注册自定义编解码器(如 WebP、渐进 JPEG)
统一管线 5.0 Image TransformerAnimated Image 全栈方案(GIF/WebP/APNG)、解码与变换在 Manager 内统一调度
精细化 5.x 后续 缩略图解码、HDR、强制解码策略(Force Decode Policy)、解码尺度与字节限制等

5.0 是重要分水岭:解码(Coder)、变换(Transformer)、缓存(Cache)、加载(Loader)在 SDWebImageManager 中形成一条清晰流水线,便于理解「图层处理」在整体中的位置。

3. 图层处理在整体架构中的位置

下图概括了从「URL 请求」到「显示到视图」的流程,并标出解码与变换所在阶段。

flowchart LR
    subgraph 输入
        A[URL / 自定义 Loader]
    end
    subgraph 加载
        B[SDImageLoader]
    end
    subgraph 解码层
        C[Data]
        D[SDImageCoder 解码]
        E[UIImage/NSImage]
    end
    subgraph 变换层
        F[SDImageTransformer]
        G[变换后图像]
    end
    subgraph 缓存与输出
        H[SDImageCache]
        I[UIImageView 等]
    end
    A --> B --> C --> D --> E --> F --> G --> H --> I

要点

  • 解码(Decoder):将压缩格式数据(JPEG/PNG/WebP/HEIC/AVIF 等)转为内存中的位图(如 UIImage/NSImage),是「数据 → 图层」的第一步。
  • 变换(Transformer):在「已解码的位图」上做几何或像素级处理(缩放、裁剪、圆角、滤镜等),输出仍是位图,再写入缓存或交给视图。
  • 二者均可在后台线程执行,避免阻塞主线程,符合 Apple 在 WWDC 等场合强调的「Image and Graphics Best Practices」[1]。

二、图像解码管线(Decoder Pipeline)

1. 解码的基础概念与双缓冲模型

在操作系统与图形栈中,图像通常以两种形式存在:

  1. 数据缓冲(Data Buffer)
    即磁盘或网络中的压缩编码数据(如 JPEG、PNG 的二进制)。体积小,但不能直接用于渲染。

  2. 图像缓冲(Image Buffer)
    解码后的像素矩阵(如 RGBA 位图),可被 GPU/CPU 渲染。其大小与分辨率(宽×高×通道数)成正比,与压缩格式无关。

因此,解码(Decoding) 的含义是:将 Data Buffer 转换为 Image Buffer。该过程是 CPU 密集型,且解码后的图像缓冲往往远大于原始数据(例如一张 4K 图片可解码为上百 MB 像素数据)。系统会在首次渲染时触发解码,若在主线程进行,易造成卡顿;若不经控制,大图会带来内存峰值与 OOM 风险。

双缓冲概念可归纳为

┌─────────────────┐     decode      ┌─────────────────┐
│  Data Buffer    │  ────────────►  │  Image Buffer   │
│ (JPEG/PNG/…)    │   (CPU 密集)     │  (像素矩阵)      │
└─────────────────┘                 └─────────────────┘
      体积较小                          体积 ∝ 宽×高×4

Apple 在 WWDC 2018「Image and Graphics Best Practices」[1] 中明确指出:解码后的缓冲区大小由图像尺寸决定,而非显示尺寸;因此在解码阶段就做下采样(Downsampling),避免先解码全尺寸再缩放的巨大内存与 CPU 开销。

2. 缩略图与下采样(Downsampling)

下采样指在解码时直接生成较小尺寸的位图,而不是先解码全图再缩放。这样既能减少内存占用,也能减少解码与后续绘制的计算量。

2.1 系统 API:ImageIO 与缩略图

在 iOS/macOS 上,推荐使用 ImageIOCGImageSourceCreateThumbnailAtIndex 在解码阶段就限制最大尺寸,从而在内存中只生成缩略图级别的像素缓冲 [2][3]。

算法思路(伪代码)

函数 DownsampleImage(数据 data, 最大边长 maxPixelSize):
    1. 使用 data 创建 CGImageSourceRef source
    2. 设置选项 options:
       - kCGImageSourceCreateThumbnailFromImageAlways: true
       - kCGImageSourceCreateThumbnailWithTransform: true
       - kCGImageSourceThumbnailMaxPixelSize: maxPixelSize
    3. thumbnail = CGImageSourceCreateThumbnailAtIndex(source, 0, options)
    4. 由 thumbnail 创建 UIImage/NSImage 并返回

这样,解码器内部可以在部分解码低分辨率解码路径上生成缩略图,避免全图解码。SDWebImage 在 5.x 中通过 SDImageCoderDecodeScaleDownLimitBytes 等能力,将「按目标尺寸或字节限制做缩略图解码」纳入其解码管线,与上述思路一致。

2.2 下采样算法与内存估算

算法步骤(与 ImageIO 语义一致)

  1. Data 创建 CGImageSourceRef,不立即解码全图。
  2. 从 source 读取图像属性(宽、高),计算缩放比例,使长边不超过 maxPixelSize
  3. 设置 kCGImageSourceThumbnailMaxPixelSizekCGImageSourceCreateThumbnailFromImageAlwayskCGImageSourceCreateThumbnailWithTransform 等选项。
  4. 调用 CGImageSourceCreateThumbnailAtIndex(source, 0, options) 得到缩略图 CGImage
  5. CGImage 创建 UIImage/NSImage 并返回。

这样解码器在内部只生成目标尺寸的像素缓冲,避免「先全图解码再缩放」的双倍内存与 CPU 开销。

2.3 内存与性能关系

下采样带来的内存节省可近似表示为:

  • 全图解码:memory ≈ width × height × 4(假设 RGBA)。
  • 限制最大边长 L 后:若等比缩放,则 memory ≈ L² × 4,与原始分辨率无关。

因此,在列表、缩略图等场景下,在解码阶段就限制最大尺寸是业界公认的最佳实践,也是 SDWebImage 解码管线优化的核心之一。

3. 渐进式解码(Progressive Decoding)

渐进式编码(如 Progressive JPEG)允许数据分块到达时逐步呈现:先看到模糊全图,再随数据增多逐步变清晰。渐进式解码即在未完整接收数据时,对当前已有数据做解码并显示,以提升感知性能(尤其在弱网环境)[4]。

流程概念

sequenceDiagram
    participant N as 网络
    participant D as 渐进式解码器
    participant V as 视图

    N->>D: 数据块 1
    D->>D: 解码当前数据
    D->>V: 显示低分辨率帧 1
    N->>D: 数据块 2
    D->>D: 更新解码状态,解码
    D->>V: 显示更清晰帧 2
    N->>D: 数据块 n(完成)
    D->>D: 最终解码
    D->>V: 显示最终图像

SDWebImage 通过 SDWebImageProgressiveCoder 协议扩展解码器:支持「增量数据」输入,每次 updateIncrementalData:finished: 时更新内部解码状态并输出当前可用的图像,供上层展示。对动图(如 GIF),还可配合 SDAnimatedImageCoder 在渐进加载时逐帧解码并驱动 SDAnimatedImageView 的渐进动画。

渐进式解码流程(伪代码)

函数 ProgressiveDecode:
    状态: 已接收数据 buffer, 解码器内部状态 decoderState
    当 收到新数据块 chunk:
        append(buffer, chunk)
        decodedFrame = decoder.decodeIncremental(buffer, decoderState)
        若 decodedFrame 非空:
            回调 onPartialImage(decodedFrame)
    当 数据接收完成:
        finalImage = decoder.finalize(buffer, decoderState)
        回调 onComplete(finalImage)

注意:渐进式解码比单次完整解码的 CPU 开销更高 [4],适合「先显示再细化」的体验需求,需在流畅度与电量之间做权衡。

4. 编解码器扩展与多格式支持

SDWebImage 将「解码 / 编码」抽象为 SDImageCoder 协议,通过 SDImageCodersManager 注册多个 Coder,按数据格式(或 MIME 类型)选择对应实现。这样可在不修改核心管线的前提下支持新格式。

解码器选择与解码流程(高层)

flowchart TD
    A[Image Data] --> B{SDImageCodersManager}
    B --> C[遍历已注册 Coder]
    C --> D{canDecodeFromData?}
    D -->|是| E[该 Coder 解码]
    D -->|否| C
    E --> F[UIImage/NSImage]
    F --> G[可选: 缩略图/字节限制]
    G --> H[解码结果]

典型 Coder 职责

方法/能力 含义
decodedImageWithData:options: 将 Data 解码为 UIImage/NSImage
encodedDataWithImage:format:options: 将图像编码为指定格式 Data
canDecodeFromData: / canEncodeToFormat: 是否支持某格式的解码/编码

动图则通过 SDAnimatedImageCoder 扩展:提供按帧解码、帧时长、循环次数等,供 SDAnimatedImage + SDAnimatedImageView 使用。内置支持 GIF、WebP、APNG、HEIC 动图等;用户也可实现自定义 Coder 并注册,从而纳入统一的加载与缓存流程。


三、图像变换管线(Transformer Pipeline)

1. 变换器的设计思想与协议

图像变换在 SDWebImage 中定义为:输入与输出均为图像对象(如 UIImage/NSImage)的运算。与 Coder(Data ↔ Image)不同,Transformer 只做 Image → Image,例如缩放、裁剪、圆角、滤镜等,对应「数字图像处理」中的几何变换与像素操作 [5]。

协议设计(概念)

协议 SDImageTransformer:
    方法 transform(image, key) -> Image?:
        输入: 原始图像、缓存 key(可选,用于生成变换后的 cache key)
        输出: 变换后的图像;失败可返回 nil

这样设计便于:

  • SDWebImageManager 中,在「解码完成」之后、「写入缓存」之前插入变换步骤;
  • 对同一 URL 可因不同变换参数得到不同 cache key,从而分别缓存原图与变换结果。

2. 内置变换器与组合管线

SDWebImage 提供多种内置 Transformer,覆盖常见 UI 需求:

变换器 功能说明
SDImageResizingTransformer 缩放到指定尺寸,支持 scaleMode(fill/aspectFit 等)
SDImageCroppingTransformer 按矩形裁剪
SDImageRoundCornerTransformer 圆角(可带边框)
SDImageRotationTransformer 按角度旋转,可选 fitSize
SDImageFlippingTransformer 水平/垂直翻转
SDImageBlurTransformer 高斯模糊
SDImageTintTransformer 颜色 tint
SDImageFilterTransformer 基于 CIFilter 的滤镜(除 watchOS 外)

组合管线:通过 SDImagePipelineTransformer 将多个 Transformer 按顺序组合,形成链式处理:

图像 → Transformer1 → Transformer2 → … → TransformerN → 最终图像

例如先裁剪再圆角再缩放,只需将三个 Transformer 放入一个 Pipeline 即可。对应伪代码:

pipeline = SDImagePipelineTransformer([CropTransformer(rect), RoundCornerTransformer(radius), ResizingTransformer(size)])
resultImage = pipeline.transform(originalImage, key)

在 Swift/Objective-C 中的用法可参见官方 Advanced Usage - Image Transformer [6]。

3. 变换与缓存的协同

变换发生在 Manager 层:
先由 Loader 得到 Data,由 Coder 解码得到 Image,再经 Transformer 得到最终 Image,最后再写入 Cache 并交给 UI。因此:

  • 原始图变换后的图可以分别缓存:
    • 原图可用 SDWebImageContextOriginalImageCache 指定单独缓存实例;
    • 变换后的图使用默认(或指定)的 Cache,其 cache key 会包含变换信息,避免不同变换结果互相覆盖。
  • 若只关心「下载 + 变换」而不写缓存,可通过 .fromLoaderOnlystoreCacheType = .none 实现,仅走 Loader → 解码 → 变换 → 回调,不读/写缓存。

变换与缓存的整体管线(含解码)

flowchart LR
    subgraph 请求
        U[URL + Context]
    end
    subgraph 缓存查询
        C1{查 Cache}
    end
    subgraph 加载与解码
        L[Loader]
        D[Coder 解码]
    end
    subgraph 变换
        T[Transformer]
    end
    subgraph 写回与展示
        C2[写 Cache]
        V[View]
    end
    U --> C1
    C1 -->|命中| V
    C1 -->|未命中| L --> D --> T --> C2 --> V

4. 应用场景简述

场景 解码侧 变换侧
列表缩略图 使用 scaleDown/limitBytes 做缩略图解码,降低内存 可选 ResizingTransformer 统一尺寸
头像/圆角 常规解码即可 RoundCornerTransformer
弱网/大图 Progressive Coder 渐进显示 可配合 Resizing 限制最终尺寸
相册/大图预览 原图或高分辨率解码 少用或仅做旋转/裁剪
动图(GIF/WebP) SDAnimatedImageCoder + 帧缓冲 一般不做几何变换,或仅对首帧做

四、类结构图分析

1. 核心类总览

SDWebImage 的类可按职责分为:入口与协调加载缓存解码变换视图扩展 六类。下表给出核心类及其职责(名称以 5.x 为主,OC/Swift 可能略有差异)。

模块 核心类 / 协议 职责简述
协调 SDWebImageManager 统一入口:协调 Loader、Cache、Coder、Transformer,执行「查缓存 → 下载 → 解码 → 变换 → 写缓存」
加载 SDImageLoader (协议) 定义加载接口:根据 URL 返回 Data 或 Image
SDWebImageDownloader 默认 Loader 实现:基于 URLSession 下载,支持并发、取消、RequestModifier
SDWebImageDownloaderOperation 单次下载任务,实现 SDWebImageDownloaderOperation 协议
缓存 SDImageCache 内存 + 磁盘二级缓存,提供 query/store/remove,支持自定义 key、过期策略
SDMemoryCache / SDDiskCache 内存层、磁盘层具体实现(5.x 可拆分)
解码 SDImageCoder (协议) 定义 Data ↔ Image 编解码,如 decodedImageWithData:options:
SDImageCodersManager 管理多个 Coder,按数据格式选择可用 Coder
SDWebImageImageIOCoder 内置 Coder 实现(JPEG/PNG/HEIC/…)
变换 SDImageTransformer (协议) 定义 Image → Image 变换,如 transformedImageWithImage:forKey:
SDImagePipelineTransformer 将多个 Transformer 串联为一条管线
SDImageResizingTransformer 内置 Transformer 实现
视图 UIImageView+WebCache 为 UIImageView 提供 sd_setImage(with:...)sd_cancelCurrentImageLoad
SDAnimatedImageView 动图展示,配合 SDAnimatedImage
UIButton+WebCache 其他控件的扩展

2. 模块划分与依赖关系

下图从「模块」维度表示各层之间的依赖方向:视图扩展依赖 Manager,Manager 依赖 Loader/Cache,解码与变换在 Manager 内被调用,Loader 只产出 Data,Cache 只做存取。

flowchart TB
    subgraph 视图层
        V1[UIImageView+WebCache]
        V2[UIButton+WebCache]
        V3[SDAnimatedImageView]
    end
    subgraph 协调层
        M[SDWebImageManager]
    end
    subgraph 加载层
        L[SDWebImageDownloader]
    end
    subgraph 缓存层
        C[SDImageCache]
    end
    subgraph 编解码层
        CM[SDImageCodersManager]
        CO[SDImageCoder 实现]
    end
    subgraph 变换层
        T[SDImageTransformer 实现]
    end
    V1 --> M
    V2 --> M
    V3 --> M
    M --> L
    M --> C
    M --> CM
    M --> T
    CM --> CO

3. 加载与缓存类结构

Loader 负责从网络(或自定义来源)获取数据;Cache 负责内存与磁盘的读写。Manager 持有两者引用,在单次请求中先问 Cache,未命中再调 Loader。

classDiagram
    class SDWebImageManager {
        -imageLoader: SDImageLoader
        -imageCache: SDImageCache
        +loadImage(with:options:context:progress:completed:)
        -callLoadImage(with:options:context:progress:completed:)
    }
    class SDImageLoader {
        <<protocol>>
        +requestImageWithURL:options:context:progress:completed()
        +canRequestImageForURL()
    }
    class SDWebImageDownloader {
        -session: URLSession
        -downloadQueue: NSOperationQueue
        +downloadImageWithURL:options:progress:completed()
    }
    class SDImageCache {
        -memoryCache: SDMemoryCache
        -diskCache: SDDiskCache
        +queryImageForKey:options:context:callback()
        +storeImage:imageData:forKey:completion()
        +removeImageForKey:withCompletion()
    }
    SDWebImageManager --> SDImageLoader : 使用
    SDWebImageManager --> SDImageCache : 使用
    SDWebImageDownloader ..|> SDImageLoader : 实现
  • SDWebImageManager:对外提供 loadImage(with:...),内部先查 imageCache,再根据需要调用 imageLoader,最后根据 context 决定是否解码、变换并写回缓存。
  • SDWebImageDownloader:实现 SDImageLoader 协议,通过 URLSession 下载,支持并发数、超时、RequestModifier;单次下载封装为 SDWebImageDownloaderOperation
  • SDImageCache:内存缓存通常用 NSCache 或自研 LRU,磁盘缓存为文件系统;query/store 的 key 由 Manager 根据 URL + context(含 transformer 等)生成。

4. 解码与变换类结构

Coder 将 Data 转为 Image(或反向);Transformer 将 Image 转为另一 Image。Manager 在「Loader 返回 Data 后」先选 Coder 解码,再按 context 中的 Transformer 做变换,得到最终 Image 再写入 Cache。

classDiagram
    class SDWebImageManager {
        -loadImage(with:...)
    }
    class SDImageCoder {
        <<protocol>>
        +decodedImageWithData:options()
        +encodedDataWithImage:format:options()
        +canDecodeFromData()
        +canEncodeToFormat()
    }
    class SDImageCodersManager {
        -coders: [SDImageCoder]
        +addCoder()
        +removeCoder()
        +canDecodeFromData()
        +decodedImageWithData:options()
    }
    class SDImageTransformer {
        <<protocol>>
        +transformerKey
        +transformedImageWithImage:forKey()
    }
    class SDImagePipelineTransformer {
        -transformers: [SDImageTransformer]
        +transformerKey
        +transformedImageWithImage:forKey()
    }
    SDWebImageManager ..> SDImageCodersManager : 解码时使用
    SDWebImageManager ..> SDImageTransformer : 变换时使用
    SDImageCodersManager --> SDImageCoder : 委托具体 Coder
    SDImagePipelineTransformer ..|> SDImageTransformer : 实现
  • SDImageCodersManager:持有一组 SDImageCoder,按 canDecodeFromData: 选出第一个能处理当前 Data 的 Coder 执行解码;编码同理。
  • SDImagePipelineTransformer:持有一组 SDImageTransformer,按顺序对 Image 依次变换;其 transformerKey 通常由各子 Transformer 的 key 拼接而成,参与缓存 key 生成。

5. View 扩展与调用链

视图扩展(如 UIImageView+WebCache)是业务最常接触的入口:内部将「当前 URL、placeholder、options、context」交给 SDWebImageManager,并把返回的加载任务与 view 关联,以便在复用时取消。

sequenceDiagram
    participant V as UIImageView
    participant Ext as UIImageView+WebCache
    participant M as SDWebImageManager
    participant C as SDImageCache
    participant L as SDWebImageDownloader

    V->>Ext: sd_setImage(with: url, ...)
    Ext->>Ext: sd_cancelCurrentImageLoad()
    Ext->>M: loadImage(with: url, context: [...])
    M->>C: queryImage(forKey:)
    alt 缓存命中
        C-->>M: image
        M-->>Ext: completed(image, .memory/.disk)
    else 未命中
        M->>L: requestImageWithURL:...
        L-->>M: data
        M->>M: 解码 + 变换
        M->>C: storeImage(forKey:)
        M-->>Ext: completed(image, .none)
    end
    Ext->>V: imageView.image = image
  • sd_setImage(with: placeholder: options: context: completed:):先对当前 view 取消未完成任务,再调 SDWebImageManager.shared.loadImage(with: url, options: options, context: context, progress: progress, completed: completed);在 completed 中把得到的 image 赋给 imageView.image(并可选执行 transition 动画)。
  • sd_cancelCurrentImageLoad():取消与该 view 绑定的 load 任务,避免 cell 复用时旧请求覆盖新图片。

将上述「核心类总览」「模块依赖」「Loader/Cache 类图」「Coder/Transformer 类图」「View 调用链」串联起来,即可形成对 SDWebImage 类结构图 的完整分析:入口在视图扩展,核心协调在 Manager,加载与缓存、解码与变换均为可插拔的协议实现,便于扩展与测试。


五、与系统及业界实践的衔接

1. Apple 图像与图形最佳实践

Apple 在 WWDC 2018「Image and Graphics Best Practices」[1] 中强调:

  • 在后台线程进行解码与下采样,避免在主线程做重 CPU 工作导致的卡顿。
  • 解码时即做下采样,使解码后的图像缓冲与显示尺寸匹配,降低内存与 CPU。
  • 预取(Prefetch):在列表等场景提前准备即将显示的图像,避免在滚动时才开始解码。

SDWebImage 的解码与变换均在后台队列执行,且支持按尺寸/字节限制的缩略图解码,与上述建议一致。其 Prefetch 能力(如 UITableView 的 prefetch 结合 sd_setImageWithURL:)可在业务层配合使用,实现「提前解码、避免滚动时卡顿」。

Force Decode 策略(5.17+):SDWebImage 引入 SDImageForceDecodePolicy,用于控制是否在加载管线中强制解码(将延迟解码的图片提前转为位图)。在部分场景下可避免在渲染阶段才触发 CA 的帧缓冲拷贝,从而降低主线程峰值与内存抖动;具体策略可根据「是否使用自定义渲染」「是否配合 Transformer」等选择,详见官方文档与 CHANGELOG。

2. 移动端图像管线研究简述

在移动端部署图像管线(含解码、缩放、轻量级「变换」)方面,业界与学界有大量工作:

  • FlexiViT [7] 等通过可变的 patch 尺寸在训练与推理时平衡精度与速度;
  • NanoFLUX [8]、SnapGen [9] 等关注在移动设备上的高效图像生成与压缩。
    这些工作与「在端侧做高效解码与分辨率控制」的目标一致:在有限算力与内存下,通过解码阶段控制(如缩略图、渐进解码)和管线化处理(解码 → 变换 → 缓存)提升体验。SDWebImage 的 Decoder + Transformer 双管线正是这一思路在「图片加载库」中的具体实现。

六、使用案例与原理分析

0. 框架结构速览

0.1 实现原理

  1. 架构图(UML 类图)

架构图(UML 类图)

  1. 流程图(方法调用顺序图)

1559217862563-364c0d60-3f2a-4db9-b5c5-e81f01cd125e.png

0.2 目录结构

  • Downloader\
    • SDWebImageDownloader\
    • SDWebImageDownloaderOperation
  • Cache\
    • SDImageCache
  • Utils\
    • SDWebImageManager\
    • SDWebImageDecoder\
    • SDWebImagePrefetcher
  • Categories\
    • UIView+WebCacheOperation\
    • UIImageView+WebCache\
    • UIImageView+HighlightedWebCache\
    • UIButton+WebCache\
    • MKAnnotationView+WebCache\
    • NSData+ImageContentType\
    • UIImage+GIF\
    • UIImage+MultiFormat\
    • UIImage+WebP
  • Other\
    • SDWebImageOperation(协议)\
    • SDWebImageCompat(宏定义、常量、通用函数)

0.3 相关类名与功能描述

  • SDWebImageDownloader:是专门用来下载图片和优化图片加载的,跟缓存没有关系
  • SDWebImageDownloaderOperation:继承于 NSOperation,用来处理下载任务的
  • SDImageCache:用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程
  • SDWebImageManager:作为 UIImageView+WebCache 背后的默默付出者,主要功能是将图片下载(SDWebImageDownloader)和图片缓存(SDImageCache)两个独立的功能组合起来
  • SDWebImageDecoder:图片解码器,用于图片下载完成后进行解码
  • SDWebImagePrefetcher:预下载图片,方便后续使用,图片下载的优先级低,其内部由 SDWebImageManager 来处理图片下载和缓存
  • UIView+WebCacheOperation:用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation
  • UIImageView+WebCache:集成 SDWebImageManager 的图片下载和缓存功能到 UIImageView 的方法中,方便调用方的简单使用
  • UIImageView+HighlightedWebCache:跟 UIImageView+WebCache 类似,也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片
  • UIButton+WebCache:跟 UIImageView+WebCache 类似,集成 SDWebImageManager 的图片下载和缓存功能到 UIButton 的方法中,方便调用方的简单使用
  • MKAnnotationView+WebCache:跟 UIImageView+WebCache 类似
  • NSData+ImageContentType:用于获取图片数据的格式(JPEG、PNG 等)
  • UIImage+GIF:用于加载 GIF 动图
  • UIImage+MultiFormat:根据不同格式的二进制数据转成 UIImage 对象
  • UIImage+WebP:用于解码并加载 WebP 图片

0.4 工作流程

工作流程

  • 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。
  • 进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo: 交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:。
  • 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。
  • SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。
  • 如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。
  • 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。
  • 如果从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 进而回调展示图片。
  • 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。
  • 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。
  • 图片下载由 NSURLConnection(3.8.0 之后使用了 NSURLSession),实现相关 delegate 来判断图片下载中、下载完成和下载失败。
  • connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
  • 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
  • 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。
  • imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
  • 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
  • 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
  • SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
  • SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

1. 典型使用案例

1.1 列表 Cell 中加载缩略图(防错位 + 下采样)

UITableView / UICollectionView 的 cell 中,若不限制图片尺寸,大图会带来内存峰值与卡顿;且 cell 复用时需避免「先显示旧图再被新图覆盖」的错位。SDWebImage 通过 URL 绑定取消机制 解决错位,通过 Transformer 限制尺寸 控制内存。

// Cell 内
func configure(with url: URL) {
    imageView.sd_cancelCurrentImageLoad()
    let transformer = SDImageResizingTransformer(
        size: CGSize(width: 120, height: 120),
        scaleMode: .aspectFill
    )
    imageView.sd_setImage(
        with: url,
        placeholderImage: UIImage(named: "placeholder"),
        context: [.imageTransformer: transformer]
    )
}

要点sd_cancelCurrentImageLoad() 会取消该 view 上未完成的请求,新 URL 加载完成后才设置,避免复用时显示错误图片。

1.2 预取(Prefetch)提前解码

利用系统预取 API 在 cell 尚未显示时就开始加载,滚动时直接从缓存读取,减少卡顿。

// 实现 UICollectionViewDataSourcePrefetching
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
    let urls = indexPaths.compactMap { model(at: $0).imageURL }
    urls.forEach { url in
        SDWebImagePrefetcher.shared.prefetchURLs([url])
    }
}

// 可选:取消不再需要的预取
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
    let urls = indexPaths.compactMap { model(at: $0).imageURL }
    SDWebImagePrefetcher.shared.cancelPrefetching(for: urls)
}

1.3 多设备/多 Channel 下的加载

在多设备场景(如同一 URL 在不同 Channel 下需要不同尺寸)中,通过 context 传入不同的 Transformer 或 Cache,使同一 URL 对应多条缓存条目。

// 列表用小图
imageView.sd_setImage(with: url, context: [
    .imageTransformer: SDImageResizingTransformer(size: CGSize(width: 80, height: 80), scaleMode: .aspectFill)
])

// 详情用原图或大图
detailImageView.sd_setImage(with: url)  // 不传 transformer,用原图

1.4 占位图 + 加载完成过渡动画

通过 sd_imageTransition 在图片从网络加载完成后做淡入等过渡,提升观感。

imageView.sd_imageTransition = .fade(0.25)
imageView.sd_setImage(with: url, placeholderImage: placeholder)

1.5 仅下载不展示(后台缓存)

希望提前把图片下载并写入缓存,供后续使用,而不绑定到某个 view。

SDWebImageManager.shared.loadImage(
    with: url,
    options: [],
    progress: nil
) { image, data, error, cacheType, finished, url in
    if let image = image, finished {
        // 已缓存,可做后续逻辑
    }
}

1.6 完成回调与错误处理

通过 completed 区分来源(内存/磁盘/网络)并处理失败与取消。

imageView.sd_setImage(with: url, placeholderImage: placeholder) { image, error, cacheType, url in
    if let error = error {
        // 可根据 error 类型提示用户或降级
        return
    }
    switch cacheType {
    case .none:   break // 本次从网络加载
    case .memory: break // 从内存缓存
    case .disk:   break // 从磁盘缓存
    @unknown default: break
    }
}

2. 更多使用案例与代码

2.1 UITableViewCell 完整示例(含复用与尺寸)

class PhotoCell: UITableViewCell {
    static let reuseId = "PhotoCell"
    @IBOutlet weak var photoImageView: UIImageView!
    @IBOutlet weak var progressView: UIProgressView!

    override func prepareForReuse() {
        super.prepareForReuse()
        photoImageView.sd_cancelCurrentImageLoad()
        photoImageView.image = nil
        progressView.progress = 0
    }

    func configure(with url: URL) {
        let size = photoImageView.bounds.size
        let transformer = SDImageResizingTransformer(
            size: size.isEmpty ? CGSize(width: 120, height: 120) : size,
            scaleMode: .aspectFill
        )
        photoImageView.sd_setImage(
            with: url,
            placeholderImage: UIImage(named: "placeholder"),
            context: [.imageTransformer: transformer],
            progress: { [weak self] received, total, _ in
                guard let self = self, total > 0 else { return }
                DispatchQueue.main.async {
                    self.progressView.progress = Float(received) / Float(total)
                }
            },
            completed: { [weak self] image, error, _, _ in
                DispatchQueue.main.async {
                    self?.progressView.isHidden = (image != nil)
                }
            }
        )
    }
}

2.2 UIButton 设置网络图片

// 设置不同 state 的图片
button.sd_setImage(with: url, for: .normal, placeholderImage: UIImage(named: "btn_placeholder"))
button.sd_setImage(with: highlightedURL, for: .highlighted)
button.sd_setBackgroundImage(with: backgroundURL, for: .normal)

// 带圆角与完成回调
let transformer = SDImageRoundCornerTransformer(radius: 8, corners: .allCorners, borderWidth: 0, borderColor: nil)
button.sd_setImage(with: url, for: .normal, placeholderImage: nil, context: [.imageTransformer: transformer]) { _, error, _, _ in
    if error != nil { print("加载失败") }
}

2.3 自定义缓存键(同一 URL 多用途)

当同一 URL 在不同业务下需要不同缓存(例如列表用缩略图、详情用原图)时,可用 cacheKeyFilter 或自定义 key。

// 方式一:通过 context 的 cacheKeyFilter 生成不同 key
let listKeyFilter: SDWebImageCacheKeyFilter = { url in
    return "list_\(url?.absoluteString ?? "")" as NSString
}
imageView.sd_setImage(with: url, context: [.cacheKeyFilter: listKeyFilter])

let detailKeyFilter: SDWebImageCacheKeyFilter = { url in
    return "detail_\(url?.absoluteString ?? "")" as NSString
}
detailImageView.sd_setImage(with: url, context: [.cacheKeyFilter: detailKeyFilter])

// 方式二:在业务层用不同 URL 或 query 区分(如服务端支持 ?size=thumb)
let listURL = url.appendingPathComponent("?size=thumb")
let detailURL = url
imageView.sd_setImage(with: listURL, context: [.cacheKeyFilter: listKeyFilter])
detailImageView.sd_setImage(with: detailURL, context: [.cacheKeyFilter: detailKeyFilter])

2.4 请求修饰(Header、Token、超时)

需要带鉴权或自定义 Header 时,使用 requestModifier

let modifier = SDWebImageDownloaderRequestModifier { request in
    var r = request
    r.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
    r.setValue("image/webp,image/*,*/*;q=0.8", forHTTPHeaderField: "Accept")
    r.timeoutInterval = 30
    return r
}
imageView.sd_setImage(
    with: url,
    context: [.requestModifier: modifier]
)

2.5 下载进度条 + 占位 + 过渡动画

imageView.sd_imageTransition = .fade(0.3)
imageView.sd_setImage(
    with: url,
    placeholderImage: UIImage(named: "placeholder"),
    options: [.retryFailed],
    progress: { [weak progressView] received, total, _ in
        guard let pv = progressView, total > 0 else { return }
        DispatchQueue.main.async {
            pv.progress = Float(received) / Float(total)
            pv.isHidden = false
        }
    },
    completed: { [weak progressView] image, _, cacheType, _ in
        DispatchQueue.main.async {
            progressView?.isHidden = true
            if image == nil { /* 显示失败占位 */ }
        }
    }
)

2.6 动图 GIF(SDAnimatedImageView)

// 使用 SDAnimatedImageView 播放 GIF/WebP/APNG
let animatedImageView = SDAnimatedImageView()
animatedImageView.sd_setImage(with: gifURL, placeholderImage: nil)

// 仅加载动图第一帧作为封面(节省内存)
animatedImageView.sd_setImage(with: gifURL, placeholderImage: nil, options: [.decodeFirstFrameOnly])

// 渐进式加载动图(边下边播)
animatedImageView.sd_setImage(with: gifURL, placeholderImage: nil, options: [.progressiveLoad])

2.7 缓存查询与手动存储

let cache = SDImageCache.shared
let key = url.absoluteString

// 查询是否已缓存
cache.containsImage(forKey: key) { cacheType in
    switch cacheType {
    case .none:   print("未缓存")
    case .memory: print("在内存")
    case .disk:   print("在磁盘")
    @unknown default: break
    }
}

// 从缓存读取(不触发下载)
cache.queryImage(forKey: key, options: nil, context: nil) { image, data, cacheType in
    if let image = image {
        imageView.image = image
    }
}

// 手动写入缓存(例如本地生成或从相册来的图)
cache.store(image, forKey: key, completion: nil)

2.8 自定义 Transformer 示例

实现 SDImageTransformer 协议,对已解码图像做自定义绘制或滤镜(以下方法名以 SDWebImage 5.x 协议为准,实际请参照当前版本头文件)。

// 实现协议:为图片加灰色边框
class GrayBorderTransformer: NSObject, SDImageTransformer {
    var transformerKey: String { "GrayBorder(\(borderWidth))" }
    let borderWidth: CGFloat

    init(borderWidth: CGFloat = 2) { self.borderWidth = borderWidth }

    func transformedImage(with image: UIImage, forKey key: String) -> UIImage? {
        let size = image.size
        UIGraphicsBeginImageContextWithOptions(size, false, image.scale)
        defer { UIGraphicsEndImageContext() }
        image.draw(at: .zero)
        UIColor.gray.setStroke()
        let path = UIBezierPath(rect: CGRect(origin: .zero, size: size).insetBy(dx: borderWidth/2, dy: borderWidth/2))
        path.lineWidth = borderWidth
        path.stroke()
        return UIGraphicsGetImageFromCurrentImageContext()
    }
}

// 使用
let transformer = GrayBorderTransformer(borderWidth: 3)
imageView.sd_setImage(with: url, context: [.imageTransformer: transformer])

2.9 强制刷新与仅从缓存读取

// 忽略缓存,强制重新下载(适用于需要刷新内容的场景)
imageView.sd_setImage(with: url, options: [.forceRefresh])

// 仅从缓存读取,没有则显示占位或报错(离线/省流量场景)
imageView.sd_setImage(with: url, options: [.onlyFromCache]) { image, error, _, _ in
    if image == nil { print("缓存中无此图") }
}

2.10 Objective-C 常用写法

// 基础加载
[imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"placeholder"]];

// 带 context 的 Transformer
id<SDImageTransformer> transformer = [SDImagePipelineTransformer transformerWithTransformers:@[
    [SDImageResizingTransformer transformerWithSize:CGSizeMake(100, 100) scaleMode:SDImageScaleModeFill],
    [SDImageRoundCornerTransformer transformerWithRadius:10 corners:SDRectCornerAllCorners borderWidth:0 borderColor:nil]
]];
[imageView sd_setImageWithURL:url placeholderImage:nil context:@{SDWebImageContextImageTransformer: transformer}];

// 取消当前加载
[imageView sd_cancelCurrentImageLoad];

// 完成回调
[imageView sd_setImageWithURL:url completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
    if (error) { NSLog(@"加载失败: %@", error); }
}];

3. 核心流程原理分析

3.1 Manager 协调的完整链路

SDWebImageManager 是整条「加载 → 解码 → 变换 → 缓存」的协调者,其内部逻辑可概括为:

flowchart TD
    A[loadImageWithURL] --> B{查缓存 key}
    B --> C[先查内存]
    C --> D{命中?}
    D -->|是| E[回调 .memory]
    D -->|否| F[再查磁盘]
    F --> G{命中?}
    G -->|是| H[解码/反序列化]
    H --> I[写回内存]
    I --> E
    G -->|否| J[构造 Loader 任务]
    J --> K[Loader 返回 Data]
    K --> L[Coder 解码]
    L --> M{有 Transformer?}
    M -->|是| N[Transformer 变换]
    M -->|否| O[得到 Image]
    N --> O
    O --> P[写内存+磁盘缓存]
    P --> Q[回调 .none 或 .disk]

要点

  • 缓存 key 由 URL(或自定义 key)与 context(如 transformer、cacheKeyFilter)共同决定,同一 URL 不同 transformer 会得到不同 key。
  • 解码与变换均在 Manager 持有的串行/并发队列 中执行,回调通过 dispatch_async(main_queue) 回到主线程,便于更新 UI。

3.2 回调与线程模型

  • progress:在下载进度回调所在线程(多为 URLSession 回调线程),若需更新 UI 需自行切主线程。
  • completed:SDWebImage 内部会派发到主线程再调用,因此 completed 里可直接操作 UI。
  • 取消:当再次对同一 view 调用 sd_setImage(with: newURL) 时,会取消该 view 上此前由 SDWebImage 发起的任务,completed 仍可能被调用一次(cancel 语义),可通过 SDWebImageOption 或检查 finished 区分。

3.3 缓存键与 Transformer 的关系

变换后的图片会以「新 key」写入缓存:通常为 key + transformerIdentifier 或等价组合。因此:

  • 原图:key = url.absoluteString(或自定义)。
  • 变换图:key = f(url, transformer),例如 url.absoluteString + "_" + transformer.identifier

这样同一 URL 可同时存在「原图」与「缩放版」「圆角版」等多份缓存,互不覆盖;原图也可通过 SDWebImageContextOriginalImageCache 写入单独缓存实例,供大图页等使用。


七、设计模式与编程思想

1. 设计模式应用

SDWebImage 在架构上大量运用经典设计模式,使扩展与维护成本可控。

模式 在 SDWebImage 中的体现 作用
外观 / 门面(Facade) SDWebImageManager 对外提供 loadImage(with:options:progress:completed:),内部协调 Loader、Cache、Coder、Transformer,调用方无需关心多级缓存与管线顺序 简化使用、隐藏复杂度
策略(Strategy) SDImageTransformerSDImageCoder 均为协议,多种实现可替换(Resizing、RoundCorner、WebP Coder 等),通过 context 或注册表注入 算法/行为可插拔,易扩展新格式与新变换
责任链 / 管道(Chain of Responsibility / Pipeline) SDImagePipelineTransformer 将多个 Transformer 串联;解码管线中 Coder 的选取也可视为「按责任链匹配 canDecodeFromData」 多步处理顺序清晰,便于组合与复用
单例 + 共享依赖(Singleton) SDWebImageManager.sharedSDImageCache.sharedSDWebImageDownloader.shared 提供默认实例,同时支持传入自定义 Cache/Loader 以打破单例 全局统一入口,又保留可测试性与多实例能力
观察者 / 回调(Observer / Callback) 通过 progresscompleted 闭包通知进度与结果;部分能力通过 delegate 扩展 异步结果与 UI 解耦
工厂思想(Factory) SDImageCodersManager 根据 Data 格式选择 Coder;Loader 根据 URL 或 scheme 选择具体 Loader 实现 创建逻辑集中,便于支持新协议与新格式

类图关系(概念层)

classDiagram
    class SDWebImageManager {
        -cache: SDImageCache
        -loader: SDImageLoader
        +loadImage(with:options:progress:completed:)
    }
    class SDImageCache {
        +store(_:forKey:)
        +queryImage(forKey:options:callback:)
    }
    class SDImageLoader {
        +loadImage(with:options:progress:completed:)
    }
    class SDImageCoder {
        <<protocol>>
        +decodedImageWithData:options:
        +canDecodeFromData:
    }
    class SDImageTransformer {
        <<protocol>>
        +transform(image:key:)
    }
    SDWebImageManager --> SDImageCache : 使用
    SDWebImageManager --> SDImageLoader : 使用
    SDWebImageManager ..> SDImageCoder : 解码时选用
    SDWebImageManager ..> SDImageTransformer : 变换时选用

2. 编程思想精华

SDWebImage 的编程思想可提炼为以下几点,对理解与模仿其设计很有帮助。

2.1 协议导向与「可替换实现」

  • CoderTransformerLoaderCache 均以协议或抽象接口呈现,具体实现可替换、可组合。
  • 新增一种图片格式或一种变换,只需实现对应协议并注册,无需改动 Manager 核心流程。这体现了开闭原则:对扩展开放,对修改关闭。

2.2 管线化与单一职责

  • 把「从 URL 到屏幕」拆成:加载 → 解码 → 变换 → 缓存 → 展示,每一步只做一件事。
  • 解码只关心 Data → Image,变换只关心 Image → Image,缓存只关心存储与查找。单一职责使每块可独立测试、优化和扩展;管线化则使数据流清晰,便于加日志、监控和插桩。

2.3 缓存键与「同一资源多形态」

  • 通过 key = f(URL, context) 的设计,同一 URL 可以对应「原图」「缩略图」「圆角图」等多条缓存,避免重复下载,又满足不同场景对尺寸/形态的需求。这体现了用键设计表达业务差异的思想。

2.4 后台处理与主线程回调

  • 解码、变换、磁盘 I/O 均在后台队列执行,completed 回调派发到主线程,兼顾性能与 UI 安全。这是移动端异步加载库的通用范式:重活放后台,结果回主线程

2.5 取消与生命周期绑定

  • View 扩展(如 UIImageView+WebCache)会把「当前正在进行的任务」与 view 绑定,当对同一 view 发起新请求时自动取消旧请求,避免错位和浪费。这体现了生命周期与请求绑定的思想,在列表场景中尤为重要。

2.6 配置通过 Context 透传

  • 不通过全局单例属性堆砌配置,而是通过 SDWebImageContext 在单次请求中传入 Cache、Transformer、Loader、CacheKeyFilter 等,使「同一 App 内不同页面/模块」可使用不同策略,且易于单元测试时注入 mock。

SDWebImage 编程思想精华一览

思想 在框架中的体现
协议导向、可替换 Coder / Transformer / Loader 协议化,新格式、新变换仅需实现协议并注册
管线化、单一职责 加载 → 解码 → 变换 → 缓存 → 展示,每步职责单一,便于扩展与测试
键设计表达多形态 同一 URL 通过 key = f(URL, context) 支持原图、缩略图、圆角图等多条缓存
后台处理、主线程回调 重 CPU/IO 在后台队列,completed 回主线程,兼顾性能与 UI 安全
生命周期绑定取消 View 与当前任务绑定,新请求自动取消旧请求,避免列表错位
Context 透传配置 单次请求级配置,避免全局状态,利于多策略并存与测试注入

八、使用示例与最佳实践

1. 使用内置变换器(缩放 + 圆角)

let transformer = SDImagePipelineTransformer(transformers: [
    SDImageResizingTransformer(size: CGSize(width: 300, height: 300), scaleMode: .fill),
    SDImageRoundCornerTransformer(radius: 20, corners: .allCorners, borderWidth: 0, borderColor: nil)
])
imageView.sd_setImage(with: url, placeholderImage: nil, context: [.imageTransformer: transformer])

2. 仅下载并变换、不写缓存

SDWebImageManager.shared.loadImage(
    with: url,
    options: [.fromLoaderOnly],
    context: [.storeCacheType: SDImageCacheType.none.rawValue, .imageTransformer: transformer],
    progress: nil
) { image, _, _, _, _, _ in
    // 使用变换后的 image
}

3. 渐进式加载(渐进解码)

imageView.sd_setImage(with: url, placeholderImage: nil, options: [.progressiveLoad])

4. 自定义 Coder 注册(以 WebP 为例)

SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)

5. 最佳实践小结

  • 列表/网格:cell 内先 sd_cancelCurrentImageLoad(),再 sd_setImage,并配合 Transformer 限制尺寸或使用下采样选项,减少内存与错位。
  • 预取:用 SDWebImagePrefetcher 或系统 prefetch API 提前加载即将出现的图片,滚动时优先命中缓存。
  • 大图/详情:列表用缩小版 Transformer,详情页用原图或单独 OriginalImageCache,避免重复下载。
  • 动图:使用 SDAnimatedImageView + SDAnimatedImage,并视情况注册 GIF/WebP/APNG 等 Coder。
  • 扩展与测试:自定义 Coder/Transformer 通过协议实现并注册;通过 context 注入自定义 Cache/Loader 便于单测与多策略并存。

九、常见面试题

1. 图片文件缓存的时间有多长?

一周。_maxCacheAge = kDefaultCacheMaxCacheAge

2. SDWebImage 的内存缓存是用什么实现的?

NSCache

3. SDWebImage 的最大并发数是多少?

maxConcurrentDownloads = 6(程序固定,可通过属性调整)

4. SDWebImage 支持动图吗?GIF

支持。示例:

#import <ImageIO/ImageIO.h>
[UIImage animatedImageWithImages:images duration:duration];

5. SDWebImage 是如何区分不同格式的图像的?

  • 根据图像数据第一个字节来判断
  • PNG:压缩比没有 JPG 高,但无损压缩,解压缩性能高,苹果推荐的图像格式
  • JPG:压缩比最高的一种图片格式,有损压缩,最多使用的场景如照相机,解压缩性能不好
  • GIF:序列帧动图,只支持 256 种颜色,曾流行于 1998~1999,有专利

6. SDWebImage 缓存图片的名称是怎么确定的?

  • 使用 md5 对完整 URL 做散列,得到 32 位字符串作为文件名;若单纯用文件名保存,重名几率高

7. SDWebImage 的内存警告是如何处理的?

  • 利用通知中心观察:
    • UIApplicationDidReceiveMemoryWarningNotification:接收到内存警告后执行 clearMemory,清理内存缓存
    • UIApplicationWillTerminateNotification:接收到应用将要终止后执行 cleanDisk,清理磁盘缓存
    • UIApplicationDidEnterBackgroundNotification:接收到应用进入后台后执行 backgroundCleanDisk,后台清理磁盘
  • 通过以上通知监听,保证缓存文件大小在控制范围内;clearDisk 可清空磁盘缓存,删除缓存目录中全部文件

参考文献

[1] Apple. Image and Graphics Best Practices. WWDC 2018, Session 219.
[2] Stack Overflow / Apple. Creating a thumbnail from UIImage using CGImageSourceCreateThumbnailAtIndex.
[3] Apple. Image decompression strategies for performance. developer.apple.com/forums/thread/653738.
[4] Ctrl.blog. Progressive JPEG loading; Google 研究:渐进解码约 3 倍于 baseline 的 CPU 开销.
[5] Wikipedia. Digital image processing.
[6] SDWebImage. Advanced Usage - Image Transformer, Custom Coder. GitHub Wiki.
[7] Beyer et al. FlexiViT: One Model for All Patch Sizes. CVPR 2023.
[8] NanoFLUX. Distillation-Driven Compression of Large Text-to-Image Generation Models for Mobile Devices. arXiv.
[9] SnapGen. Taming High-Resolution Text-to-Image Models for Mobile Devices. arXiv 2024.

04-研究优秀开源框架@响应式编程@iOS | RxSwift框架:从使用到源码解析

📋 目录


一、RxSwift框架使用详解

1. RxSwift框架概述

RxSwift 是 ReactiveX(Reactive Extensions)的 Swift 实现,是一个用于处理异步事件流的函数式响应式编程框架。

1.1 什么是RxSwift

RxSwift 基于观察者模式,允许你通过组合不同的操作符来处理异步事件序列。它提供了声明式的 API 来处理时间序列数据。

核心特点:

  • 响应式编程:基于观察者模式的事件驱动编程
  • 函数式编程:使用高阶函数和操作符组合
  • 类型安全:充分利用 Swift 的类型系统
  • 跨平台:基于 ReactiveX 标准,与其他平台一致
  • 丰富的操作符:提供大量操作符处理各种场景

1.2 RxSwift vs Combine

特性 RxSwift Combine
平台 跨平台(iOS、macOS、watchOS、tvOS) Apple 生态(iOS 13+)
语言 Swift Swift
官方支持 ❌ 第三方(ReactiveX) ✅ Apple 官方
最低版本 iOS 8.0+ iOS 13.0+
API风格 ReactiveX 标准 Apple 风格
学习曲线 陡峭 中等
生态 丰富(RxCocoa、RxDataSources等) 官方集成(SwiftUI)

1.3 RxSwift生态系统

  • RxSwift:核心框架
  • RxCocoa:UIKit/AppKit 集成
  • RxDataSources:TableView/CollectionView 数据源
  • RxTest:测试工具
  • RxBlocking:阻塞操作符(用于测试)

1.4 安装方式

CocoaPods:

pod 'RxSwift', '~> 6.0'
pod 'RxCocoa', '~> 6.0'

SPM:

dependencies: [
    .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.0.0")
]

1.5 编程思想(背后的范式与理念)

为什么要先谈编程思想?
会用 RxSwift 的 API(ObservablesubscribemapflatMap 等)不等于能写好响应式架构。很多「看起来能跑」的代码其实仍是用响应式语法写命令式逻辑(例如在 subscribe 里写满 if-else、嵌套请求),难以测试、难以复用。先理解背后的范式与理念,再写代码,才能做到「用对场景、写对抽象、边界清晰」。RxSwift 与 Combine 同属 ReactiveX 一脉,背后的编程思想高度一致;理解这些思想有助于写出更清晰、可维护的响应式代码。

范式定位:FRP(函数式响应式编程)
RxSwift 是 FRP(Functional Reactive Programming) 的一种实现:用函数式组合与不可变方式,处理响应式事件流。不是「要么函数式要么响应式」,而是两者结合——流用操作符做纯变换(函数式),用订阅对事件做出反应(响应式)。了解这一点,就不会把 Rx 单纯当成「另一种回调封装」,而是从「流 + 变换 + 订阅」的视角设计数据与 UI 的边界。


(1)响应式编程(Reactive Programming)

  • 核心:将「数据与事件」视为随时间发生的事件序列,通过订阅对序列中的每一项做出反应,而不是主动轮询或层层回调。
  • 在 RxSwift 中Observable 表示一条事件流,Observer 通过 subscribe 订阅后,在 onNext / onError / onCompleted 中响应;按钮点击、网络返回、定时器都可统一为 Observable,用同一套操作符处理。
  • 思维转变:从「先调 A,等回调再调 B」变为「当流里出现某类事件时,执行 B」,逻辑由数据/事件驱动。

(2)声明式 vs 命令式

维度 命令式(Imperative) 声明式(Declarative)
关注点 「怎么做」:显式控制顺序与分支 「做什么」:描述结果与数据变换关系
典型写法 for 循环、if-else、嵌套回调 链式操作符:map / filter / flatMap / combineLatest
在 RxSwift 中 手写「请求 → 回调里解析 → 再请求」 observable.map(...).flatMap(...).subscribe(...) 描述整条流水线

声明式让「数据从哪来、怎么变、到哪去」一目了然,便于阅读和单元测试。

从 OOP/命令式到响应式的思维转变:传统写法习惯「谁持有谁、谁调谁」——对象持有状态,方法里 if-else 控制流程,异步靠回调或 delegate。响应式则把「谁在什么时候产生什么」抽象成流,把「对数据的处理」抽象成操作符链,把「最终消费」放在订阅里。习惯后,你会先想「有哪些事件源」「它们如何组合、变换」,再写具体订阅逻辑,而不是一上来就写一堆属性和回调。

同一需求的两种写法对比(搜索框防抖 + 请求 + 只取非空):
命令式常见写法是:在文本回调里设 Timer、取消上一次请求、判断非空再发请求、在回调里更新 UI,逻辑分散在多处。用 RxSwift 可以写成一条「流」:

// 响应式:一条链描述「输入 → 防抖 → 非空过滤 → 请求 → 主线程更新」
searchTextField.rx.text.orEmpty
    .debounce(.milliseconds(300), scheduler: MainScheduler.instance)
    .filter { !$0.isEmpty }
    .flatMapLatest { query in api.search(query) }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { results in self.updateUI(results) })
    .disposed(by: disposeBag)

这样,「防抖」「过滤空串」「只保留最后一次请求」「切回主线程」都体现在操作符上,阅读时一眼能看出数据流;单元测试时可以对 Observable 链单独测,而不必依赖 UI。

(3)函数式思想(组合与不可变)

  • 组合(Composition):每个操作符只做一件事,通过 .map().filter().distinctUntilChanged() 等组合成完整逻辑,而不是在一个闭包里写尽所有逻辑。
  • 不可变(Immutability):操作符不修改原 Observable,而是返回新的 Observable;原流不变,便于复用和推理。
  • 副作用边界:纯变换放在操作符链中,副作用(UI 更新、写库、弹窗)集中在 subscribe 的闭包里,便于测试和并发安全。

(4)流与时间(Streams & Time)

  • 把所有「会随时间产生的事件」都视为时间序列:next、next、…、completed/error。
  • 时间相关操作符:debounce(静默一段时间后取最新)、throttle(间隔内只取第一个/最后一个)、delay(延后发射),统一表达「何时」而不只是「何值」。

(5)观察者与发布-订阅

  • 观察者模式:Observer 订阅 Observable,在事件发生时被通知。RxSwift 的 subscribe(onNext:onError:onCompleted:) 就是在注册观察者。
  • 发布-订阅:生产端(Observable)与消费端(Observer)解耦,通过 Disposable 表示一次订阅的生命周期;Rx 的「热/冷」流、背压(部分算子)都是在这一模型上的扩展。

(6)设计原则在 Rx 中的体现

原则 在 RxSwift 中的体现
单一职责 每个操作符只做一种变换(map 只做映射,filter 只做过滤),复杂逻辑由链式组合完成。
关注点分离 数据获取与变换在 Observable 链中,线程切换用 subscribeOn/observeOn,副作用集中在 subscribe
依赖倒置 业务依赖「Observable 流」的抽象,而不依赖具体如何产生事件(网络、本地、Mock 都可替换)。
开闭原则 通过新操作符或新 Observable 扩展行为,而不必修改已有链;原流不可变,易于复用。

小结:RxSwift 用声明式事件流(Observable)和可组合操作符,在观察者/发布-订阅模型下做响应式的异步与事件处理,并用 Scheduler 控制线程与时机。掌握这些思想后,再写「为什么用 map 而不是在 subscribe 里写一大段」「为什么需要 observeOn/subscribeOn」会更自然。


2. 核心概念

2.1 Observable(可观察序列)

Observable 是 RxSwift 的核心,表示可以观察的事件序列。

protocol ObservableType {
    associatedtype Element
    func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element
}

特点:

  • 可以发出零个或多个事件
  • 可能以完成或错误结束
  • 是值类型(struct)
  • 不可变(每次操作返回新的 Observable)

事件类型:

enum Event<Element> {
    case next(Element)      // 下一个元素
    case error(Swift.Error) // 错误
    case completed          // 完成
}

示例:

// 创建一个简单的 Observable
let observable = Observable<String>.just("Hello, RxSwift!")

observable.subscribe(onNext: { value in
    print(value)  // 输出: Hello, RxSwift!
}, onError: { error in
    print("错误: \(error)")
}, onCompleted: {
    print("完成")
})
.disposed(by: disposeBag)

// 使用数组创建 Observable
let arrayObservable = Observable.from([1, 2, 3, 4, 5])

arrayObservable.subscribe(onNext: { value in
    print(value)  // 依次输出: 1, 2, 3, 4, 5
})
.disposed(by: disposeBag)

2.2 Observer(观察者)

Observer 是接收 Observable 事件的协议。

protocol ObserverType {
    associatedtype Element
    func on(_ event: Event<Element>)
}

内置 Observer:

  • onNext:接收下一个元素
  • onError:接收错误
  • onCompleted:接收完成事件

示例:

let observable = Observable.from([1, 2, 3])

observable.subscribe(
    onNext: { value in
        print("收到值: \(value)")
    },
    onError: { error in
        print("错误: \(error)")
    },
    onCompleted: {
        print("完成")
    }
)
.disposed(by: disposeBag)

2.3 Disposable(可释放资源)

Disposable 表示订阅关系,用于取消订阅和释放资源。

protocol Disposable {
    func dispose()
}

DisposeBag:

class ViewController: UIViewController {
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Observable.just("Hello")
            .subscribe(onNext: { print($0) })
            .disposed(by: disposeBag)  // 自动管理生命周期
    }
}

3. Observable与Observer

3.1 创建Observable

just

创建只发出一个元素的 Observable。

let observable = Observable.just("Hello")
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
from

从数组或序列创建 Observable。

let observable = Observable.from([1, 2, 3])
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
of

从多个元素创建 Observable。

let observable = Observable.of(1, 2, 3)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
create

自定义创建 Observable。

let observable = Observable<String>.create { observer in
    observer.onNext("A")
    observer.onNext("B")
    observer.onCompleted()
    return Disposables.create()
}

observable.subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
empty

创建不发出任何元素的 Observable。

let observable = Observable<Int>.empty()
    .subscribe(
        onNext: { print($0) },
        onCompleted: { print("完成") }
    )
    .disposed(by: disposeBag)
// 输出: 完成
never

创建永不发出事件也永不完成的 Observable。

let observable = Observable<Int>.never()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 无输出
error

创建立即发出错误的 Observable。

enum MyError: Error {
    case customError
}

let observable = Observable<Int>.error(MyError.customError)
    .subscribe(
        onNext: { print($0) },
        onError: { print("错误: \($0)") }
    )
    .disposed(by: disposeBag)
// 输出: 错误: customError
range

创建发出指定范围内整数的 Observable。

let observable = Observable.range(start: 1, count: 5)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1, 2, 3, 4, 5
repeatElement

重复发出指定元素。

let observable = Observable.repeatElement("Hello")
    .take(3)  // 只取前3个
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: Hello, Hello, Hello
interval

按指定时间间隔发出整数。

let observable = Observable<Int>.interval(
    .seconds(1),
    scheduler: MainScheduler.instance
)
.take(5)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 每秒输出: 0, 1, 2, 3, 4
timer

延迟指定时间后发出元素。

let observable = Observable<Int>.timer(
    .seconds(2),
    scheduler: MainScheduler.instance
)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)
// 2秒后输出: 0

3.2 自定义Observable

struct CustomObservable<Element>: ObservableType {
    typealias Element = Element
    
    private let _subscribe: (AnyObserver<Element>) -> Disposable
    
    init(_ subscribe: @escaping (AnyObserver<Element>) -> Disposable) {
        self._subscribe = subscribe
    }
    
    func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element {
        let anyObserver = AnyObserver(observer)
        return _subscribe(anyObserver)
    }
}

// 使用
let custom = CustomObservable<Int> { observer in
    observer.onNext(1)
    observer.onNext(2)
    observer.onCompleted()
    return Disposables.create()
}

custom.subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

4. Operators操作符

4.1 转换操作符

map

转换每个元素。

Observable.from([1, 2, 3])
    .map { $0 * 2 }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 2, 4, 6
flatMap

将 Observable 发出的元素转换为 Observable,然后合并。

Observable.from(["A", "B", "C"])
    .flatMap { letter in
        Observable.from([1, 2]).map { "\(letter)\($0)" }
    }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: A1, A2, B1, B2, C1, C2
flatMapLatest

只保留最新的内部 Observable。

Observable.from(["A", "B", "C"])
    .flatMapLatest { letter in
        Observable.just(letter).delay(.seconds(1), scheduler: MainScheduler.instance)
    }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 只输出: C(A和B被取消)
scan

累积值。

Observable.from([1, 2, 3, 4, 5])
    .scan(0, accumulator: +)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1, 3, 6, 10, 15
buffer

缓冲元素。

Observable.from([1, 2, 3, 4, 5, 6, 7, 8])
    .buffer(timeSpan: .seconds(1), count: 3, scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: [1, 2, 3], [4, 5, 6], [7, 8]
window

将 Observable 分割为多个 Observable。

Observable.from([1, 2, 3, 4, 5, 6])
    .window(timeSpan: .seconds(1), count: 2, scheduler: MainScheduler.instance)
    .flatMap { $0 }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

4.2 过滤操作符

filter

过滤元素。

Observable.from([1, 2, 3, 4, 5])
    .filter { $0 % 2 == 0 }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 2, 4
distinctUntilChanged

移除连续重复的元素。

Observable.from([1, 1, 2, 2, 3, 3])
    .distinctUntilChanged()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1, 2, 3
elementAt

获取指定索引的元素。

Observable.from([1, 2, 3, 4, 5])
    .elementAt(2)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 3
first / last

获取第一个或最后一个元素。

Observable.from([1, 2, 3, 4, 5])
    .first()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1

Observable.from([1, 2, 3, 4, 5])
    .last()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 5
take / takeLast

获取前几个或后几个元素。

Observable.from([1, 2, 3, 4, 5])
    .take(3)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 1, 2, 3

Observable.from([1, 2, 3, 4, 5])
    .takeLast(3)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 3, 4, 5
skip / skipLast

跳过前几个或后几个元素。

Observable.from([1, 2, 3, 4, 5])
    .skip(2)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 3, 4, 5
debounce

防抖,等待指定时间后发出最新值。

let subject = PublishSubject<String>()

subject
    .debounce(.milliseconds(500), scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

subject.onNext("H")     // 不输出
subject.onNext("He")    // 不输出
subject.onNext("Hel")   // 不输出
subject.onNext("Hell")  // 不输出
subject.onNext("Hello") // 0.5秒后输出: Hello
throttle

节流,在指定时间间隔内只发出第一个值。

let subject = PublishSubject<String>()

subject
    .throttle(.seconds(1), scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

subject.onNext("A")  // 立即输出: A
subject.onNext("B")  // 不输出(1秒内)
subject.onNext("C")  // 不输出(1秒内)
// 1秒后
subject.onNext("D")  // 输出: D

4.3 组合操作符

startWith

在序列开始前插入元素。

Observable.from([1, 2, 3])
    .startWith(0)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 0, 1, 2, 3
merge

合并多个 Observable。

let subject1 = PublishSubject<Int>()
let subject2 = PublishSubject<Int>()

Observable.merge(subject1, subject2)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

subject1.onNext(1)  // 输出: 1
subject2.onNext(2)  // 输出: 2
subject1.onNext(3)  // 输出: 3
combineLatest

组合多个 Observable 的最新值。

let subject1 = PublishSubject<String>()
let subject2 = PublishSubject<Int>()

Observable.combineLatest(subject1, subject2)
    .subscribe(onNext: { value1, value2 in
        print("\(value1): \(value2)")
    })
    .disposed(by: disposeBag)

subject1.onNext("A")  // 无输出(等待 subject2)
subject2.onNext(1)    // 输出: A: 1
subject1.onNext("B")  // 输出: B: 1
subject2.onNext(2)    // 输出: B: 2
zip

按顺序组合多个 Observable。

let subject1 = PublishSubject<String>()
let subject2 = PublishSubject<Int>()

Observable.zip(subject1, subject2)
    .subscribe(onNext: { value1, value2 in
        print("\(value1): \(value2)")
    })
    .disposed(by: disposeBag)

subject1.onNext("A")  // 等待 subject2
subject1.onNext("B")  // 等待 subject2
subject2.onNext(1)    // 输出: A: 1
subject2.onNext(2)    // 输出: B: 2
withLatestFrom

当源 Observable 发出元素时,使用另一个 Observable 的最新值。

let button = PublishSubject<Void>()
let textField = PublishSubject<String>()

button
    .withLatestFrom(textField)
    .subscribe(onNext: { text in
        print("按钮点击,文本: \(text)")
    })
    .disposed(by: disposeBag)

textField.onNext("Hello")  // 无输出
textField.onNext("World")  // 无输出
button.onNext(())          // 输出: 按钮点击,文本: World
switchLatest

切换到最新的内部 Observable。

let subject1 = PublishSubject<Int>()
let subject2 = PublishSubject<Int>()
let source = PublishSubject<Observable<Int>>()

source
    .switchLatest()
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

source.onNext(subject1)
subject1.onNext(1)  // 输出: 1
subject1.onNext(2)  // 输出: 2

source.onNext(subject2)
subject1.onNext(3)  // 不输出(已切换)
subject2.onNext(4)  // 输出: 4

4.4 错误处理操作符

catchError

捕获错误并返回备用 Observable。

enum MyError: Error {
    case failure
}

let observable = Observable<String>.error(MyError.failure)
    .catchError { error -> Observable<String> in
        print("捕获错误: \(error)")
        return Observable.just("备用值")
    }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 捕获错误: failure, 备用值
catchErrorJustReturn

用默认值替换错误。

let observable = Observable<String>.error(MyError.failure)
    .catchErrorJustReturn("默认值")
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: 默认值
retry

重试失败的 Observable。

var attempts = 0

let observable = Observable<String>.create { observer in
    attempts += 1
    if attempts < 3 {
        observer.onError(MyError.failure)
    } else {
        observer.onNext("成功")
        observer.onCompleted()
    }
    return Disposables.create()
}
.retry(2)  // 最多重试 2 次
.subscribe(
    onNext: { print($0) },
    onError: { print("错误: \($0)") }
)
.disposed(by: disposeBag)
// 输出: 成功
retryWhen

根据条件重试。

let observable = Observable<String>.error(MyError.failure)
    .retryWhen { errors in
        errors.enumerated().flatMap { index, error -> Observable<Int> in
            if index < 2 {
                return Observable<Int>.timer(.seconds(index + 1), scheduler: MainScheduler.instance)
            } else {
                return Observable.error(error)
            }
        }
    }
    .subscribe(
        onNext: { print($0) },
        onError: { print("最终错误: \($0)") }
    )
    .disposed(by: disposeBag)

4.5 工具操作符

do

执行副作用操作。

Observable.from([1, 2, 3])
    .do(onNext: { print("即将发出: \($0)") },
        onError: { print("错误: \($0)") },
        onCompleted: { print("完成") },
        onSubscribe: { print("订阅") },
        onDispose: { print("释放") })
    .subscribe(onNext: { print("收到: \($0)") })
    .disposed(by: disposeBag)
delay

延迟发出元素。

Observable.from([1, 2, 3])
    .delay(.seconds(1), scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 1秒后依次输出: 1, 2, 3
delaySubscription

延迟订阅。

Observable.from([1, 2, 3])
    .delaySubscription(.seconds(1), scheduler: MainScheduler.instance)
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 1秒后开始输出: 1, 2, 3
materialize / dematerialize

将事件序列化/反序列化。

Observable.from([1, 2, 3])
    .materialize()
    .subscribe(onNext: { event in
        print(event)  // 输出: next(1), next(2), next(3), completed
    })
    .disposed(by: disposeBag)
timeout

超时处理。

Observable<Int>.never()
    .timeout(.seconds(2), scheduler: MainScheduler.instance)
    .subscribe(
        onNext: { print($0) },
        onError: { print("超时: \($0)") }
    )
    .disposed(by: disposeBag)
// 2秒后输出: 超时: RxError.timeout

5. Subjects

Subjects 既是 Observable 又是 Observer,可以手动发送事件。

5.1 PublishSubject

不保存当前值,只向订阅者发送订阅后的事件。

let subject = PublishSubject<String>()

// 订阅1
subject.subscribe(onNext: { print("订阅1: \($0)") })
    .disposed(by: disposeBag)

subject.onNext("A")  // 输出: 订阅1: A

// 订阅2
subject.subscribe(onNext: { print("订阅2: \($0)") })
    .disposed(by: disposeBag)

subject.onNext("B")  // 输出: 订阅1: B, 订阅2: B
subject.onCompleted()

5.2 BehaviorSubject

保存当前值,新订阅者会立即收到当前值。

let subject = BehaviorSubject<String>(value: "初始值")

// 订阅1
subject.subscribe(onNext: { print("订阅1: \($0)") })
    .disposed(by: disposeBag)
// 输出: 订阅1: 初始值

subject.onNext("新值")  // 输出: 订阅1: 新值

// 订阅2
subject.subscribe(onNext: { print("订阅2: \($0)") })
    .disposed(by: disposeBag)
// 输出: 订阅2: 新值(立即收到当前值)

5.3 ReplaySubject

保存指定数量的最近值,新订阅者会收到这些值。

let subject = ReplaySubject<String>.create(bufferSize: 2)

subject.onNext("A")
subject.onNext("B")
subject.onNext("C")

// 订阅
subject.subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)
// 输出: B, C(最近2个值)

5.4 AsyncSubject

只发出最后一个值(在完成时)。

let subject = AsyncSubject<String>()

subject.subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

subject.onNext("A")  // 不输出
subject.onNext("B")  // 不输出
subject.onNext("C")  // 不输出
subject.onCompleted()  // 输出: C

6. Schedulers调度器

Schedulers 决定操作在哪个线程执行。

6.1 内置Scheduler

MainScheduler

主线程调度器。

Observable.just(1)
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { value in
        // 在主线程执行
        print(Thread.isMainThread)  // true
    })
    .disposed(by: disposeBag)
SerialDispatchQueueScheduler

串行队列调度器。

let scheduler = SerialDispatchQueueScheduler(
    qos: .userInitiated,
    internalSerialQueueName: "custom.queue"
)

Observable.just(1)
    .observeOn(scheduler)
    .subscribe(onNext: { value in
        // 在后台线程执行
    })
    .disposed(by: disposeBag)
ConcurrentDispatchQueueScheduler

并发队列调度器。

let scheduler = ConcurrentDispatchQueueScheduler(
    qos: .background
)

Observable.from([1, 2, 3])
    .observeOn(scheduler)
    .subscribe(onNext: { value in
        // 在后台线程执行
    })
    .disposed(by: disposeBag)
OperationQueueScheduler

操作队列调度器。

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

let scheduler = OperationQueueScheduler(operationQueue: queue)

Observable.from([1, 2, 3, 4, 5])
    .observeOn(scheduler)
    .subscribe(onNext: { value in
        // 在操作队列执行
    })
    .disposed(by: disposeBag)

6.2 subscribeOn vs observeOn

  • subscribeOn:指定订阅在哪个线程执行
  • observeOn:指定后续操作在哪个线程执行
Observable.create { observer in
    print("订阅线程: \(Thread.current)")
    observer.onNext(1)
    observer.onCompleted()
    return Disposables.create()
}
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.observeOn(MainScheduler.instance)
.subscribe(onNext: { value in
    print("接收线程: \(Thread.current)")
})
.disposed(by: disposeBag)

7. 错误处理

7.1 错误类型

enum RxError: Swift.Error {
    case unknown
    case disposed
    case timeout
    case noElements
    case moreThanOneElement
}

enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

7.2 错误处理策略

func fetchData() -> Observable<String> {
    return Observable.create { observer in
        // 模拟网络请求
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            observer.onError(NetworkError.noData)
        }
        return Disposables.create()
    }
}

fetchData()
    .catchError { error -> Observable<String> in
        // 捕获错误,返回备用 Observable
        return Observable.just("默认数据")
    }
    .retry(3)  // 重试 3 次
    .subscribe(
        onNext: { print($0) },
        onError: { print("最终错误: \($0)") }
    )
    .disposed(by: disposeBag)

8. 内存管理

8.1 DisposeBag

自动管理订阅的生命周期。

class ViewController: UIViewController {
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Observable.just("Hello")
            .subscribe(onNext: { print($0) })
            .disposed(by: disposeBag)  // 自动管理
    }
    
    // viewController 释放时,disposeBag 会自动释放所有订阅
}

8.2 避免循环引用

class ViewModel {
    private let disposeBag = DisposeBag()
    
    func setup() {
        Observable.just("Data")
            .subscribe(onNext: { [weak self] value in
                // 使用 weak self 避免循环引用
                self?.process(value)
            })
            .disposed(by: disposeBag)
    }
    
    private func process(_ value: String) {
        // 处理数据
    }
}

8.3 takeUntil

在指定条件满足时自动取消订阅。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Observable.interval(.seconds(1), scheduler: MainScheduler.instance)
            .takeUntil(self.rx.deallocated)  // viewController 释放时自动取消
            .subscribe(onNext: { print($0) })
            .disposed(by: disposeBag)
    }
}

9. 与UIKit集成

9.1 RxCocoa基础

RxCocoa 提供了 UIKit 的 Rx 扩展。

import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var button: UIButton!
    @IBOutlet weak var label: UILabel!
    
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 文本输入绑定
        textField.rx.text
            .bind(to: label.rx.text)
            .disposed(by: disposeBag)
        
        // 按钮点击
        button.rx.tap
            .subscribe(onNext: { [weak self] in
                self?.handleButtonTap()
            })
            .disposed(by: disposeBag)
    }
}

9.2 常用绑定

// UILabel
label.rx.text.onNext("Hello")
label.rx.attributedText.onNext(attributedString)

// UITextField
textField.rx.text
    .subscribe(onNext: { text in
        print("文本: \(text ?? "")")
    })
    .disposed(by: disposeBag)

// UIButton
button.rx.tap
    .subscribe(onNext: {
        print("按钮点击")
    })
    .disposed(by: disposeBag)

// UISwitch
switch.rx.isOn
    .subscribe(onNext: { isOn in
        print("开关: \(isOn)")
    })
    .disposed(by: disposeBag)

// UISlider
slider.rx.value
    .subscribe(onNext: { value in
        print("值: \(value)")
    })
    .disposed(by: disposeBag)

9.3 TableView绑定

import RxDataSources

class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    
    private let disposeBag = DisposeBag()
    private let items = BehaviorSubject<[String]>(value: ["Item 1", "Item 2", "Item 3"])
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let dataSource = RxTableViewSectionedReloadDataSource<String> { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = item
            return cell
        }
        
        items
            .map { [SectionModel(model: "", items: $0)] }
            .bind(to: tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
    }
}

10. 实际应用场景

10.1 网络请求

struct API {
    static func fetchUser(id: Int) -> Observable<User> {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        
        return URLSession.shared.rx.data(request: URLRequest(url: url))
            .map { data in
                try JSONDecoder().decode(User.self, from: data)
            }
            .observeOn(MainScheduler.instance)
    }
}

API.fetchUser(id: 1)
    .subscribe(
        onNext: { user in
            print("用户: \(user)")
        },
        onError: { error in
            print("错误: \(error)")
        }
    )
    .disposed(by: disposeBag)

10.2 用户输入处理

class SearchViewModel {
    private let disposeBag = DisposeBag()
    let searchText = BehaviorSubject<String>(value: "")
    let results = BehaviorSubject<[String]>(value: [])
    
    init() {
        searchText
            .debounce(.milliseconds(500), scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .filter { !$0.isEmpty }
            .flatMapLatest { query -> Observable<[String]> in
                return self.search(query: query)
                    .catchErrorJustReturn([])
            }
            .bind(to: results)
            .disposed(by: disposeBag)
    }
    
    private func search(query: String) -> Observable<[String]> {
        // 实现搜索逻辑
        return Observable.just(["结果1", "结果2"])
    }
}

10.3 组合多个数据源

class DashboardViewModel {
    private let disposeBag = DisposeBag()
    let user = BehaviorSubject<User?>(value: nil)
    let posts = BehaviorSubject<[Post]>(value: [])
    let isLoading = BehaviorSubject<Bool>(value: false)
    
    func loadData() {
        isLoading.onNext(true)
        
        let userObservable = API.fetchUser(id: 1)
        let postsObservable = API.fetchPosts()
        
        Observable.zip(userObservable, postsObservable)
            .observeOn(MainScheduler.instance)
            .subscribe(
                onNext: { [weak self] user, posts in
                    self?.user.onNext(user)
                    self?.posts.onNext(posts)
                    self?.isLoading.onNext(false)
                },
                onError: { [weak self] error in
                    self?.isLoading.onNext(false)
                    print("错误: \(error)")
                }
            )
            .disposed(by: disposeBag)
    }
}

10.4 表单验证(多字段实时校验)

多字段表单:用户名、密码、确认密码实时校验,用 combineLatest 聚合多流,用 map 产出错误文案或是否可提交。

class FormViewModel {
    private let disposeBag = DisposeBag()

    let username = BehaviorRelay<String>(value: "")
    let password = BehaviorRelay<String>(value: "")
    let confirmPassword = BehaviorRelay<String>(value: "")

    let usernameError = BehaviorRelay<String?>(value: nil)
    let isFormValid = BehaviorRelay<Bool>(value: false)

    init() {
        // 用户名:非空 + 长度
        username
            .map { name in
                if name.isEmpty { return "请输入用户名" }
                if name.count < 3 { return "至少 3 个字符" }
                return nil
            }
            .bind(to: usernameError)
            .disposed(by: disposeBag)

        // 三字段 combineLatest,任一变化都重新计算表单是否有效
        Observable.combineLatest(username, password, confirmPassword)
            .map { name, pwd, confirm in
                if name.isEmpty || pwd.isEmpty { return false }
                if pwd != confirm { return false }
                if pwd.count < 6 { return false }
                return true
            }
            .bind(to: isFormValid)
            .disposed(by: disposeBag)
    }
}

// VC 中绑定
viewModel.isFormValid
    .bind(to: submitButton.rx.isEnabled)
    .disposed(by: disposeBag)
viewModel.usernameError
    .bind(to: usernameErrorLabel.rx.text)
    .disposed(by: disposeBag)

10.5 NotificationCenter 转 Observable

系统通知或自定义通知转为 Observable,便于在链中 mapfilterobserveOn

// 键盘即将显示:取键盘 frame
let keyboardWillShow = NotificationCenter.default.rx
    .notification(UIResponder.keyboardWillShowNotification)
    .map { notification -> CGRect in
        (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? .zero
    }
    .observeOn(MainScheduler.instance)

keyboardWillShow
    .subscribe(onNext: { frame in
        print("键盘高度: \(frame.height)")
    })
    .disposed(by: disposeBag)

// 自定义通知
extension Notification.Name {
    static let myCustomEvent = Notification.Name("MyCustomEvent")
}
let customObservable = NotificationCenter.default.rx.notification(.myCustomEvent)

10.6 Timer 与周期任务

Observable.interval 做定时轮询,或用 Observable.timer 做延迟/单次任务。

// 每 1 秒发一个递增整数,主线程接收
let timerObservable = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
    .take(10)  // 只取 10 次
    .subscribe(onNext: { tick in
        print("tick: \(tick)")
    })
    .disposed(by: disposeBag)

// 延迟 2 秒后执行一次
Observable<Int>.timer(.seconds(2), scheduler: MainScheduler.instance)
    .subscribe(onNext: { _ in
        print("2 秒后执行")
    })
    .disposed(by: disposeBag)

// 轮询接口:每 5 秒请求一次,直到满足条件
Observable<Int>.interval(.seconds(5), scheduler: MainScheduler.instance)
    .flatMapLatest { _ in API.pollStatus() }
    .takeWhile { !$0.isDone }
    .subscribe(onNext: { status in
        print("状态: \(status)")
    })
    .disposed(by: disposeBag)

10.7 请求重试与超时

retry 在失败时重新订阅上游;timeout 超时未完成则发 error;配合 catchError 做兜底。

URLSession.shared.rx.data(request: request)
    .timeout(.seconds(10), scheduler: MainScheduler.instance)
    .retry(3)
    .map { data in try JSONDecoder().decode(User.self, from: data) }
    .catchError { _ in Observable.just(User.placeholder) }
    .observeOn(MainScheduler.instance)
    .subscribe(
        onNext: { user in
            // 更新 UI
        },
        onError: { error in
            print("错误: \(error)")
        }
    )
    .disposed(by: disposeBag)

10.8 多源竞速(主备 / race)

主接口失败时切到备用接口,用 catchError 切流;或 merge + take(1) 实现「谁先完成用谁」。

// 主接口失败时用备用接口
func loadFromPrimaryOrFallback() -> Observable<Data> {
    let primary = URLSession.shared.rx.data(request: primaryRequest)
    let fallback = URLSession.shared.rx.data(request: fallbackRequest)
    return primary.catchError { _ in fallback }
}

// 显式 race:两个请求谁先完成用谁
func race<Element>(_ a: Observable<Element>, _ b: Observable<Element>) -> Observable<Element> {
    Observable.merge(a, b).take(1)
}

10.9 节流与防抖组合(搜索 + 按钮防重复点击)

搜索框用 debounce 减少请求频率;提交按钮用 throttle 防止连续点击重复提交。

// 搜索:防抖 + 去重 + 非空 + flatMapLatest 只保留最后一次请求
searchBar.rx.text.orEmpty
    .debounce(.milliseconds(400), scheduler: MainScheduler.instance)
    .distinctUntilChanged()
    .filter { !$0.isEmpty }
    .flatMapLatest { query in
        API.search(query: query).catchErrorJustReturn([])
    }
    .observeOn(MainScheduler.instance)
    .bind(to: results)
    .disposed(by: disposeBag)

// 提交按钮:节流 1 秒内只响应一次
submitButton.rx.tap
    .throttle(.seconds(1), scheduler: MainScheduler.instance)
    .subscribe(onNext: { [weak self] in
        self?.submit()
    })
    .disposed(by: disposeBag)

10.10 RxCocoa 进阶:UISearchBar、RefreshControl、DelegateProxy

UISearchBarrx.textrx.searchButtonClicked 组合做「点击搜索」或「实时搜索」。

// 点击搜索按钮时用当前文本请求
searchBar.rx.searchButtonClicked
    .withLatestFrom(searchBar.rx.text.orEmpty)
    .filter { !$0.isEmpty }
    .flatMapLatest { API.search(query: $0) }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { [weak self] results in
        self?.updateResults(results)
    })
    .disposed(by: disposeBag)

UIRefreshControl:下拉刷新与 isRefreshing 绑定。

refreshControl.rx.controlEvent(.valueChanged)
    .flatMapLatest { [weak self] _ in
        self?.loadData() ?? Observable.never()
    }
    .observeOn(MainScheduler.instance)
    .subscribe(
        onNext: { [weak self] _ in
            self?.refreshControl.endRefreshing()
        },
        onError: { [weak self] _ in
            self?.refreshControl.endRefreshing()
        }
    )
    .disposed(by: disposeBag)

DelegateProxy 示例(UITableView 点击):RxCocoa 已为常用控件提供 rx 扩展,如需自定义可继承 DelegateProxy

// 使用 RxCocoa 内置的 itemSelected
tableView.rx.itemSelected
    .subscribe(onNext: { indexPath in
        print("选中: \(indexPath)")
    })
    .disposed(by: disposeBag)

tableView.rx.modelSelected(Item.self)
    .subscribe(onNext: { item in
        print("选中项: \(item)")
    })
    .disposed(by: disposeBag)

10.11 页面生命周期与 takeUntil

在 VC 中让订阅随页面消失而自动取消:用 rx.deallocatingtakeUntil(self.rx.deallocated),避免重复订阅和泄漏。

class ViewController: UIViewController {
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 方式一:统一丢进 disposeBag,VC 释放时一起 dispose
        someObservable
            .subscribe(onNext: { })
            .disposed(by: disposeBag)

        // 方式二:显式「直到某事件发生就结束」(如直到页面即将消失)
        someObservable
            .takeUntil(rx.deallocated)
            .subscribe(onNext: { })
            .disposed(by: disposeBag)
    }
}

10.12 CollectionView 与 RxDataSources

使用 RxDataSources 的 Section 模型驱动 UICollectionView,与 TableView 用法类似(Item 为业务模型类型,需与 Cell 一致)。

import RxDataSources

typealias Section = SectionModel<String, Item>  // Item 为业务模型
let dataSource = RxCollectionViewSectionedReloadDataSource<Section> { dataSource, collectionView, indexPath, item in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! ItemCell
    cell.configure(with: item)
    return cell
}

items
    .map { [Section(model: "列表", items: $0)] }
    .bind(to: collectionView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

10.13 双向绑定与 ControlProperty

RxCocoa 的 ControlProperty 支持双向绑定:一方是「用户输入」,一方是「模型/ViewModel」。

// 将 TextField 与 BehaviorRelay 双向绑定(需自己写绑定逻辑,或使用 RxCocoa 的 bind)
// 单向:ViewModel -> UI
viewModel.username
    .bind(to: textField.rx.text)
    .disposed(by: disposeBag)

// 单向:UI -> ViewModel
textField.rx.text.orEmpty
    .bind(to: viewModel.username)
    .disposed(by: disposeBag)

// 若需「初始值 + 用户修改都同步」,两行都写即可(Relay 与控件类型匹配时)

10.14 错误流与用户提示

将网络/业务错误统一转为「可展示的提示」,用 materialize()catchError 转成另一种元素类型,再在 UI 层订阅。

API.fetchUser(id: 1)
    .materialize()
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { [weak self] event in
        switch event {
        case .next(let user):
            self?.showUser(user)
        case .error(let error):
            self?.showToast("加载失败: \(error.localizedDescription)")
        case .completed:
            break
        }
    })
    .disposed(by: disposeBag)

二、RxSwift框架源码解析

1. 架构设计

1.1 整体架构

RxSwift 采用协议导向的设计,核心是三个协议:

ObservableType (可观察类型)
    ↓
ObserverType (观察者类型)
    ↓
Disposable (可释放资源)

数据流:

Observable → Observer
     ↑          ↓
     └── 反馈 ──┘

1.2 核心协议层次

// 第一层:ObservableType 协议
protocol ObservableType {
    associatedtype Element
    func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element
}

// 第二层:ObserverType 协议
protocol ObserverType {
    associatedtype Element
    func on(_ event: Event<Element>)
}

// 第三层:Disposable 协议
protocol Disposable {
    func dispose()
}

1.3 事件类型

enum Event<Element> {
    case next(Element)
    case error(Swift.Error)
    case completed
}

2. Observable协议实现

2.1 ObservableType协议定义

public protocol ObservableType {
    associatedtype Element
    
    func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element
}

2.2 Observable实现

public class Observable<Element>: ObservableType {
    public typealias Element = Element
    
    internal init() {}
    
    public func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element {
        rxAbstractMethod()
    }
    
    public func asObservable() -> Observable<Element> {
        return self
    }
}

关键点:

  • Observable 是抽象类
  • subscribe 方法需要子类实现
  • 使用 rxAbstractMethod() 防止直接实例化

2.3 Just实现分析

final private class Just<Element>: Producer<Element> {
    private let element: Element
    
    init(element: Element) {
        self.element = element
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == Element {
        let sink = JustSink(parent: self, observer: observer, cancel: cancel)
        let subscription = sink.run()
        return (sink: sink, subscription: subscription)
    }
}

final private class JustSink<Observer: ObserverType>: Sink<Observer>, ObserverType {
    typealias Element = Observer.Element
    typealias Parent = Just<Element>
    
    private let parent: Parent
    
    init(parent: Parent, observer: Observer, cancel: Cancelable) {
        self.parent = parent
        super.init(observer: observer, cancel: cancel)
    }
    
    func on(_ event: Event<Element>) {
        switch event {
        case .next:
            forwardOn(.next(parent.element))
            forwardOn(.completed)
            self.dispose()
        case .error, .completed:
            forwardOn(event)
            self.dispose()
        }
    }
    
    func run() -> Disposable {
        forwardOn(.next(parent.element))
        forwardOn(.completed)
        return Disposables.create()
    }
}

关键点:

  • Just 继承自 Producer
  • 使用 JustSink 处理订阅逻辑
  • 立即发出元素并完成

2.4 Create实现分析

final private class AnonymousObservable<Element>: Producer<Element> {
    typealias SubscribeHandler = (AnyObserver<Element>) -> Disposable
    
    private let subscribeHandler: SubscribeHandler
    
    init(_ subscribeHandler: @escaping SubscribeHandler) {
        self.subscribeHandler = subscribeHandler
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == Element {
        let sink = AnonymousObservableSink(observer: observer, cancel: cancel)
        let subscription = sink.run(self)
        return (sink: sink, subscription: subscription)
    }
}

final private class AnonymousObservableSink<Observer: ObserverType>: Sink<Observer>, ObserverType {
    typealias Element = Observer.Element
    typealias Parent = AnonymousObservable<Element>
    
    private let parent: Parent
    
    init(observer: Observer, cancel: Cancelable) {
        self.parent = AnonymousObservable(subscribeHandler: { observer in
            // 包装观察者
            return Disposables.create()
        })
        super.init(observer: observer, cancel: cancel)
    }
    
    func on(_ event: Event<Element>) {
        switch event {
        case .next:
            forwardOn(event)
        case .error, .completed:
            forwardOn(event)
            self.dispose()
        }
    }
    
    func run(_ parent: Parent) -> Disposable {
        return parent.subscribeHandler(AnyObserver(self))
    }
}

关键点:

  • AnonymousObservable 使用闭包创建
  • AnyObserver 包装观察者
  • 支持自定义订阅逻辑

3. Observer协议实现

3.1 ObserverType协议定义

public protocol ObserverType {
    associatedtype Element
    
    func on(_ event: Event<Element>)
}

3.2 AnyObserver实现

public struct AnyObserver<Element>: ObserverType {
    public typealias Element = Element
    
    private let observer: AnyObserverBase<Element>
    
    public init<Observer: ObserverType>(_ observer: Observer)
        where Observer.Element == Element {
        self.observer = ObserverBox(observer)
    }
    
    public func on(_ event: Event<Element>) {
        observer.on(event)
    }
}

private class AnyObserverBase<Element>: ObserverType {
    func on(_ event: Event<Element>) {
        rxAbstractMethod()
    }
}

private final class ObserverBox<Observer: ObserverType>: AnyObserverBase<Observer.Element> {
    private let observer: Observer
    
    init(_ observer: Observer) {
        self.observer = observer
    }
    
    override func on(_ event: Event<Observer.Element>) {
        observer.on(event)
    }
}

关键点:

  • AnyObserver 是类型擦除包装器
  • 使用 ObserverBox 存储具体观察者
  • 实现观察者的多态

3.3 Sink实现

class Sink<Observer: ObserverType>: Disposable {
    typealias Element = Observer.Element
    
    private let observer: Observer
    private let cancel: Cancelable
    private var disposed = false
    
    init(observer: Observer, cancel: Cancelable) {
        self.observer = observer
        self.cancel = cancel
    }
    
    final func forwardOn(_ event: Event<Element>) {
        if isDisposed {
            return
        }
        observer.on(event)
    }
    
    final func forwardOn(_ event: Event<Element>, _ disposeHandler: @escaping () -> Void) {
        if isDisposed {
            return
        }
        observer.on(event)
        disposeHandler()
    }
    
    func dispose() {
        if !disposed {
            disposed = true
            cancel.dispose()
        }
    }
    
    var isDisposed: Bool {
        return disposed
    }
}

关键点:

  • Sink 是观察者的基类
  • 提供 forwardOn 方法转发事件
  • 管理订阅的生命周期

4. Operators实现原理

4.1 Map操作符实现

extension ObservableType {
    public func map<Result>(_ transform: @escaping (Element) -> Result) -> Observable<Result> {
        return Map(source: self.asObservable(), transform: transform)
    }
}

final private class Map<SourceType, ResultType>: Producer<ResultType> {
    typealias Transform = (SourceType) -> ResultType
    
    private let source: Observable<SourceType>
    private let transform: Transform
    
    init(source: Observable<SourceType>, transform: @escaping Transform) {
        self.source = source
        self.transform = transform
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == ResultType {
        let sink = MapSink(transform: transform, observer: observer, cancel: cancel)
        let subscription = source.subscribe(sink)
        return (sink: sink, subscription: subscription)
    }
}

final private class MapSink<SourceType, Observer: ObserverType>: Sink<Observer>, ObserverType {
    typealias ResultType = Observer.Element
    typealias Transform = (SourceType) -> ResultType
    
    private let transform: Transform
    
    init(transform: @escaping Transform, observer: Observer, cancel: Cancelable) {
        self.transform = transform
        super.init(observer: observer, cancel: cancel)
    }
    
    func on(_ event: Event<SourceType>) {
        switch event {
        case .next(let element):
            do {
                let mappedElement = try transform(element)
                forwardOn(.next(mappedElement))
            } catch {
                forwardOn(.error(error))
                dispose()
            }
        case .error(let error):
            forwardOn(.error(error))
            dispose()
        case .completed:
            forwardOn(.completed)
            dispose()
        }
    }
}

关键点:

  • Map 是新的 Observable,包装源 Observable
  • 创建 MapSink 进行转换
  • 错误处理:转换失败时发出错误

4.2 Filter操作符实现

extension ObservableType {
    public func filter(_ predicate: @escaping (Element) -> Bool) -> Observable<Element> {
        return Filter(source: self.asObservable(), predicate: predicate)
    }
}

final private class Filter<Element>: Producer<Element> {
    typealias Predicate = (Element) -> Bool
    
    private let source: Observable<Element>
    private let predicate: Predicate
    
    init(source: Observable<Element>, predicate: @escaping Predicate) {
        self.source = source
        self.predicate = predicate
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == Element {
        let sink = FilterSink(predicate: predicate, observer: observer, cancel: cancel)
        let subscription = source.subscribe(sink)
        return (sink: sink, subscription: subscription)
    }
}

final private class FilterSink<Observer: ObserverType>: Sink<Observer>, ObserverType {
    typealias Element = Observer.Element
    typealias Predicate = (Element) -> Bool
    
    private let predicate: Predicate
    
    init(predicate: @escaping Predicate, observer: Observer, cancel: Cancelable) {
        self.predicate = predicate
        super.init(observer: observer, cancel: cancel)
    }
    
    func on(_ event: Event<Element>) {
        switch event {
        case .next(let element):
            do {
                let satisfies = try predicate(element)
                if satisfies {
                    forwardOn(.next(element))
                }
            } catch {
                forwardOn(.error(error))
                dispose()
            }
        case .error, .completed:
            forwardOn(event)
            dispose()
        }
    }
}

关键点:

  • 不满足条件时不转发事件
  • 满足条件时才传递给下游

4.3 FlatMap操作符实现

extension ObservableType {
    public func flatMap<Source: ObservableConvertibleType>(
        _ selector: @escaping (Element) -> Source
    ) -> Observable<Source.Element> {
        return FlatMap(source: self.asObservable(), selector: selector)
    }
}

final private class FlatMap<SourceElement, SourceSequence: ObservableConvertibleType>: Producer<SourceSequence.Element> {
    typealias Selector = (SourceElement) -> SourceSequence
    
    private let source: Observable<SourceElement>
    private let selector: Selector
    
    init(source: Observable<SourceElement>, selector: @escaping Selector) {
        self.source = source
        self.selector = selector
    }
    
    override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable)
        where Observer.Element == SourceSequence.Element {
        let sink = FlatMapSink(selector: selector, observer: observer, cancel: cancel)
        let subscription = sink.run(source)
        return (sink: sink, subscription: subscription)
    }
}

final private class FlatMapSink<SourceElement, SourceSequence: ObservableConvertibleType, Observer: ObserverType>: MergeSink<SourceSequence, Observer>
    where Observer.Element == SourceSequence.Element {
    
    typealias Selector = (SourceElement) -> SourceSequence
    
    private let selector: Selector
    
    init(selector: @escaping Selector, observer: Observer, cancel: Cancelable) {
        self.selector = selector
        super.init(observer: observer, cancel: cancel)
    }
    
    override func on(_ event: Event<SourceElement>) {
        switch event {
        case .next(let element):
            do {
                let innerObservable = try selector(element).asObservable()
                subscribeInner(innerObservable, group: group)
            } catch {
                forwardOn(.error(error))
                dispose()
            }
        case .error(let error):
            forwardOn(.error(error))
            dispose()
        case .completed:
            groupCompleted()
        }
    }
}

关键点:

  • 管理多个内部 Observable 订阅
  • 使用 MergeSink 合并结果
  • 需要复杂的生命周期管理

5. Subjects实现原理

5.1 PublishSubject实现

public final class PublishSubject<Element>: Observable<Element>, SubjectType, Cancelable, ObserverType, SynchronizedUnsubscribeType {
    public typealias SubjectObserverType = PublishSubject<Element>
    
    typealias Observers = AnyObserver<Element>.s
    typealias DisposeKey = Observers.KeyType
    
    private let lock = RecursiveLock()
    private var observers: Observers = Observers()
    private var isDisposed = false
    private var stopped = false
    private var stoppedEvent: Event<Element>?
    
    public override init() {
        super.init()
    }
    
    public func on(_ event: Event<Element>) {
        dispatch(synchronized_on(event), event)
    }
    
    func synchronized_on(_ event: Event<Element>) -> Observers {
        lock.lock()
        defer { lock.unlock() }
        
        switch event {
        case .next:
            if isDisposed || stopped {
                return Observers()
            }
            return observers
        case .completed, .error:
            if stoppedEvent == nil {
                stoppedEvent = event
                stopped = true
                let observers = self.observers
                self.observers.removeAll()
                return observers
            }
            return Observers()
        }
    }
    
    public override func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element {
        lock.lock()
        defer { lock.unlock() }
        
        if let stoppedEvent = stoppedEvent {
            observer.on(stoppedEvent)
            return Disposables.create()
        }
        
        if isDisposed {
            observer.on(.error(RxError.disposed))
            return Disposables.create()
        }
        
        let key = observers.insert(observer.on)
        return SubscriptionDisposable(owner: self, key: key)
    }
    
    func synchronizedUnsubscribe(_ disposeKey: DisposeKey) {
        lock.lock()
        defer { lock.unlock() }
        observers.removeKey(disposeKey)
    }
}

关键点:

  • 使用锁保护 observers 集合
  • 不保存当前值,新订阅者不会收到历史值
  • 使用 SubscriptionDisposable 管理订阅

5.2 BehaviorSubject实现

public final class BehaviorSubject<Element>: Observable<Element>, SubjectType, ObserverType, SynchronizedUnsubscribeType {
    public typealias SubjectObserverType = BehaviorSubject<Element>
    
    typealias Observers = AnyObserver<Element>.s
    typealias DisposeKey = Observers.KeyType
    
    private let lock = RecursiveLock()
    private var observers: Observers = Observers()
    private var isDisposed = false
    private var stoppedEvent: Event<Element>?
    private var element: Element
    
    public init(value: Element) {
        self.element = value
        super.init()
    }
    
    public var value: Element {
        lock.lock()
        defer { lock.unlock() }
        return element
    }
    
    public func on(_ event: Event<Element>) {
        dispatch(synchronized_on(event), event)
    }
    
    func synchronized_on(_ event: Event<Element>) -> Observers {
        lock.lock()
        defer { lock.unlock() }
        
        if stoppedEvent != nil || isDisposed {
            return Observers()
        }
        
        switch event {
        case .next(let element):
            self.element = element
        case .error, .completed:
            stoppedEvent = event
        }
        
        return observers
    }
    
    public override func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable
        where Observer.Element == Element {
        lock.lock()
        defer { lock.unlock() }
        
        if let stoppedEvent = stoppedEvent {
            observer.on(stoppedEvent)
            return Disposables.create()
        }
        
        if isDisposed {
            observer.on(.error(RxError.disposed))
            return Disposables.create()
        }
        
        let key = observers.insert(observer.on)
        observer.on(.next(element))  // 立即发送当前值
        
        return SubscriptionDisposable(owner: self, key: key)
    }
}

关键点:

  • 保存当前值 element
  • 新订阅者立即收到当前值
  • 使用锁保护状态

6. Schedulers实现原理

6.1 SchedulerType协议

public protocol SchedulerType {
    var now: RxTime { get }
    
    func schedule<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable
    
    func scheduleRelative<StateType>(_ state: StateType, dueTime: RxTimeInterval, action: @escaping (StateType) -> Disposable) -> Disposable
    
    func schedulePeriodic<StateType>(_ state: StateType, startAfter: RxTimeInterval, period: RxTimeInterval, action: @escaping (StateType) -> StateType) -> Disposable
}

6.2 MainScheduler实现

public final class MainScheduler: SerialDispatchQueueScheduler {
    private let mainQueue: DispatchQueue
    
    public static let instance = MainScheduler()
    
    public static let asyncInstance = SerialDispatchQueueScheduler(
        serialQueue: DispatchQueue.main
    )
    
    private init() {
        mainQueue = DispatchQueue.main
        super.init(serialQueue: mainQueue)
    }
    
    public static func ensureExecutingOnScheduler(errorMessage: String? = nil) {
        if !DispatchQueue.isMain {
            rxFatalError(errorMessage ?? "Executing on background thread. Please use `MainScheduler.instance.schedule` to schedule work on main thread.")
        }
    }
}

关键点:

  • 使用 DispatchQueue.main
  • 提供单例实例
  • 提供线程检查方法

6.3 SerialDispatchQueueScheduler实现

public class SerialDispatchQueueScheduler: SchedulerType {
    public typealias TimeInterval = Foundation.TimeInterval
    public typealias Time = Date
    
    private let configuration: DispatchQueueConfiguration
    private let serialQueue: DispatchQueue
    
    public var now: RxTime {
        return Date()
    }
    
    public init(serialQueue: DispatchQueue, leeway: DispatchTimeInterval = DispatchTimeInterval.nanoseconds(0)) {
        self.serialQueue = serialQueue
        self.configuration = DispatchQueueConfiguration(
            queue: serialQueue,
            leeway: leeway
        )
    }
    
    public final func schedule<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable {
        return self.scheduleInternal(state, action: action)
    }
    
    func scheduleInternal<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable {
        let cancel = SingleAssignmentDisposable()
        
        serialQueue.async {
            if cancel.isDisposed {
                return
            }
            cancel.setDisposable(action(state))
        }
        
        return cancel
    }
    
    public final func scheduleRelative<StateType>(_ state: StateType, dueTime: RxTimeInterval, action: @escaping (StateType) -> Disposable) -> Disposable {
        return scheduleRelativeInternal(state, dueTime: dueTime, action: action)
    }
    
    func scheduleRelativeInternal<StateType>(_ state: StateType, dueTime: RxTimeInterval, action: @escaping (StateType) -> Disposable) -> Disposable {
        let deadline = now.addingTimeInterval(dueTime)
        
        let cancel = SingleAssignmentDisposable()
        
        serialQueue.asyncAfter(deadline: deadline) {
            if cancel.isDisposed {
                return
            }
            cancel.setDisposable(action(state))
        }
        
        return cancel
    }
}

关键点:

  • 使用 DispatchQueue 执行任务
  • 支持立即和延迟调度
  • 使用 SingleAssignmentDisposable 管理取消

7. 背压处理机制

7.1 背压问题

当生产者产生数据的速度快于消费者处理数据的速度时,会产生背压问题。

7.2 背压处理策略

RxSwift 主要通过以下方式处理背压:

  1. 请求机制:Observer 可以控制请求的数据量
  2. 缓冲:使用 buffer 操作符缓冲数据
  3. 节流:使用 throttledebounce 控制数据流速度
  4. 采样:使用 sample 采样数据

7.3 背压处理示例

class BackpressureObserver: ObserverType {
    typealias Element = Int
    
    private var buffer: [Int] = []
    private let bufferSize: Int
    private var subscription: Subscription?
    
    init(bufferSize: Int = 10) {
        self.bufferSize = bufferSize
    }
    
    func on(_ event: Event<Int>) {
        switch event {
        case .next(let element):
            buffer.append(element)
            
            // 处理缓冲区
            processBuffer()
            
            // 如果缓冲区未满,可以继续接收
            if buffer.count < bufferSize {
                // 继续接收
            }
        case .error, .completed:
            // 处理完成
            processRemaining()
        }
    }
    
    private func processBuffer() {
        while !buffer.isEmpty {
            let value = buffer.removeFirst()
            print("处理: \(value)")
        }
    }
    
    private func processRemaining() {
        processBuffer()
    }
}

8. 性能优化策略

8.1 值类型优化

RxSwift 大量使用值类型(struct),避免堆分配:

// 值类型,零成本抽象
struct Just<Element>: ObservableType { }
struct Map<SourceType, ResultType>: ObservableType { }
struct Filter<Element>: ObservableType { }

8.2 类型擦除

使用 asObservable() 隐藏具体类型:

extension ObservableType {
    public func asObservable() -> Observable<Element> {
        return Observable.create { observer in
            return self.subscribe(observer)
        }
    }
}

8.3 延迟执行

使用 deferred 延迟创建 Observable:

let deferred = Observable.deferred {
    // 只在订阅时执行
    return expensiveOperation()
}

8.4 共享订阅

使用 share() 共享 Observable:

let shared = expensiveObservable()
    .share()  // 多个订阅者共享同一个 Observable

shared.subscribe(onNext: { })  // 订阅1
shared.subscribe(onNext: { })  // 订阅2(共享执行)

8.5 内存优化

  • 使用 DisposeBag 自动管理订阅
  • 使用 weak self 避免循环引用
  • 及时取消不需要的订阅

📚 总结

RxSwift 框架的核心优势

  1. 跨平台标准:基于 ReactiveX 标准,与其他平台一致
  2. 丰富的操作符:提供大量操作符处理各种场景
  3. 类型安全:充分利用 Swift 类型系统
  4. 性能优化:值类型、零成本抽象
  5. 生态丰富:RxCocoa、RxDataSources 等扩展

学习建议

  1. 从基础开始:理解 Observable、Observer、Disposable
  2. 实践操作符:熟悉常用操作符的使用
  3. 理解调度器:掌握 subscribeOnobserveOn
  4. 阅读源码:深入理解实现原理
  5. 实际应用:在项目中应用 RxSwift

RxSwift vs Combine

  • RxSwift:适合需要支持 iOS 8+ 的项目,API 更丰富
  • Combine:适合 iOS 13+ 项目,与系统深度集成

文档版本:v1.0
最后更新:2026年1月15日
参考文献:RxSwift GitHub Repository, ReactiveX Documentation

03-研究优秀开源框架@响应式编程@iOS | ReactiveCocoa框架:从使用到源码解析

📋 目录


一、ReactiveCocoa框架使用详解

1. ReactiveCocoa框架概述

ReactiveCocoa(简称 RAC)是一个基于 ReactiveSwift 的响应式编程框架,用于处理异步事件流和状态管理。它是 GitHub 开源的项目,提供了声明式的 API 来处理时间序列数据。

1.1 什么是ReactiveCocoa

ReactiveCocoa 是一个函数式响应式编程(FRP)框架,允许你通过组合不同的操作符来处理异步事件序列。它提供了声明式的 API 来处理时间序列数据。

核心特点:

  • 函数式响应式编程:基于函数式编程和响应式编程的结合
  • 类型安全:充分利用 Swift 的类型系统
  • 状态管理:提供 Property 和 MutableProperty 管理状态
  • Action模式:提供 Action 处理用户交互
  • UIKit集成:深度集成 UIKit 控件

1.2 ReactiveCocoa vs RxSwift vs Combine

特性 ReactiveCocoa RxSwift Combine
平台 iOS、macOS 跨平台 Apple 生态(iOS 13+)
语言 Swift Swift Swift
官方支持 ❌ GitHub 开源 ❌ 第三方 ✅ Apple 官方
核心类型 Signal、SignalProducer Observable Publisher
状态管理 Property、MutableProperty BehaviorSubject @Published
Action模式 ✅ Action
学习曲线 陡峭 陡峭 中等
生态 ReactiveSwift、ReactiveObjC RxCocoa SwiftUI

1.3 ReactiveCocoa生态系统

  • ReactiveSwift:核心框架,提供 Signal、SignalProducer 等
  • ReactiveCocoa:UIKit/AppKit 集成,提供控件绑定
  • ReactiveObjC:Objective-C 版本

1.4 安装方式

CocoaPods:

pod 'ReactiveSwift', '~> 7.0'
pod 'ReactiveCocoa', '~> 12.0'

SPM:

dependencies: [
    .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "7.0.0"),
    .package(url: "https://github.com/ReactiveCocoa/ReactiveCocoa.git", from: "12.0.0")
]

1.5 编程思想(背后的范式与理念)

ReactiveCocoa 明确标榜函数式响应式编程(FRP),将函数式与响应式结合;理解其背后的编程思想,能更好地区分 Signal / SignalProducer、Property、Action 的适用场景。

(1)函数式响应式编程(FRP)

  • 核心:在「响应式」的事件流之上,用函数式的方式组合与变换——把「随时间发生的事件」视为可映射、可过滤、可合并的值,通过纯函数组合成新流,而不是在观察者闭包里写满副作用。
  • 在 RAC 中Signal / SignalProducer 表示事件流,mapfilterflatMap 等操作符对流做纯变换,observestart 才真正消费并产生副作用;流与副作用边界清晰。
  • 与「仅响应式」的对比:FRP 强调「流即数据」,用转换与组合表达业务逻辑,观察者只做「最后一步」的响应,便于测试和复用。

(2)声明式 vs 命令式

维度 命令式(Imperative) 声明式(Declarative)
关注点 「怎么做」:显式顺序与分支 「做什么」:描述数据/事件如何变换与约束
典型写法 回调嵌套、状态变量、if-else 链式操作符:map / filter / combineLatest
在 RAC 中 手写「请求 → 回调里判断 → 再请求」 signal.map(...).flatMap(...).observeValues(...) 描述整条流水线

声明式让「事件从哪来、如何变换、到哪去」一目了然,便于阅读和单元测试。

(3)函数式思想(组合与不可变)

  • 组合(Composition):每个操作符只做一件事,通过 .map().filter().flatMap(.latest) 等组合成完整逻辑;小能力组合成大能力,避免巨型闭包。
  • 不可变(Immutability):操作符不修改原 Signal/SignalProducer,而是返回新的;原流不变,便于复用和推理。
  • 副作用边界:纯变换放在操作符链中,副作用(UI 更新、写库、弹窗)集中在 observeValues / startWithValuesAction 的 execution 中,便于测试和并发安全。

(4)流与时间(Streams & Time)

  • 把所有「会随时间产生的事件」都视为时间序列:value、value、…、completed/failed/interrupted。
  • RAC 区分热信号(Signal)冷信号(SignalProducer):热信号有订阅即开始发送、多订阅者共享同一时间线;冷信号每次 start 才执行、每次订阅独立。时间相关操作符如 debouncethrottle 表达「何时」而不只是「何值」。

(5)观察者与「推」「拉」

  • 观察者模式:Observer 通过 observe 订阅 Signal,或通过 start 启动 SignalProducer,在事件发生时被通知。
  • 推模型:Signal 是「推」——事件由发送端推动,观察者被动接收;SignalProducer 是「按需拉」——只有 start 时才创建并执行,适合表示「一次异步操作」或「延迟计算」。

(6)Action 与「意图-执行」分离

  • 思想:用户操作(点击、下拉)是意图,网络请求、校验、弹窗是执行;将「意图」与「执行」分离,便于禁用、重试、统一错误处理。
  • 在 RAC 中Action 接收输入(如按钮 tap 或输入值),内部用 SignalProducer 描述一次执行,输出与错误统一由 Action 暴露;UI 只绑定「能否执行」与「执行结果」,不写一堆 isLoadingerror 状态。

小结:ReactiveCocoa 用声明式事件流(Signal/SignalProducer)和可组合操作符,在函数式响应式的范式下做异步与事件处理;用 Property 管理可变状态、用 Action 封装「意图-执行」,并用 Scheduler 控制线程。掌握这些思想后,再区分「用 Signal 还是 SignalProducer」「何时用 Property、何时用 Action」会更自然。


2. 核心概念

2.1 Signal(信号)

Signal 是 ReactiveCocoa 的核心类型,表示一个可以观察的事件流。

protocol SignalProtocol {
    associatedtype Value
    associatedtype Error: Swift.Error
    
    func observe(_ observer: Observer<Value, Error>) -> Disposable?
}

特点:

  • 可以发出零个或多个值
  • 可能以完成或错误结束
  • 是引用类型(class)
  • 热信号(Hot Signal):有订阅者时立即开始发送事件

事件类型:

enum Event<Value, Error: Swift.Error> {
    case value(Value)      // 值事件
    case failed(Error)    // 错误事件
    case completed        // 完成事件
    case interrupted      // 中断事件
}

示例:

let (signal, observer) = Signal<String, Never>.pipe()

signal.observeValues { value in
    print("收到值: \(value)")
}

observer.send(value: "Hello")
observer.send(value: "World")
observer.sendCompleted()

2.2 SignalProducer(信号生产者)

SignalProducer 是延迟创建 Signal 的类型,类似于 RxSwift 的 Observable。

struct SignalProducer<Value, Error: Swift.Error> {
    private let startHandler: (Observer<Value, Error>, Lifetime) -> Void
    
    func start(_ observer: Observer<Value, Error>) -> Disposable
}

特点:

  • 冷信号(Cold Signal):只有在被订阅时才开始发送事件
  • 每次订阅都会创建新的 Signal
  • 适合表示异步操作

示例:

let producer = SignalProducer<String, Never> { observer, lifetime in
    observer.send(value: "Hello")
    observer.send(value: "World")
    observer.sendCompleted()
}

producer.startWithValues { value in
    print("收到值: \(value)")
}

2.3 Observer(观察者)

Observer 是接收 Signal 事件的类型。

final class Observer<Value, Error: Swift.Error> {
    func send(value: Value)
    func send(error: Error)
    func sendCompleted()
    func sendInterrupted()
}

示例:

let (signal, observer) = Signal<Int, Never>.pipe()

signal.observe { event in
    switch event {
    case .value(let value):
        print("值: \(value)")
    case .completed:
        print("完成")
    case .failed(let error):
        print("错误: \(error)")
    case .interrupted:
        print("中断")
    }
}

observer.send(value: 1)
observer.send(value: 2)
observer.sendCompleted()

2.4 Disposable(可释放资源)

Disposable 表示订阅关系,用于取消订阅和释放资源。

protocol Disposable {
    func dispose()
}

CompositeDisposable:

let disposable = CompositeDisposable()

disposable += signal.observeValues { value in
    print(value)
}

disposable += anotherSignal.observeValues { value in
    print(value)
}

// 释放所有订阅
disposable.dispose()

3. Signal与SignalProducer

3.1 Signal创建方式

pipe

创建 Signal 和 Observer。

let (signal, observer) = Signal<String, Never>.pipe()

signal.observeValues { print($0) }
observer.send(value: "Hello")
never

创建永不发出事件的 Signal。

let signal = Signal<Int, Never>.never()
signal.observeValues { print($0) }  // 永远不会执行
empty

创建立即完成的 Signal。

let signal = Signal<Int, Never>.empty()
signal.observeCompleted { print("完成") }
failed

创建立即失败的 Signal。

enum MyError: Error {
    case failure
}

let signal = Signal<Int, MyError>.failed(.failure)
signal.observeFailed { print("错误: \($0)") }

3.2 SignalProducer创建方式

init

使用闭包创建 SignalProducer。

let producer = SignalProducer<String, Never> { observer, lifetime in
    observer.send(value: "A")
    observer.send(value: "B")
    observer.sendCompleted()
}

producer.startWithValues { print($0) }
value

创建发出单个值的 SignalProducer。

let producer = SignalProducer<String, Never>(value: "Hello")
producer.startWithValues { print($0) }
values

从序列创建 SignalProducer。

let producer = SignalProducer<String, Never>(values: ["A", "B", "C"])
producer.startWithValues { print($0) }
error

创建立即失败的 SignalProducer。

let producer = SignalProducer<Int, MyError>(error: .failure)
producer.startWithFailed { print("错误: \($0)") }
empty

创建立即完成的 SignalProducer。

let producer = SignalProducer<Int, Never>.empty
producer.startWithCompleted { print("完成") }
never

创建永不发出事件的 SignalProducer。

let producer = SignalProducer<Int, Never>.never
producer.startWithValues { print($0) }  // 永远不会执行

3.3 Signal vs SignalProducer

Signal(热信号):

  • 立即开始发送事件
  • 多个观察者共享同一个事件流
  • 适合表示已经发生的事件

SignalProducer(冷信号):

  • 延迟创建,只有在订阅时才开始
  • 每个观察者获得独立的事件流
  • 适合表示异步操作

转换:

// SignalProducer -> Signal
let producer = SignalProducer<String, Never>(value: "Hello")
let signal = producer.promoteToSignal()

// Signal -> SignalProducer
let (signal, observer) = Signal<String, Never>.pipe()
let producer = SignalProducer(signal)

4. Property与MutableProperty

4.1 Property

Property 是不可变的状态容器,表示一个随时间变化的值。

protocol PropertyProtocol {
    associatedtype Value
    
    var value: Value { get }
    var signal: Signal<Value, Never> { get }
    var producer: SignalProducer<Value, Never> { get }
}

特点:

  • 只读属性
  • 提供当前值
  • 提供 Signal 和 SignalProducer 观察变化

示例:

let property = Property(value: "初始值")

// 获取当前值
print(property.value)  // 输出: 初始值

// 观察变化
property.signal.observeValues { value in
    print("值变化: \(value)")
}

4.2 MutableProperty

MutableProperty 是可变的状态容器。

final class MutableProperty<Value>: MutablePropertyProtocol {
    var value: Value { get set }
    var signal: Signal<Value, Never> { get }
    var producer: SignalProducer<Value, Never> { get }
    
    init(_ value: Value)
}

特点:

  • 可读写属性
  • 修改值时会发出事件
  • 新观察者会立即收到当前值

示例:

let property = MutableProperty("初始值")

// 观察变化
property.signal.observeValues { value in
    print("值变化: \(value)")
}
// 立即输出: 值变化: 初始值

// 修改值
property.value = "新值"  // 输出: 值变化: 新值
property.value = "另一个值"  // 输出: 值变化: 另一个值

4.3 Property绑定

双向绑定:

let property1 = MutableProperty("")
let property2 = MutableProperty("")

// 双向绑定
property1 <~ property2
property2 <~ property1

property1.value = "Hello"  // property2.value 也变为 "Hello"
property2.value = "World"  // property1.value 也变为 "World"

单向绑定:

let source = MutableProperty("源")
let target = MutableProperty("目标")

// 单向绑定:source -> target
target <~ source

source.value = "新值"  // target.value 也变为 "新值"
target.value = "修改"  // source.value 不变

5. Action

Action 是 ReactiveCocoa 特有的类型,用于处理用户交互和异步操作。

5.1 Action基本使用

let action = Action<String, String, Never> { input in
    return SignalProducer { observer, lifetime in
        // 执行异步操作
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            observer.send(value: "结果: \(input)")
            observer.sendCompleted()
        }
    }
}

// 执行 Action
action.apply("输入").startWithValues { result in
    print(result)  // 输出: 结果: 输入
}

5.2 Action状态

Action 提供多个状态 Signal:

let action = Action<String, String, Never> { input in
    return SignalProducer(value: "结果: \(input)")
}

// 观察执行状态
action.isExecuting.signal.observeValues { isExecuting in
    print("执行中: \(isExecuting)")
}

// 观察值
action.values.observeValues { value in
    print("值: \(value)")
}

// 观察错误
action.errors.observeValues { error in
    print("错误: \(error)")
}

// 执行
action.apply("输入").start()

5.3 Action与UIButton绑定

let action = Action<Void, String, Never> {
    return SignalProducer(value: "按钮点击")
}

// 绑定到按钮
button.reactive.pressed = CocoaAction(action) { _ in }

// 观察结果
action.values.observeValues { result in
    print(result)
}

6. Operators操作符

6.1 转换操作符

map

转换每个值。

SignalProducer(values: [1, 2, 3])
    .map { $0 * 2 }
    .startWithValues { print($0) }
// 输出: 2, 4, 6
flatMap

将 Signal 发出的值转换为 SignalProducer,然后合并。

SignalProducer(values: ["A", "B", "C"])
    .flatMap(.latest) { letter in
        SignalProducer(values: [1, 2]).map { "\(letter)\($0)" }
    }
    .startWithValues { print($0) }
// 输出: A1, A2, B1, B2, C1, C2
scan

累积值。

SignalProducer(values: [1, 2, 3, 4, 5])
    .scan(0, +)
    .startWithValues { print($0) }
// 输出: 1, 3, 6, 10, 15

6.2 过滤操作符

filter

过滤值。

SignalProducer(values: [1, 2, 3, 4, 5])
    .filter { $0 % 2 == 0 }
    .startWithValues { print($0) }
// 输出: 2, 4
skip

跳过前几个值。

SignalProducer(values: [1, 2, 3, 4, 5])
    .skip(first: 2)
    .startWithValues { print($0) }
// 输出: 3, 4, 5
take

获取前几个值。

SignalProducer(values: [1, 2, 3, 4, 5])
    .take(first: 3)
    .startWithValues { print($0) }
// 输出: 1, 2, 3
distinctUntilChanged

移除连续重复的值。

SignalProducer(values: [1, 1, 2, 2, 3, 3])
    .distinctUntilChanged()
    .startWithValues { print($0) }
// 输出: 1, 2, 3

6.3 组合操作符

combineLatest

组合多个 Signal 的最新值。

let (signal1, observer1) = Signal<String, Never>.pipe()
let (signal2, observer2) = Signal<Int, Never>.pipe()

signal1.combineLatest(with: signal2)
    .observeValues { value1, value2 in
        print("\(value1): \(value2)")
    }

observer1.send(value: "A")  // 无输出(等待 signal2)
observer2.send(value: 1)    // 输出: A: 1
observer1.send(value: "B")  // 输出: B: 1
observer2.send(value: 2)    // 输出: B: 2
merge

合并多个 Signal。

let (signal1, observer1) = Signal<Int, Never>.pipe()
let (signal2, observer2) = Signal<Int, Never>.pipe()

signal1.merge(with: signal2)
    .observeValues { print($0) }

observer1.send(value: 1)  // 输出: 1
observer2.send(value: 2)  // 输出: 2
observer1.send(value: 3)  // 输出: 3
zip

按顺序组合多个 Signal。

let (signal1, observer1) = Signal<String, Never>.pipe()
let (signal2, observer2) = Signal<Int, Never>.pipe()

signal1.zip(with: signal2)
    .observeValues { value1, value2 in
        print("\(value1): \(value2)")
    }

observer1.send(value: "A")  // 等待 signal2
observer1.send(value: "B")  // 等待 signal2
observer2.send(value: 1)    // 输出: A: 1
observer2.send(value: 2)    // 输出: B: 2

6.4 时间操作符

debounce

防抖,等待指定时间后发出最新值。

let (signal, observer) = Signal<String, Never>.pipe()

signal.debounce(0.5, on: QueueScheduler.main)
    .observeValues { print($0) }

observer.send(value: "H")     // 不输出
observer.send(value: "He")    // 不输出
observer.send(value: "Hel")   // 不输出
observer.send(value: "Hell")  // 不输出
observer.send(value: "Hello") // 0.5秒后输出: Hello
throttle

节流,在指定时间间隔内只发出第一个值。

let (signal, observer) = Signal<String, Never>.pipe()

signal.throttle(1.0, on: QueueScheduler.main)
    .observeValues { print($0) }

observer.send(value: "A")  // 立即输出: A
observer.send(value: "B")  // 不输出(1秒内)
observer.send(value: "C")  // 不输出(1秒内)
// 1秒后
observer.send(value: "D")  // 输出: D
delay

延迟发出值。

SignalProducer(values: [1, 2, 3])
    .delay(1.0, on: QueueScheduler.main)
    .startWithValues { print($0) }
// 1秒后依次输出: 1, 2, 3

7. Schedulers调度器

7.1 内置Scheduler

QueueScheduler

队列调度器。

// 主队列
let mainScheduler = QueueScheduler.main

// 后台队列
let backgroundScheduler = QueueScheduler(
    qos: .background,
    name: "background.queue"
)

SignalProducer(value: 1)
    .start(on: backgroundScheduler)
    .observe(on: mainScheduler)
    .startWithValues { value in
        print(Thread.isMainThread)  // true
    }
UIScheduler

UI 调度器(主线程)。

let uiScheduler = UIScheduler()

SignalProducer(value: 1)
    .observe(on: uiScheduler)
    .startWithValues { value in
        print(Thread.isMainThread)  // true
    }

7.2 start vs observe

  • start:指定 SignalProducer 在哪个调度器上开始执行
  • observe:指定观察者在哪个调度器上接收事件
SignalProducer { observer, lifetime in
    print("执行线程: \(Thread.current)")
    observer.send(value: 1)
    observer.sendCompleted())
}
.start(on: QueueScheduler(qos: .background))
.observe(on: UIScheduler())
.startWithValues { value in
    print("接收线程: \(Thread.current)")
}

8. 错误处理

8.1 错误类型

enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

8.2 错误处理操作符

catch

捕获错误并返回备用 SignalProducer。

SignalProducer<String, NetworkError>(error: .noData)
    .catch { error -> SignalProducer<String, Never> in
        print("捕获错误: \(error)")
        return SignalProducer(value: "备用值")
    }
    .startWithValues { print($0) }
retry

重试失败的 SignalProducer。

var attempts = 0

SignalProducer<String, NetworkError> { observer, lifetime in
    attempts += 1
    if attempts < 3 {
        observer.send(error: .noData)
    } else {
        observer.send(value: "成功")
        observer.sendCompleted()
    }
}
.retry(upTo: 2)  // 最多重试 2 次
.start(
    value: { print($0) },
    failed: { print("错误: \($0)") }
)
flatMapError

将错误转换为值。

SignalProducer<String, NetworkError>(error: .noData)
    .flatMapError { error in
        SignalProducer(value: "错误: \(error)")
    }
    .startWithValues { print($0) }

9. 内存管理

9.1 Lifetime

Lifetime 用于管理 SignalProducer 的生命周期。

let producer = SignalProducer<String, Never> { observer, lifetime in
    let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
        observer.send(value: "Tick")
    }
    
    lifetime.observeEnded {
        timer.invalidate()
    }
}

let disposable = producer.startWithValues { print($0) }

// 取消订阅时,timer 会自动失效
disposable.dispose()

9.2 避免循环引用

class ViewModel {
    private let property = MutableProperty("")
    
    func setup() {
        property.signal.observeValues { [weak self] value in
            self?.process(value)
        }
    }
    
    private func process(_ value: String) {
        // 处理数据
    }
}

10. 与UIKit集成

10.1 Reactive扩展

ReactiveCocoa 为 UIKit 控件提供了 Reactive 扩展。

import ReactiveSwift
import ReactiveCocoa

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var button: UIButton!
    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 文本输入绑定
        label.reactive.text <~ textField.reactive.continuousTextValues
        
        // 按钮点击
        button.reactive.pressed = CocoaAction(Action { [weak self] _ in
            return SignalProducer(value: "按钮点击")
        })
    }
}

10.2 常用绑定

// UILabel
label.reactive.text <~ property.producer.map { $0 }

// UITextField
textField.reactive.text <~ property.producer.map { $0 }
property <~ textField.reactive.continuousTextValues

// UIButton
button.reactive.pressed = CocoaAction(action)

// UISwitch
switch.reactive.isOn <~ property.producer.map { $0 }
property <~ switch.reactive.isOnValues

// UISlider
slider.reactive.value <~ property.producer.map { Float($0) }
property <~ slider.reactive.values.map { Int($0) }

11. 实际应用场景

11.1 网络请求

struct API {
    static func fetchUser(id: Int) -> SignalProducer<User, NetworkError> {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        
        return URLSession.shared.reactive.data(with: URLRequest(url: url))
            .attemptMap { data, _ in
                try JSONDecoder().decode(User.self, from: data)
            }
            .observe(on: UIScheduler())
    }
}

API.fetchUser(id: 1)
    .start(
        value: { user in
            print("用户: \(user)")
        },
        failed: { error in
            print("错误: \(error)")
        }
    )

11.2 用户输入处理

class SearchViewModel {
    let searchText = MutableProperty("")
    let results = MutableProperty<[String]>([])
    
    init() {
        results <~ searchText.producer
            .debounce(0.5, on: QueueScheduler.main)
            .skipRepeats()
            .filter { !$0.isEmpty }
            .flatMap(.latest) { query -> SignalProducer<[String], Never> in
                return self.search(query: query)
                    .flatMapError { _ in SignalProducer(value: []) }
            }
    }
    
    private func search(query: String) -> SignalProducer<[String], NetworkError> {
        // 实现搜索逻辑
        return SignalProducer(value: ["结果1", "结果2"])
    }
}

二、ReactiveCocoa框架源码解析

1. 架构设计

1.1 整体架构

ReactiveCocoa 采用协议导向的设计,核心是 Signal 和 SignalProducer。

Signal (热信号)
    ↓
Observer
    ↓
Event (value/failed/completed/interrupted)

SignalProducer (冷信号)
    ↓
Observer
    ↓
Signal

1.2 核心协议层次

// Signal 协议
protocol SignalProtocol {
    associatedtype Value
    associatedtype Error: Swift.Error
    
    func observe(_ observer: Observer<Value, Error>) -> Disposable?
}

// SignalProducer 协议
protocol SignalProducerProtocol {
    associatedtype Value
    associatedtype Error: Swift.Error
    
    func start(_ observer: Observer<Value, Error>) -> Disposable
}

2. Signal实现原理

2.1 Signal类实现

public final class Signal<Value, Error: Swift.Error>: SignalProtocol {
    private let generator: (Observer<Value, Error>) -> Disposable?
    private var observers: Bag<Observer<Value, Error>> = Bag()
    private let lock = NSRecursiveLock()
    
    public init(_ generator: @escaping (Observer<Value, Error>) -> Disposable?) {
        self.generator = generator
    }
    
    public func observe(_ observer: Observer<Value, Error>) -> Disposable? {
        lock.lock()
        defer { lock.unlock() }
        
        let token = observers.insert(observer)
        
        // 如果是第一个观察者,开始生成事件
        if observers.count == 1 {
            let disposable = generator(Observer { [weak self] event in
                self?.send(event)
            })
            
            return CompositeDisposable(
                disposable,
                Disposable { [weak self] in
                    self?.lock.lock()
                    self?.observers.remove(using: token)
                    self?.lock.unlock()
                }
            )
        }
        
        return Disposable { [weak self] in
            self?.lock.lock()
            self?.observers.remove(using: token)
            self?.lock.unlock()
        }
    }
    
    private func send(_ event: Event<Value, Error>) {
        lock.lock()
        let currentObservers = observers
        lock.unlock()
        
        for observer in currentObservers {
            observer.send(event)
        }
        
        // 如果是终止事件,清理观察者
        if event.isTerminating {
            lock.lock()
            observers.removeAll()
            lock.unlock()
        }
    }
}

关键点:

  • Signal 是引用类型(class)
  • 使用 Bag 存储多个观察者
  • 使用锁保护共享状态
  • 第一个观察者订阅时开始生成事件

2.2 pipe实现

extension Signal {
    public static func pipe() -> (Signal<Value, Error>, Observer<Value, Error>) {
        let observer = Observer<Value, Error>()
        let signal = Signal<Value, Error> { observer in
            // 将外部 observer 的事件转发给内部 observer
            return observer.observe { event in
                observer.send(event)
            }
        }
        
        return (signal, observer)
    }
}

关键点:

  • pipe 创建 Signal 和 Observer 对
  • Observer 可以手动发送事件
  • 适合将命令式代码转换为响应式代码

3. SignalProducer实现原理

3.1 SignalProducer结构

public struct SignalProducer<Value, Error: Swift.Error>: SignalProducerProtocol {
    private let startHandler: (Observer<Value, Error>, Lifetime) -> Void
    
    public init(_ startHandler: @escaping (Observer<Value, Error>, Lifetime) -> Void) {
        self.startHandler = startHandler
    }
    
    public func start(_ observer: Observer<Value, Error>) -> Disposable {
        let lifetime = Lifetime()
        let compositeDisposable = CompositeDisposable()
        
        lifetime.observeEnded {
            compositeDisposable.dispose()
        }
        
        startHandler(observer, lifetime)
        
        return compositeDisposable
    }
}

关键点:

  • SignalProducer 是值类型(struct)
  • 每次 start 都会创建新的 Signal
  • 使用 Lifetime 管理资源生命周期

3.2 SignalProducer转换

extension SignalProducer {
    public var signal: Signal<Value, Error> {
        return Signal { observer in
            return self.start(observer)
        }
    }
}

extension Signal {
    public var producer: SignalProducer<Value, Error> {
        return SignalProducer { observer, lifetime in
            let disposable = self.observe(observer)
            lifetime.observeEnded {
                disposable?.dispose()
            }
        }
    }
}

关键点:

  • SignalProducer 可以转换为 Signal
  • Signal 可以转换为 SignalProducer
  • 转换是延迟的,不会立即执行

4. Property实现原理

4.1 Property协议

public protocol PropertyProtocol {
    associatedtype Value
    
    var value: Value { get }
    var signal: Signal<Value, Never> { get }
    var producer: SignalProducer<Value, Never> { get }
}

4.2 MutableProperty实现

public final class MutableProperty<Value>: MutablePropertyProtocol {
    private let lock = NSRecursiveLock()
    private var _value: Value
    private let observer: Observer<Value, Never>
    private let signal: Signal<Value, Never>
    
    public var value: Value {
        get {
            lock.lock()
            defer { lock.unlock() }
            return _value
        }
        set {
            lock.lock()
            let oldValue = _value
            _value = newValue
            lock.unlock()
            
            if oldValue != newValue {
                observer.send(value: newValue)
            }
        }
    }
    
    public init(_ value: Value) {
        _value = value
        let (signal, observer) = Signal<Value, Never>.pipe()
        self.signal = signal
        self.observer = observer
        
        // 立即发送初始值
        observer.send(value: value)
    }
    
    public var producer: SignalProducer<Value, Never> {
        return SignalProducer { observer, lifetime in
            // 立即发送当前值
            observer.send(value: self.value)
            
            // 观察后续变化
            let disposable = self.signal.observe(observer)
            lifetime.observeEnded {
                disposable?.dispose()
            }
        }
    }
}

关键点:

  • MutableProperty 是引用类型
  • 使用锁保护 _value
  • 值变化时发出事件
  • producer 会立即发送当前值

4.3 绑定操作符实现

infix operator <~ : BindingPrecedence

public func <~ <Source: SignalProducerProtocol, Destination: BindingTargetProtocol>(
    destination: Destination,
    source: Source
) -> Disposable
where Source.Value == Destination.Value, Source.Error == Never {
    return source.startWithValues { value in
        destination.consume(value)
    }
}

public func <~ <Source: SignalProtocol, Destination: BindingTargetProtocol>(
    destination: Destination,
    source: Source
) -> Disposable?
where Source.Value == Destination.Value, Source.Error == Never {
    return source.observeValues { value in
        destination.consume(value)
    }
}

关键点:

  • <~ 操作符实现单向绑定
  • 自动管理订阅生命周期
  • 支持 Signal 和 SignalProducer

5. Action实现原理

5.1 Action结构

public final class Action<Input, Output, Error: Swift.Error> {
    private let executeClosure: (Input) -> SignalProducer<Output, Error>
    private let isEnabledProperty: MutableProperty<Bool>
    private let eventsObserver: Observer<Event<Output, Error>, Never>
    
    public let isEnabled: Property<Bool>
    public let isExecuting: Property<Bool>
    public let values: Signal<Output, Never>
    public let errors: Signal<Error, Never>
    public let events: Signal<Event<Output, Error>, Never>
    
    public init(enabledIf: Property<Bool> = Property(value: true),
                execute: @escaping (Input) -> SignalProducer<Output, Error>) {
        self.executeClosure = execute
        self.isEnabledProperty = MutableProperty(true)
        self.isEnabled = Property(capturing: isEnabledProperty)
        
        let (eventsSignal, eventsObserver) = Signal<Event<Output, Error>, Never>.pipe()
        self.events = eventsSignal
        self.eventsObserver = eventsObserver
        
        self.values = events.map { $0.value }.skipNil()
        self.errors = events.map { $0.error }.skipNil()
        
        let isExecutingProperty = MutableProperty(false)
        self.isExecuting = Property(capturing: isExecutingProperty)
        
        // 监听执行状态
        events.observeValues { event in
            switch event {
            case .value:
                isExecutingProperty.value = true
            case .completed, .failed, .interrupted:
                isExecutingProperty.value = false
            }
        }
    }
    
    public func apply(_ input: Input) -> SignalProducer<Output, Error> {
        return SignalProducer { observer, lifetime in
            guard self.isEnabled.value else {
                observer.sendInterrupted()
                return
            }
            
            let producer = self.executeClosure(input)
            let disposable = producer.start { event in
                self.eventsObserver.send(value: event)
                observer.send(event)
            }
            
            lifetime.observeEnded {
                disposable.dispose()
            }
        }
    }
}

关键点:

  • Action 封装异步操作
  • 提供执行状态(isEnabled、isExecuting)
  • 提供值、错误、事件流
  • 可以禁用 Action

6. Operators实现原理

6.1 map实现

extension SignalProducer {
    public func map<U>(_ transform: @escaping (Value) -> U) -> SignalProducer<U, Error> {
        return SignalProducer { observer, lifetime in
            self.start { event in
                switch event {
                case .value(let value):
                    observer.send(value: transform(value))
                case .failed(let error):
                    observer.send(error: error)
                case .completed:
                    observer.sendCompleted()
                case .interrupted:
                    observer.sendInterrupted()
                }
            }
        }
    }
}

关键点:

  • map 创建新的 SignalProducer
  • 转换每个值事件
  • 保持其他事件不变

6.2 filter实现

extension SignalProducer {
    public func filter(_ predicate: @escaping (Value) -> Bool) -> SignalProducer<Value, Error> {
        return SignalProducer { observer, lifetime in
            self.start { event in
                switch event {
                case .value(let value):
                    if predicate(value) {
                        observer.send(value: value)
                    }
                case .failed(let error):
                    observer.send(error: error)
                case .completed:
                    observer.sendCompleted()
                case .interrupted:
                    observer.sendInterrupted()
                }
            }
        }
    }
}

关键点:

  • filter 创建新的 SignalProducer
  • 只转发满足条件的值
  • 保持其他事件不变

6.3 flatMap实现

extension SignalProducer {
    public func flatMap<U>(_ strategy: FlattenStrategy, _ transform: @escaping (Value) -> SignalProducer<U, Error>) -> SignalProducer<U, Error> {
        return SignalProducer { observer, lifetime in
            let flattenProducer = self.map(transform).flatten(strategy)
            let disposable = flattenProducer.start(observer)
            lifetime.observeEnded {
                disposable.dispose()
            }
        }
    }
}

关键点:

  • flatMap 支持多种策略(.latest、.merge、.concat)
  • 管理多个内部 SignalProducer
  • 需要复杂的生命周期管理

7. Schedulers实现原理

7.1 Scheduler协议

public protocol Scheduler {
    func schedule(_ action: @escaping () -> Void) -> Disposable?
    func schedule(after date: Date, action: @escaping () -> Void) -> Disposable?
    func schedule(after date: Date, interval: TimeInterval, action: @escaping () -> Void) -> Disposable?
}

7.2 QueueScheduler实现

public final class QueueScheduler: Scheduler {
    public let queue: DispatchQueue
    
    public init(qos: DispatchQoS = .default, name: String = "org.reactivecocoa.ReactiveSwift.QueueScheduler") {
        self.queue = DispatchQueue(label: name, qos: qos)
    }
    
    public static let main = QueueScheduler(queue: .main, name: "org.reactivecocoa.ReactiveSwift.QueueScheduler.main")
    
    public func schedule(_ action: @escaping () -> Void) -> Disposable? {
        let disposable = SimpleDisposable()
        queue.async {
            if !disposable.isDisposed {
                action()
            }
        }
        return disposable
    }
    
    public func schedule(after date: Date, action: @escaping () -> Void) -> Disposable? {
        let disposable = SimpleDisposable()
        let timeInterval = date.timeIntervalSinceNow
        queue.asyncAfter(deadline: .now() + timeInterval) {
            if !disposable.isDisposed {
                action()
            }
        }
        return disposable
    }
}

关键点:

  • QueueScheduler 使用 DispatchQueue
  • 支持立即和延迟调度
  • 支持取消调度

8. 生命周期管理

8.1 Lifetime实现

public final class Lifetime {
    private let token: Token
    private var observers: Bag<() -> Void> = Bag()
    private let lock = NSRecursiveLock()
    
    public init() {
        token = Token()
    }
    
    public func observeEnded(_ action: @escaping () -> Void) {
        lock.lock()
        let isEnded = token.isEnded
        if !isEnded {
            observers.insert(action)
        }
        lock.unlock()
        
        if isEnded {
            action()
        }
    }
    
    deinit {
        token.markEnded()
        lock.lock()
        let currentObservers = observers
        observers.removeAll()
        lock.unlock()
        
        for observer in currentObservers {
            observer()
        }
    }
}

关键点:

  • Lifetime 管理资源生命周期
  • 对象释放时自动执行清理操作
  • 使用 observeEnded 注册清理回调

8.2 Disposable管理

public final class CompositeDisposable: Disposable {
    private var disposables: [Disposable] = []
    private let lock = NSRecursiveLock()
    private var isDisposed = false
    
    public init(_ disposables: Disposable...) {
        self.disposables = disposables
    }
    
    public func add(_ disposable: Disposable?) {
        guard let disposable = disposable else { return }
        
        lock.lock()
        if isDisposed {
            lock.unlock()
            disposable.dispose()
            return
        }
        
        disposables.append(disposable)
        lock.unlock()
    }
    
    public func dispose() {
        lock.lock()
        guard !isDisposed else {
            lock.unlock()
            return
        }
        
        isDisposed = true
        let currentDisposables = disposables
        disposables.removeAll()
        lock.unlock()
        
        for disposable in currentDisposables {
            disposable.dispose()
        }
    }
}

关键点:

  • CompositeDisposable 管理多个 Disposable
  • 线程安全
  • 一次性释放所有资源

9. 性能优化策略

9.1 值类型优化

SignalProducer 是值类型,避免堆分配:

// 值类型,零成本抽象
struct SignalProducer<Value, Error: Swift.Error> { }

9.2 延迟执行

SignalProducer 延迟创建 Signal:

let producer = SignalProducer<String, Never> { observer, lifetime in
    // 只在 start 时执行
    observer.send(value: "Hello")
}

9.3 共享执行

使用 share() 共享 SignalProducer:

let shared = expensiveProducer().share()

shared.startWithValues { }  // 订阅1
shared.startWithValues { }  // 订阅2(共享执行)

9.4 内存优化

  • 使用 weak 引用避免循环引用
  • 使用 Lifetime 自动管理资源
  • 及时释放不需要的订阅

📚 总结

ReactiveCocoa 的核心优势

  1. Property 状态管理:提供 Property 和 MutableProperty 管理状态
  2. Action 模式:提供 Action 处理用户交互和异步操作
  3. 类型安全:充分利用 Swift 类型系统
  4. 生命周期管理:使用 Lifetime 自动管理资源
  5. UIKit 集成:深度集成 UIKit 控件

学习建议

  1. 理解 Signal vs SignalProducer:掌握热信号和冷信号的区别
  2. 理解 Property:掌握状态管理
  3. 理解 Action:掌握用户交互处理
  4. 阅读源码:深入理解实现原理
  5. 实际应用:在项目中应用 ReactiveCocoa

文档版本:v1.0
最后更新:2026年1月15日
参考文献:ReactiveCocoa GitHub Repository, ReactiveSwift Source Code

02-研究优秀开源框架@响应式编程@iOS | Combine框架:源码解析


二、Combine框架源码解析

1. 架构设计

1.1 整体架构

Combine 采用协议导向的设计,核心是三个协议:

Publisher (发布者)
    ↓
Subscription (订阅关系)
    ↓
Subscriber (订阅者)

数据流:

Publisher → Subscription → Subscriber
     ↑                          ↓
     └────────── 反馈 ──────────┘

1.2 核心协议层次

// 第一层:Publisher 协议
protocol Publisher {
    associatedtype Output
    associatedtype Failure: Error
    func receive<S: Subscriber>(subscriber: S)
}

// 第二层:Subscription 协议
protocol Subscription: Cancellable {
    func request(_ demand: Subscribers.Demand)
}

// 第三层:Subscriber 协议
protocol Subscriber: CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure: Error
    func receive(subscription: Subscription)
    func receive(_ input: Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Failure>)
}

1.3 内部架构分层(三层视图)

Combine 从内到外可以理解为协议层 → 实现层 → 调度层,三者共同决定「谁在何时、何地、以何种方式」传递事件。

架构分层示意:

┌─────────────────────────────────────────────────────────────────────────────┐
│ 调度层 (Scheduler)                                                           │
│  · 决定事件在哪个线程/队列执行                                                 │
│  · subscribe(on:) / receive(on:) / 时间类操作符(debounce, delay) 依赖调度器     │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 实现层 (Concrete Types)                                                       │
│  · Just / Future / PassthroughSubject / Publishers.Map / Sink / Assign ...   │
│  · 每个操作符 = 新 Publisher + 中间 Subscriber,形成链式实现                    │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 协议层 (Protocols)                                                            │
│  · Publisher:定义「可被订阅」的契约                                           │
│  · Subscription:定义「请求/取消」的契约                                       │
│  · Subscriber:定义「接收值/完成」的契约                                       │
└─────────────────────────────────────────────────────────────────────────────┘
  • 协议层:只规定接口(Output/Failure、receive(subscription/input/completion)、request(demand)),不关心具体类型。
  • 实现层:所有 JustMapFilterSink 等具体类型都遵循上述协议,并通过「包装上游 + 向下游转发」组成链条。
  • 调度层:由 Scheduler 协议抽象(如 DispatchQueueRunLoop),操作符在需要时把回调投递到指定调度器执行,从而控制线程与时机。

1.4 响应者链(订阅链)

一次 publisher.map(...).filter(...).sink(...) 会在内部形成一条从上游到下游的订阅链:每一环都是一个 Publisher,下游订阅上游,最末端是真正的 Subscriber(如 Sink)。值沿这条链自上而下传递,Demand 可自下而上反馈。

响应者链结构图:

  [上游]          [操作符]           [操作符]          [终端]
   Just    →    Map<Int,String>  →  Filter<String>  →   Sink
    │                 │                    │               │
    │  subscribe      │  subscribe         │  subscribe    │
    │ ◄───────────────┼────────────────────┼───────────────┤
    │                 │                    │               │
    │  receive(S)     │  receive(S)        │  receive(S)   │
    │  创建 Subscription                    │               │
    │  向下游传 subscription                │               │
    │                 │  request(demand)   │               │
    │                 │ ◄──────────────────┼───────────────┤
    │  receive(1)     │  receive("1")       │  receive("1") │
    │                 │  receive(2)        │  (若通过)     │
    │                 │  receive("2")      │  receive("2") │
    │                 │  ...               │  ...          │
    │  receive(.finished)                   │               │
    │                 │  receive(completion)                │
    │                 │                    │  receive(completion)
    │                 │                    │               │
    ▼                 ▼                    ▼               ▼

要点:

  • 谁是谁的上游/下游:例如 Just(1).map { "\($0)" } 中,Just 是上游,Publishers.Map<Just<Int>, String> 是下游;.sink 时,Sink 是整条链的最终下游。
  • 订阅方向:下游调用 upstream.receive(subscriber: self),即「下游作为 Subscriber 被上游接收」,从而建立订阅。
  • 值传递方向:上游通过 subscriber.receive(value) 把值交给下游;若下游是另一个操作符的包装 Subscriber,该 Subscriber 会做变换后再调用自己的下游的 receive,形成链式传递。
  • Demand 反馈receive(_ input:) 返回 Subscribers.Demand,上游(或中间层)根据该返回值决定是否继续发送、发送多少,实现背压。

1.5 信息流流转(从订阅到结束)

从调用 subscribe(如 .sink(...))到收到完成,整条链上的调用顺序是固定的,可归纳为建立订阅 → 请求 Demand → 多次下发值 → 下发完成

阶段一:建立订阅(自上而下)

  sink(...) 被调用
       │
       ▼
  Sink 作为 Subscriber 被传给最下游 Publisher(如 Filter)
       │
       ▼
  Filter.receive(subscriber: Sink)  →  创建 FilterSubscriber,包装 Sink
       │
       ▼
  FilterSubscriber 作为 Subscriber 被传给上游(Map)
       │
       ▼
  Map.receive(subscriber: FilterSubscriber)  →  创建 MapSubscriber,包装 FilterSubscriber
       │
       ▼
  MapSubscriber 作为 Subscriber 被传给上游(Just)
       │
       ▼
  Just.receive(subscriber: MapSubscriber)  →  创建 Subscription(如 SimpleSubscription)
       │
       ▼
  subscriber.receive(subscription:)  从 Just 一路向下传递到 Sink
       │
       ▼
  Sink 保存 subscription,并调用 subscription.request(.unlimited)  [或 .max(n)]

阶段二:请求与下发(上游 → 下游)

  Subscription.request(demand)  [由 Sink 发起]
       │
       ▼
  上游(如 Just)开始向 MapSubscriber 发送值:subscriber.receive(1)
       │
       ▼
  MapSubscriber.receive(1)  →  transform(1)  →  downstream.receive("1")
       │
       ▼
  FilterSubscriber.receive("1")  →  若通过,downstream.receive("1");否则 return .max(1)
       │
       ▼
  Sink.receive("1")  →  执行 sink 的 receiveValue 闭包;返回 .none 或新 Demand
       │
       ▼
  (可选)Demand 沿链返回,上游据此决定是否继续 send

阶段三:完成

  上游发送 subscriber.receive(completion: .finished) 或 .failure(e)
       │
       ▼
  沿链向下传递 completion,每一层收到后转发给 downstream
       │
       ▼
  Sink.receive(completion:)  →  执行 receiveCompletion 闭包;置空 subscription
       │
       ▼
  订阅结束,链上各层可释放资源

信息流总览图(时序):

  Subscriber (Sink)               中间层 (Map/Filter)              Publisher (Just)
        │                                  │                              │
        │  receive(subscriber:)            │                              │
        │ ◄─────────────────────────────────────────────────────────────┤
        │                                  │  receive(subscriber:)        │
        │                                  │ ◄────────────────────────────┤
        │                                  │                              │
        │  receive(subscription:)         │  receive(subscription:)      │  create
        │ ◄─────────────────────────────────────────────────────────────┤
        │                                  │                              │
        │  request(.unlimited)            │  request(...)                 │
        │ ─────────────────────────────────────────────────────────────► │
        │                                  │                              │
        │  receive(1)                      │  receive(1) → "1"            │  send 1
        │ ◄─────────────────────────────────────────────────────────────┤
        │  receive("1")  [若经 Map]        │                              │
        │ ◄─────────────────────────────────────────────────────────────┤
        │  receive(completion:)            │  receive(completion:)        │  send completion
        │ ◄─────────────────────────────────────────────────────────────┤
        │                                  │                              │

1.6 核心协议关系小结

角色 职责 在链中的位置
Publisher 提供 receive(subscriber:),被订阅时创建 Subscription 并下发给 Subscriber 链中每一环(含操作符)都是 Publisher
Subscription 响应 request(_ demand) 向上游要数据;实现 cancel() 结束订阅 通常由最上游(如 Just)创建,引用传给下游
Subscriber 接收 receive(subscription:)receive(_ input:)receive(completion:);通过返回值反馈 Demand 链中每一环的「下游」都是 Subscriber;终端是 Sink/Assign

理解上述内部架构、响应者链、信息流后,再看任意操作符的源码,都可以套用「新 Publisher 包装上游 + 新 Subscriber 包装下游,在 receive(_ input:) 里做变换再转发」这一模式。

Mermaid 数据流图(可选渲染):

sequenceDiagram
    participant S as Sink(Subscriber)
    participant F as Filter
    participant M as Map
    participant J as Just(Publisher)

    S->>F: receive(subscriber: S)
    F->>M: receive(subscriber: FilterSub)
    M->>J: receive(subscriber: MapSub)
    J->>M: receive(subscription)
    M->>F: receive(subscription)
    F->>S: receive(subscription)
    S->>S: subscription.request(.unlimited)
    S->>F: request 向上传递
    F->>M: request
    M->>J: request
    J->>M: receive(1)
    M->>F: receive("1")
    F->>S: receive("1")
    J->>M: receive(completion)
    M->>F: receive(completion)
    F->>S: receive(completion)

2. Publisher协议实现

2.1 Publisher协议定义

public protocol Publisher {
    /// 发布的值类型
    associatedtype Output
    
    /// 错误类型
    associatedtype Failure: Error
    
    /// 接收订阅者
    func receive<S>(subscriber: S) 
        where S: Subscriber, 
              S.Input == Output, 
              S.Failure == Failure
}

2.2 Just实现分析

public struct Just<Output>: Publisher {
    public typealias Failure = Never
    
    public let output: Output
    
    public init(_ output: Output) {
        self.output = output
    }
    
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Never {
        // 创建订阅
        let subscription = Subscriptions.SimpleSubscription(
            subscriber: subscriber,
            output: output
        )
        subscriber.receive(subscription: subscription)
    }
}

关键点:

  • Just 是值类型(struct)
  • 立即发布值并完成
  • 错误类型是 Never(不会失败)

2.3 Future实现分析

public struct Future<Output, Failure: Error>: Publisher {
    public typealias Output = Output
    public typealias Failure = Failure
    
    private let promise: (@escaping (Result<Output, Failure>) -> Void) -> Void
    
    public init(_ attemptToFulfill: @escaping (@escaping (Result<Output, Failure>) -> Void) -> Void) {
        self.promise = attemptToFulfill
    }
    
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Failure {
        let subscription = FutureSubscription(
            subscriber: subscriber,
            promise: promise
        )
        subscriber.receive(subscription: subscription)
    }
}

private final class FutureSubscription<Output, Failure: Error, S: Subscriber>: Subscription 
    where S.Input == Output, S.Failure == Failure {
    
    private var subscriber: S?
    private let promise: (@escaping (Result<Output, Failure>) -> Void) -> Void
    private var hasFulfilled = false
    
    init(subscriber: S, promise: @escaping (@escaping (Result<Output, Failure>) -> Void) -> Void) {
        self.subscriber = subscriber
        self.promise = promise
    }
    
    func request(_ demand: Subscribers.Demand) {
        guard !hasFulfilled else { return }
        hasFulfilled = true
        
        promise { [weak self] result in
            guard let self = self, let subscriber = self.subscriber else { return }
            
            switch result {
            case .success(let value):
                _ = subscriber.receive(value)
                subscriber.receive(completion: .finished)
            case .failure(let error):
                subscriber.receive(completion: .failure(error))
            }
            
            self.subscriber = nil
        }
    }
    
    func cancel() {
        subscriber = nil
    }
}

关键点:

  • Future 是值类型,但内部使用引用类型 FutureSubscription
  • 只执行一次 promise
  • 使用 hasFulfilled 防止重复执行

3. Subscriber协议实现

3.1 Subscriber协议定义

public protocol Subscriber: CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure: Error
    
    func receive(subscription: Subscription)
    func receive(_ input: Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Failure>)
}

3.2 Sink实现分析

public struct Sink<Input, Failure: Error>: Subscriber, Cancellable {
    public typealias Input = Input
    public typealias Failure = Failure
    
    private let receiveValue: (Input) -> Void
    private let receiveCompletion: (Subscribers.Completion<Failure>) -> Void
    private var subscription: Subscription?
    
    public init(
        receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
        receiveValue: @escaping (Input) -> Void
    ) {
        self.receiveCompletion = receiveCompletion
        self.receiveValue = receiveValue
    }
    
    public func receive(subscription: Subscription) {
        self.subscription = subscription
        subscription.request(.unlimited)  // 请求无限值
    }
    
    public func receive(_ input: Input) -> Subscribers.Demand {
        receiveValue(input)
        return .none  // 不再请求更多值(因为已经请求了 .unlimited)
    }
    
    public func receive(completion: Subscribers.Completion<Failure>) {
        receiveCompletion(completion)
        subscription = nil
    }
    
    public func cancel() {
        subscription?.cancel()
        subscription = nil
    }
}

关键点:

  • Sink 是值类型,但内部持有 Subscription 引用
  • 默认请求 .unlimited
  • 完成或取消时清理 subscription

3.3 Assign实现分析

public struct Assign<Root, Input>: Subscriber, Cancellable {
    public typealias Input = Input
    public typealias Failure = Never
    
    public let object: Root
    public let keyPath: ReferenceWritableKeyPath<Root, Input>
    private var subscription: Subscription?
    
    public init(object: Root, keyPath: ReferenceWritableKeyPath<Root, Input>) {
        self.object = object
        self.keyPath = keyPath
    }
    
    public func receive(subscription: Subscription) {
        self.subscription = subscription
        subscription.request(.unlimited)
    }
    
    public func receive(_ input: Input) -> Subscribers.Demand {
        object[keyPath: keyPath] = input
        return .none
    }
    
    public func receive(completion: Subscribers.Completion<Never>) {
        subscription = nil
    }
    
    public func cancel() {
        subscription?.cancel()
        subscription = nil
    }
}

关键点:

  • 使用 ReferenceWritableKeyPath 修改对象属性
  • 错误类型是 Never(不会失败)

4. Operators实现原理

4.1 Map操作符实现

extension Publisher {
    public func map<T>(_ transform: @escaping (Output) -> T) -> Publishers.Map<Self, T> {
        return Publishers.Map(upstream: self, transform: transform)
    }
}

extension Publishers {
    public struct Map<Upstream: Publisher, Output>: Publisher {
        public typealias Failure = Upstream.Failure
        
        public let upstream: Upstream
        public let transform: (Upstream.Output) -> Output
        
        public init(upstream: Upstream, transform: @escaping (Upstream.Output) -> Output) {
            self.upstream = upstream
            self.transform = transform
        }
        
        public func receive<S>(subscriber: S) 
            where S: Subscriber, S.Input == Output, S.Failure == Failure {
            let mapSubscriber = MapSubscriber(
                downstream: subscriber,
                transform: transform
            )
            upstream.receive(subscriber: mapSubscriber)
        }
    }
    
    private struct MapSubscriber<Upstream: Publisher, Downstream: Subscriber>: Subscriber {
        typealias Input = Upstream.Output
        typealias Failure = Upstream.Failure
        
        let downstream: Downstream
        let transform: (Upstream.Output) -> Downstream.Input
        
        func receive(subscription: Subscription) {
            downstream.receive(subscription: subscription)
        }
        
        func receive(_ input: Upstream.Output) -> Subscribers.Demand {
            let transformed = transform(input)
            return downstream.receive(transformed)
        }
        
        func receive(completion: Subscribers.Completion<Failure>) {
            downstream.receive(completion: completion)
        }
    }
}

关键点:

  • Map 是新的 Publisher,包装上游 Publisher
  • 创建中间 Subscriber 进行转换
  • 保持错误类型不变

4.2 Filter操作符实现

extension Publisher {
    public func filter(_ predicate: @escaping (Output) -> Bool) -> Publishers.Filter<Self> {
        return Publishers.Filter(upstream: self, predicate: predicate)
    }
}

extension Publishers {
    public struct Filter<Upstream: Publisher>: Publisher {
        public typealias Output = Upstream.Output
        public typealias Failure = Upstream.Failure
        
        public let upstream: Upstream
        public let predicate: (Output) -> Bool
        
        public init(upstream: Upstream, predicate: @escaping (Output) -> Bool) {
            self.upstream = upstream
            self.predicate = predicate
        }
        
        public func receive<S>(subscriber: S) 
            where S: Subscriber, S.Input == Output, S.Failure == Failure {
            let filterSubscriber = FilterSubscriber(
                downstream: subscriber,
                predicate: predicate
            )
            upstream.receive(subscriber: filterSubscriber)
        }
    }
    
    private struct FilterSubscriber<Upstream: Publisher, Downstream: Subscriber>: Subscriber {
        typealias Input = Upstream.Output
        typealias Failure = Upstream.Failure
        
        let downstream: Downstream
        let predicate: (Input) -> Bool
        
        func receive(subscription: Subscription) {
            downstream.receive(subscription: subscription)
        }
        
        func receive(_ input: Input) -> Subscribers.Demand {
            if predicate(input) {
                return downstream.receive(input)
            } else {
                return .max(1)  // 请求下一个值
            }
        }
        
        func receive(completion: Subscribers.Completion<Failure>) {
            downstream.receive(completion: completion)
        }
    }
}

关键点:

  • 不满足条件时返回 .max(1) 继续请求
  • 满足条件时才传递给下游

4.3 FlatMap操作符实现

extension Publisher {
    public func flatMap<T, P: Publisher>(
        maxPublishers: Subscribers.Demand = .unlimited,
        _ transform: @escaping (Output) -> P
    ) -> Publishers.FlatMap<P, Self> 
        where P.Failure == Failure {
        return Publishers.FlatMap(
            upstream: self,
            maxPublishers: maxPublishers,
            transform: transform
        )
    }
}

extension Publishers {
    public struct FlatMap<NewPublisher: Publisher, Upstream: Publisher>: Publisher 
        where NewPublisher.Failure == Upstream.Failure {
        
        public typealias Output = NewPublisher.Output
        public typealias Failure = Upstream.Failure
        
        public let upstream: Upstream
        public let maxPublishers: Subscribers.Demand
        public let transform: (Upstream.Output) -> NewPublisher
        
        public init(
            upstream: Upstream,
            maxPublishers: Subscribers.Demand,
            transform: @escaping (Upstream.Output) -> NewPublisher
        ) {
            self.upstream = upstream
            self.maxPublishers = maxPublishers
            self.transform = transform
        }
        
        public func receive<S>(subscriber: S) 
            where S: Subscriber, S.Input == Output, S.Failure == Failure {
            let flatMapSubscriber = FlatMapSubscriber(
                downstream: subscriber,
                maxPublishers: maxPublishers,
                transform: transform
            )
            upstream.receive(subscriber: flatMapSubscriber)
        }
    }
    
    private final class FlatMapSubscriber<Upstream: Publisher, NewPublisher: Publisher, Downstream: Subscriber>: Subscriber {
        typealias Input = Upstream.Output
        typealias Failure = Upstream.Failure
        
        private let downstream: Downstream
        private let maxPublishers: Subscribers.Demand
        private let transform: (Input) -> NewPublisher
        private var activeSubscriptions: [AnyCancellable] = []
        private var subscription: Subscription?
        private var demand: Subscribers.Demand = .none
        
        init(
            downstream: Downstream,
            maxPublishers: Subscribers.Demand,
            transform: @escaping (Input) -> NewPublisher
        ) {
            self.downstream = downstream
            self.maxPublishers = maxPublishers
            self.transform = transform
        }
        
        func receive(subscription: Subscription) {
            self.subscription = subscription
            downstream.receive(subscription: InnerSubscription(parent: self))
        }
        
        func receive(_ input: Input) -> Subscribers.Demand {
            let newPublisher = transform(input)
            let cancellable = newPublisher.sink(
                receiveCompletion: { [weak self] completion in
                    self?.handleCompletion(completion)
                },
                receiveValue: { [weak self] value in
                    _ = self?.downstream.receive(value)
                }
            )
            activeSubscriptions.append(cancellable)
            return .none
        }
        
        func receive(completion: Subscribers.Completion<Failure>) {
            // 处理完成
        }
        
        private func handleCompletion(_ completion: Subscribers.Completion<Failure>) {
            // 处理内部 Publisher 完成
        }
    }
}

关键点:

  • 管理多个内部 Publisher 订阅
  • 使用 maxPublishers 限制并发数
  • 需要复杂的生命周期管理

5. Subjects实现原理

5.1 PassthroughSubject实现

public final class PassthroughSubject<Output, Failure: Error>: Subject {
    private var subscribers: [AnySubscriber<Output, Failure>] = []
    private let lock = NSRecursiveLock()
    
    public func send(_ value: Output) {
        lock.lock()
        defer { lock.unlock() }
        
        let currentSubscribers = subscribers
        for subscriber in currentSubscribers {
            _ = subscriber.receive(value)
        }
    }
    
    public func send(completion: Subscribers.Completion<Failure>) {
        lock.lock()
        defer { lock.unlock() }
        
        let currentSubscribers = subscribers
        subscribers.removeAll()
        
        for subscriber in currentSubscribers {
            subscriber.receive(completion: completion)
        }
    }
    
    public func send(subscription: Subscription) {
        // 实现 Subject 协议
    }
    
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Failure {
        lock.lock()
        defer { lock.unlock() }
        
        let anySubscriber = AnySubscriber(subscriber)
        subscribers.append(anySubscriber)
        
        subscriber.receive(subscription: PassthroughSubscription(
            subject: self,
            subscriber: anySubscriber
        ))
    }
    
    private func removeSubscriber(_ subscriber: AnySubscriber<Output, Failure>) {
        lock.lock()
        defer { lock.unlock() }
        
        subscribers.removeAll { $0 === subscriber }
    }
}

private final class PassthroughSubscription<Output, Failure: Error>: Subscription {
    weak var subject: PassthroughSubject<Output, Failure>?
    let subscriber: AnySubscriber<Output, Failure>
    var demand: Subscribers.Demand = .none
    
    init(
        subject: PassthroughSubject<Output, Failure>,
        subscriber: AnySubscriber<Output, Failure>
    ) {
        self.subject = subject
        self.subscriber = subscriber
    }
    
    func request(_ demand: Subscribers.Demand) {
        self.demand += demand
    }
    
    func cancel() {
        subject?.removeSubscriber(subscriber)
        subject = nil
    }
}

关键点:

  • 使用锁保护 subscribers 数组
  • 不保存当前值,新订阅者不会收到历史值
  • 使用 weak 引用避免循环引用

5.2 CurrentValueSubject实现

public final class CurrentValueSubject<Output, Failure: Error>: Subject {
    private var subscribers: [AnySubscriber<Output, Failure>] = []
    private let lock = NSRecursiveLock()
    private var _value: Output
    
    public var value: Output {
        get {
            lock.lock()
            defer { lock.unlock() }
            return _value
        }
        set {
            send(newValue)
        }
    }
    
    public init(_ value: Output) {
        self._value = value
    }
    
    public func send(_ value: Output) {
        lock.lock()
        defer { lock.unlock() }
        
        _value = value
        let currentSubscribers = subscribers
        for subscriber in currentSubscribers {
            _ = subscriber.receive(value)
        }
    }
    
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Failure {
        lock.lock()
        defer { lock.unlock() }
        
        let anySubscriber = AnySubscriber(subscriber)
        subscribers.append(anySubscriber)
        
        subscriber.receive(subscription: CurrentValueSubscription(
            subject: self,
            subscriber: anySubscriber
        ))
        
        // 立即发送当前值
        _ = subscriber.receive(_value)
    }
}

关键点:

  • 保存当前值 _value
  • 新订阅者立即收到当前值
  • 使用锁保护状态

6. Schedulers实现原理

6.1 Scheduler协议

public protocol Scheduler {
    associatedtype SchedulerTimeType: Strideable where SchedulerTimeType.Stride: SchedulerTimeIntervalConvertible
    associatedtype SchedulerOptions
    
    var now: SchedulerTimeType { get }
    var minimumTolerance: SchedulerTimeType.Stride { get }
    
    func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void)
    func schedule(
        after date: SchedulerTimeType,
        tolerance: SchedulerTimeType.Stride,
        options: SchedulerOptions?,
        _ action: @escaping () -> Void
    )
    func schedule(
        after date: SchedulerTimeType,
        interval: SchedulerTimeType.Stride,
        tolerance: SchedulerTimeType.Stride,
        options: SchedulerOptions?,
        _ action: @escaping () -> Void
    ) -> Cancellable
}

6.2 DispatchQueue Scheduler实现

extension DispatchQueue: Scheduler {
    public struct SchedulerOptions {
        public var qos: DispatchQoS
        public var flags: DispatchWorkItemFlags
        public var group: DispatchGroup?
    }
    
    public struct SchedulerTimeType: Strideable {
        public let dispatchTime: DispatchTime
        
        public func distance(to other: SchedulerTimeType) -> Stride {
            return Stride(dispatchTime.uptimeNanoseconds - other.dispatchTime.uptimeNanoseconds)
        }
        
        public func advanced(by n: Stride) -> SchedulerTimeType {
            return SchedulerTimeType(
                dispatchTime: DispatchTime(uptimeNanoseconds: dispatchTime.uptimeNanoseconds + n.magnitude)
            )
        }
    }
    
    public var now: SchedulerTimeType {
        return SchedulerTimeType(dispatchTime: .now())
    }
    
    public var minimumTolerance: SchedulerTimeType.Stride {
        return SchedulerTimeType.Stride(0)
    }
    
    public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
        if let options = options {
            async(group: options.group, qos: options.qos, flags: options.flags, execute: action)
        } else {
            async(execute: action)
        }
    }
    
    public func schedule(
        after date: SchedulerTimeType,
        tolerance: SchedulerTimeType.Stride,
        options: SchedulerOptions?,
        _ action: @escaping () -> Void
    ) {
        let deadline = date.dispatchTime
        if let options = options {
            asyncAfter(deadline: deadline, qos: options.qos, flags: options.flags, execute: action)
        } else {
            asyncAfter(deadline: deadline, execute: action)
        }
    }
}

关键点:

  • DispatchQueue 适配为 Scheduler
  • 使用 DispatchTime 作为时间类型
  • 支持 QoS 和 DispatchGroup

7. 背压处理机制

7.1 Demand系统

extension Subscribers {
    public struct Demand: Equatable, Hashable {
        public static let unlimited: Demand
        public static let max: (Int) -> Demand
        public static let none: Demand
        
        public static func + (lhs: Demand, rhs: Demand) -> Demand
        public static func - (lhs: Demand, rhs: Demand) -> Demand
        public static func += (lhs: inout Demand, rhs: Demand)
        public static func -= (lhs: inout Demand, rhs: Demand)
    }
}

Demand 的作用:

  • 控制 Publisher 发送值的速度
  • 实现背压(backpressure)
  • 防止内存溢出

7.2 背压处理示例

class BackpressureSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never
    
    private var subscription: Subscription?
    private let bufferSize: Int
    private var buffer: [Int] = []
    
    init(bufferSize: Int = 10) {
        self.bufferSize = bufferSize
    }
    
    func receive(subscription: Subscription) {
        self.subscription = subscription
        // 初始请求 bufferSize 个值
        subscription.request(.max(bufferSize))
    }
    
    func receive(_ input: Int) -> Subscribers.Demand {
        buffer.append(input)
        
        // 处理缓冲区
        processBuffer()
        
        // 如果缓冲区未满,请求更多值
        if buffer.count < bufferSize {
            return .max(1)
        } else {
            return .none  // 暂停请求
        }
    }
    
    func receive(completion: Subscribers.Completion<Never>) {
        // 处理完成
    }
    
    private func processBuffer() {
        // 处理缓冲区中的数据
        while !buffer.isEmpty {
            let value = buffer.removeFirst()
            print("处理: \(value)")
        }
        
        // 处理完后请求更多值
        subscription?.request(.max(bufferSize - buffer.count))
    }
}

8. 性能优化策略

8.1 值类型优化

Combine 大量使用值类型(struct),避免堆分配:

// 值类型,零成本抽象
struct Just<Output>: Publisher { }
struct Map<Upstream, Output>: Publisher { }
struct Filter<Upstream>: Publisher { }

8.2 类型擦除(eraseToAnyPublisher)

eraseToAnyPublisher() 是 Combine 中非常重要的方法,用于隐藏 Publisher 的具体类型,只暴露 OutputFailure 类型信息。这在需要统一返回类型、简化接口、避免类型泄露等场景中非常有用。

8.2.1 为什么需要类型擦除

问题:类型泄露(Type Leakage)

Combine 的操作符链式调用会产生复杂的嵌套类型,这些类型信息会"泄露"到函数签名中:

// ❌ 问题:类型过于复杂,难以维护
func fetchUserData() -> Publishers.Map<
    Publishers.FlatMap<
        Publishers.Catch<
            Publishers.Map<
                URLSession.DataTaskPublisher,
                User
            >,
            Just<User>
        >,
        Publishers.Map<
            Publishers.Debounce<
                PassthroughSubject<String, Never>,
                RunLoop
            >,
            URLSession.DataTaskPublisher
        >
    >,
    String
> {
    // 实现...
}

// ✅ 解决:使用 eraseToAnyPublisher() 简化类型
func fetchUserData() -> AnyPublisher<String, Never> {
    // 实现...
    return publisher.eraseToAnyPublisher()
}

类型擦除的优势:

  1. 简化接口:隐藏内部实现细节,只暴露必要的类型信息(Output 和 Failure)
  2. 统一返回类型:不同分支可以返回不同的具体 Publisher,但统一为 AnyPublisher
  3. 避免类型泄露:防止复杂的嵌套类型污染 API
  4. 提高可维护性:修改内部实现不影响外部接口
8.2.2 eraseToAnyPublisher 的基本用法

基本语法:

extension Publisher {
    /// 将 Publisher 转换为 AnyPublisher,隐藏具体类型
    public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
        return AnyPublisher(self)
    }
}

使用示例:

// 示例1:函数返回类型简化
func loadData() -> AnyPublisher<String, Error> {
    return URLSession.shared.dataTaskPublisher(for: url)
        .map(\.data)
        .compactMap { String(data: $0, encoding: .utf8) }
        .mapError { $0 as Error }
        .eraseToAnyPublisher()  // 隐藏 URLSession.DataTaskPublisher 等具体类型
}

// 示例2:条件分支统一返回类型
func fetchData(useCache: Bool) -> AnyPublisher<Data, Error> {
    if useCache {
        return loadFromCache()
            .eraseToAnyPublisher()  // Just<Data, Error> -> AnyPublisher
    } else {
        return loadFromNetwork()
            .eraseToAnyPublisher()  // URLSession.DataTaskPublisher -> AnyPublisher
    }
}

func loadFromCache() -> Just<Data> {
    return Just(Data())
}

func loadFromNetwork() -> URLSession.DataTaskPublisher {
    return URLSession.shared.dataTaskPublisher(for: url)
}
8.2.3 AnyPublisher 的内部实现

AnyPublisher 使用类型擦除模式(Type Erasure Pattern),通过包装具体 Publisher 来隐藏类型信息:

public struct AnyPublisher<Output, Failure: Error>: Publisher {
    // 使用内部 Box 类型来存储具体的 Publisher
    private let box: _AnyPublisherBox<Output, Failure>
    
    /// 初始化:接受任何符合 Publisher 协议的类型
    public init<P: Publisher>(_ publisher: P) 
        where P.Output == Output, P.Failure == Failure {
        // 将具体 Publisher 包装到 Box 中
        self.box = _AnyPublisherBox(publisher)
    }
    
    /// 实现 Publisher 协议:转发给内部 Box
    public func receive<S>(subscriber: S) 
        where S: Subscriber, S.Input == Output, S.Failure == Failure {
        box.receive(subscriber: subscriber)
    }
}

// 内部 Box 类(简化版实现)
private class _AnyPublisherBox<Output, Failure: Error> {
    private let _receive: (AnySubscriber<Output, Failure>) -> Void
    
    init<P: Publisher>(_ publisher: P) 
        where P.Output == Output, P.Failure == Failure {
        // 保存 publisher 的 receive 方法
        self._receive = { subscriber in
            publisher.receive(subscriber: subscriber)
        }
    }
    
    func receive<S: Subscriber>(_ subscriber: S) 
        where S.Input == Output, S.Failure == Failure {
        let anySubscriber = AnySubscriber(subscriber)
        _receive(anySubscriber)
    }
}

实现原理:

  • AnyPublisher 是值类型(struct),但内部持有引用类型的 Box
  • Box 存储具体 Publisher 的 receive 方法
  • 通过闭包捕获和转发,实现类型擦除
8.2.4 常见使用场景

场景1:函数返回类型统一

class DataService {
    // 不同方法返回不同的具体 Publisher,但统一为 AnyPublisher
    func fetchUser() -> AnyPublisher<User, Error> {
        return URLSession.shared.dataTaskPublisher(for: userURL)
            .map(\.data)
            .decode(type: User.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
    
    func fetchPosts() -> AnyPublisher<[Post], Error> {
        return URLSession.shared.dataTaskPublisher(for: postsURL)
            .map(\.data)
            .decode(type: [Post].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
    
    func fetchComments() -> AnyPublisher<[Comment], Never> {
        return Just([])  // 示例:返回 Just
            .eraseToAnyPublisher()
    }
}

场景2:条件分支统一类型

func loadData(source: DataSource) -> AnyPublisher<Data, Error> {
    switch source {
    case .network:
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .eraseToAnyPublisher()
            
    case .cache:
        return loadFromCache()
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
            
    case .mock:
        return Just(mockData)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}

enum DataSource {
    case network
    case cache
    case mock
}

场景3:操作符链中的类型擦除

class SearchViewModel: ObservableObject {
    @Published var searchText: String = ""
    
    var searchResults: AnyPublisher<[String], Never> {
        $searchText
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .removeDuplicates()
            .flatMap { query -> AnyPublisher<[String], Never> in
                if query.isEmpty {
                    return Just([])
                        .eraseToAnyPublisher()
                } else {
                    return self.performSearch(query: query)
                        .catch { _ in Just([]) }
                        .eraseToAnyPublisher()
                }
            }
            .eraseToAnyPublisher()  // 最终统一类型
    }
    
    private func performSearch(query: String) -> AnyPublisher<[String], Error> {
        // 搜索实现
        return URLSession.shared.dataTaskPublisher(for: searchURL)
            .map(\.data)
            .decode(type: [String].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

场景4:协议中的类型擦除

protocol DataRepository {
    func fetchData() -> AnyPublisher<Data, Error>
}

class NetworkRepository: DataRepository {
    func fetchData() -> AnyPublisher<Data, Error> {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .eraseToAnyPublisher()
    }
}

class MockRepository: DataRepository {
    func fetchData() -> AnyPublisher<Data, Error> {
        return Just(mockData)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}
8.2.5 何时使用 eraseToAnyPublisher

应该使用的情况:

  1. 函数返回类型:公开 API 需要返回 Publisher 时
  2. 协议要求:协议方法需要返回 Publisher 时
  3. 条件分支:不同分支返回不同类型,需要统一时
  4. 存储属性:需要存储 Publisher 但不想暴露具体类型时
  5. 简化接口:避免类型泄露到外部时

不应该使用的情况:

  1. 内部实现:只在内部使用的 Publisher,不需要擦除
  2. 性能敏感:类型擦除有轻微性能开销(包装和转发)
  3. 需要具体类型:需要访问具体 Publisher 的特殊方法时

示例对比:

// ✅ 正确:公开 API 使用类型擦除
class API {
    static func fetchUser(id: Int) -> AnyPublisher<User, Error> {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: User.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

// ❌ 不必要:内部实现不需要类型擦除
class ViewModel {
    private func setupBinding() {
        // 不需要 eraseToAnyPublisher,因为只在内部使用
        $searchText
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .sink { [weak self] text in
                self?.performSearch(text)
            }
            .store(in: &cancellables)
    }
}
8.2.6 类型擦除的性能考虑

性能开销:

  1. 内存开销AnyPublisher 需要额外的 Box 包装,增加一个间接层
  2. 调用开销:方法调用需要通过 Box 转发,有轻微的性能损失
  3. 优化机会:编译器无法对擦除后的类型进行特殊优化

性能对比:

// 直接使用具体类型(性能更好)
let publisher: Publishers.Map<URLSession.DataTaskPublisher, Data> = ...

// 使用类型擦除(有轻微开销)
let publisher: AnyPublisher<Data, Error> = ...
    .eraseToAnyPublisher()

建议:

  • 在公开 API 中使用类型擦除,简化接口
  • 在内部实现中尽量保持具体类型,获得更好的性能
  • 性能敏感的场景谨慎使用
8.2.7 与其他类型擦除方法对比

Combine 提供了多种类型擦除方法:

方法 用途 示例
eraseToAnyPublisher() 擦除 Publisher 类型 publisher.eraseToAnyPublisher()
AnySubscriber 擦除 Subscriber 类型 AnySubscriber(subscriber)
AnyCancellable 擦除 Cancellable 类型 AnyCancellable(cancellable)

统一使用模式:

// Publisher 类型擦除
let anyPublisher: AnyPublisher<String, Error> = publisher
    .eraseToAnyPublisher()

// Subscriber 类型擦除(内部使用)
let anySubscriber = AnySubscriber(subscriber)

// Cancellable 类型擦除(存储订阅)
let cancellable = AnyCancellable(subscription)
8.2.8 常见错误与注意事项

错误1:忘记类型擦除导致编译错误

// ❌ 错误:类型不匹配
func fetchData() -> AnyPublisher<Data, Error> {
    if condition {
        return Just(data)  // 类型是 Just<Data, Never>,不匹配
    } else {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)  // 类型是 Publishers.Map<...>,不匹配
    }
}

// ✅ 正确:使用 eraseToAnyPublisher 统一类型
func fetchData() -> AnyPublisher<Data, Error> {
    if condition {
        return Just(data)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    } else {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .eraseToAnyPublisher()
    }
}

错误2:过度使用类型擦除

// ❌ 不必要:每个操作符都擦除
let publisher = [1, 2, 3].publisher
    .map { $0 * 2 }
    .eraseToAnyPublisher()  // 不必要
    .filter { $0 > 2 }
    .eraseToAnyPublisher()  // 不必要
    .sink { print($0) }

// ✅ 正确:只在最后需要时擦除
let publisher = [1, 2, 3].publisher
    .map { $0 * 2 }
    .filter { $0 > 2 }
    .eraseToAnyPublisher()  // 只在需要统一类型时使用

错误3:类型擦除后无法访问具体方法

// ❌ 错误:AnyPublisher 没有具体 Publisher 的特殊方法
let publisher: AnyPublisher<String, Error> = ...
publisher.someSpecificMethod()  // 编译错误:AnyPublisher 没有此方法

// ✅ 正确:在擦除前使用具体方法
let publisher = specificPublisher
    .someSpecificMethod()  // 先使用具体方法
    .eraseToAnyPublisher()  // 再擦除类型
8.2.9 最佳实践总结
  1. 公开 API 使用类型擦除:简化接口,隐藏实现细节
  2. 内部实现保持具体类型:获得更好的性能和类型信息
  3. 条件分支统一类型:使用 eraseToAnyPublisher() 统一返回类型
  4. 避免过度使用:只在必要时使用,不要每个操作符都擦除
  5. 注意性能影响:性能敏感场景谨慎使用

代码示例:

// 最佳实践示例
class DataManager {
    // ✅ 公开方法:使用类型擦除
    func fetchData() -> AnyPublisher<Data, Error> {
        return internalFetchData()
            .eraseToAnyPublisher()
    }
    
    // ✅ 内部方法:保持具体类型
    private func internalFetchData() -> URLSession.DataTaskPublisher {
        return URLSession.shared.dataTaskPublisher(for: url)
    }
    
    // ✅ 条件分支:统一返回类型
    func loadData(from source: DataSource) -> AnyPublisher<Data, Error> {
        switch source {
        case .network:
            return networkFetch()
                .eraseToAnyPublisher()
        case .cache:
            return cacheFetch()
                .eraseToAnyPublisher()
        }
    }
}

通过 eraseToAnyPublisher(),我们可以在保持类型安全的同时,简化 API 接口,提高代码的可维护性和可读性。

8.3 延迟执行

使用 Deferred 延迟创建 Publisher:

let deferred = Deferred {
    // 只在订阅时执行
    return expensiveOperation()
}

8.4 共享订阅

使用 share() 共享 Publisher:

let shared = expensivePublisher()
    .share()  // 多个订阅者共享同一个 Publisher

shared.sink { }  // 订阅1
shared.sink { }  // 订阅2(共享执行)

📚 总结

Combine 框架的核心优势

  1. 类型安全:充分利用 Swift 类型系统
  2. 性能优化:值类型、零成本抽象
  3. 声明式编程:代码更简洁、易读
  4. 异步处理:优雅处理异步操作
  5. 系统集成:与 SwiftUI、Foundation 深度集成

学习建议

  1. 从基础开始:理解 Publisher、Subscriber、Subscription
  2. 实践操作符:熟悉常用操作符的使用
  3. 理解背压:掌握 Demand 系统
  4. 阅读源码:深入理解实现原理
  5. 实际应用:在项目中应用 Combine

01-研究优秀开源框架@响应式编程@iOS | Combine框架:使用介绍

📋 目录


一、Combine框架使用详解

1. Combine框架概述

Combine 是 Apple 在 WWDC 2019 推出的响应式编程框架,用于处理异步事件流。它基于 ReactiveX 的设计思想,提供了声明式的 API 来处理时间序列数据。

1.1 什么是Combine

Combine 是一个声明式的 Swift 框架,用于处理随时间变化的值。它允许你通过组合(combine)不同的操作符来创建复杂的数据处理管道。

核心特点:

  • 声明式编程:描述"做什么"而不是"怎么做"
  • 函数式编程:使用高阶函数和操作符组合
  • 类型安全:充分利用 Swift 的类型系统
  • 异步处理:优雅地处理异步操作
  • 错误处理:统一的错误处理机制

1.2 Combine vs 其他框架

特性 Combine RxSwift ReactiveSwift
平台 Apple 生态(iOS 13+) 跨平台 跨平台
语言 Swift Swift Swift
官方支持 ✅ Apple 官方 ❌ 第三方 ❌ 第三方
性能 高度优化 良好 良好
学习曲线 中等 陡峭 陡峭
与系统集成 深度集成 需要适配 需要适配

1.3 适用场景

  • 网络请求:处理 API 响应
  • 用户输入:处理文本输入、按钮点击
  • 数据绑定:UI 与数据模型的双向绑定
  • 状态管理:管理应用状态变化
  • 事件处理:处理通知、定时器等事件

1.4 编程思想(背后的范式与理念)

Combine 的 API 和设计深受几种编程思想影响,理解这些思想能更快抓住「为什么这样写」而不是「怎么背 API」。

(1)响应式编程(Reactive Programming)

  • 核心:把「数据与事件」抽象成随时间推进的流,通过订阅对流中的每个值做出反应,而不是轮询或回调嵌套。
  • 在 Combine 中Publisher 就是一条流,Subscriber 订阅后对每个 receive(_ input:) 做出反应;用户输入、网络结果、定时器都可以统一成同一种「流」,用同一套操作符处理。
  • 与命令式的对比:命令式是「先做 A,再做 B,再根据结果做 C」;响应式是「当流里出现满足某条件的数据时,做 C」,逻辑由数据驱动。

(2)声明式 vs 命令式

维度 命令式(Imperative) 声明式(Declarative)
关注点 「怎么做」:一步步写清执行顺序与分支 「做什么」:描述期望的结果与约束
典型写法 循环、if-else、回调里再调回调 链式操作符:map / filter / combineLatest
在 Combine 中 手写「请求 → 等回调 → 解析 → 再请求」 publisher.map(...).flatMap(...).sink(...) 描述数据如何变换与消费

声明式让「数据流」一目了然,可读性和可测试性更好;Combine 的链式调用就是声明式的一种体现。

(3)函数式思想(Composition & Immutability)

  • 组合(Composition):小能力组合成大能力。每个操作符只做一件事(map 只做变换、filter 只做过滤),通过 .map(...).filter(...) 组合成完整管道,而不是写一个巨大的闭包。
  • 不可变(Immutability):操作符不修改上游 Publisher,而是返回新的 Publisher;上游保持不变,便于推理和复用。
  • 纯函数倾向:变换用无副作用的闭包(给定相同输入得到相同输出),副作用集中在 sinkassign 等「终端」处,便于测试和并发。

(4)流与时间序列(Streams & Time)

  • 把一切可观测的「变化」都看成时间上的序列:第 1 个值、第 2 个值、……、完成或错误。
  • 操作符可以针对「时间」语义:debounce(等一段时间再发)、throttle(间隔内只发一次)、delay(延后发射),从而统一处理「何时」而不只是「何值」。

(5)观察者与发布-订阅(Observer & Pub-Sub)

  • 观察者模式:观察者订阅被观察对象,在状态变化时得到通知。Combine 里 Subscriber 观察 Publisher。
  • 发布-订阅:发布者与订阅者解耦,通过「订阅」建立连接;Combine 用 Subscription 表示这次连接,用 request(Demand) 控制拉取节奏,是带背压的发布-订阅。

把以上几点串起来:Combine 用声明式(Publisher)和组合式操作符,在发布-订阅模型下做响应式的数据与事件处理,并借 Scheduler 控制时间与线程。理解这些思想后,再看到「为什么用 map 而不是在 sink 里写一坨」「为什么要 subscribe(on:) / receive(on:)」就会更自然。

1.5 原理概览(为何这样设计)

Combine 的核心理念可以概括为以下几点,便于后续理解「架构」与「信息流」:

理念 说明
发布-订阅 Publisher 不主动推数据,只有 Subscriber 通过 Subscription.request(demand) 请求后,才按需发送;这样下游可以控制节奏,避免被上游淹没。
背压(Backpressure) Subscriber.receive(_ input:) 的返回值类型是 Subscribers.Demand,表示「还能再要多少」;上游根据 Demand 决定是否继续发送,实现流控。
链式不可变 每个操作符(map、filter 等)都返回新的 Publisher,不修改原 Publisher;整条链是值类型组合,易于推理和测试。
调度与线程 谁在哪个线程执行由 Scheduler 决定;subscribe(on:) 指定「上游与订阅建立」所在线程,receive(on:) 指定「下游收值」所在线程,便于 UI 与后台分离。

后续「二、源码解析」中的内部架构、响应者链、信息流会与上述四点一一对应。


2. 核心概念

2.1 Publisher(发布者)

Publisher 是 Combine 的核心协议,表示可以发布值的类型。

protocol Publisher {
    associatedtype Output
    associatedtype Failure: Error
    
    func receive<S>(subscriber: S) where S: Subscriber, 
        S.Input == Output, S.Failure == Failure
}

特点:

  • 可以发布零个或多个值
  • 可能以完成或错误结束
  • 是值类型(struct)
  • 不可变(每次操作返回新的 Publisher)

示例:

// 创建一个简单的 Publisher:Just 发布单个值后立即完成
let publisher = Just("Hello, Combine!")
    .sink { value in
        print(value)  // 输出: Hello, Combine!
    }

// 使用 Sequence 的 publisher 扩展,将数组转为发布者,按序发布每个元素
let arrayPublisher = [1, 2, 3, 4, 5].publisher
    .sink { value in
        print(value)  // 依次输出: 1, 2, 3, 4, 5
    }

2.2 Subscriber(订阅者)

Subscriber 是接收 Publisher 发布值的协议。

protocol Subscriber: CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure: Error
    
    func receive(subscription: Subscription)
    func receive(_ input: Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Failure>)
}

内置 Subscriber:

  • sink:最简单的订阅方式
  • assign:将值赋给对象的属性

示例:

// 使用 sink 订阅:同时处理「值」与「完成/错误」
let cancellable = [1, 2, 3].publisher
    .sink(
        receiveCompletion: { completion in
            // 流结束时的回调:.finished 或 .failure(error)
            switch completion {
            case .finished:
                print("完成")
            case .failure(let error):
                print("错误: \(error)")
            }
        },
        receiveValue: { value in
            print("收到值: \(value)")
        }
    )

// 使用 assign 订阅:将每个发布的值赋给对象的某个属性(KeyPath)
class ViewModel {
    @Published var count: Int = 0
}

let viewModel = ViewModel()
let cancellable = [1, 2, 3].publisher
    .assign(to: \.count, on: viewModel)  // 最终 viewModel.count == 3

2.3 Subscription(订阅)

Subscription 表示订阅关系,控制数据流的生命周期。

protocol Subscription: Cancellable, CustomCombineIdentifierConvertible {
    func request(_ demand: Subscribers.Demand)
}

关键点:

  • 控制数据流的开始和结束
  • 实现背压(backpressure)控制
  • 可以取消订阅

示例:

// 自定义 Subscriber,演示背压:通过 request(.max(3)) 只拉取 3 个值
class CustomSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never
    
    func receive(subscription: Subscription) {
        // 建立订阅后,主动请求最多 3 个值(背压控制)
        subscription.request(.max(3))
    }
    
    func receive(_ input: Int) -> Subscribers.Demand {
        print("收到: \(input)")
        // 返回 .none 表示本轮不再请求更多;上游最多只会发 3 个
        return .none
    }
    
    func receive(completion: Subscribers.Completion<Never>) {
        print("完成")
    }
}

let subscriber = CustomSubscriber()
// 数组有 5 个元素,但只会收到 1、2、3
[1, 2, 3, 4, 5].publisher.subscribe(subscriber)

3. Publisher与Subscriber

3.1 内置Publisher类型

Just

发布单个值然后完成。

// Just:有订阅时发布一个值并立即发送 .finished
let just = Just("Hello")
    .sink { value in
        print(value)  // 输出: Hello
    }
Future

异步执行操作并发布结果。

// Future:封装异步回调,只执行一次,结果通过 promise 发布
func fetchData() -> Future<String, Error> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            promise(.success("数据加载完成"))
        }
    }
}

fetchData()
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("错误: \(error)")
            }
        },
        receiveValue: { value in
            print(value)  // 输出: 数据加载完成
        }
    )
Deferred

延迟创建 Publisher,直到有订阅者。

// Deferred:闭包在「第一次被订阅」时才执行,避免创建时就产生副作用
let deferred = Deferred {
    Future { promise in
        print("开始执行")
        promise(.success("结果"))
    }
}

// 此时不会执行(只创建了 Deferred,未订阅)
print("创建完成")

// 订阅时才执行内部 Future,并收到 "结果"
deferred.sink { value in
    print(value)  // 输出: 开始执行, 结果
}
Empty

不发布任何值,可选择立即完成或永不完成。Empty 是 Combine 中非常有用的占位符 Publisher,常用于条件分支、错误处理、以及保持订阅活跃。

基本用法:

// 立即完成:不发送任何 value,只发送 completion
let empty = Empty<String, Never>(completeImmediately: true)
    .sink(
        receiveCompletion: { _ in print("完成") },
        receiveValue: { _ in }
    )

// 永不完成:既不发值也不发 completion,常用于测试或「占位」
let never = Empty<String, Never>(completeImmediately: false)

Empty 的占位操作:

Empty 最常见的用途是作为占位符 Publisher,在条件不满足时提供一个"空"的 Publisher,避免返回 Optional 或处理 nil 的情况。

1. 条件分支中的占位

// 场景:根据条件返回不同的 Publisher
func fetchData(shouldFetch: Bool) -> AnyPublisher<String, Never> {
    if shouldFetch {
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .compactMap { String(data: $0, encoding: .utf8) }
            .replaceError(with: "")
            .eraseToAnyPublisher()
    } else {
        // 使用 Empty 作为占位,不执行任何操作
        return Empty(completeImmediately: true)
            .eraseToAnyPublisher()
    }
}

// 使用:无论条件如何,返回类型都是 AnyPublisher<String, Never>
fetchData(shouldFetch: true)
    .sink { print($0) }  // 正常接收数据

fetchData(shouldFetch: false)
    .sink { print($0) }  // 立即完成,不接收任何值

2. 错误处理中的占位

// 在 catch 中使用 Empty 作为备用 Publisher
func loadUserData() -> AnyPublisher<User, Error> {
    return URLSession.shared.dataTaskPublisher(for: userURL)
        .map(\.data)
        .decode(type: User.self, decoder: JSONDecoder())
        .catch { error -> AnyPublisher<User, Error> in
            if error is DecodingError {
                // 解码错误时返回空 Publisher,不发送任何值
                return Empty(completeImmediately: true)
                    .eraseToAnyPublisher()
            } else {
                // 其他错误继续传播
                return Fail(error: error)
                    .eraseToAnyPublisher()
            }
        }
        .eraseToAnyPublisher()
}

3. flatMap 中的条件占位

// 在 flatMap 中根据条件决定是否执行操作
class SearchViewModel: ObservableObject {
    @Published var searchText: String = ""
    @Published var results: [String] = []
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $searchText
            .flatMap { query -> AnyPublisher<[String], Never> in
                if query.isEmpty {
                    // 空查询时返回 Empty,不执行搜索
                    return Empty(completeImmediately: true)
                        .eraseToAnyPublisher()
                } else {
                    // 执行搜索
                    return self.search(query: query)
                        .catch { _ in Just([]) }
                        .eraseToAnyPublisher()
                }
            }
            .assign(to: \.results, on: self)
            .store(in: &cancellables)
    }
    
    private func search(query: String) -> AnyPublisher<[String], Error> {
        // 搜索实现
        return Just(["结果1", "结果2"])
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}

4. 使用 Empty 保持订阅活跃(常驻任务)

Empty 的 completeImmediately: false 模式可以创建一个永不完成的 Publisher,这在需要保持订阅活跃、贯穿整个程序生命周期的场景中非常有用。

场景1:常驻的后台任务

class BackgroundTaskManager {
    private var cancellables = Set<AnyCancellable>()
    
    // 创建一个永不完成的 Empty 作为基础流
    private let keepAlive = Empty<Never, Never>(completeImmediately: false)
        .eraseToAnyPublisher()
    
    func startBackgroundTask() {
        // 使用 flatMap 将 Empty 转换为周期性的任务流
        keepAlive
            .flatMap { _ -> AnyPublisher<Date, Never> in
                // 每 5 秒执行一次任务
                return Timer.publish(every: 5.0, on: .main, in: .common)
                    .autoconnect()
                    .map { _ in Date() }
                    .eraseToAnyPublisher()
            }
            .sink { [weak self] date in
                self?.performBackgroundTask(at: date)
            }
            .store(in: &cancellables)
    }
    
    private func performBackgroundTask(at date: Date) {
        print("执行后台任务: \(date)")
        // 执行实际的后台任务,如数据同步、状态检查等
    }
    
    func stopBackgroundTask() {
        cancellables.removeAll()  // 取消所有订阅
    }
}

场景2:常驻的事件监听

class AppLifecycleManager {
    private var cancellables = Set<AnyCancellable>()
    
    func startMonitoring() {
        // 使用 Empty 作为基础流,保持订阅活跃
        Empty<Never, Never>(completeImmediately: false)
            .flatMap { _ -> AnyPublisher<Notification, Never> in
                // 监听多个通知
                let appWillEnterForeground = NotificationCenter.default
                    .publisher(for: UIApplication.willEnterForegroundNotification)
                
                let appDidEnterBackground = NotificationCenter.default
                    .publisher(for: UIApplication.didEnterBackgroundNotification)
                
                // 合并多个通知流
                return Publishers.Merge(appWillEnterForeground, appDidEnterBackground)
                    .eraseToAnyPublisher()
            }
            .sink { [weak self] notification in
                self?.handleAppLifecycleEvent(notification)
            }
            .store(in: &cancellables)
    }
    
    private func handleAppLifecycleEvent(_ notification: Notification) {
        switch notification.name {
        case UIApplication.willEnterForegroundNotification:
            print("应用即将进入前台")
        case UIApplication.didEnterBackgroundNotification:
            print("应用进入后台")
        default:
            break
        }
    }
}

场景3:常驻的心跳/保活机制

class HeartbeatManager {
    private var cancellables = Set<AnyCancellable>()
    private let heartbeatInterval: TimeInterval = 30.0
    
    func startHeartbeat() {
        // 使用 Empty 保持订阅,然后转换为心跳流
        Empty<Never, Never>(completeImmediately: false)
            .flatMap { [weak self] _ -> AnyPublisher<Void, Never> in
                guard let self = self else {
                    return Empty(completeImmediately: true).eraseToAnyPublisher()
                }
                
                // 创建心跳定时器
                return Timer.publish(every: self.heartbeatInterval, on: .main, in: .common)
                    .autoconnect()
                    .map { _ in () }
                    .eraseToAnyPublisher()
            }
            .sink { [weak self] _ in
                self?.sendHeartbeat()
            }
            .store(in: &cancellables)
    }
    
    private func sendHeartbeat() {
        // 发送心跳请求
        print("发送心跳: \(Date())")
        // 实际实现:发送网络请求保持连接活跃
    }
    
    func stopHeartbeat() {
        cancellables.removeAll()
    }
}

场景4:常驻的数据同步任务

class DataSyncManager {
    private var cancellables = Set<AnyCancellable>()
    private let syncInterval: TimeInterval = 60.0
    
    func startAutoSync() {
        // 使用 Empty 作为基础流,保持订阅贯穿应用生命周期
        Empty<Never, Never>(completeImmediately: false)
            .flatMap { [weak self] _ -> AnyPublisher<SyncResult, Never> in
                guard let self = self else {
                    return Empty(completeImmediately: true).eraseToAnyPublisher()
                }
                
                // 创建周期性同步流
                return Timer.publish(every: self.syncInterval, on: .main, in: .common)
                    .autoconnect()
                    .flatMap { _ -> AnyPublisher<SyncResult, Never> in
                        return self.performSync()
                            .catch { _ in Just(SyncResult.failure) }
                            .eraseToAnyPublisher()
                    }
                    .eraseToAnyPublisher()
            }
            .sink { [weak self] result in
                self?.handleSyncResult(result)
            }
            .store(in: &cancellables)
    }
    
    private func performSync() -> AnyPublisher<SyncResult, Error> {
        // 执行数据同步
        return Just(SyncResult.success)
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
    
    private func handleSyncResult(_ result: SyncResult) {
        print("同步结果: \(result)")
    }
    
    enum SyncResult {
        case success
        case failure
    }
}

场景5:更优雅的常驻任务实现(推荐方式)

虽然 Empty 可以用来保持订阅,但更推荐使用 Timer.publish().autoconnect()PassthroughSubject 来实现常驻任务:

class BetterBackgroundTaskManager {
    private var cancellables = Set<AnyCancellable>()
    
    func startBackgroundTask() {
        // 方式1:直接使用 Timer(推荐)
        Timer.publish(every: 5.0, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                self?.performTask()
            }
            .store(in: &cancellables)
        
        // 方式2:使用 PassthroughSubject 控制(更灵活)
        let taskTrigger = PassthroughSubject<Void, Never>()
        
        taskTrigger
            .sink { [weak self] _ in
                self?.performTask()
            }
            .store(in: &cancellables)
        
        // 可以手动触发或结合 Timer
        Timer.publish(every: 5.0, on: .main, in: .common)
            .autoconnect()
            .sink { _ in taskTrigger.send() }
            .store(in: &cancellables)
    }
    
    private func performTask() {
        print("执行任务")
    }
}

Empty 占位操作的最佳实践:

  1. 类型一致性:使用 Empty 时确保类型匹配(Output 和 Failure)
  2. 立即完成 vs 永不完成
    • completeImmediately: true:用于条件分支,表示"跳过此分支"
    • completeImmediately: false:用于保持订阅活跃,但更推荐使用 Timer 或 Subject
  3. 结合 eraseToAnyPublisher():在使用 Empty 时通常需要类型擦除,以保持类型一致性
  4. 避免过度使用:对于常驻任务,优先考虑 Timer 或 PassthroughSubject,Empty 更适合作为占位符

Empty 的常见使用模式总结:

使用场景 completeImmediately 说明
条件分支占位 true 条件不满足时返回空流
错误处理占位 true 某些错误情况下不发送值
测试占位 false 测试中模拟永不完成的流
保持订阅(不推荐) false 可用但更推荐 Timer/Subject

注意事项:

  • Empty 是值类型(struct),每次创建都是新实例
  • completeImmediately: false 的 Empty 会保持订阅活跃,但不会发送任何值
  • 对于常驻任务,虽然可以用 Empty 实现,但使用 Timer 或 PassthroughSubject 更直观和高效
Fail

立即发布错误。

// Fail:有订阅时立即发送 .failure(error),不发送任何正常值
enum MyError: Error {
    case customError
}

let fail = Fail<String, MyError>(error: .customError)
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("错误: \(error)")
            }
        },
        receiveValue: { _ in }
    )
Sequence

从序列创建 Publisher。

// 符合 Sequence 的类型都有 .publisher,按顺序发布元素
let sequence = (1...5).publisher
    .sink { value in
        print(value)  // 输出: 1, 2, 3, 4, 5
    }

3.2 自定义Publisher

// 自定义 Publisher:从数组按需发布元素,遵循背压
struct CustomPublisher: Publisher {
    typealias Output = Int
    typealias Failure = Never
    
    let values: [Int]
    
    func receive<S>(subscriber: S) where S: Subscriber, 
        S.Input == Output, S.Failure == Failure {
        // 收到订阅者时,创建自定义 Subscription 并下发给订阅者
        let subscription = CustomSubscription(
            subscriber: subscriber,
            values: values
        )
        subscriber.receive(subscription: subscription)
    }
}

// 自定义 Subscription:根据 request(demand) 按需从 values 取数并下发
class CustomSubscription<S: Subscriber>: Subscription 
    where S.Input == Int, S.Failure == Never {
    
    var subscriber: S?
    let values: [Int]
    var currentIndex = 0
    var requested: Subscribers.Demand = .none
    
    init(subscriber: S, values: [Int]) {
        self.subscriber = subscriber
        self.values = values
    }
    
    func request(_ demand: Subscribers.Demand) {
        requested += demand
        
        // 在 demand 允许且还有数据时,逐个下发
        while requested > .none && currentIndex < values.count {
            let value = values[currentIndex]
            currentIndex += 1
            requested -= .max(1)
            
            _ = subscriber?.receive(value)
        }
        
        if currentIndex >= values.count {
            subscriber?.receive(completion: .finished)
            cancel()
        }
    }
    
    func cancel() {
        subscriber = nil
    }
}

// 使用自定义 Publisher:行为等价于 [1,2,3].publisher
let custom = CustomPublisher(values: [1, 2, 3])
    .sink { value in
        print(value)  // 输出: 1, 2, 3
    }

4. Operators操作符

4.1 转换操作符

map

转换每个值。

// map:对每个元素做变换,类型可改变
[1, 2, 3].publisher
    .map { $0 * 2 }
    .sink { print($0) }  // 输出: 2, 4, 6
flatMap

将多个 Publisher 扁平化。

// flatMap:每个元素映射为一个新 Publisher,再把这些 Publisher 的输出「压平」成一条流
["A", "B", "C"].publisher
    .flatMap { letter in
        (1...2).publisher.map { "\(letter)\($0)" }
    }
    .sink { print($0) }  // 输出: A1, A2, B1, B2, C1, C2
compactMap

过滤 nil 值。

// compactMap:类似 map,但闭包返回 Optional;nil 会被丢弃,不往下游发
["1", "2", "abc", "3"].publisher
    .compactMap { Int($0) }
    .sink { print($0) }  // 输出: 1, 2, 3
scan

累积值。

// scan:给定初始值,每收到一个元素就与当前累积值做运算,并下发新的累积值
[1, 2, 3, 4, 5].publisher
    .scan(0, +)
    .sink { print($0) }  // 输出: 1, 3, 6, 10, 15

4.2 过滤操作符

filter

过滤值。

// filter:只下发谓词为 true 的值
[1, 2, 3, 4, 5].publisher
    .filter { $0 % 2 == 0 }
    .sink { print($0) }  // 输出: 2, 4
removeDuplicates

移除重复值。

// removeDuplicates:连续相同只发第一个,相当于「相邻去重」
[1, 1, 2, 2, 3, 3].publisher
    .removeDuplicates()
    .sink { print($0) }  // 输出: 1, 2, 3
first / last

获取第一个或最后一个值。

// first:只取第一个元素,取到后发完成
[1, 2, 3, 4, 5].publisher
    .first()
    .sink { print($0) }  // 输出: 1

// last:必须等上游完成,再发最后一个元素
[1, 2, 3, 4, 5].publisher
    .last()
    .sink { print($0) }  // 输出: 5
dropFirst / dropLast

丢弃前几个或后几个值。

// dropFirst(n):跳过前 n 个,只发后面的
[1, 2, 3, 4, 5].publisher
    .dropFirst(2)
    .sink { print($0) }  // 输出: 3, 4, 5

4.3 组合操作符

combineLatest

组合多个 Publisher 的最新值。

// combineLatest:两边都至少发过一个值后,每次任一边发新值就组合「两边当前最新值」下发
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .combineLatest(publisher2)
    .sink { value1, value2 in
        print("\(value1): \(value2)")
    }

publisher1.send("A")  // 无输出(publisher2 尚未发过值)
publisher2.send(1)    // 输出: A: 1
publisher1.send("B")  // 输出: B: 1(用 B 与 2 的最新值 1 组合)
publisher2.send(2)    // 输出: B: 2
merge

合并多个 Publisher。

// merge:多个流合并成一条,哪个先发就先收到哪个,类型必须相同
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .merge(with: publisher2)
    .sink { print($0) }

publisher1.send(1)  // 输出: 1
publisher2.send(2)  // 输出: 2
publisher1.send(3)  // 输出: 3
zip

按顺序组合多个 Publisher。

// zip:按「第 n 个与第 n 个」配对,凑齐一对才下发,顺序严格
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .zip(publisher2)
    .sink { value1, value2 in
        print("\(value1): \(value2)")
    }

publisher1.send("A")  // 等待 publisher2 的第一个值
publisher1.send("B")  // 等待 publisher2 的第二个值
publisher2.send(1)    // 输出: A: 1
publisher2.send(2)    // 输出: B: 2

4.4 时间操作符

debounce

防抖,等待指定时间后发布最新值。

// debounce:在一段时间内没有新值时,才把「最后一次收到的值」发出去(适合搜索框)
let subject = PassthroughSubject<String, Never>()

subject
    .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
    .sink { print($0) }

subject.send("H")     // 不输出(等待 0.5s)
subject.send("He")    // 重置等待
subject.send("Hel")   // 重置等待
subject.send("Hell")  // 重置等待
subject.send("Hello") // 0.5 秒内无新值,输出: Hello
throttle

节流,在指定时间间隔内只发布第一个值。

// throttle:在时间窗口内只取一个值;latest: false 取窗口内第一个,true 取最后一个
let subject = PassthroughSubject<String, Never>()

subject
    .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: false)
    .sink { print($0) }

subject.send("A")  // 立即输出: A,开启 1 秒窗口
subject.send("B")  // 不输出(1 秒内)
subject.send("C")  // 不输出(1 秒内)
// 1 秒后
subject.send("D")  // 输出: D
delay

延迟发布值。

// delay:每个元素都延后指定时间再下发,相对顺序不变
[1, 2, 3].publisher
    .delay(for: .seconds(1), scheduler: DispatchQueue.main)
    .sink { print($0) }  // 1 秒后依次输出: 1, 2, 3

4.5 错误处理操作符

catch

捕获错误并返回备用 Publisher。

// catch:上游失败时用闭包返回一个备用 Publisher,流继续用备用流
enum MyError: Error {
    case failure
}

let publisher = Fail<String, MyError>(error: .failure)
    .catch { error -> Just<String> in
        print("捕获错误: \(error)")
        return Just("备用值")
    }
    .sink { print($0) }  // 输出: 捕获错误: failure, 备用值
retry

重试失败的 Publisher。

// retry(n):失败时重新订阅上游最多 n 次(这里是 2 次,共最多 3 次执行)
var attempts = 0

let publisher = Future<String, Error> { promise in
    attempts += 1
    if attempts < 3 {
        promise(.failure(NSError(domain: "test", code: 1)))
    } else {
        promise(.success("成功"))
    }
}
.retry(2)  // 最多重试 2 次,第 3 次成功
.sink(
    receiveCompletion: { print($0) },
    receiveValue: { print($0) }  // 输出: 成功
)
replaceError

用默认值替换错误。

// replaceError:失败时不发错误,改为发一个默认值并正常结束
let publisher = Fail<String, MyError>(error: .failure)
    .replaceError(with: "默认值")
    .sink { print($0) }  // 输出: 默认值

5. Subjects

Subjects 既是 Publisher 又是 Subscriber,可以手动发送值。

5.1 PassthroughSubject

直接传递值,不保存当前值。

// PassthroughSubject:只转发 send 的值,不存当前值,后订阅的收不到之前的值
let subject = PassthroughSubject<String, Never>()

// 订阅1
let cancellable1 = subject.sink { print("订阅1: \($0)") }

subject.send("A")  // 输出: 订阅1: A

// 订阅2:之后 send 的值两个订阅都会收到
let cancellable2 = subject.sink { print("订阅2: \($0)") }

subject.send("B")  // 输出: 订阅1: B, 订阅2: B

5.2 CurrentValueSubject

保存当前值,新订阅者会立即收到当前值。

// CurrentValueSubject:持有当前 value,新订阅者会先收到当前值再收后续 send
let subject = CurrentValueSubject<String, Never>("初始值")

// 订阅1:立即收到初始值
let cancellable1 = subject.sink { print("订阅1: \($0)") }
// 输出: 订阅1: 初始值

subject.value = "新值"  // 输出: 订阅1: 新值

// 订阅2:一订阅就收到当前值 "新值"
let cancellable2 = subject.sink { print("订阅2: \($0)") }
// 输出: 订阅2: 新值(立即收到当前值)

5.3 @Published 属性包装器

自动创建 Publisher。

// @Published:属性变化时自动发值;$name 是该属性的 Publisher
class ViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    
    init() {
        // 监听 name 的变化,防抖后处理
        $name
            .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
            .sink { [weak self] newName in
                print("名称变化: \(newName)")
            }
            .store(in: &cancellables)
    }
    
    private var cancellables = Set<AnyCancellable>()
}

let viewModel = ViewModel()
viewModel.name = "张三"  // 0.5 秒后输出: 名称变化: 张三

5.4 把属性变成 Publisher 的使用案例

在 Combine 中,有多种方式可以将属性转换为 Publisher,每种方式适用于不同的场景。理解这些方式有助于更好地使用 Combine 进行响应式编程。

5.4.1 使用 @Published 属性包装器(推荐)

@Published 是 Combine 中最常用和推荐的方式,特别适合在 ViewModel 或 ObservableObject 中使用。

基本用法:

class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    @Published var isLoggedIn: Bool = false
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 使用 $name 访问 Publisher
        $name
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .sink { [weak self] newName in
                print("名称变化: \(newName)")
                // 可以触发其他操作,如搜索、验证等
            }
            .store(in: &cancellables)
        
        // 监听多个属性
        Publishers.CombineLatest($name, $age)
            .sink { [weak self] name, age in
                print("用户信息: \(name), \(age)")
            }
            .store(in: &cancellables)
    }
}

特点:

  • ✅ 自动创建 Publisher(通过 $属性名 访问)
  • ✅ 类型安全,编译时检查
  • ✅ 与 SwiftUI 深度集成
  • ✅ 自动发送初始值(可通过 dropFirst() 跳过)
5.4.2 使用 CurrentValueSubject

CurrentValueSubject 适合需要手动控制发布时机的场景,或者需要将非 @Published 属性转换为 Publisher。

基本用法:

class SettingsManager {
    // 方式1:直接使用 CurrentValueSubject 作为存储属性
    private let _theme = CurrentValueSubject<String, Never>("light")
    var theme: String {
        get { _theme.value }
        set { _theme.value = newValue }
    }
    
    // 暴露 Publisher
    var themePublisher: AnyPublisher<String, Never> {
        _theme.eraseToAnyPublisher()
    }
    
    // 方式2:将普通属性包装为 CurrentValueSubject
    private var _userName: String = ""
    private let userNameSubject = CurrentValueSubject<String, Never>("")
    
    var userName: String {
        get { _userName }
        set {
            _userName = newValue
            userNameSubject.send(newValue)
        }
    }
    
    var userNamePublisher: AnyPublisher<String, Never> {
        userNameSubject.eraseToAnyPublisher()
    }
}

实际应用场景:

class NetworkManager {
    private let _connectionStatus = CurrentValueSubject<ConnectionStatus, Never>(.disconnected)
    
    var connectionStatus: ConnectionStatus {
        get { _connectionStatus.value }
    }
    
    var connectionStatusPublisher: AnyPublisher<ConnectionStatus, Never> {
        _connectionSubject.eraseToAnyPublisher()
    }
    
    func connect() {
        // 网络连接逻辑
        _connectionStatus.send(.connecting)
        // ... 连接成功后
        _connectionStatus.send(.connected)
    }
    
    enum ConnectionStatus {
        case disconnected
        case connecting
        case connected
    }
}

// 使用
let networkManager = NetworkManager()
networkManager.connectionStatusPublisher
    .sink { status in
        print("连接状态: \(status)")
    }
    .store(in: &cancellables)
5.4.3 使用 PassthroughSubject(不保存当前值)

PassthroughSubject 适合事件类型的属性,不需要保存当前值,只关注变化事件。

基本用法:

class ButtonViewModel {
    // 按钮点击事件
    let buttonTap = PassthroughSubject<Void, Never>()
    
    // 用户操作事件
    let userAction = PassthroughSubject<UserAction, Never>()
    
    enum UserAction {
        case login
        case logout
        case refresh
    }
}

// 使用
let viewModel = ButtonViewModel()
viewModel.buttonTap
    .sink { print("按钮被点击") }
    .store(in: &cancellables)

viewModel.userAction
    .sink { action in
        switch action {
        case .login: print("用户登录")
        case .logout: print("用户登出")
        case .refresh: print("刷新数据")
        }
    }
    .store(in: &cancellables)

// 触发事件
viewModel.buttonTap.send()
viewModel.userAction.send(.login)
5.4.4 使用 KVO(Key-Value Observing)

对于 NSObject 的子类,可以使用 KVO 将属性转换为 Publisher。

基本用法:

import Combine

class Person: NSObject {
    @objc dynamic var name: String = ""
    @objc dynamic var age: Int = 0
}

// 使用
let person = Person()

// 将 KVO 属性转换为 Publisher
person.publisher(for: \.name, options: [.initial, .new])
    .sink { name in
        print("姓名变化: \(name)")
    }
    .store(in: &cancellables)

person.publisher(for: \.age, options: [.initial, .new])
    .sink { age in
        print("年龄变化: \(age)")
    }
    .store(in: &cancellables)

// 修改属性会触发 Publisher
person.name = "张三"  // 输出: 姓名变化: 张三
person.age = 25      // 输出: 年龄变化: 25

KVO Options 说明:

  • .initial:订阅时立即发送当前值
  • .new:属性变化时发送新值
  • .old:属性变化时发送旧值
  • .prior:变化前发送旧值,变化后发送新值
5.4.5 使用 NotificationCenter

将系统通知或自定义通知转换为 Publisher。

基本用法:

// 系统通知
let keyboardWillShow = NotificationCenter.default
    .publisher(for: UIResponder.keyboardWillShowNotification)
    .map { notification -> CGRect in
        (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? .zero
    }

keyboardWillShow
    .sink { frame in
        print("键盘高度: \(frame.height)")
    }
    .store(in: &cancellables)

// 自定义通知
extension Notification.Name {
    static let userDidLogin = Notification.Name("userDidLogin")
    static let dataDidUpdate = Notification.Name("dataDidUpdate")
}

let userLoginPublisher = NotificationCenter.default
    .publisher(for: .userDidLogin)
    .compactMap { $0.userInfo?["user"] as? User }

userLoginPublisher
    .sink { user in
        print("用户登录: \(user.name)")
    }
    .store(in: &cancellables)

// 发送通知
NotificationCenter.default.post(
    name: .userDidLogin,
    object: nil,
    userInfo: ["user": currentUser]
)
5.4.6 使用 Timer 将时间属性转换为 Publisher

将定时器转换为 Publisher,用于周期性更新。

基本用法:

class ClockViewModel {
    // 方式1:使用 Timer.publish
    var currentTime: AnyPublisher<Date, Never> {
        Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .eraseToAnyPublisher()
    }
    
    // 方式2:创建可控制的定时器
    private var timerCancellable: AnyCancellable?
    
    func startTimer() {
        timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] date in
                self?.updateTime(date)
            }
    }
    
    func stopTimer() {
        timerCancellable?.cancel()
        timerCancellable = nil
    }
    
    private func updateTime(_ date: Date) {
        // 更新时间
    }
}
5.4.7 组合多个属性 Publisher

使用 Combine 操作符组合多个属性 Publisher。

场景1:表单验证

class FormViewModel: ObservableObject {
    @Published var username: String = ""
    @Published var password: String = ""
    @Published var confirmPassword: String = ""
    
    // 组合多个属性,实时验证表单
    var isFormValid: AnyPublisher<Bool, Never> {
        Publishers.CombineLatest3($username, $password, $confirmPassword)
            .map { username, password, confirmPassword in
                !username.isEmpty &&
                password.count >= 6 &&
                password == confirmPassword
            }
            .eraseToAnyPublisher()
    }
    
    // 用户名验证
    var usernameValidation: AnyPublisher<String?, Never> {
        $username
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .map { username in
                if username.isEmpty {
                    return "用户名不能为空"
                } else if username.count < 3 {
                    return "用户名至少3个字符"
                }
                return nil
            }
            .eraseToAnyPublisher()
    }
}

场景2:搜索功能

class SearchViewModel: ObservableObject {
    @Published var searchText: String = ""
    @Published var selectedCategory: String = "all"
    @Published var sortOrder: SortOrder = .ascending
    
    enum SortOrder {
        case ascending
        case descending
    }
    
    // 组合多个条件,触发搜索
    var searchTrigger: AnyPublisher<(String, String, SortOrder), Never> {
        Publishers.CombineLatest3($searchText, $selectedCategory, $sortOrder)
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .eraseToAnyPublisher()
    }
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        searchTrigger
            .sink { [weak self] text, category, order in
                self?.performSearch(text: text, category: category, order: order)
            }
            .store(in: &cancellables)
    }
    
    private func performSearch(text: String, category: String, order: SortOrder) {
        // 执行搜索
    }
}

场景3:实时计算属性

class ShoppingCartViewModel: ObservableObject {
    @Published var items: [CartItem] = []
    @Published var discount: Double = 0.0
    @Published var shippingFee: Double = 0.0
    
    // 实时计算总价
    var totalPrice: AnyPublisher<Double, Never> {
        Publishers.CombineLatest3($items, $discount, $shippingFee)
            .map { items, discount, shippingFee in
                let subtotal = items.reduce(0) { $0 + $1.price * Double($1.quantity) }
                let discounted = subtotal * (1 - discount)
                return discounted + shippingFee
            }
            .eraseToAnyPublisher()
    }
    
    // 商品数量变化时自动更新
    var itemCount: AnyPublisher<Int, Never> {
        $items
            .map { $0.reduce(0) { $0 + $1.quantity } }
            .eraseToAnyPublisher()
    }
}

struct CartItem {
    let id: String
    var quantity: Int
    let price: Double
}
5.4.8 属性转换的最佳实践

1. 选择合适的转换方式

场景 推荐方式 原因
ViewModel/ObservableObject @Published 与 SwiftUI 集成,自动管理
需要手动控制发布时机 CurrentValueSubject 更灵活的控制
事件类型(不保存状态) PassthroughSubject 只关注事件,不保存值
NSObject 子类 KVO .publisher(for:) 利用现有 KVO 机制
系统通知 NotificationCenter.publisher 系统级事件
定时更新 Timer.publish 周期性更新

2. 避免内存泄漏

// ✅ 正确:使用 weak self
class ViewModel: ObservableObject {
    @Published var data: String = ""
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $data
            .sink { [weak self] value in
                self?.processData(value)
            }
            .store(in: &cancellables)
    }
    
    private func processData(_ value: String) {
        // 处理数据
    }
}

// ❌ 错误:强引用循环
class ViewModel: ObservableObject {
    @Published var data: String = ""
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $data
            .sink { [self] value in  // 强引用 self
                self.processData(value)
            }
            .store(in: &cancellables)
    }
}

3. 使用 dropFirst() 跳过初始值

class ViewModel: ObservableObject {
    @Published var searchText: String = ""
    
    init() {
        // 跳过初始值,只在用户输入时触发
        $searchText
            .dropFirst()  // 跳过初始的 ""
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .sink { [weak self] text in
                self?.performSearch(text)
            }
            .store(in: &cancellables)
    }
}

4. 类型擦除保持接口简洁

class DataManager {
    private let _data = CurrentValueSubject<[String], Never>([])
    
    // ✅ 暴露类型擦除的 Publisher
    var dataPublisher: AnyPublisher<[String], Never> {
        _data.eraseToAnyPublisher()
    }
    
    // ❌ 不推荐:直接暴露 CurrentValueSubject
    // var dataPublisher: CurrentValueSubject<[String], Never> { _data }
}

5. 组合多个属性的模式

// 模式1:CombineLatest(所有属性都变化时触发)
Publishers.CombineLatest($name, $age)
    .sink { name, age in
        // name 或 age 任一变化都会触发
    }

// 模式2:Zip(需要成对变化)
Publishers.Zip($name, $age)
    .sink { name, age in
        // name 和 age 必须都变化一次才触发
    }

// 模式3:Merge(任一变化时触发)
Publishers.Merge($name.map { "name: \($0)" }, $age.map { "age: \($0)" })
    .sink { message in
        // name 或 age 变化都会触发
    }

6. Schedulers调度器

Schedulers 决定操作在哪个线程执行。

6.1 内置Scheduler

DispatchQueue
// subscribe(on:):订阅与上游工作在哪个调度器;receive(on:):下游收值在哪个调度器(常用主线程更新 UI)
[1, 2, 3].publisher
    .subscribe(on: DispatchQueue.global())  // 在后台线程执行订阅与上游
    .receive(on: DispatchQueue.main)        // 在主线程接收并执行 sink
    .sink { print($0) }
RunLoop
// RunLoop 也符合 Scheduler,可在当前 RunLoop 上调度
[1, 2, 3].publisher
    .subscribe(on: RunLoop.current)
    .sink { print($0) }
OperationQueue
// OperationQueue 可作为 Scheduler,可限制并发数
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

[1, 2, 3, 4, 5].publisher
    .subscribe(on: queue)
    .sink { print($0) }

6.2 ImmediateScheduler

立即执行,用于测试。

// ImmediateScheduler:不延迟,立即在当前上下文执行,常用于测试
let scheduler = ImmediateScheduler.shared

[1, 2, 3].publisher
    .receive(on: scheduler)
    .sink { print($0) }

7. 错误处理

7.1 错误类型

// 定义领域错误类型,便于在 Publisher 链中统一处理
enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

func fetchData() -> AnyPublisher<String, NetworkError> {
    // setFailureType:把 Never 等改成指定 Failure 类型;eraseToAnyPublisher 隐藏具体类型
    return Just("数据")
        .setFailureType(to: NetworkError.self)
        .eraseToAnyPublisher()
}

7.2 错误处理策略

// 组合使用:先 catch 兜底、再 retry、最后 replaceError 保证 sink 只收到值
fetchData()
    .catch { error -> Just<String> in
        return Just("默认数据")
    }
    .retry(3)  // 失败时最多重试 3 次
    .replaceError(with: "错误时的默认值")  // 若仍失败,发默认值并正常结束
    .sink { value in
        print(value)
    }

8. 内存管理

8.1 AnyCancellable

保存订阅,防止提前释放。

// 订阅返回 Cancellable,不保存会被立即释放导致订阅断开;用 Set 集中管理
class ViewController {
    private var cancellables = Set<AnyCancellable>()
    
    func setupBinding() {
        $name
            .sink { print($0) }
            .store(in: &cancellables)  // 把订阅存进集合,生命周期与 ViewController 一致
    }
}

8.2 Store 内容管理机制

Store 的作用与原理

store(in:) 方法是 Combine 中管理订阅生命周期的核心机制。理解其工作原理对于正确使用 Combine 至关重要。

8.2.1 AnyCancellable 的本质
// AnyCancellable 是类型擦除的 Cancellable 包装器
public struct AnyCancellable: Cancellable, Hashable {
    private let _cancel: () -> Void
    
    public init(_ cancel: @escaping () -> Void) {
        self._cancel = cancel
    }
    
    public func cancel() {
        _cancel()
    }
    
    // 当 AnyCancellable 被释放时,自动调用 cancel()
    deinit {
        cancel()
    }
}

关键特性:

  • AnyCancellable 是值类型(struct),但内部持有取消操作的闭包
  • AnyCancellable 实例被释放时,会自动调用 cancel() 方法
  • 这确保了订阅在持有者释放时能够正确清理
8.2.2 store(in:) 方法的工作原理
extension Cancellable {
    /// 将 Cancellable 存储到 Set 中,延长其生命周期
    public func store(in set: inout Set<AnyCancellable>) {
        set.insert(AnyCancellable(self))
    }
    
    /// 将 Cancellable 存储到 AnyCancellable 中(单个订阅场景)
    public func store(in cancellable: inout AnyCancellable?) {
        cancellable = AnyCancellable(self)
    }
}

工作流程:

1. 调用 .sink(...) 或 .assign(...) 返回 Cancellable
   ↓
2. 调用 .store(in: &cancellables)
   ↓
3. 将 Cancellable 包装成 AnyCancellable
   ↓
4. 插入到 Set<AnyCancellable> 中
   ↓
5. Set 持有 AnyCancellable,延长订阅生命周期
   ↓
6. 当对象(如 ViewController)释放时,Set 也被释放
   ↓
7. Set 中所有 AnyCancellable 的 deinit 被调用
   ↓
8. 每个 AnyCancellable 的 cancel() 被调用
   ↓
9. 订阅被取消,资源被清理
8.2.3 Set<AnyCancellable> 的管理策略

为什么使用 Set?

// Set 的优势:
// 1. 自动去重(AnyCancellable 实现了 Hashable)
// 2. 高效的插入和查找
// 3. 批量管理多个订阅

class ViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    @Published var email: String = ""
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 多个订阅可以统一管理
        $name
            .sink { print("Name: \($0)") }
            .store(in: &cancellables)
        
        $age
            .sink { print("Age: \($0)") }
            .store(in: &cancellables)
        
        $email
            .sink { print("Email: \($0)") }
            .store(in: &cancellables)
    }
    
    // 当 ViewModel 释放时,所有订阅自动取消
}

生命周期管理示例:

class ViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>()
    private let viewModel = ViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBindings()
    }
    
    func setupBindings() {
        // 订阅1:监听数据变化
        viewModel.$data
            .receive(on: DispatchQueue.main)
            .sink { [weak self] data in
                self?.updateUI(with: data)
            }
            .store(in: &cancellables)
        
        // 订阅2:监听错误
        viewModel.$error
            .compactMap { $0 }
            .sink { [weak self] error in
                self?.showError(error)
            }
            .store(in: &cancellables)
        
        // 订阅3:网络请求
        viewModel.fetchData()
            .sink(
                receiveCompletion: { [weak self] completion in
                    if case .failure(let error) = completion {
                        self?.handleError(error)
                    }
                },
                receiveValue: { [weak self] data in
                    self?.handleData(data)
                }
            )
            .store(in: &cancellables)
    }
    
    // 当 ViewController 被释放时:
    // 1. cancellables Set 被释放
    // 2. Set 中所有 AnyCancellable 的 deinit 被调用
    // 3. 所有订阅自动取消,避免内存泄漏
}
8.2.4 手动管理 vs 自动管理

手动管理(不推荐):

class ViewController: UIViewController {
    private var cancellable: AnyCancellable?
    
    func setupBinding() {
        // 需要手动保存,容易忘记
        cancellable = $name
            .sink { print($0) }
        
        // 如果忘记保存,订阅会立即被释放
        $age
            .sink { print($0) }  // ❌ 立即释放,不会收到任何值
    }
    
    // 需要手动取消
    deinit {
        cancellable?.cancel()
    }
}

自动管理(推荐):

class ViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>()
    
    func setupBinding() {
        // 自动管理,无需手动 cancel
        $name
            .sink { print($0) }
            .store(in: &cancellables)
        
        $age
            .sink { print($0) }
            .store(in: &cancellables)
        
        // 对象释放时自动清理所有订阅
    }
}
8.2.5 条件性订阅管理

场景:需要动态添加/移除订阅

class ViewModel: ObservableObject {
    @Published var isEnabled: Bool = false
    @Published var data: String = ""
    
    private var cancellables = Set<AnyCancellable>()
    private var dataSubscription: AnyCancellable?
    
    init() {
        // 监听启用状态,动态管理数据订阅
        $isEnabled
            .sink { [weak self] enabled in
                if enabled {
                    self?.startDataSubscription()
                } else {
                    self?.stopDataSubscription()
                }
            }
            .store(in: &cancellables)
    }
    
    private func startDataSubscription() {
        // 创建新的订阅
        dataSubscription = $data
            .sink { print("Data: \($0)") }
        
        // 手动管理单个订阅
        // 注意:这里不使用 store(in: &cancellables),因为需要单独控制
    }
    
    private func stopDataSubscription() {
        // 手动取消订阅
        dataSubscription?.cancel()
        dataSubscription = nil
    }
}

更好的方式:使用条件操作符

class ViewModel: ObservableObject {
    @Published var isEnabled: Bool = false
    @Published var data: String = ""
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 使用 filter 或 flatMap 实现条件订阅,统一管理
        $isEnabled
            .filter { $0 }  // 只在启用时继续
            .flatMap { [weak self] _ -> AnyPublisher<String, Never> in
                guard let self = self else {
                    return Empty().eraseToAnyPublisher()
                }
                return self.$data.eraseToAnyPublisher()
            }
            .sink { print("Data: \($0)") }
            .store(in: &cancellables)
    }
}
8.2.6 Store 的最佳实践

1. 统一管理位置

class ViewModel: ObservableObject {
    // ✅ 推荐:在类的顶部声明,统一管理
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        setupSubscriptions()
    }
    
    private func setupSubscriptions() {
        // 所有订阅都在这里设置
        setupDataSubscription()
        setupErrorSubscription()
    }
}

2. 避免在闭包中创建新的 Set

// ❌ 错误:每次调用都创建新的 Set
func loadData() {
    var cancellables = Set<AnyCancellable>()
    API.fetchData()
        .sink { }
        .store(in: &cancellables)  // 函数返回后立即释放
}

// ✅ 正确:使用实例属性
class ViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    func loadData() {
        API.fetchData()
            .sink { }
            .store(in: &cancellables)  // 生命周期与 ViewModel 一致
    }
}

3. 在 SwiftUI 中的使用

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        Text(viewModel.text)
            .onAppear {
                // SwiftUI 中,使用 @StateObject 或 @ObservedObject
                // 订阅会自动管理,但也可以手动管理
                viewModel.$text
                    .sink { print($0) }
                    .store(in: &viewModel.cancellables)
            }
    }
}

class ViewModel: ObservableObject {
    @Published var text: String = ""
    var cancellables = Set<AnyCancellable>()  // 注意:在 SwiftUI 中可能需要 internal
}

4. 测试中的管理

class ViewModelTests: XCTestCase {
    func testSubscription() {
        let viewModel = ViewModel()
        var cancellables = Set<AnyCancellable>()
        var receivedValues: [String] = []
        
        viewModel.$data
            .sink { receivedValues.append($0) }
            .store(in: &cancellables)
        
        viewModel.data = "test"
        
        // 测试完成后,cancellables 会自动清理
        XCTAssertEqual(receivedValues, ["test"])
    }
}
8.2.7 Store 的内部实现细节

AnyCancellable 的 Hashable 实现:

extension AnyCancellable: Hashable {
    public func hash(into hasher: inout Hasher) {
        // 使用对象标识符(ObjectIdentifier)作为哈希值
        // 这确保了每个 AnyCancellable 实例都是唯一的
        hasher.combine(ObjectIdentifier(self))
    }
    
    public static func == (lhs: AnyCancellable, rhs: AnyCancellable) -> Bool {
        // 使用对象标识符比较
        return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
    }
}

为什么 Set 可以自动去重:

// 每个 AnyCancellable 实例都有唯一的对象标识符
// 即使包装相同的 Cancellable,也是不同的 AnyCancellable 实例
let cancellable1 = publisher.sink { }
let cancellable2 = publisher.sink { }

var set = Set<AnyCancellable>()
set.insert(AnyCancellable(cancellable1))  // 插入成功
set.insert(AnyCancellable(cancellable2))  // 插入成功(不同的实例)

// 但如果尝试插入相同的 AnyCancellable:
let anyCancellable = AnyCancellable(cancellable1)
set.insert(anyCancellable)  // 插入成功
set.insert(anyCancellable)  // 插入失败(已存在)
8.2.8 常见错误与解决方案

错误1:忘记 store

// ❌ 错误:订阅立即被释放
func setupBinding() {
    $name.sink { print($0) }  // 立即释放,不会收到任何值
}

// ✅ 正确:使用 store
func setupBinding() {
    $name
        .sink { print($0) }
        .store(in: &cancellables)
}

错误2:在局部作用域中 store

// ❌ 错误:函数返回后 Set 被释放
func loadData() {
    var cancellables = Set<AnyCancellable>()
    API.fetchData()
        .sink { }
        .store(in: &cancellables)
    // 函数返回后,cancellables 被释放,订阅被取消
}

// ✅ 正确:使用实例属性
class ViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    func loadData() {
        API.fetchData()
            .sink { }
            .store(in: &cancellables)
    }
}

错误3:循环引用导致无法释放

// ❌ 错误:强引用循环
class ViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    func setup() {
        $data.sink { [self] value in  // 强引用 self
            self.process(value)
        }
        .store(in: &cancellables)
        // self → cancellables → AnyCancellable → 闭包 → self(循环)
    }
}

// ✅ 正确:使用 weak self
class ViewModel {
    private var cancellables = Set<AnyCancellable>()
    
    func setup() {
        $data.sink { [weak self] value in  // 弱引用
            self?.process(value)
        }
        .store(in: &cancellables)
    }
}

8.3 避免循环引用

// 在 sink 里用到 self 时用 [weak self],避免 self → cancellables → 闭包 → self 的循环
class ViewModel {
    @Published var data: String = ""
    
    func setup() {
        $data
            .sink { [weak self] value in
                self?.process(value)
            }
            .store(in: &cancellables)
    }
    
    private func process(_ value: String) {
        // 处理数据
    }
    
    private var cancellables = Set<AnyCancellable>()
}

9. 实际应用场景

9.1 网络请求

// 使用 dataTaskPublisher 将请求转为 Publisher,再 map/decode 成模型
struct API {
    static func fetchUser(id: Int) -> AnyPublisher<User, Error> {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: User.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

API.fetchUser(id: 1)
    .receive(on: DispatchQueue.main)  // 回到主线程再更新 UI
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("错误: \(error)")
            }
        },
        receiveValue: { user in
            print("用户: \(user)")
        }
    )
    .store(in: &cancellables)

9.2 用户输入处理

// 搜索框:防抖 + 去重 + 非空过滤 + flatMap 发请求,结果用 assign 写回 @Published
class SearchViewModel: ObservableObject {
    @Published var searchText: String = ""
    @Published var results: [String] = []
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $searchText
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .removeDuplicates()
            .filter { !$0.isEmpty }
            .flatMap { query -> AnyPublisher<[String], Never> in
                return self.search(query: query)
                    .catch { _ in Just([]) }  // 失败时给空数组,保持 Never
                    .eraseToAnyPublisher()
            }
            .assign(to: \.results, on: self)
            .store(in: &cancellables)
    }
    
    private func search(query: String) -> AnyPublisher<[String], Error> {
        return Just(["结果1", "结果2"])
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
}

9.3 组合多个数据源

// Zip 等两个请求都完成后再一起处理,适合「同时拉用户与帖子」再更新 UI
class DashboardViewModel: ObservableObject {
    @Published var user: User?
    @Published var posts: [Post] = []
    @Published var isLoading: Bool = false
    
    private var cancellables = Set<AnyCancellable>()
    
    func loadData() {
        isLoading = true
        
        let userPublisher = API.fetchUser(id: 1)
        let postsPublisher = API.fetchPosts()
        
        Publishers.Zip(userPublisher, postsPublisher)
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    self?.isLoading = false
                    if case .failure(let error) = completion {
                        print("错误: \(error)")
                    }
                },
                receiveValue: { [weak self] user, posts in
                    self?.user = user
                    self?.posts = posts
                }
            )
            .store(in: &cancellables)
    }
}

10. 更多使用案例

10.1 表单验证(多字段实时校验)

// 用 map 生成错误文案 / 是否有效,assign 到 @Published,实现实时校验
class FormViewModel: ObservableObject {
    @Published var username: String = ""
    @Published var password: String = ""
    @Published var confirmPassword: String = ""
    @Published var isFormValid: Bool = false
    @Published var usernameError: String?
    @Published var passwordError: String?

    private var cancellables = Set<AnyCancellable>()

    init() {
        // 用户名:非空 + 长度,错误信息写回 usernameError
        $username
            .map { name in
                if name.isEmpty { return "请输入用户名" }
                if name.count < 3 { return "至少 3 个字符" }
                return nil
            }
            .assign(to: \.usernameError, on: self)
            .store(in: &cancellables)

        // 三字段 combineLatest,任一变化都重新计算表单是否有效
        Publishers.CombineLatest3($username, $password, $confirmPassword)
            .map { name, pwd, confirm in
                if name.isEmpty || pwd.isEmpty { return false }
                if pwd != confirm { return false }
                if pwd.count < 6 { return false }
                return true
            }
            .assign(to: \.isFormValid, on: self)
            .store(in: &cancellables)
    }
}

10.2 NotificationCenter 转 Publisher

// 系统通知转成 Publisher,再 map 出需要的 payload(如键盘 frame)
let keyboardWillShow = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
    .map { notification in
        (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? .zero
    }
    .receive(on: DispatchQueue.main)

keyboardWillShow
    .sink { frame in
        print("键盘高度: \(frame.height)")
    }
    .store(in: &cancellables)

// 自定义通知名同样用 publisher(for:)
extension Notification.Name {
    static let myCustomEvent = Notification.Name("MyCustomEvent")
}
let customPublisher = NotificationCenter.default.publisher(for: .myCustomEvent)

10.3 Timer 与周期任务

// Timer.publish + autoconnect:按间隔持续发当前日期,需手动 cancel 停止
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
    .autoconnect()
    .sink { date in
        print("tick: \(date)")
    }
// 5 秒后断开
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    timerPublisher.cancel()
}

// 或用 delay + flatMap 递归实现「间隔重复任务」
func repeatingTask(interval: TimeInterval) -> AnyPublisher<Date, Never> {
    Just(Date())
        .delay(for: .seconds(interval), scheduler: DispatchQueue.main)
        .flatMap { _ in repeatingTask(interval: interval) }
        .eraseToAnyPublisher()
}

10.4 SwiftUI 与 @Published 深度绑定

// @Published 变化时同步到 UserDefaults;dropFirst 避免 init 时的初始值触发写入
class SettingsViewModel: ObservableObject {
    @Published var isDarkMode: Bool = false
    @Published var fontSize: Double = 14

    private var cancellables = Set<AnyCancellable>()

    init() {
        $isDarkMode
            .dropFirst()
            .sink { UserDefaults.standard.set($0, forKey: "darkMode") }
            .store(in: &cancellables)

        $fontSize
            .dropFirst()
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .sink { UserDefaults.standard.set($0, forKey: "fontSize") }
            .store(in: &cancellables)
    }
}

// SwiftUI 通过 @ObservedObject 与 $ 绑定,自动刷新
struct SettingsView: View {
    @ObservedObject var viewModel: SettingsViewModel
    var body: some View {
        Toggle("深色模式", isOn: $viewModel.isDarkMode)
        Slider(value: $viewModel.fontSize, in: 10...24)
    }
}

10.5 多源竞速(先到先用)

// 主源失败时用 catch 切到备用源,实现主/备切换
func loadFromPrimaryOrFallback() -> AnyPublisher<Data, Error> {
    let primary = URLSession.shared.dataTaskPublisher(for: primaryURL)
        .map(\.data)
        .mapError { $0 as Error }
    let fallback = URLSession.shared.dataTaskPublisher(for: fallbackURL)
        .map(\.data)
        .mapError { $0 as Error }

    return primary
        .catch { _ in fallback }
        .eraseToAnyPublisher()
}

// 显式 race:merge 后取 first(),即「谁先完成用谁」
extension Publishers {
    static func race<A: Publisher, B: Publisher>(_ a: A, _ b: B) -> AnyPublisher<A.Output, A.Failure>
    where A.Output == B.Output, A.Failure == B.Failure {
        a.merge(with: b)
            .first()
            .eraseToAnyPublisher()
    }
}

10.6 KVO 替代(观察对象属性)

// NSObject + @objc dynamic 可用 .publisher(for:options:) 转成 Combine 流,替代 KVO
class Person: NSObject {
    @objc dynamic var name: String = ""
}

let person = Person()
let namePublisher = person.publisher(for: \.name, options: [.initial, .new])
    .compactMap { $0 as? String }
    .sink { print("name: \($0)") }

person.name = "张三"  // 输出: name: 张三

10.7 请求重试与超时

// timeout:超时未完成则发失败;retry + catch 实现重试与最终兜底
URLSession.shared.dataTaskPublisher(for: url)
    .timeout(.seconds(10), scheduler: DispatchQueue.main)
    .retry(3)
    .map(\.data)
    .decode(type: User.self, decoder: JSONDecoder())
    .catch { error -> Just<User> in
        return Just(User.placeholder)
    }
    .receive(on: DispatchQueue.main)
    .sink(
        receiveCompletion: { _ in },
        receiveValue: { user in
            // 更新 UI
        }
    )
    .store(in: &cancellables)

10.8 节流与防抖组合(搜索 + 连续点击)

// 搜索:防抖,避免每次按键都请求;失败时用 catch 给空数组
$searchText
    .debounce(for: .milliseconds(400), scheduler: RunLoop.main)
    .removeDuplicates()
    .flatMap { query in
        searchAPI(query: query).catch { _ in Just([]) }.eraseToAnyPublisher()
    }
    .receive(on: DispatchQueue.main)
    .assign(to: \.results, on: self)
    .store(in: &cancellables)

// 按钮:节流 1 秒内只响应一次,防止重复提交
buttonTapPublisher
    .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: false)
    .sink { submit() }
    .store(in: &cancellables)

04-研究优秀开源框架@UI布局@iOS | SwiftUI 布局:从使用到原理解析与编程思想

本文以严格论证的方式系统介绍 SwiftUI 的布局体系:设计哲学、声明式语法与核心容器(Stack、Frame、padding、alignment、Spacer 等)、布局流程中的提议(Proposal)与响应(Response)机制、iOS 16+ 的 Layout 协议与自定义布局,以及与 Auto Layout / Frame 的对比;文末提炼 SwiftUI 布局中所蕴含的设计模式编程思想,形成可复用的知识体系。内容参考 Apple 官方文档与 WWDC 技术 session。


目录


一、SwiftUI 布局概述与设计哲学

1.1 定位与历史

SwiftUI 是 Apple 于 2019 年推出的声明式 UI 框架,随 iOS 13 / macOS 10.15 发布。其布局系统不再基于 Auto Layout 的约束,而是基于视图树 + 父向子传递提议、子向父返回尺寸与位置的“协商”模型,最终由系统在底层将结果映射为渲染所需的 frame(或等价表示)。

  • 声明式:开发者描述“视图是什么、如何组合”,而非“如何设置 frame 或约束”;布局由框架根据视图树与修饰符推导。
  • 单一数据源:视图由状态驱动;状态变化触发视图树更新,布局随之重算,无需手写 layoutSubviews 或更新约束。

1.2 核心思想:提议与响应

SwiftUI 的布局可抽象为两阶段:

  1. 父 → 子:提议(Proposal)
    父视图向子视图提供一个提议尺寸(如“可用空间是 300×200”“请给出你的理想尺寸”),即 LayoutProposal 或等价概念(不同版本 API 名称可能不同)。

  2. 子 → 父:响应(Response)
    子视图根据提议返回自己的尺寸(以及可选的对齐锚点等);父视图根据所有子视图的响应,决定子视图的位置与自身尺寸,并可能再次上报给更上层。

因此,布局是自上而下提议、自下而上响应的递归过程;最终每个视图获得一个在父坐标系中的位置与尺寸,用于渲染。这与 Auto Layout 的“全局约束求解”不同,也与 Frame 的“直接赋值”不同。

1.3 与 Auto Layout 的关系

在 Apple 的实现中,SwiftUI 视图在底层仍会映射为 UIKit/AppKit 的视图或图层;部分场景下会生成约束或等价几何,但对开发者不可见。开发者只需面对 SwiftUI 的声明式 API;理解“提议与响应”即可推理布局行为,无需关心底层是否使用约束。


二、SwiftUI 布局使用详解

2.1 容器与堆叠:VStack、HStack、ZStack

  • VStack:垂直排列子视图;可指定 alignment(如 .leading、.center)、spacing
  • HStack:水平排列子视图;同样支持 alignment 与 spacing。
  • ZStack:重叠排列(类似图层叠加);可指定 alignment 与层叠顺序。

子视图的尺寸由自身内容与约束(如 frame、fixedSize)决定;容器根据子视图的尺寸与 spacing 计算自身尺寸,并在可用空间内对齐。

VStack(alignment: .leading, spacing: 8) {
    Text("Title")
    Text("Subtitle")
}

2.2 Frame 与尺寸修饰符

  • frame(width:height:alignment:)
    指定视图的建议尺寸或固定尺寸。例如 frame(width: 100, height: 50) 表示希望该视图占 100×50;若子视图有更大内在需求,可能被裁剪或与布局行为结合(取决于具体约束)。

  • frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)
    提供最小/理想/最大宽高,布局系统在可用空间内在此范围内选择。

  • fixedSize()
    视图“理想尺寸”优先,不受父视图提议的压缩;等价于强烈表达内在尺寸,可能导致溢出或需要滚动。

2.3 Padding 与 Spacer

  • padding(_:)
    在视图四周增加内边距;布局时视为该视图需要“额外空间”,父视图会为其预留。

  • Spacer()
    在 Stack 中占据剩余空间,将其他子视图推向一侧或两端;最小尺寸为 0,会随可用空间伸缩。

2.4 对齐与 alignment

  • alignment 在 Stack 中决定子视图在交叉轴上的对齐方式(如 VStack 中为水平方向)。
  • alignmentGuide
    可自定义视图的“对齐基准”(如让文字按基线对齐),供父视图在对齐时使用。

2.5 安全区域与边距

  • safeAreaInsetignoresSafeArea
    控制内容是否延伸进安全区(刘海、Home Indicator 等);布局时安全区会影响可用空间。

2.6 列表与滚动

  • ListScrollView
    内容可滚动;内部子视图的布局仍遵循提议-响应,但容器会提供“可滚动区域”的尺寸与内容尺寸,驱动滚动视图的 contentSize 与偏移。

2.7 GeometryReader 与几何信息

  • GeometryReader
    在布局时向子视图提供父视图给出的提议空间的尺寸与局部坐标信息(GeometryProxy:size、safeAreaInsets 等)。子视图可根据这些信息决定自身布局;注意 GeometryReader 会尽可能占据父视图提供的全部空间(表现为“贪婪”),常需配合 frame 限制其尺寸。几何信息是自上而下在布局阶段传递的,与“提议→响应”一致。

2.8 提议类型(Proposal)简述

系统在布局时使用的提议通常包含多种“意图”:

  • 未指定(unspecified):子视图可返回任意合理尺寸。
  • 固定尺寸:父视图要求子视图占满给定宽高。
  • 最小/最大:在范围内由子视图选择;与 frame(minWidth:maxWidth:...) 等修饰符对应。

子视图的 sizeThatFits(proposal:)(或等价 API)根据提议返回尺寸;容器再根据子视图的响应进行摆放。理解提议类型有助于正确实现自定义 Layout 与预测系统容器行为。


三、SwiftUI 布局原理解析

3.1 布局流程(提议与响应)的递归

形式化地,设父视图为 (P),子视图为 (C_1, \ldots, C_n):

  1. (P) 根据自身获得的父级提议与自身约束,计算可分配给子视图的空间
  2. (P) 向每个 (C_i) 发送提议(如可用空间或未指定)。
  3. 每个 (C_i) 返回尺寸(及可选对齐信息)。
  4. (P) 根据子视图的尺寸与 spacing、alignment,计算每个 (C_i) 的位置与 (P) 的总尺寸
  5. (P) 将自身总尺寸作为响应返回给其父视图。

根视图(如 Window 的根 ContentView)获得的提议通常来自窗口/屏幕的可用区域;最终递归完成后,每个视图都有确定的位置与尺寸,用于渲染。

3.2 Layout 协议(iOS 16+)

iOS 16 引入 Layout 协议,允许开发者自定义布局容器,显式参与“提议-响应”流程:

  • sizeThatFits(proposal:subviews:cache:)
    根据提议与子视图的尺寸,返回容器自身尺寸。内部需对每个子视图调用 subview.sizeThatFits(proposal) 获取其尺寸,再按自定义规则汇总。

  • placeSubviews(in:proposal:subviews:cache:)
    在给定 bounds 内,为每个子视图指定位置(通过 subview.place(at:anchor:proposal:))。位置与尺寸需与 sizeThatFits 阶段的逻辑一致,否则会出现布局错位。

示例(水平均分三列):容器收到提议后,将宽度均分给三个子视图,分别用固定宽度提议询问子视图高度,取最大高度作为容器高度;在 placeSubviews 中按三列放置,垂直居中。这体现了“先问尺寸、再放位置”的两阶段一致性。

3.3 视图树与值类型

SwiftUI 的 View 是值类型;视图树由 body 的递归求值构成,每次状态变化可能产生新的视图树。布局系统对当前视图树执行提议-响应,因此布局是纯函数式的:相同视图树与相同提议得到相同布局结果,无隐式全局状态(与 Auto Layout 的全局约束池不同)。

3.4 PreferenceKey 与自下而上的几何传递

除“父→子提议、子→父尺寸”外,SwiftUI 提供 PreferenceKey:子视图可向上传递任意值(如自身尺寸、偏移),父视图通过 .onPreferenceChangebackground(GeometryReader { ... }) 等读取。这实现了自下而上的几何信息回传,常用于“根据子视图尺寸调整父视图”或实现依赖子视图尺寸的滚动、标注等,与布局阶段的“响应”互补。


四、与 Auto Layout / Frame 的对比

维度 Frame Auto Layout SwiftUI
表达方式 命令式赋值 声明式约束 声明式视图树 + 修饰符
计算方式 手写计算 约束求解(Cassowary) 提议-响应递归
适配 手写 约束随容器变化 提议随空间与状态变化
平台 UIKit/AppKit UIKit/AppKit SwiftUI(底层可桥接 UIKit)

SwiftUI 与 Auto Layout 都属“声明式”,但 SwiftUI 不暴露约束概念,而是通过容器 + 修饰符 + 提议-响应表达布局,更贴近“从外到内分配空间、从内到外汇报尺寸”的直觉,适合声明式 UI 的组件化与组合。


五、设计模式与编程思想提炼

5.1 设计模式

模式 体现
组合模式 视图树是“组合”结构:容器(VStack/HStack)与叶子(Text/Image)统一为 View 协议;容器对子视图执行布局,与 Masonry 的“单条与复合同一接口”思想一致。
策略模式 不同容器(VStack、HStack、自定义 Layout)是不同的布局策略;同一组子视图在不同容器中呈现不同排列。
模板方法 布局流程由框架定义(提议 → 子响应 → 放置);Layout 协议的 sizeThatFitsplaceSubviews 是子类/实现类填充的“步骤”。
单一数据源 视图由状态驱动;布局由当前视图树与提议唯一决定,无二次手写 frame 或约束,避免状态不一致。

5.2 编程思想

思想 体现
声明式 描述“是什么”而非“怎么做”;布局意图通过容器与修饰符表达,由框架执行。
组合优于继承 复杂界面由简单视图与容器组合而成,而非通过继承重写 layoutSubviews。
单向数据流 状态 → 视图树 → 布局 → 渲染;布局是状态的派生,无反向“布局写回状态”(除显式回调)。
可组合性 小视图组合成大视图,布局规则随组合自然形成;与 Auto Layout 的“约束可组合”异曲同工。

5.3 思维导图

mindmap
  root((SwiftUI 布局))
    使用
      VStack HStack ZStack
      frame padding Spacer
      alignment safeArea
    原理
      提议 父向子提供空间
      响应 子向父回报尺寸
      Layout 协议 自定义容器
    设计模式
      组合 视图树 容器与叶子
      策略 不同布局策略
      模板方法 sizeThatFits placeSubviews
    编程思想
      声明式 描述是什么
      组合优于继承
      单向数据流

5.4 可复用设计清单(按“想实现什么”选模式)

目标 推荐模式/思想 说明
统一处理容器与叶子视图的布局 组合模式 容器与叶子都遵从 View;容器负责向子视图提议并放置,与 Masonry 的“单条与复合同一接口”思想一致。
支持多种排列方式(竖排、横排、网格等) 策略模式 不同布局容器(VStack、HStack、自定义 Layout)即不同策略;同一组子视图换容器即换布局。
在系统布局流程中插入自定义规则 模板方法 实现 Layout 协议的 sizeThatFits 与 placeSubviews,在框架规定的两阶段中填入自己的逻辑。
布局结果由状态唯一决定、可复现 单一数据源 视图树由状态派生,布局由视图树与提议唯一决定;不手写 frame,避免双源。
子视图尺寸/位置影响父视图决策 PreferenceKey + 自下而上传递 布局阶段外的“几何回传”,用于依赖子尺寸的父级逻辑。

5.5 小结

  • SwiftUI 布局:基于提议-响应的递归,由容器与修饰符表达意图;iOS 16+ 的 Layout 协议支持自定义布局容器;GeometryReader、PreferenceKey 提供几何信息与自下而上传递。
  • 与 Auto Layout / Frame:SwiftUI 同属声明式,但以“空间分配与尺寸回报”替代“约束求解”;与 Frame 的“直接赋值”差异更大。
  • 设计模式:组合(视图树)、策略(布局策略)、模板方法(Layout 协议)、单一数据源(状态驱动布局)。
  • 编程思想:声明式、组合优于继承、单向数据流、可组合性。理解这些有助于在 SwiftUI 中正确使用与扩展布局,并在自研声明式 UI 中复用上述思想。

参考文献

[1] Apple. SwiftUI Documentation. Developer Documentation.
[2] Apple. Layout and presentation. WWDC / SwiftUI sessions.
[3] Apple. Creating custom layouts with Layout protocol. iOS 16+ Developer Documentation.
[4] 本系列《06-Auto Layout与Frame:原理、使用与编程思想》— 传统布局体系对比。
[5] 本系列《05-Masonry框架:从使用到源码解析》《04-SnapKit框架:从使用到源码解析》— Auto Layout DSL 与编程思想。


延伸阅读

  • Auto Layout 与 Frame:本系列《06-Auto Layout与Frame:原理、使用与编程思想》— 传统两套布局体系与编程思想对照。
  • Masonry / SnapKit:本系列《05-Masonry框架》《04-SnapKit框架》— 约束 DSL 与组合、工厂、流式接口等模式在布局中的体现。

文档版本:基于 SwiftUI 公开 API 与 Apple 技术文档整理,具体行为以当前系统版本为准。

03-研究优秀开源框架@UI布局@iOS | Auto Layout 与 Frame:原理、使用与编程思想

本文以严格论证的方式系统介绍 iOS/macOS 下的两套核心布局体系:Frame 布局(基于几何矩形与手动计算)与 Auto Layout(基于约束与 Cassowary 求解)。涵盖历史演进、数学与系统原理、API 使用、适用场景对比,并在文末提炼布局系统中所蕴含的设计模式编程思想,形成可复用的知识体系。内容参考 Apple 官方文档、Cassowary 论文及业界实践。


目录


一、布局问题的形式化与两套体系的定位

1.1 布局问题在 UI 中的抽象

在图形界面中,布局(Layout) 指在给定容器与子元素的前提下,确定每个子元素在屏幕上的位置与尺寸,使界面满足设计意图且能适配不同屏幕与方向。形式化地,可表述为:

  • 输入:视图树(父子关系)、设计约束(如“按钮居中”“列表填满”)、可用空间(如 safe area、窗口大小)。
  • 输出:每个视图的 frame(或等价几何描述),即 ( (x, y, width, height) ) 或 CGRect。

因此,无论采用何种布局体系,最终落地的仍是每个视图的 frame;差异在于“由谁、以何种规则”计算这些 frame。

1.2 两套体系的核心区分

维度 Frame 布局 Auto Layout
决策主体 开发者显式设置或计算每个视图的 frame(或 bounds/center)。 开发者声明约束(线性等式/不等式),由系统求解器计算满足约束的 frame。
数学本质 直接赋值;无全局方程组。 约束系统 → 线性方程组(Cassowary)→ 求唯一解或按优先级松弛。
适配方式 需手写逻辑(如根据 superview.bounds 计算子 view 的 frame)。 通过约束关系与优先级自动随容器与内在尺寸变化而重算。
典型 API view.frame = CGRect(...)view.boundsview.center NSLayoutConstraint、约束激活、Content Hugging / Compression Resistance。

结论:Frame 是“命令式、一次一视图”的几何赋值;Auto Layout 是“声明式、全局约束”的求解。二者可并存(同一 app 中不同视图用不同方式),但同一视图不应混用(若使用 Auto Layout,则不应再直接改其 frame,应由约束驱动)。


二、Frame 布局体系详解

2.1 历史与定位

在 Auto Layout 引入之前,iOS/macOS 应用普遍采用 Frame 布局:通过设置 UIView.frame(或 boundscenter)直接指定视图在父视图坐标系中的位置与大小。其思想来源于早期桌面与移动 GUI 的“绝对/相对坐标”模型,与 Cocoa 的视图层级(view hierarchy)紧密相关。

  • 坐标系:每个视图拥有自己的 bounds(以自身为原点的矩形)和在其父视图坐标系中的 frame。子视图的 frame 是相对于父视图的 bounds 的。
  • 布局时机:开发者通常在 layoutSubviews(或 viewDidLayoutSubviews)中根据当前 bounds 计算并设置子视图的 frame,或直接在业务逻辑中赋值。

2.2 核心概念与 API

2.2.1 frame、bounds、center

  • frame:视图在父视图坐标系中的矩形(origin + size);修改 frame 会改变视图在父视图中的位置与大小。
  • bounds:视图自身坐标系中的矩形,通常 origin 为 (0,0),size 与 frame.size 一致;修改 bounds 可做滚动、缩放等(如 UIScrollView 的 contentSize 通过 bounds 等概念体现)。
  • center:视图在父视图坐标系中的中心点;与 frame 等价描述,满足 center = frame.origin + (frame.size.width/2, frame.size.height/2)

关系式(以 CGRect 表示):

[ \text{frame.origin} = \text{center} - (\text{frame.size.width}/2,\ \text{frame.size.height}/2) ]

因此指定 frame 与指定 center + size 在信息上等价;不同 API 仅便于不同表达意图。

2.2.2 布局流程中的参与时机

在 UIKit/AppKit 中,与 Frame 布局相关的关键调用链包括:

  1. setNeedsLayout / layoutIfNeeded:标记需要重新布局或立即触发布局。
  2. layoutSubviews(子类重写):在此处根据当前视图的 bounds 计算并设置子视图的 frame。
  3. viewDidLayoutSubviews(控制器):布局已完成后回调,可在此做依赖 frame 的后续逻辑。

伪代码(典型 Frame 布局子类)

override func layoutSubviews() {
    super.layoutSubviews()
    let w = bounds.width
    let h = bounds.height
    // 例如:左侧 1/3 放 label,右侧 2/3 放 button
    label.frame = CGRect(x: 0, y: 0, width: w / 3, height: h)
    button.frame = CGRect(x: w / 3, y: 0, width: 2 * w / 3, height: h)
}

2.3 坐标系统与变换

  • 坐标系:父视图的 bounds 决定其坐标空间;子视图的 frame 在该空间中定义。根视图(如 UIWindow 的 rootViewController.view)的 frame 通常与 window 的 bounds 一致(除状态栏等)。
  • 变换transform(如旋转、缩放)不改变 frame 的“逻辑”含义,但改变渲染形状;布局时若依赖 frame,需注意 transform 对 hitTesting 与布局计算的影响。Auto Layout 与 transform 可共存,但约束描述的是“未变换”的几何。

2.4 优点与局限(严格论证)

优点

  • 可预测性:每帧的几何由当前代码唯一决定,无隐式求解,便于推理与调试。
  • 性能:无约束求解与迭代,仅算术与赋值,适合对性能敏感的列表或动画。
  • 完全控制:可实现任意自定义布局逻辑(如环形排布、不规则网格)。

局限

  • 适配成本:不同屏幕尺寸、方向、安全区、动态类型需手写分支,易遗漏或重复。
  • 可维护性:复杂界面中“谁在何时改了什么 frame”难以追踪,易产生耦合。
  • 与系统特性脱节:无法直接利用 Content Hugging / Compression Resistance、约束优先级等,需自行实现等价逻辑。

因此,Frame 布局更适合:布局规则简单、对性能要求高、或需完全自定义几何的场景;复杂、多适配的 UI 更推荐 Auto Layout 或上层 DSL(如 Masonry/SnapKit)。


三、Auto Layout 约束布局体系详解

3.1 历史与理论基础

Auto Layout 于 2011 年在 macOS Lion 引入,iOS 6 起支持;其数学基础是 Cassowary 约束求解算法(Badros et al., UIST 1997)。核心思想:将“布局意图”表述为关于几何变量的线性等式与不等式,由求解器在满足约束层次(优先级)的前提下,得到唯一确定的 frame。

3.1.1 约束的线性形式

设视图 (V) 的几何变量为 (x, y, w, h)(如 left, top, width, height)。一条约束可写为:

  • 等式:( a_1 x_1 + a_2 x_2 + \cdots = b )
  • 不等式:( a_1 x_1 + a_2 x_2 + \cdots \leq b ) 或 (\geq b)

例如:“视图 A 的左边 = 视图 B 的右边 + 8”即 ( A.\text{left} = B.\text{right} + 8);“视图宽度 = 100”即 (w = 100)。系统将整套约束表示为线性方程组(或带不等式与松弛变量),由 Cassowary 增量求解,得到每个变量的值,进而得到各视图的 frame。

松弛变量(Slack Variables)与可行性:不等式约束在求解时常引入松弛变量,将 (\leq) 转为等式参与单纯形法;Cassowary 通过对偶单纯形在约束层次下最小化违反量。当 Required 约束无法同时满足时系统无解,会报错;非 Required 约束在冲突时被松弛,保证解的存在性。这一数学性质保证了“优先级 + 松弛”的语义与实现一致性。

3.1.2 约束层次(Constraint Hierarchy)

Cassowary 支持强弱约束:高优先级约束必须满足,低优先级在冲突时可被松弛(违反),从而避免无解。Apple 将优先级映射为 UILayoutPriority(0–1000);Required(1000)必须满足,其余为可选,冲突时低优先级被打破。

3.1.3 增量求解与布局传递

约束系统支持增量更新:增删或修改约束后,求解器仅重新求解受影响部分,而非全量重算,适合交互式 UI(窗口缩放、动画中更新 constant)。布局时,引擎先求解根视图的约束,再向下传递尺寸与位置,最终各视图的 frame 被写入;layoutSubviews 在此时被调用,但 Auto Layout 管理的子视图 frame 已由引擎设置。

3.2 核心概念与 API

3.2.1 约束的组成

一条约束可抽象为五元组(及扩展):

  • Item1, Attribute1:第一个对象与属性(如 view.left)。
  • Relation:Equal / LessThanOrEqual / GreaterThanOrEqual。
  • Item2, Attribute2:第二个对象与属性(可为 nil,表示与常量比较)。
  • Multiplier, Constant:线性关系中的系数与常数,即 ( \text{attr1} = \text{attr2} \times \text{multiplier} + \text{constant} )。

系统 API 示例(Swift):

NSLayoutConstraint(
    item: subview,
    attribute: .left,
    relatedBy: .equal,
    toItem: superview,
    attribute: .left,
    multiplier: 1,
    constant: 20
)

表示:subview.left = superview.left × 1 + 20。

3.2.2 内在尺寸(Intrinsic Content Size)与 CHCR

部分视图(如 UILabel、UIButton)有内在尺寸:根据内容(文字、图片)可计算出“理想”宽高。布局引擎将内在尺寸视为一组约束参与求解;Content Hugging(抗拉伸)与 Compression Resistance(抗压缩)的优先级决定在空间不足或过剩时,视图是否愿意被压缩或拉大。二者与显式约束共同决定最终 frame。

3.2.3 布局流程中的参与时机

  1. 约束被激活(isActive = true)后加入引擎。
  2. 当视图需要布局时(如 bounds 变化、约束变化),引擎重新求解约束系统,得到新 frame。
  3. 视图的 layoutSubviews 仍会被调用,但子视图的 frame 由引擎写入,开发者通常不再在 layoutSubviews 中改子视图 frame(否则与约束冲突)。

因此,使用 Auto Layout 时,约束是唯一真实来源;直接改 frame 会被后续布局覆盖,不推荐。

3.3 make / remake / update 的语义(与 Masonry/SnapKit 一致)

在使用 DSL(如 Masonry/SnapKit)时,常见三种入口:

  • make:追加约束,不移除已有约束。
  • remake:先移除该视图上由 DSL 管理的约束,再按闭包重新添加。
  • update:仅更新已存在约束的 constant(或 multiplier/priority),不增删约束条数。

系统原生 API 中对应为:添加新约束(activate)、移除约束(deactivate)、修改 constraint.constant。理解三者差异有助于正确选用,避免约束重复或遗漏。

3.4 安全区与布局边距

  • Safe Area:iOS 11+ 引入 safeAreaLayoutGuide,约束可相对于安全区(避开刘海、Home Indicator 等)而非视图边。将子视图约束到 view.safeAreaLayoutGuide 可自动适配不同设备与方向。
  • Layout MarginslayoutMarginsGuide 提供可配置的内边距参考,约束可相对于 margins 以统一留白;与 safe area 结合可表达“在安全区内再留 margin”的语义。

3.5 约束冲突与调试

当约束过多或相互矛盾时,引擎按优先级从高到低尝试满足;无法同时满足时,低优先级约束被打破,并在控制台报错(或 Xcode 中标红)。调试时可为约束设置 identifier,便于在报错与约束列表中定位。Ambiguous Layout 表示约束不足,存在多解,引擎会选其一但行为不可依赖,需补全约束。


四、Frame 与 Auto Layout 的对比与选型

维度 Frame Auto Layout
表达方式 命令式,直接赋值几何 声明式,声明关系与常数
适配 手写逻辑 约束随容器与内在尺寸自动重算
性能 无求解开销 有求解与布局传递开销
复杂度 简单界面简单,复杂界面易失控 简单界面略重,复杂界面更清晰
与系统集成 需手动处理安全区、CHCR 等 安全区、CHCR、优先级等原生支持

选型建议

  • 以 Auto Layout 为主:常规 UI、多尺寸适配、与 IB 与 SwiftUI 混用场景。
  • 以 Frame 为辅:列表 cell 内高度计算、自定义绘制视图、对性能极敏感的路径。
  • 同一视图不混用:一旦使用 Auto Layout 管理某视图,则不再直接改其 frame,由约束驱动。

五、设计模式与编程思想提炼

5.1 布局系统中的设计模式

模式 体现 说明
策略模式 Frame 与 Auto Layout 是两种不同的“布局策略”;同一视图树可选用不同策略(不同子视图用不同方式)。 将“如何计算 frame”从“何时触发布局”中分离,便于扩展新布局策略(如 SwiftUI 的布局协议)。
模板方法 layoutSubviews 是布局流程中的“钩子”;子类重写以插入自定义布局逻辑(Frame),或依赖系统在 Auto Layout 中写入 frame。 框架定义布局流程骨架,子类或系统填充具体步骤。
观察者与响应链 bounds 变化、约束变化会触发 setNeedsLayout → layoutIfNeeded → layoutSubviews;约束激活/失效会通知引擎。 变更驱动重算,避免轮询。
单一数据源 Auto Layout 中约束是 frame 的唯一真实来源;直接改 frame 与约束冲突,违背单一数据源。 减少状态不一致与难以复现的 bug。

5.2 编程思想

思想 体现
声明式 vs 命令式 Auto Layout 声明“关系与常数”,由引擎求解;Frame 命令式地“赋值”。声明式更利于适配与维护,命令式更直接、可控。
关注点分离 “要什么布局”(约束或计算式)与“何时、以何顺序布局”(引擎或 layoutSubviews)分离;业务代码描述意图,框架负责执行。
约束与松弛 Cassowary 的约束层次与松弛变量体现“必须满足”与“尽量满足”的层次化需求,对应到 API 即优先级与 CHCR。
可组合性 约束可独立添加、移除、激活、失效;子视图的约束与父视图的约束组合成全局系统,体现可组合设计。

5.3 思维导图:布局体系与编程思想

mindmap
  root((布局体系))
    Frame
      直接赋值 frame/bounds/center
      命令式 一次一视图
      layoutSubviews 中计算
    Auto Layout
      约束 线性等式/不等式
      Cassowary 求解
      声明式 全局一致
    设计模式
      策略 两种布局策略
      模板方法 layoutSubviews 钩子
      单一数据源 约束即真相
    编程思想
      声明式 vs 命令式
      关注点分离 意图与执行
      约束层次 优先级与松弛

5.4 可复用设计清单(按“想实现什么”选模式)

目标 推荐模式/思想 说明
支持多种布局方式并存(如部分视图用 Frame、部分用约束) 策略模式 将“如何计算 frame”抽象为策略,按视图或层级选用。
在固定流程中插入自定义布局逻辑 模板方法 重写 layoutSubviews,在系统布局流程的“钩子”中写入 frame 计算。
保证布局结果唯一、可复现 单一数据源 约束或 frame 计算为唯一真相来源,避免多处修改同一视图几何。
适配多尺寸、多设备 声明式约束 + 优先级 用约束表达关系与常数,用优先级处理冲突与可选约束。
高性能、完全自定义几何 Frame + layoutSubviews 无求解开销,逻辑完全可控。

5.5 小结

  • Frame:命令式、几何直接赋值,适合简单或高性能、强自定义场景;适配与逻辑需手写。
  • Auto Layout:声明式、约束驱动,由 Cassowary 求解;适合复杂 UI、多适配与系统特性集成。
  • 设计模式:策略(布局策略)、模板方法(layoutSubviews)、单一数据源(约束为真来源)。
  • 编程思想:声明式与命令式取舍、关注点分离、约束层次与可组合性。理解二者原理与适用边界,有助于在业务中正确选型并在自研布局库中复用上述思想。

参考文献

[1] Apple. Auto Layout Guide. Developer Documentation.
[2] Apple. View Programming Guide for iOS. Developer Documentation.
[3] Badros, G. J., Borning, A., & Marriott, K. (1997). Solving Linear Arithmetic Constraints for User Interface Applications. UIST 1997.
[4] Cassowary. Constraint Solving Toolkit. constraints.cs.washington.edu/cassowary/
[5] 本系列《05-Masonry框架:从使用到源码解析》— Auto Layout 与 Cassowary 在 DSL 中的运用。


延伸阅读

  • SwiftUI 布局:本系列《07-SwiftUI布局:从使用到原理解析与编程思想》— 声明式布局的提议-响应模型与 Layout 协议。
  • Masonry / SnapKit:本系列《05-Masonry框架》《04-SnapKit框架》— Auto Layout 的链式 DSL 与设计模式在布局 API 中的体现。

文档版本:基于 Apple 官方文档与 Cassowary 理论整理,实现细节以当前系统为准。

02-研究优秀开源框架@UI布局@iOS | SnapKit 框架:从使用到源码解析

本文系统介绍 iOS/macOS 下的 Auto Layout DSL 库 SnapKit:技术演进、核心原理、应用场景与源码结构,并引用约束求解理论与业界实践。


📋 目录


一、SnapKit 使用详解

1. 框架概述

SnapKit 是面向 Swift 的 Auto Layout DSL(领域特定语言),用于在代码中以声明式、链式语法描述视图的布局约束,替代冗长的 NSLayoutConstraint 手写与 Visual Format 字符串。其设计目标可概括为:

  • 可读性:约束意图接近自然语言(如“左边等于父视图左边”“宽度等于父视图一半”)。
  • 类型安全与简洁:利用 Swift 的类型与闭包,减少样板代码。
  • 可维护性:链式调用便于增删约束、设置优先级与标识,便于调试冲突。

SnapKit 是 Masonry(Objective-C 时代同类型库)在 Swift 生态中的继任者,二者同属 SnapKit 组织 维护,在 GitHub 上均获得大量 Star(SnapKit 约 20k+),被广泛应用于 iOS/macOS 应用的纯代码布局场景 [1]


2. 历史演进

技术演进可概括为:手写约束 → Visual Format → Masonry(OC DSL)→ SnapKit(Swift DSL),并与 Apple 布局技术的演进并行。

┌─────────────────────────────────────────────────────────────────────────┐
│                    布局方式演进(示意)                                    │
├─────────────────────────────────────────────────────────────────────────┤
│  1997       2006–2011        2011           2014           2015+        │
│  Cassowary  手写             引入           Masonry        SnapKit      │
│  算法论文    NSLayoutConstraint  Auto Layout  (OC DSL)      (Swift DSL)  │
│  发表       (Mac)             (iOS 6+)      链式语法       闭包+链式     │
└─────────────────────────────────────────────────────────────────────────┘
阶段 代表 特点
手写约束 NSLayoutConstraint(item:attribute:relatedBy:toItem:attribute:multiplier:constant:) 冗长、易出错、难以阅读。
Visual Format V:|[a]-[b]| 字符串描述,类型不安全,复杂布局难表达。
Masonry Objective-C,Block 链式 链式 DSL、可读性高,成为 OC 时代事实标准。
SnapKit Swift,Closure 链式 延续 Masonry 思想,利用 Swift 语法与类型,支持 labeled() 等调试与快捷 API。

SnapKit 与 Masonry 的对应关系可理解为:同一套“用链式 DSL 描述约束”的设计哲学,从 Objective-C 迁移到 Swift,并针对 Swift 做了 API 与实现上的优化 [2]


3. 理论基础:Auto Layout 与 Cassowary

SnapKit 的约束最终仍通过 Auto Layout 交给系统布局引擎执行。Auto Layout 的数学基础是 Cassowary 约束求解算法,理解其思想有助于理解“约束冲突”“优先级”“内在尺寸”等概念。

3.1 Cassowary 算法简述

Cassowary 是一种 增量式线性约束求解算法,基于对偶单纯形法(dual simplex),用于求解由 线性等式与不等式 组成的约束系统 [[3]][[4]]。其特点包括:

  • 线性:约束可写成形如 (a_1 x_1 + a_2 x_2 + \cdots = b) 或 (\le/\ge) 的形式,与“视图 A 的左边 = 视图 B 的右边 + 常数”等布局关系一致。
  • 增量:可动态增删约束并高效重新求解,适合交互式 UI(窗口缩放、动画中更新约束)。
  • 约束层次(constraint hierarchy):支持 requiredpreferred(优先级),在约束冲突时按优先级舍弃或松弛部分约束,避免无解。

参考文献

  • 原始论文:Solving Linear Arithmetic Constraints for User Interface Applications,UIST 1997 [[5]]。
  • 扩展与实现:The Cassowary Linear Arithmetic Constraint Solving Algorithm,ACM TOCHI;Washington 大学 Cassowary 工具包 [[6]]。

3.2 从约束描述到线性关系(概念)

Auto Layout 将每条约束映射为关于视图几何变量(如 left, right, width, centerX)的线性等式或不等式。SnapKit 所写的“左边等于父视图左边 + 20”即对应:

  • 变量:view.leftsuperview.left
  • 关系:view.left = superview.left + 20

多约束组成方程组,由 Cassowary 求解得到每个变量的值,从而得到各视图的 frame。优先级 对应 Cassowary 的强弱约束:高优先级必须满足,低优先级在冲突时可被违反。

3.3 流程图:从 SnapKit 到屏幕像素(概念层)

flowchart LR
  A[SnapKit API 调用] --> B[ConstraintMaker 等 DSL]
  B --> C[Constraint 描述对象]
  C --> D[NSLayoutConstraint]
  D --> E[Auto Layout 引擎]
  E --> F[Cassowary 求解]
  F --> G[布局结果 / frame]
  G --> H[渲染到屏幕]

4. 核心概念

4.1 约束的组成

在 Auto Layout 中,一条约束可抽象为:

Item1.Attribute1 Relation Item2.Attribute2 * Multiplier + Constant

例如:“视图 A 的右边 = 视图 B 的左边 - 8”即 A.right = B.left - 8。SnapKit 的链式 API 就是对这五元组(Item1, Attribute1, Relation, Item2, Attribute2, Multiplier, Constant)的封装,并增加 优先级(Priority)标识(Identifier) 等元数据。

4.2 优先级与内在尺寸

概念 说明
约束优先级 UILayoutPriority(0–1000),数值越大越优先;系统在冲突时打破低优先级约束。
Content Hugging “抗拉伸”:视图不愿比其内在内容尺寸更大;优先级高则更易保持紧凑。
Compression Resistance “抗压缩”:视图不愿比其内在内容尺寸更小;优先级高则更不易被压缩。

Label、Button 等有 intrinsicContentSize 的控件依赖 CHCR 与其它约束共同决定最终尺寸;SnapKit 可通过 .contentCompressionResistancePriority / .contentHuggingPriority 等设置(若 API 支持)或直接操作 UIView 的对应属性。

4.3 思维导图:SnapKit 概念关系

mindmap
  root((SnapKit))
    使用入口
      makeConstraints / remakeConstraints / updateConstraints
      removeConstraints
    描述对象
      ConstraintMaker
      ConstraintItem
      Constraint
    约束属性
      left right top bottom
      width height centerX centerY
      edges size margins
    关系与修饰
      equalTo offset multipliedBy priority
    底层
      NSLayoutConstraint
      Auto Layout / Cassowary

5. API 与使用模式

5.1 基本用法

// 示例:子视图填满父视图边距
view.addSubview(subview)
subview.snp.makeConstraints { make in
    make.edges.equalToSuperview()
}

// 等价于四条约束:left/top/right/bottom 分别等于 superview
// 示例:水平居中,宽度为父视图一半,距顶 20
subview.snp.makeConstraints { make in
    make.centerX.equalToSuperview()
    make.width.equalToSuperview().multipliedBy(0.5)
    make.top.equalToSuperview().offset(20)
}

5.2 常用 API 对照(伪代码语义)

SnapKit 写法 含义(伪代码)
make.left.equalToSuperview() self.left = superview.left
make.width.equalTo(100) self.width = 100
make.top.equalTo(other.snp.bottom).offset(8) self.top = other.bottom + 8
make.size.equalTo(CGSize(width: 80, height: 80)) self.width = 80, self.height = 80
make.edges.equalToSuperview() 四边与 superview 对齐
make.center.equalToSuperview() centerX/Y 与 superview 对齐
make.width.equalToSuperview().multipliedBy(0.5) self.width = superview.width * 0.5
make.priority(.high).priority(750) 为该条约束设置优先级

5.3 make / remake / update

  • makeConstraints:在已有约束基础上追加新约束,不删除旧约束。
  • remakeConstraints先移除该视图上由 SnapKit 管理的约束,再按闭包重新添加,适合布局整体变化。
  • updateConstraints仅更新闭包中涉及到的约束的 constant(或部分属性),不改变约束条数或关系,适合仅改“间距/常量”的动画或响应式布局。
// 伪代码:remake 的语义
func remakeConstraints(_ closure: (ConstraintMaker) -> Void) {
    removeSnapKitConstraints()
    makeConstraints(closure)
}

5.4 SnapKit 与 Masonry 对照

维度 Masonry(OC) SnapKit(Swift)
语法载体 Block ^(MASConstraintMaker *make){} Closure { make in }
链式返回 返回 MASConstraint 等 返回 ConstraintMakerExtendable 等
多属性快捷 edgessize edgessizemargins
调试 无内置标识 labeled("xxx") 设置 constraint identifier
维护 SnapKit 组织,OC 项目常用 SnapKit 组织,Swift 项目主流

6. 应用场景与最佳实践

场景 建议
纯代码 UI 用 SnapKit 替代手写 NSLayoutConstraint,可读性和维护性更好。
动态布局 remakeConstraintsupdateConstraints 配合 UIView.animate 更新 constant,实现动画。
列表 Cell prepareForReuse 中避免重复添加约束,可 remake 或复用约束并只更新 constant。
多分辨率/多设备 multipliedBy、比例、优先级与 CHCR 适配不同宽度与安全区域。
约束冲突调试 使用 labeled() 为约束设置 identifier,便于在控制台或 Xcode 中识别。

7. 使用案例详解

以下案例覆盖常见 UI 场景,便于直接套用或改编。

7.1 单视图:居中与尺寸

// 场景:一个头像视图,居中显示,固定 80x80
let avatarView = UIImageView()
view.addSubview(avatarView)
avatarView.snp.makeConstraints { make in
    make.center.equalToSuperview()
    make.size.equalTo(CGSize(width: 80, height: 80))
}
// 场景:宽度为父视图 60%,高度 44,水平居中,距顶 100
let button = UIButton(type: .system)
view.addSubview(button)
button.snp.makeConstraints { make in
    make.centerX.equalToSuperview()
    make.top.equalToSuperview().offset(100)
    make.width.equalToSuperview().multipliedBy(0.6)
    make.height.equalTo(44)
}

7.2 多视图垂直/水平排列

// 场景:标题 + 副标题垂直排列,整体居中,间距 8
let titleLabel = UILabel()
let subtitleLabel = UILabel()
view.addSubview(titleLabel)
view.addSubview(subtitleLabel)

titleLabel.snp.makeConstraints { make in
    make.centerX.equalToSuperview()
    make.top.equalToSuperview().offset(60)
}
subtitleLabel.snp.makeConstraints { make in
    make.centerX.equalTo(titleLabel)
    make.top.equalTo(titleLabel.snp.bottom).offset(8)
}
// 场景:三个等宽按钮水平排列,填满父视图左右边距,间距 12
let leftBtn = UIButton()
let midBtn = UIButton()
let rightBtn = UIButton()
[leftBtn, midBtn, rightBtn].forEach { view.addSubview($0) }

leftBtn.snp.makeConstraints { make in
    make.left.equalToSuperview().offset(16)
    make.centerY.equalToSuperview()
    make.height.equalTo(44)
}
midBtn.snp.makeConstraints { make in
    make.left.equalTo(leftBtn.snp.right).offset(12)
    make.centerY.equalTo(leftBtn)
    make.width.height.equalTo(leftBtn)
}
rightBtn.snp.makeConstraints { make in
    make.left.equalTo(midBtn.snp.right).offset(12)
    make.right.equalToSuperview().offset(-16)
    make.centerY.equalTo(midBtn)
    make.width.height.equalTo(midBtn)
}

7.3 安全区域与边距

// 场景:内容贴安全区域,四边留 16pt
let contentView = UIView()
view.addSubview(contentView)
contentView.snp.makeConstraints { make in
    make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(16)
    make.left.equalTo(view.safeAreaLayoutGuide.snp.left).offset(16)
    make.right.equalTo(view.safeAreaLayoutGuide.snp.right).offset(-16)
    make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-16)
}
// 使用 edges 的等价写法(SnapKit 对 safeArea 的封装)
contentView.snp.makeConstraints { make in
    make.edges.equalTo(view.safeAreaLayoutGuide).inset(UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))
}

7.4 卡片式布局(内边距 + 圆角容器)

// 场景:卡片内有一个标题和一段正文,整体有内边距
let card = UIView()
let titleLabel = UILabel()
let bodyLabel = UILabel()
card.addSubview(titleLabel)
card.addSubview(bodyLabel)
view.addSubview(card)

card.snp.makeConstraints { make in
    make.left.right.equalToSuperview().inset(20)
    make.top.equalToSuperview().offset(100)
    // 高度由内容撑起,不写 bottom,由子视图约束反推
}
titleLabel.snp.makeConstraints { make in
    make.top.left.right.equalToSuperview().inset(16)
}
bodyLabel.snp.makeConstraints { make in
    make.top.equalTo(titleLabel.snp.bottom).offset(8)
    make.left.right.equalToSuperview().inset(16)
    make.bottom.equalToSuperview().offset(-16)  // 决定 card 的底部
}

7.5 UIScrollView 内容布局

// 场景:ScrollView 内纵向堆叠内容,可滚动
let scrollView = UIScrollView()
let contentView = UIView()
scrollView.addSubview(contentView)
view.addSubview(scrollView)

scrollView.snp.makeConstraints { make in
    make.edges.equalToSuperview()
}
contentView.snp.makeConstraints { make in
    make.edges.equalToSuperview()
    make.width.equalTo(scrollView)  // 宽度与 scrollView 一致,避免横向滚动
    // 高度由子视图约束决定,最后子视图的 bottom 约束到 contentView.bottom
}

// 在 contentView 内继续添加子视图,最后一个子视图的 bottom 约束到 contentView
let lastView = UIView()
contentView.addSubview(lastView)
lastView.snp.makeConstraints { make in
    make.left.right.top.equalToSuperview()
    make.height.equalTo(200)
    make.bottom.equalToSuperview().offset(-20)  // 关键:撑开 contentView 高度
}

7.6 TableView Cell 内约束

// 在 UITableViewCell 子类中
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    contentView.addSubview(titleLabel)
    contentView.addSubview(iconView)

    titleLabel.snp.makeConstraints { make in
        make.left.equalToSuperview().offset(16)
        make.centerY.equalToSuperview()
        make.right.lessThanOrEqualTo(iconView.snp.left).offset(-8)
    }
    iconView.snp.makeConstraints { make in
        make.right.equalToSuperview().offset(-16)
        make.centerY.equalToSuperview()
        make.size.equalTo(CGSize(width: 24, height: 24))
    }
}

override func prepareForReuse() {
    super.prepareForReuse()
    // 不要在这里再次 makeConstraints,否则会重复添加;若需更新内容用 updateConstraints 或只改 constant
}

7.7 动态布局:remake 与 update

// 场景:根据状态切换“展开/收起”,用 remake 重做约束
func setExpanded(_ expanded: Bool) {
    contentView.snp.remakeConstraints { make in
        make.left.right.top.equalToSuperview()
        if expanded {
            make.height.equalTo(200)
        } else {
            make.height.equalTo(60)
        }
    }
    UIView.animate(withDuration: 0.3) {
        self.layoutIfNeeded()
    }
}
// 场景:只改间距,用 update 更新 constant,适合动画
var topOffset: Constraint?
view.snp.makeConstraints { make in
    topOffset = make.top.equalToSuperview().offset(20).constraint
    make.left.right.equalToSuperview()
    make.height.equalTo(100)
}
// 后续
topOffset?.update(offset: 80)
UIView.animate(withDuration: 0.25) {
    view.superview?.layoutIfNeeded()
}

7.8 优先级与可选的“最大宽度”

// 场景:标签最大宽度为父视图 70%,但若内容更短则保持 intrinsic 宽度
label.snp.makeConstraints { make in
    make.left.equalToSuperview().offset(16)
    make.centerY.equalToSuperview()
    make.width.lessThanOrEqualToSuperview().multipliedBy(0.7).priority(.high)
    // 不写 right,由 CHCR 与 lessThanOrEqualTo 共同决定
}

7.9 约束标识与调试

view.snp.makeConstraints { make in
    make.top.equalToSuperview().offset(20).labeled("headerTop")
    make.left.right.equalToSuperview().labeled("headerHorizontal")
}
// 约束冲突时,在控制台或 Xcode 中可根据 "headerTop" 等快速定位视图与约束

7.10 小结表

场景 推荐 API 要点
单视图居中/尺寸 makeConstraints + center / size / equalToSuperview() addSubview 再约束
多视图排列 先定一个基准视图,其余 equalTo(基准.snp.xxx) 注意谁决定总高度/总宽度
安全区域 view.safeAreaLayoutGuide.snp.xxxedges.equalTo(safeArea).inset() 适配刘海与 Home Indicator
卡片/内边距 父视图不设高度,子视图 bottom.equalToSuperview() 撑开 避免固定高度,利于多行文本
ScrollView contentView.width.equalTo(scrollView) + 最底部子视图 bottom 约束到 contentView 撑开 contentSize
Cell makeConstraints 在 init 中只做一次,prepareForReuse 不重复添加 可配合 remake 或只更新 constant
动态/动画 remakeConstraints 整体重做,updateConstraints 只改 constant 动画前改约束,动画内 layoutIfNeeded()
优先级/可选约束 .priority(.high)lessThanOrEqualTo 与 CHCR 配合,避免冲突

二、SnapKit 源码解析

1. 整体架构

SnapKit 的代码结构可分层为:DSL 入口层约束描述层约束实体层系统桥接层

flowchart TB
  subgraph 入口
    A[View.snp.makeConstraints]
  end
  subgraph DSL
    B[ConstraintMaker]
    C[ConstraintDescription]
    D[ConstraintItem]
  end
  subgraph 约束实体
    E[Constraint]
    F[ConstraintViewAttributes]
  end
  subgraph 系统
    G[NSLayoutConstraint]
    H[LayoutConstraint]
  end
  A --> B
  B --> C
  C --> D
  C --> E
  E --> F
  E --> G
  G --> H
  • 入口View.snp 返回 ConstraintViewDSL,其上提供 makeConstraints / remakeConstraints / updateConstraints 等方法,接收 (ConstraintMaker) -> Void 闭包。
  • ConstraintMaker:闭包中的 make 对象,持有当前视图(ConstraintItem)及一组 ConstraintDescription;每次调用 make.left.equalTo(...) 等会生成或更新一条 ConstraintDescription。
  • ConstraintDescription:描述“某属性 与 某目标 的 关系、倍数、常量、优先级”,可生成多条 Constraint(例如 edges 生成四条)。
  • Constraint:封装最终要安装的 NSLayoutConstraint(或其子类),负责 install() / uninstall() 与状态管理。

1.1 关键类型与职责(对照源码)

类型 文件/模块 职责简述
ConstraintViewDSL View+DSL 通过 view.snp 暴露,提供 makeConstraints / remakeConstraints / updateConstraints,持有 view。
ConstraintMaker ConstraintMaker 闭包参数 make,持有 ConstraintItem(当前视图)和 [ConstraintDescription],提供 left/right/top/bottom 等入口。
ConstraintDescription ConstraintDescription 描述单条或多条约束(如 edges 对应 4 条),持有 relation、target、multiplier、constant、priority,可生成 Constraint。
ConstraintItem ConstraintItem 对 UIView/ UILayoutGuide 的抽象,提供 layoutConstraintItem(用于 NSLayoutConstraint 的 firstItem/secondItem)。
Constraint Constraint 对应一条 NSLayoutConstraint,实现 install() 时创建并激活,uninstall() 时 deactivate 并置空引用。
LayoutConstraint LayoutConstraint NSLayoutConstraint 子类,用于在 install 时做兼容或扩展(如与 SnapKit 的关联标记)。

1.2 约束的生命周期(创建 → 安装 → 更新/移除)

makeConstraints { make in
    make.left.equalToSuperview().offset(20)   // 1. 生成 ConstraintDescription,加入 maker
}
// 2. 闭包返回后,maker 将 description 转为 Constraint,再对每个 Constraint 调用 install()
// 3. install() 内部:new NSLayoutConstraint(...); constraint.isActive = true
// 4. 若后续调用 remakeConstraints,先 uninstall 所有已安装的 Constraint,再重新执行闭包并 install

2. DSL 链与构建器模式

SnapKit 的链式 API 采用 流式接口(Fluent Interface)构建器思想:每次调用返回可继续链式调用的对象,逐步补全“属性、关系、目标、倍数、常量、优先级”。

典型调用链在概念上可拆成:

make.left          → 选定“左边界”为当前约束属性
  .equalTo(superview)  → 关系为 equal,目标为 superview(默认同属性 left)
  .offset(20)       → constant = 20
  .priority(.high) → 优先级

对应到源码中的角色(名称可能随版本略有差异):

类型/协议 作用
ConstraintMaker 入口,提供 left/right/top/bottom/width/height/centerX/centerY/edges/size 等,返回可继续链式的对象。
ConstraintMakerExtendable 扩展 edgessizemargins 等组合属性。
ConstraintMakerRelatable 提供 equalTolessThanOrEqualTogreaterThanOrEqualTo,确定“关系 + 目标”。
ConstraintMakerEditable 提供 offsetmultipliedBydividedBy 等,设置 constant 与 multiplier。
ConstraintMakerPriortizable 提供 priority(...),设置约束优先级。
ConstraintMakerFinalizable 结束链,可能返回 Constraint 供后续引用或批量操作。

因此,像 make.width.equalToSuperview().dividedBy(2).priority(100) 的调用会依次经过:选定属性 → 设关系与目标 → 设倍数 → 设优先级,最终生成一条 Constraint 描述并加入 Maker 的列表,在闭包结束后统一 install

2.1 链式调用的返回类型(协议串联)

链的每一步返回不同协议类型,使下一句只能调用合法方法,形成“约束描述”的逐步补全:

make.width                    → ConstraintMakerExtendable (可继续 .equalTo / .lessThanOrEqualTo 等)
  .equalToSuperview()         → ConstraintMakerEditable (可继续 .offset / .multipliedBy 等)
  .dividedBy(2)               → ConstraintMakerPriortizable (可继续 .priority)
  .priority(100)              → ConstraintMakerFinalizable (可 .constraint 取引用或结束)

源码中通过协议 + 泛型实现:例如 ConstraintMakerExtendableequalTo(_:) 返回 ConstraintMakerEditable,这样就不能在未设置目标前写 offset,保证调用顺序正确。

2.2 组合属性(edges / size / center)的展开

当写 make.edges.equalToSuperview() 时,内部会展开为四条约束描述:

  • make.left.equalToSuperview()
  • make.right.equalToSuperview()
  • make.top.equalToSuperview()
  • make.bottom.equalToSuperview()

每条仍走完整的链(equalTo → offset → priority),最终得到 4 个 Constraint 对象。sizecenter 同理,分别对应 2 条约束。因此一个 ConstraintDescription 可以对应多个 Constraint,在 collect 阶段会全部加入 Maker 的列表,在 install 阶段逐一安装。


3. 约束的生成与安装

3.1 安装流程(泳道图)

sequenceDiagram
  participant U as 开发者
  participant V as View.snp
  participant M as ConstraintMaker
  participant C as Constraint
  participant S as 系统 Auto Layout

  U->>V: makeConstraints { make in ... }
  V->>M: 创建 Maker(view)
  V->>M: 执行闭包(make)
  loop 每条约束描述
    U->>M: make.xxx.equalTo(...).offset(...)
    M->>M: 添加 ConstraintDescription
  end
  M->>C: 生成 Constraint 并 collect
  V->>C: install()
  loop 每条 Constraint
    C->>S: 创建/激活 NSLayoutConstraint
  end
  S-->>V: 布局更新

3.2 算法说明(约束收集与安装)

约束安装可简化为两阶段:

  1. 收集阶段:闭包执行过程中,不立即创建 NSLayoutConstraint,而是将“视图、属性、关系、目标、multiplier、constant、priority”存入 ConstraintDescription,再在适当时机(如访问 constraint 或闭包结束)生成 Constraint 对象并加入列表。
  2. 安装阶段:对列表中每个 Constraint 调用 install(),其内部根据描述创建 NSLayoutConstraint,并调用 isActive = true(或旧版 addConstraint)将约束加入视图层级,由系统布局引擎求解。

伪代码(安装逻辑概念)

function Constraint.install():
    if alreadyInstalled then return
    let c = NSLayoutConstraint(
        item: self.view, attribute: self.attr,
        relatedBy: self.relation,
        toItem: self.targetView, attribute: self.targetAttr,
        multiplier: self.multiplier, constant: self.constant
    )
    c.priority = self.priority
    c.isActive = true
    self.layoutConstraint = c
    mark as installed

3.3 ConstraintDescription → Constraint 的生成时机

  • 单属性(如 make.width.equalTo(100)):在链结束(闭包内该句执行完)时,Maker 根据当前 Description 生成一个 Constraint,加入内部数组;若链上有 .constraint,则同时返回给调用方保存。
  • 组合属性(如 make.edges.equalToSuperview()):一条 Description 会展开成多个 Constraint(edges → 4 个),全部加入数组。
  • 闭包整体结束:Maker 的 install() 被调用,遍历所有已收集的 Constraint,依次执行各自的 install(),此时才创建并激活 NSLayoutConstraint

因此“生成 Constraint”与“安装到系统”是分离的:先收集、后统一安装,便于支持 remake(先 uninstall 再重新 make)和批量操作。

3.4 uninstall 与 remake 的配合

remakeConstraints 的语义等价于:

1. 取出该 view 上由 SnapKit 管理的所有 Constraint(通过关联对象或 view 上的标记)
2. 对每个 Constraint 调用 uninstall():layoutConstraint.isActive = false,并清空对 NSLayoutConstraint 的引用
3. 再执行 makeConstraints(closure),重新收集并 install

这样可避免旧约束残留导致的冲突或多余约束。


4. 与系统 Auto Layout 的衔接

SnapKit 不实现自己的布局引擎,而是 生成并激活 NSLayoutConstraint,完全依赖系统 Auto Layout(及底层 Cassowary 求解器)。因此:

  • 性能:约束求解与布局计算由系统完成,SnapKit 只影响“约束的创建与组织方式”。
  • 兼容性:与 Interface Builder、手写约束、其他第三方布局库生成的约束可混用,只要约束系统一致(无冲突或冲突可被优先级解决)。
  • 调试:约束冲突、无法满足的约束等仍由系统报错;SnapKit 的 labeled() 可为生成的 NSLayoutConstraint.identifier 赋值,便于在 Xcode 中识别。

4.1 SnapKit 属性与 NSLayoutConstraint.Attribute 的对应

SnapKit 的 ConstraintAttribute(left, right, top, bottom, width, height, centerX, centerY 等)在 install 时会被映射为系统的 NSLayoutConstraint.Attribute

SnapKit 概念 NSLayoutConstraint.Attribute
left .left
right .right
top .top
bottom .bottom
leading .leading
trailing .trailing
width .width
height .height
centerX .centerX
centerY .centerY
leftMargin / rightMargin 等 .leftMargin, .rightMargin, ...

multiplier、constant、relation、priority 则直接传给 NSLayoutConstraint 的对应参数;toItemsecondAttribute 来自 ConstraintDescription 的 target(ConstraintItem),若目标为常数(如 equalTo(100)),则 toItem 为 nil,secondAttribute.notAnAttribute


5. 关键数据结构与约束映射

5.1 ConstraintItem:视图与 LayoutGuide 的统一抽象

系统 API 中,约束的 firstItem / secondItem 可以是 UIViewUILayoutGuide。SnapKit 用 ConstraintItem 封装二者,对外只暴露“某个对象 + 其 snp 描述”,这样 equalToSuperview()equalTo(view.safeAreaLayoutGuide.snp.top) 可以走同一套链式 API。内部在生成 NSLayoutConstraint 时,从 ConstraintItem 取出真正的 layoutConstraintItem(UIView 或 UILayoutGuide)作为 firstItem/secondItem。

5.2 约束描述到 NSLayoutConstraint 的构造(概念代码)

一条 Constraint 在 install() 时大致等价于:

// 概念代码,非逐字源码
func install() {
    guard layoutConstraint == nil else { return }
    let firstItem = description.view.layoutConstraintItem!
    let secondItem = description.target?.layoutConstraintItem  // 可为 nil
    let c = NSLayoutConstraint(
        item: firstItem,
        attribute: description.attribute.layoutAttribute,
        relatedBy: description.relation,
        toItem: secondItem,
        attribute: secondItem != nil ? description.targetAttribute.layoutAttribute : .notAnAttribute,
        multiplier: description.multiplier,
        constant: description.constant
    )
    c.priority = description.priority
    c.identifier = description.label
    c.isActive = true
    self.layoutConstraint = c
}

理解这一点即可知道:SnapKit 不参与求解,只负责“描述 → NSLayoutConstraint → isActive = true”,布局结果完全由系统 Auto Layout(及 Cassowary)决定。

5.3 updateConstraints 的“只改 constant”实现

updateConstraintsmakeConstraints 共用同一套收集逻辑,但语义是“更新已存在的约束”。实现上通常通过约束匹配:根据“视图 + 属性”(以及可选的 target)找到之前由 SnapKit 安装的 Constraint,只调用其 update(offset:) / update(inset:) 等,修改底层 NSLayoutConstraint.constant,而不新增或删除约束。因此适合“布局关系不变、只改间距或尺寸常量”的动画或响应式更新。


三、设计模式与延伸

与 Masonry 一脉相承,SnapKit 在架构中同样运用了多种设计模式与编程思想;因采用 Swift 与协议导向设计,部分实现方式与 Masonry(OC)不同,但目标一致:可读的链式 DSL、统一的约束抽象、先描述后安装。下表对照 SnapKit 与 Masonry,便于与《05-Masonry框架:从使用到源码解析》对照学习。

模式/技巧 在 SnapKit 中的体现 与 Masonry 对照
组合思想 单条约束(如 make.left)与复合约束(如 make.edges)对外同一套链式 API;edges / size / center 在内部展开为多条 Constraint,统一通过 ConstraintMaker 收集、再逐一 install。无显式 Composite 类,但“单条与复合同一接口”的思想一致。 Masonry 用 MASConstraint 协议 + MASViewConstraint(叶子)+ MASCompositeConstraint(组合)形成约束树;SnapKit 用协议链 + 一条 Description 对应多条 Constraint 实现类似效果。
工厂/构建器思想 ConstraintMaker 根据访问的属性(left、width、edges…)创建或填充 ConstraintDescription,调用方不直接 Constraint(...);闭包内“描述”、闭包外统一 install,符合“构建器 + 两阶段”的模式。 Masonry 的 MASConstraintMaker 按属性创建 MASViewConstraint / MASCompositeConstraint,形态上更接近简单工厂;SnapKit 的 Maker 更突出“分步填写再构建”的构建器角色。
链式/流式接口 每一步返回不同协议类型(ConstraintMakerExtendable → Relatable → Editable → Priortizable → Finalizable),既形成链式调用,又用类型约束“先设目标再设 offset/priority”,避免错误顺序。 Masonry 用 Block 属性 getter 返回“带返回值的 Block”,Block 内 return self 形成链;SnapKit 用 Swift 协议与泛型在编译期保证链的顺序。
类型安全与多态入口 Swift 泛型与重载实现 equalTo(100)equalTo(CGSize)equalTo(view) 等统一入口,无需 OC 的“装箱”;编译器区分类型,无运行时 BoxValue。 Masonry 用 MASBoxValue 将标量/结构体装箱为 id,再走 equalTo:;SnapKit 用语言特性替代,思想一致(统一入口、多类型支持)。
两阶段处理 闭包内只向 Maker 追加 ConstraintDescription / Constraint,不立即创建 NSLayoutConstraint;闭包结束后再统一 install,便于 remake(先 uninstall 再 make)与批量操作。 与 Masonry 的“block(maker) 只登记,[maker install] 再创建并激活”完全一致。

提炼与串联:上述模式与思想在 SnapKit 中的协作关系、与 Masonry 的异同,以及可复用要点,见 §六、编程思想与设计模式提炼总结。更详细的模式定义与伪代码可参考本系列《05-Masonry框架:从使用到源码解析》中的“简单工厂 | 工厂方法 | 抽象工厂”“组合模式与约束树”“链式语法完整解析”等小节。


四、SnapKit 中的优秀编程思想

SnapKit 能成为 iOS 布局 DSL 的事实标准,不仅因为功能完善,更因为其背后一系列可复用的编程思想。理解这些思想有助于在业务代码或自研库中写出更易读、可维护的 API。

1. DSL(领域特定语言):用“布局语言”说话

思想:不暴露通用编程语言的细枝末节,而是提供一套贴近领域(这里是“布局约束”)的词汇和语法,让代码读起来像在描述布局本身。

对比:系统 API 是“给引擎传参数”;SnapKit 是“用布局语言写句子”。

// 系统 API:面向“约束引擎”,不直观
NSLayoutConstraint(
    item: subview,
    attribute: .left,
    relatedBy: .equal,
    toItem: superview,
    attribute: .left,
    multiplier: 1,
    constant: 20
)

// SnapKit:面向“布局意图”,读即懂
subview.snp.makeConstraints { make in
    make.left.equalToSuperview().offset(20)
}

可复用的点:在业务里遇到“一坨参数、含义不清”的 API 时,可以封装一层 DSL:用类型 + 闭包 + 链式方法,把“做什么”说清楚,把“怎么做”藏进实现。


2. 流式接口(Fluent Interface):链式调用表达顺序

思想:每一步方法返回“可继续操作”的对象,让多步操作写成一串链,顺序即逻辑,无需临时变量。

代码示例

// 链式:属性 → 关系与目标 → 常量/倍数 → 优先级,一气呵成
label.snp.makeConstraints { make in
    make.top.equalToSuperview().offset(16)
         .labeled("titleTop")           // 链上可继续加修饰
    make.left.equalToSuperview().offset(20)
    make.right.lessThanOrEqualToSuperview().offset(-20)
         .priority(.high)
}

设计要点:返回值类型随链变化(如 equalTo 后返回支持 offset 的类型),编译器保证“先设目标再设 offset”,避免错误顺序。这种思想在构建查询、配置对象时同样适用。


3. 构建器模式(Builder):分步构建复杂对象

思想:约束是一个“复杂对象”(属性、关系、目标、倍数、常量、优先级)。不一次性传 7 个参数,而是用多个小方法分步填写,最后统一“安装”。

代码示例

// 构建器:先描述,后安装
view.snp.makeConstraints { make in
    // 步骤 1:选属性
    make.width
        // 步骤 2:设关系与目标
        .equalToSuperview()
        // 步骤 3:设倍数与常量
        .multipliedBy(0.5)
        .offset(0)
        // 步骤 4:设优先级(可选)
        .priority(.medium)
}
// 闭包结束后统一 install,而非每写一句就加一条系统约束

可复用的点:任何“多参数、多可选、有顺序”的配置,都可以用 Builder:一个入口方法接收闭包,闭包里对“builder 对象”调用多个 setter,最后在闭包外统一执行(如网络请求的 Builder、配置文件的 Builder)。


3.5 组合模式统一接口:单条与复合用同一套 API(对照 Masonry)

思想:调用方不区分“单条约束”还是“多条约束的集合”,都通过同一套链式 API 操作;复合约束(如 edgessize)在内部展开为多条 Constraint,但对外呈现一致。

在 SnapKit 中的体现make.leftmake.edges 都返回可继续链式的类型(如 ConstraintMakerExtendable / Relatable),都可继续 .equalTo(...).offset(...).priority(...)make.edges.equalToSuperview() 内部会展开为 left/right/top/bottom 四条 Constraint 并加入 Maker,与 Masonry 的 MASCompositeConstraint(edges 对应四条 MASViewConstraint)思想一致。详见 二、2.2 组合属性(edges / size / center)的展开

与 Masonry 对照:Masonry 用 MASConstraint 协议 + MASViewConstraint(叶子)+ MASCompositeConstraint(组合)形成显式约束树;SnapKit 用“一条 Description 对应多条 Constraint”实现同一语义,无单独 Composite 类,但“单条与复合同一接口”的用法一致。


3.6 两阶段处理:先描述,再安装(对照 Masonry)

思想:闭包执行阶段只“收集意图”,不立刻产生副作用(不立刻创建或激活 NSLayoutConstraint);等闭包结束后再统一 install,便于去重、批量激活、remake(先 uninstall 再 make)。

在 SnapKit 中的体现view.snp.makeConstraints { make in ... } 中,闭包内 make.xxx 只向 ConstraintMaker 追加 ConstraintDescription 或生成 Constraint 并加入列表;闭包返回后,框架再对列表中每个 Constraint 调用 install(),此时才创建并激活 NSLayoutConstraint。与 Masonry 的“block(maker) 只登记,[maker install] 再创建并激活”完全一致。详见 二、3. 约束的生成与安装


4. 类型安全与协议拆分:用类型约束“能写什么”

思想:通过协议 + 泛型把“当前能调用的方法”限定在类型里。例如:只有调用了 equalTo 之后才允许调用 offset;只有调用了 offset 之后才允许调用 priority。这样错误顺序在编译期就会报错。

概念示例(对应 SnapKit 的协议链):

// 伪代码:协议链保证“先选目标再设常量”
protocol ConstraintMakerExtendable {
    var left: ConstraintMakerRelatable { get }
    var width: ConstraintMakerRelatable { get }
}
protocol ConstraintMakerRelatable {
    func equalTo(_ other: ConstraintItem) -> ConstraintMakerEditable
    func equalTo(_ constant: CGFloat) -> ConstraintMakerEditable
}
protocol ConstraintMakerEditable {
    func offset(_ c: CGFloat) -> ConstraintMakerPriortizable
    func multipliedBy(_ m: CGFloat) -> ConstraintMakerPriortizable
}
protocol ConstraintMakerPriortizable {
    func priority(_ p: UILayoutPriority) -> ConstraintMakerFinalizable
}
// 因此:make.left.offset(20) 会编译错误,因为 left 之后必须先 equalTo

业务中的用法:例如“配置请求”时,可以设计成:只有设置了 URL 才能设置 Method,只有设置了 Method 才能设置 Body,避免漏配或顺序错乱。


5. 闭包与延迟执行:描述与执行分离

思想:约束的描述(闭包内的 make.xxx)和执行(真正创建并激活 NSLayoutConstraint)分离。闭包负责“声明要什么”,框架在闭包返回后统一“收集、生成、安装”。

代码示例

// 闭包内只“描述”,不立刻生效
view.snp.makeConstraints { make in
    make.edges.equalToSuperview()
}
// 这里才真正 install;若内部用 remake,会先 uninstall 再根据闭包重新 install

好处:可以统一做“去重、校验、批量安装、与旧约束对比”等逻辑,而不让调用方关心。在其他场景里,例如“先收集所有配置再一次性提交”“先构建命令再执行”,也适合“闭包描述 + 闭包外执行”的模式。


6. 单一职责与分层:谁只做一件事

思想

  • ConstraintMaker:只负责“收集约束描述”。
  • ConstraintDescription:只负责“一条/多条约束的参数”。
  • Constraint:只负责“对应一条 NSLayoutConstraint 的安装/卸载”。
  • View + snp:只负责“入口和闭包调度”。

每一层只做一件事,便于测试和替换;例如以后要支持“约束预览”或“导出为 IB 约束”,只需在描述层或安装层加一层,而不必改 DSL 写法。

代码层面的体现

  • 改 constant 用 Constraint.update(offset:),不碰 Maker。
  • 改约束集合用 remakeConstraints,由 Maker 重新收集再安装,Constraint 只负责单条的生命周期。

7. 可读性与“表达意图”:命名即文档

思想:API 命名直接表达意图,而不是实现细节。例如 equalToSuperview()equalTo(view.superview!) 更贴近“与父视图对齐”的意图;labeled("headerTop") 直接表达“方便调试时识别”。

代码示例

// 意图明确:居中、宽度为父视图一半、距顶 20
avatar.snp.makeConstraints { make in
    make.centerX.equalToSuperview()
    make.width.equalToSuperview().multipliedBy(0.5)
    make.top.equalToSuperview().offset(20)
}

// 意图明确:四边与安全区域对齐并留内边距
content.snp.makeConstraints { make in
    make.edges.equalTo(view.safeAreaLayoutGuide).inset(16)
}

可复用的点:对外 API 尽量用“业务/领域术语”命名(如 equalToSuperviewinset),内部实现可以用技术术语(如 layoutConstraintItemconstant),让调用方代码即文档。


8. 小结:思想与可复用场景

编程思想 SnapKit 中的体现 可复用场景举例
DSL 布局专用词汇与语法 配置、查询、脚本类 API
流式接口 链式 make.xxx.equalTo().offset() 配置对象、查询构建器
构建器模式 分步填约束再统一 install 多参数配置、请求/命令构建
组合模式统一接口 单条(make.left)与复合(make.edges)同一套 API,内部展开多条 Constraint 树形结构、批量操作、配置项分组
两阶段处理 闭包内只描述,闭包外统一 install 批量提交、事务、布局、表单校验
类型安全与协议拆分 不同链阶段返回不同协议 有顺序的配置、状态机式 API
闭包 + 延迟执行 闭包内描述,闭包外安装 批量提交、事务式操作
单一职责与分层 Maker / Description / Constraint 各管一事 任何多步骤的领域逻辑
表达意图的命名 equalToSuperview、labeled、inset 所有对外 API 设计

五、高级应用与注意点

5.1 动画中更新约束

// 仅更新 constant,不增删约束
view.snp.updateConstraints { make in
    make.top.equalToSuperview().offset(newOffset)
}
UIView.animate(withDuration: 0.3) {
    view.superview?.layoutIfNeeded()
}

5.2 约束的引用与批量操作

部分场景需要保留对某条约束的引用(例如单独改 constant 或 priority),SnapKit 支持在闭包中返回或捕获约束:

var widthConstraint: Constraint?
view.snp.makeConstraints { make in
    widthConstraint = make.width.equalTo(100).constraint
}
// 后续可修改
widthConstraint?.update(offset: 200)

5.3 安全区域与可读区域

在 iOS 11+ 中,应结合 safeAreaLayoutGuide 做刘海与 Home Indicator 适配;SnapKit 通过 make.top.equalTo(view.safeAreaLayoutGuide.snp.top) 或封装好的安全区 API(视版本而定)与系统安全区对齐,避免内容被遮挡。


六、编程思想与设计模式提炼总结

本节对 SnapKit 中使用的设计模式编程思想做统一提炼,并与 Masonry 做简要对照,便于在其它 DSL、配置类 API 或自研框架中复用。更详细的模式定义、伪代码与“按目标选模式”清单可参考本系列《05-Masonry框架:从使用到源码解析》中的 五、编程思想与设计模式提炼总结

6.1 思维导图:SnapKit 设计模式与编程思想总览

mindmap
  root((SnapKit 思想与模式))
    设计模式
      组合思想
        单条与复合同一 API
        edges/size 展开为多条 Constraint
      工厂/构建器思想
        ConstraintMaker 按属性创建 Description
        闭包内描述 闭包外 install
      链式/流式接口
        协议链 ConstraintMakerExtendable → Editable → Priortizable
        每步返回可继续链的类型
    编程思想
      DSL
        布局词汇 left equalTo offset
        代码即文档
      两阶段处理
        阶段一 闭包内收集描述
        阶段二 闭包外统一 install
      类型安全
        泛型与重载 equalTo(CGFloat)/equalTo(View)
        无需装箱 编译期区分
      单一职责与分层
        Maker / Description / Constraint 各管一事
    与 Masonry 对照
      同一套“链式 DSL + 两阶段”哲学
      Swift 协议链 vs OC Block 返回 self
      无显式 Composite 类 语义一致

6.2 设计模式与编程思想提炼表(与 Masonry 对照)

模式/思想 SnapKit 中的体现 Masonry 对照
组合思想 单条与复合(edges/size)同一套链式 API;一条 Description 可对应多条 Constraint。 MASConstraint 协议 + MASViewConstraint(叶子)+ MASCompositeConstraint(组合)形成约束树。
工厂/构建器 ConstraintMaker 根据访问属性创建/填充 ConstraintDescription;分步填写再统一 install。 MASConstraintMaker 按属性创建 MASViewConstraint / MASCompositeConstraint(简单工厂形态)。
流式接口 协议链保证每步返回可链类型,编译期约束顺序。 Block getter 返回“带返回值的 Block”,Block 内 return self。
两阶段 闭包内只描述,闭包外统一 install。 block(maker) 只登记,[maker install] 再创建并激活。
类型/多态入口 Swift 泛型与重载,equalTo(100)/equalTo(CGSize)/equalTo(view)。 MASBoxValue 装箱,mas_equalTo 宏统一走 equalTo:。

6.3 小结:一句话提炼

  • 组合:单条与复合同一接口,复合在内部展开为多条 Constraint。
  • 构建器:Maker 分步收集描述,闭包外统一 install。
  • 流式:协议链每步返回可链类型,顺序即逻辑。
  • 两阶段:先描述后执行,便于 remake、批量与扩展。
  • 类型安全:Swift 泛型与重载替代 OC 装箱,思想一致。

SnapKit 与 Masonry 在“链式 DSL + 两阶段 + 组合式约束抽象”上保持同一套设计哲学,差异主要来自语言特性(Swift 协议与泛型 vs OC Block 与 id)。理解并提炼后,可在任意“配置型、构建型、DSL 型”的 API 设计中按需复用;与 Masonry 的对照有助于在 OC 与 Swift 项目间迁移或做技术选型。


附录:参考文献与延伸阅读

参考文献

[1] SnapKit. SnapKit. GitHub. github.com/SnapKit/Sna…

[2] Larder. What's in your Larder: iOS layout DSLs. larder.io/blog/larder…

[3] Cassowary. Solving constraint systems. cassowary.readthedocs.io/en/latest/t…

[4] Apple. Auto Layout Guide. Developer Documentation.

[5] Badros, G. J., Borning, A., & Marriott, K. (1997). Solving Linear Arithmetic Constraints for User Interface Applications. Proceedings of the 1997 ACM Symposium on User Interface Software and Technology (UIST).

[6] University of Washington. Cassowary Constraint Solving Toolkit. constraints.cs.washington.edu/cassowary/

[7] Vasarhelyi, A. Behind the Scenes with Auto Layout or How to Solve Constraints with the Cassowary Algorithm. iOSConfSG. speakerdeck.com/vasarhelyia…

延伸阅读

  • Masonry:SnapKit 的 Objective-C 前身。本系列《05-Masonry框架:从使用到源码解析》中的三、设计模式与延伸四、优秀编程思想五、编程思想与设计模式提炼总结详细展开组合模式、工厂/链式、两阶段、装箱等,与本文 §三、§四、§六 对照可加深对“链式 DSL + 两阶段”设计哲学的理解。
  • Auto Layout 内在尺寸:Content Hugging 与 Compression Resistance 在 Apple《Auto Layout Guide》中的说明。
  • Cassowary 论文与技术报告:深入理解约束层次与增量求解,便于分析复杂布局冲突与性能。

01-研究优秀开源框架@UI布局@iOS | Masonry 框架:从使用到源码解析

本文结合科技文献、学术论文与业界实践,系统介绍 iOS/macOS 下的 Auto Layout DSL 库 Masonry:技术演进、核心原理(含 Cassowary 约束求解)、应用场景、源码架构与设计模式,并配有流程图、泳道图与思维导图。内容涵盖库的源码剖析及大厂使用心得,从基础概念到高级应用形成完整知识体系。


目录


一、Masonry 使用详解

1. 框架概述

Masonry 是面向 Objective-CAuto Layout DSL(领域特定语言),用于在代码中以声明式、链式语法描述视图的布局约束,替代冗长的 NSLayoutConstraint 手写与 Visual Format 字符串 [[1]]。其设计目标可概括为:

  • 可读性:约束意图接近自然语言(如“左边等于父视图左边”“宽度等于 100”)。
  • 简洁性:用 Block 链式调用替代多参数、多行的系统 API。
  • 可维护性:链式调用便于增删约束、设置优先级与调试冲突。

Masonry 由 SnapKit 组织 在 GitHub 上维护,采用 MIT 协议;其 Swift 继任者为 SnapKit,二者共享同一套“链式 DSL 描述约束”的设计哲学 [[2]]。在 Objective-C 时代,Masonry 成为纯代码 Auto Layout 的事实标准之一,被广泛应用于 iOS/macOS 项目。


2. 历史演进

布局方式的演进与 Apple 布局技术、学术成果及开源生态并行,可概括为如下时间线。

flowchart LR
  subgraph 学术与系统
    A[1997 Cassowary 论文]
    B[2011 Auto Layout 引入]
    C[iOS 6 正式支持]
  end
  subgraph 开发方式
    D[手写 NSLayoutConstraint]
    E[Visual Format]
    F[2014 Masonry]
    G[2015+ SnapKit]
  end
  A --> B
  B --> C
  C --> D
  D --> E
  E --> F
  F --> G
阶段 代表 特点
手写约束 NSLayoutConstraint(item:attribute:relatedBy:toItem:attribute:multiplier:constant:) 冗长、易出错、难以阅读 [[3]]。
Visual Format V:|[a]-[b]| 字符串描述,类型不安全,复杂布局难表达。
Masonry Objective-C,Block 链式 链式 DSL、可读性高,成为 OC 时代事实标准 [[1]]。
SnapKit Swift,Closure 链式 延续 Masonry 思想,面向 Swift 生态。

Apple 于 2011 年在 macOS Lion(及后续 iOS 6)中采用 Cassowary 作为布局引擎 [[4]][[5]],将约束转化为线性方程组求解;第三方 DSL 如 Masonry 正是在系统 API 仍显冗长的背景下流行起来的 [[6]]。


3. 理论基础:Auto Layout 与 Cassowary

Masonry 的约束最终仍通过 Auto Layout 交给系统布局引擎执行。Auto Layout 的数学基础是 Cassowary 约束求解算法,理解其思想有助于理解“约束冲突”“优先级”“内在尺寸”等概念。

3.1 Cassowary 算法简述

Cassowary 是一种 增量式线性约束求解算法,基于对偶单纯形法(dual simplex),用于求解由 线性等式与不等式 组成的约束系统 [[7]][[8]]。其特点包括:

  • 线性:约束可写成形如 (a_1 x_1 + a_2 x_2 + \cdots = b) 或 (\le/\ge) 的形式,与“视图 A 的左边 = 视图 B 的右边 + 常数”等布局关系一致。
  • 增量:可动态增删约束并高效重新求解,适合交互式 UI(窗口缩放、动画中更新约束)。
  • 约束层次(constraint hierarchy):支持 requiredpreferred(优先级),在约束冲突时按优先级舍弃或松弛部分约束,避免无解。

约束层次与松弛原理(简述):Cassowary 将约束按优先级分层(如 required=1000,high=750,low=250)。求解时先满足最高层;若存在冲突则引入松弛变量,允许低优先级约束在“尽量满足”的意义下被违反,从而得到唯一解 [[9]]。例如“宽度 = 父视图一半”与“宽度 ≥ 100”冲突时,若前者优先级较低,则在小屏上会优先保证 width ≥ 100。

参考文献

  • 原始论文:Solving Linear Arithmetic Constraints for User Interface Applications,UIST 1997 [[9]]。
  • 扩展与实现:Washington 大学 Cassowary 工具包 [[10]]。

3.2 从约束描述到线性关系(概念)

Auto Layout 将每条约束映射为关于视图几何变量(如 left, right, width, centerX)的线性等式或不等式。Masonry 所写的“左边等于父视图左边 + 20”即对应:

  • 变量:view.leftsuperview.left
  • 关系:view.left = superview.left + 20

多约束组成方程组,由 Cassowary 求解得到每个变量的值,从而得到各视图的 frame。优先级 对应 Cassowary 的强弱约束:高优先级必须满足,低优先级在冲突时可被违反。

约束的线性形式(概念):单条约束可写为线性等式或不等式,例如
( \text{view.left} = \text{superview.left} + 20 )
或带倍数:( \text{view.width} = \text{superview.width} \times 0.5 )。Cassowary 将整套约束表示为 ( A\bm{x} = \bm{b} )(或 (\le/\ge)),在满足约束层次的前提下求 (\bm{x})(各几何变量)[[9]]。

约束求解顺序(概念):系统在布局时并非“从左到右”或“从顶到底”逐视图计算,而是将所有约束汇总为全局线性系统,由 Cassowary 一次性求解;因此修改任意一条约束或某个视图的 intrinsicContentSize,都可能触发整棵视图树的布局重算。Masonry 只负责生成约束,不参与求解顺序。

3.3 流程图:从 Masonry 到屏幕像素(概念层)

flowchart LR
  A[Masonry API 调用] --> B[MASConstraintMaker]
  B --> C[MASConstraint 描述]
  C --> D[NSLayoutConstraint]
  D --> E[Auto Layout 引擎]
  E --> F[Cassowary 求解]
  F --> G[布局结果 / frame]
  G --> H[渲染到屏幕]

4. 核心概念

4.1 约束的组成

在 Auto Layout 中,一条约束可抽象为:

Item1.Attribute1 Relation Item2.Attribute2 * Multiplier + Constant

例如:“视图 A 的右边 = 视图 B 的左边 - 8”即 A.right = B.left - 8Relation 常见为 Equal、LessThanOrEqual、GreaterThanOrEqual,在 Masonry 中对应 equalTolessThanOrEqualTogreaterThanOrEqualTo。Masonry 的链式 API 就是对这五元组(Item1, Attribute1, Relation, Item2, Attribute2, Multiplier, Constant)的封装,并增加 优先级(Priority)标识(Identifier) 等元数据。

4.2 优先级与内在尺寸

概念 说明
约束优先级 UILayoutPriority(0–1000),数值越大越优先;系统在冲突时打破低优先级约束。
Content Hugging “抗拉伸”:视图不愿比其内在内容尺寸更大。
Compression Resistance “抗压缩”:视图不愿比其内在内容尺寸更小。

Label、Button、ImageView 等有 intrinsicContentSize 的控件依赖 CHCR 与其它约束共同决定最终尺寸;Masonry 可通过 mas_remakeConstraints 等配合系统 API 设置 CHCR。在 Xcode 中可在 Size Inspector 中为视图设置 Content Hugging / Compression Resistance 的优先级(数值越大越“坚持”)。

CHCR 与显式约束的配合原理:布局引擎在确定视图尺寸时,会同时考虑(1)显式约束(如 width = 100)、(2)内在尺寸(如 Label 根据文字算出的宽高)、(3)CHCR 优先级。当“显式约束 + 内在尺寸”存在冗余或冲突时,CHCR 决定谁“让步”:Content Hugging 高则视图不易被拉大,Compression Resistance 高则不易被压小。例如两 Label 横向排列且未固定宽度时,会按 CHCR 分配剩余空间。

flowchart LR
  A[显式约束] --> C[布局引擎]
  B[内在尺寸 + CHCR] --> C
  C --> D[最终 frame]

4.3 约束冲突与满足(概念)

当约束过多或相互矛盾时,系统会按优先级从高到低尝试满足;无法同时满足的约束中,低优先级的会被打破并报错(或在调试时标红)。Masonry 通过 .priority(...) 设置单条约束的优先级,便于在“理想布局”与“保底布局”之间做权衡。

4.4 思维导图:Masonry 概念关系

mindmap
  root((Masonry))
    使用入口
      mas_makeConstraints
      mas_remakeConstraints
      mas_updateConstraints
    描述对象
      MASConstraintMaker
      MASViewAttribute
      MASConstraint
    约束属性
      left right top bottom
      width height centerX centerY
      edges size margins
    关系与修饰
      equalTo mas_equalTo offset multipliedBy priority
    底层
      NSLayoutConstraint
      Auto Layout / Cassowary

5. API 与使用模式

5.1 基本用法(Objective-C)

// 示例:子视图填满父视图边距
[view addSubview:subview];
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(view);
}];
// 示例:水平居中,宽度 100,距顶 20
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    make.centerX.equalTo(self.view);
    make.width.mas_equalTo(100);
    make.top.equalTo(self.view.mas_top).offset(20);
}];

5.2 常用 API 对照(伪代码语义)

Masonry 写法 含义(伪代码)
make.left.equalTo(superview) self.left = superview.left
make.width.mas_equalTo(100) self.width = 100
make.top.equalTo(other.mas_bottom).offset(8) self.top = other.bottom + 8
make.size.mas_equalTo(CGSizeMake(80, 80)) self.width = 80, self.height = 80
make.edges.equalToSuperview() 四边与 superview 对齐
make.center.equalTo(superview) centerX/Y 与 superview 对齐
make.width.equalTo(superview).multipliedBy(0.5) self.width = superview.width * 0.5
make.priority(MASLayoutPriorityDefaultHigh) 为该条约束设置优先级

5.3 make / remake / update

  • mas_makeConstraints:在已有约束基础上追加新约束,不删除旧约束。入口内部会将 translatesAutoresizingMaskIntoConstraints 设为 NO,无需手动设置。
  • mas_remakeConstraints先移除该视图上由 Masonry 管理的约束,再按 Block 重新添加,适合布局整体变化。
  • mas_updateConstraints仅更新Block 中涉及到的约束的 constant(或部分属性),不改变约束条数或关系,适合仅改“间距/常量”的动画或响应式布局。

伪代码(remake 的语义)

function mas_remakeConstraints(block):
    uninstallAllMasonryConstraints()
    mas_makeConstraints(block)

5.4 使用案例集

以下案例覆盖常见布局需求,便于对照理解 API 与约束语义。

案例 1:内边距与四边对齐

// 子视图相对父视图四周各留 20pt
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(superview).with.insets(UIEdgeInsetsMake(20, 20, 20, 20));
}];
// 等价于:left = superview.left+20, right = superview.right-20, top/bottom 同理

案例 2:居中 + 固定尺寸

[avatarView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.center.equalTo(self.view);
    make.size.mas_equalTo(CGSizeMake(80, 80));
}];

案例 3:两视图水平排列,等分宽度

[viewA mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(container.mas_left);
    make.top.bottom.equalTo(container);
    make.width.equalTo(viewB);
}];
[viewB mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(viewA.mas_right).offset(8);
    make.right.equalTo(container.mas_right);
    make.top.bottom.equalTo(container);
}];

案例 4:安全区域与 LayoutGuide(避免被导航栏/标签栏遮挡)

[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.mas_topLayoutGuideBottom);  // 在导航栏下方
    make.left.right.equalTo(self.view);
    make.bottom.equalTo(self.mas_bottomLayoutGuideTop); // 在标签栏上方
}];

案例 5:动画中更新约束 constant

// 先 make 建立约束,并保存对某条约束的引用
__block MASConstraint *topConstraint;
[box mas_makeConstraints:^(MASConstraintMaker *make) {
    topConstraint = make.top.equalTo(self.view.mas_top).offset(100);
    make.centerX.equalTo(self.view);
    make.size.mas_equalTo(CGSizeMake(100, 100));
}];
// 后续动画中只改 constant,用 update
[box mas_updateConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.view.mas_top).offset(200);
}];
[UIView animateWithDuration:0.3 animations:^{ [self.view layoutIfNeeded]; }];

案例 6:列表 Cell 内多子视图(避免重复添加)

- (void)setupConstraints {
    [_iconView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.contentView).offset(16);
        make.centerY.equalTo(self.contentView);
        make.size.mas_equalTo(CGSizeMake(44, 44));
    }];
    [_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(_iconView.mas_right).offset(12);
        make.centerY.equalTo(self.contentView);
        make.right.lessThanOrEqualTo(self.contentView).offset(-16);
    }];
}
- (void)prepareForReuse {
    [super prepareForReuse];
    // 不在此重复 mas_makeConstraints;若布局需随数据巨变,可 mas_remakeConstraints
}

案例 7:优先级与比例(宽度为父视图一半,但最低 100)

[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    make.centerX.equalTo(self.view);
    make.width.equalTo(self.view).multipliedBy(0.5).priorityHigh();
    make.width.mas_greaterThanOrEqualTo(100).priorityRequired();
    make.top.equalTo(self.view).offset(20);
}];

案例 8:与原生 NSLayoutConstraint 对比

// 原生:一条“左边等于父视图左边+20”需整行多参数
[NSLayoutConstraint constraintWithItem:subview
                             attribute:NSLayoutAttributeLeft
                             relatedBy:NSLayoutRelationEqual
                                toItem:superview
                             attribute:NSLayoutAttributeLeft
                            multiplier:1.0
                              constant:20];

// Masonry:语义相同,一行表达
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(superview).offset(20);
}];

6. 应用场景与最佳实践

场景 建议
纯代码 UI 用 Masonry 替代手写 NSLayoutConstraint,可读性和维护性更好。
动态布局 mas_remakeConstraintsmas_updateConstraints 配合动画更新约束。
列表 Cell prepareForReuse 中避免重复添加约束,可 mas_remakeConstraints 或复用约束并只更新 constant。
UIScrollView 内子视图 子视图约束需相对 scrollView 的 contentLayoutGuide(或四边 + 明确宽/高以确定 contentSize),避免约束不足导致布局歧义。
多分辨率/多设备 multipliedBy、比例、优先级与 CHCR 适配不同宽度与安全区域。
约束冲突调试 为约束设置 identifier(若使用支持该特性的版本),便于在 Xcode 中识别。

7. 业界实践与大厂使用心得

Masonry 自 2013 年由 Jonas Budelmann 创建以来 [[14]],在 iOS 社区被广泛采用,其设计影响了后续 SnapKit、SwiftUI 等布局思路;业界总结的实践与“大厂”级项目的使用方式,可作为理论之外的补充参考。

7.1 开发效率与代码量

  • 代码量对比:相比原生 NSLayoutConstraint 多参数、多行写法,使用 Masonry 可将布局代码量减少约 60%–80%;原本需 20 余行的约束描述,用 Masonry 往往 3–5 行即可表达相同意图 [[14]][[15]]。
  • 可读性与错误率:链式语法使“左边等于某视图右边 + 间距”等意图一目了然,强类型接口减少参数顺序错误;新成员更容易理解现有布局逻辑 [[15]][[16]]。

7.2 三个核心 API 的选型(结合源码语义)

方法 行为(结合源码) 典型场景
mas_makeConstraints: 不移除已有 Masonry 约束,在 Maker 中追加新约束并 install 初始布局、逐步添加约束
mas_remakeConstraints: 先 uninstall 该视图上所有由 Masonry 管理的约束,再执行 block 重新 make 并 install 布局整体变化(如横竖屏、显隐导致结构变化)
mas_updateConstraints: 只更新已存在约束的 constant(或部分可更新字段),不增删约束条数 动画中改间距、响应式微调

选型原则:能 update 就不 remake,能 remake 就不在外部手动移除再 make,以降低遗漏或重复约束的风险 [[16]]。

7.3 常见实践场景(来自社区与项目总结)

  • 相对父视图edgescentersize 配合 insets/offset 实现内边距与居中;安全区域可用 mas_topLayoutGuide/mas_bottomLayoutGuide 或 Safe Area API 避免视图穿透导航栏/标签栏 [[16]][[17]]。
  • 相对兄弟视图equalTo(other.mas_left)equalTo(other.mas_bottom).offset(8) 等明确描述视图间关系;列表 Cell 内多视图约束建议在 prepareForReuse 中统一 remake 或只更新 constant,避免重复添加 [[17]]。
  • 复合约束edges(四边)、size(宽高)、center(中心)一次生成多条约束,既减少重复代码又保证语义一致 [[14]]。

7.4 思维导图:API 选型与场景

mindmap
  root((Masonry 实践))
    初始布局
      mas_makeConstraints
      只增不减
    布局巨变
      mas_remakeConstraints
      先卸后建
    微调/动画
      mas_updateConstraints
      只改 constant
    适配与安全
      LayoutGuide / Safe Area
      multipliedBy 比例

二、Masonry 源码解析

Masonry框架的类结构

1. 整体架构与类结构

Masonry 的代码结构可分层为:DSL 入口层约束描述层(Maker + 组合约束)约束实体层(MASConstraint)系统桥接层(NSLayoutConstraint)

flowchart TB
  subgraph 入口
    A[View.mas_makeConstraints]
  end
  subgraph DSL
    B[MASConstraintMaker]
    C[MASCompositeConstraint]
    D[MASViewAttribute]
  end
  subgraph 约束实体
    E[MASViewConstraint]
    F[MASLayoutConstraint]
  end
  subgraph 系统
    G[NSLayoutConstraint]
  end
  A --> B
  B --> C
  B --> E
  C --> E
  E --> D
  E --> F
  F --> G
  • 入口UIView+MASAdditions 为视图提供 mas_makeConstraints: / mas_remakeConstraints: / mas_updateConstraints:,接收 (MASConstraintMaker *) Block。
  • MASConstraintMaker:Block 中的 make 对象,持有当前视图及一组约束描述;调用 make.leftmake.edges 等会返回 MASConstraint(可能是复合或单条)。Maker 提供基础属性(left、top、right、bottom、leading、trailing)、尺寸(width、height)、居中(centerX、centerY、baseline)、边距(*Margin)及复合属性(edges、size、center)[[18]]。
  • MASCompositeConstraint:组合多条 MASViewConstraint(如 edges 对应 left/right/top/bottom 四条),形成树状结构,对应组合模式
  • MASViewConstraint:描述单条约束(某属性 与 某目标 的 关系、倍数、常量、优先级),最终生成 MASLayoutConstraint(NSLayoutConstraint 子类)并安装。

1.2 源码级调用链:从 make.left 到约束创建

所有“单属性”约束(如 left、width)在 Maker 中最终都通过 addConstraintWithLayoutAttribute: 统一入口创建;复合属性(如 edges)则在该方法上层按多个 NSLayoutAttribute 分别调用。流程可概括为:

flowchart LR
  A[make.left] --> B[addConstraintWithLayoutAttribute: Left]
  B --> C[constraint: addConstraintWithLayoutAttribute:]
  C --> D[MASViewConstraint 创建]
  D --> E[加入 Maker 的约束数组]
  E --> F[install 时生成 NSLayoutConstraint]

对应源码逻辑(伪代码) [[18]]:

// MASConstraintMaker
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)attr {
    return [self constraint:nil addConstraintWithLayoutAttribute:attr];
}
// 若为复合属性(如 edges),则创建 MASCompositeConstraint 并为其添加多条 MASViewConstraint;
// 否则创建单条 MASViewConstraint,存入 constraintMaker 的约束列表,供 install 时统一安装。

1.3 结合掘金文章:从 make 到 install 的完整链路

以下内容综合自掘金文章《Masonry实现原理并没有那么可怕》[[19]],与源码对照便于理解 Maker、链式多属性及 install 的细节。

(1)mas_makeConstraints: 入口与两阶段

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;  // 手写约束前必须关闭 autoresizing 转约束
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);   // 阶段一:Block 内 make.xxx 只往 maker 里“登记”约束
    return [constraintMaker install];  // 阶段二:统一创建 NSLayoutConstraint 并添加到视图
}

make 即传入 Block 的 MASConstraintMaker 实例,负责约束的创建与最终的 install [[19]]。

(2)make.left 的三步到 MASViewConstraint

  • Step 1make.left 调用 addConstraintWithLayoutAttribute:NSLayoutAttributeLeft
  • Step 2addConstraintWithLayoutAttribute: 内部调 constraint:nil addConstraintWithLayoutAttribute:layoutAttribute(单属性时第一个参数为 nil)。
  • Step 3constraint:addConstraintWithLayoutAttribute: 中创建 MASViewAttribute(封装 View + NSLayoutAttribute)、MASViewConstraint(firstViewAttribute + 后续 secondViewAttribute);若当前 constraint 为 nil,则将 newConstraint 加入 maker 的 constraints 数组并返回。

MASViewAttribute 可理解为“视图 + 布局属性”的可变元组;MASViewConstraint 即一条约束描述,持有 firstViewAttribute 与 secondViewAttribute [[19]]。

(3)make.top.left 的链式多属性:委托与复合替换

make.top 返回的是 MASViewConstraint,而 MASViewConstraint 的父类 MASConstraint 同样定义了 left、right、top 等属性。这些属性的实现会委托回 Maker

// MASViewConstraint 中
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
    return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];  // delegate 即 Maker
}

此时传入的 constraint 不再为 nil(是当前的 MASViewConstraint)。在 Maker 的 constraint:addConstraintWithLayoutAttribute: 里会创建 MASCompositeConstraint,把“已有约束 + 新约束”包成组合,并调用 constraint:shouldBeReplacedWithConstraint:,在 constraints 数组中找到原约束的位置,用 composite 替换,从而 make.top.left 在数组中表现为一条“组合约束”而非两条独立项 [[19]]。

小结(与掘金文章总结一致):MASConstraintMaker 作为工厂,生产并管理 MASViewConstraint(单条)与 MASCompositeConstraint(组合);二者均遵循 MASConstraint 抽象,对外统一接口;View+MASAdditions 作为与外界交互的入口,把复杂的约束创建与安装封装在内部,仅暴露简单的 mas_makeConstraints: 等 API [[19]]。

(4)equalTo 与 equalToWithRelation

equalTo(...) 内部对应 equalToWithRelation。若传入的是数组(多目标),会复制当前 MASViewConstraint 并为每个目标设置 secondViewAttribute,包装成 MASCompositeConstraint,同样通过 shouldBeReplacedWithConstraint 替换进 maker;若传入单个对象,则设置 secondViewAttributereturn self,支持继续 .offset().priority() [[19]]。


2. 组合模式与约束树

Masonry 采用 组合设计模式(Composite Pattern):将对象组合成树状结构以表示“部分-整体”的层次结构,使客户端对叶子节点(单条约束)和组合节点(如 edges、size)的使用方式一致 [[11]]。

注意:此处的“组合”指结构型设计模式中的 Composite,而非“组合优于继承”的泛称。

2.1 组合模式三要素

Masonry 采用了经典的 组合设计模式(Composite Pattern)。

2.1.1 定义

将对象组合成树状结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象(Leaf)和组合对象(Composite)的使用具有一致性。 注意:这个组合模式不是“组合优于继承”的那种组合,是狭义的指代一种特定的场景(树状结构)

2.1.2 三个设定
  • Component 协议:树中的组件(Leaf、Composite)都实现同一协议,使客户端可统一对待。
  • Leaf:无子节点的叶子组件,对应单条约束。
  • Composite:容器组件,持有子节点(Leaf 或其他 Composite),操作时递归子节点。

结构关系见下方 Mermaid 图与角色对照表。

角色 在 Masonry 中的对应
Component 协议 MASConstraint 协议,树中所有节点(叶子与组合)都实现该协议。
Leaf MASViewConstraint:无子约束,对应单条 NSLayoutConstraint。
Composite MASCompositeConstraint:持有多个 MASConstraint(可再为叶子或组合),如 edges 包含 left/right/top/bottom。
flowchart TB
  subgraph Composite
    A[MASCompositeConstraint edges]
    A --> B[MASViewConstraint left]
    A --> C[MASViewConstraint right]
    A --> D[MASViewConstraint top]
    A --> E[MASViewConstraint bottom]
  end
  subgraph Leaf
    B
    C
    D
    E
  end

2.2 在 Cocoa Touch 中的类比

UIView 的层级本身也是组合结构:子视图可包含更多子视图,形成树;Masonry 的约束树与视图树解耦,但都采用“统一接口处理单点与集合”的思想。

2.3 Swift 实现示例(组合模式)

import Foundation

// 一:Component协议:树中的组件(Leaf、Composite)都需要实现这个协议
protocol File {
    var name: String { get set }
    func showInfo()
}

// 二:Leaf:树结构中的一个没有子元素的组件
class TextFile: File {
    var name: String
    init(name: String) {
        self.name = name
    }
    func showInfo() {
        print("(name) is TextFile")
    }
}

class ImageFile: File {
    var name: String
    init(name: String) {
        self.name = name
    }
    func showInfo() {
        print("(name) is ImageFile")
    }
}

class VideoFile: File {
    var name: String
    init(name: String) {
        self.name = name
    }
    func showInfo() {
        print("(name) is VideoFile")
    }
}

// 三:Composite:容器,与Leaf不同的是有子元素,用来存储Leaf和其他Composite
class Fold: File {
    var name: String
    private(set) var files: [File] = []
    init(name: String) {
        self.name = name
    }
    func showInfo() {
        print("(name) is Fold")
        files.forEach { (file) in
            file.showInfo()
        }
    }
    func addFile(file: File)  {
        files.append(file)
    }
}

class Client {
    init() {
    }
    func test() {
        let fold1: Fold = Fold.init(name: "fold1")
        let fold2: Fold = Fold.init(name: "fold2")
        let text1: TextFile = TextFile.init(name: "text1")
        let text2: TextFile = TextFile.init(name: "text2")
        let image1: ImageFile = ImageFile.init(name: "image1")
        let image2: ImageFile = ImageFile.init(name: "image2")
        let video1: VideoFile = VideoFile.init(name: "video1")
        let video2: VideoFile = VideoFile.init(name: "video2")
        fold1.addFile(file: text1)
        fold2.addFile(file: text2)
        fold1.addFile(file: image1)
        fold2.addFile(file: image2)
        fold1.addFile(file: video1)
        fold2.addFile(file: video2)
        fold1.addFile(file: fold2)
        fold1.showInfo()
    }
}

2.4 参考资料


3. 工厂模式与链式语法

本节单独展开 Masonry 中工厂模式链式语法的设计与实现:前者负责“按需创建约束对象”,后者负责“让约束描述可连续书写、易读易维护”。


扩展:简单工厂 | 工厂方法 | 抽象工厂 三种模式辨析

在分析 Masonry 的“工厂”角色之前,先对 GoF 及业界常说的三类工厂型创建模式做一统一定义与对比,便于理解 Masonry 更贴近哪一种、以及为何不采用另一种。

1)简单工厂模式(Simple Factory)

定义:由一个具体工厂类根据参数/类型决定创建哪一种具体产品,并返回产品的抽象类型给调用方。不属于 GoF 23 种设计模式之一,但实践中极为常见。

核心特征

  • 一个工厂类:无抽象工厂接口、无工厂子类,所有创建逻辑集中在一个类的一个方法(或若干静态/实例方法)里。
  • 根据参数分支:如 create(type) 内部用 if/switch 或字典映射,type == "A"new ProductA(),否则 new ProductB()
  • 返回抽象类型:方法签名返回抽象产品(接口或基类),调用方只依赖抽象,不依赖 ConcreteProductA/B。

结构示意

flowchart LR
  C[Client] --> F[SimpleFactory]
  F --> P1[ProductA]
  F --> P2[ProductB]
  F --> P3[ProductC]
  P1 --> I[Product 接口]
  P2 --> I
  P3 --> I
  C --> I

伪代码

// 抽象产品
interface Product { void doSomething(); }

// 具体产品
class ProductA : Product { ... }
class ProductB : Product { ... }

// 简单工厂:一个类,一个方法,根据参数创建
class SimpleFactory {
    Product create(String type) {
        if (type == "A") return new ProductA();
        if (type == "B") return new ProductB();
        throw new UnsupportedTypeException(type);
    }
}

// 调用方
Product p = factory.create("A");
p.doSomething();

优点:实现简单、调用方与具体产品解耦(只依赖 Product)。缺点:新增产品必须修改工厂类内部分支,违反开闭原则;工厂类职责随产品增多而膨胀。


2)工厂方法模式(Factory Method,GoF)

定义:定义用于创建对象的抽象方法(工厂方法),由子类决定实例化哪一个具体产品类。将“创建哪种产品”的决策推迟到子类,符合开闭原则。

核心特征

  • 抽象 Creator + 多个 ConcreteCreator:抽象工厂(或基类)声明 createProduct() 抽象方法;每个具体产品对应一个具体工厂子类,在子类中 return new ConcreteProduct()
  • 一厂一产品:通常一个 ConcreteCreator 只生产一种 ConcreteProduct(或一个产品族中的一种)。
  • 调用方依赖抽象:依赖抽象 Creator 和抽象 Product,通过多态获得具体产品,扩展时只需新增子类,无需改原有类。

结构示意

flowchart TB
  subgraph 调用方
    Client
  end
  subgraph 抽象层
    Creator["Creator\n+ factoryMethod()"]
    Product["Product"]
  end
  subgraph 具体层
    CreatorA["ConcreteCreatorA\n+ factoryMethod() → ProductA"]
    CreatorB["ConcreteCreatorB\n+ factoryMethod() → ProductB"]
    ProductA[ProductA]
    ProductB[ProductB]
  end
  Client --> Creator
  Creator --> CreatorA
  Creator --> CreatorB
  CreatorA --> ProductA
  CreatorB --> ProductB
  ProductA --> Product
  ProductB --> Product

伪代码

// 抽象产品
interface Product { void doSomething(); }
class ProductA : Product { ... }
class ProductB : Product { ... }

// 抽象创建者:声明工厂方法
abstract class Creator {
    abstract Product factoryMethod();
    void someOperation() { Product p = factoryMethod(); p.doSomething(); }
}

// 具体创建者:各负责一种产品
class ConcreteCreatorA : Creator {
    Product factoryMethod() { return new ProductA(); }
}
class ConcreteCreatorB : Creator {
    Product factoryMethod() { return new ProductB(); }
}

// 调用方依赖 Creator 抽象,由外部注入 ConcreteCreatorA 或 B
Creator c = new ConcreteCreatorA();
c.someOperation();

与简单工厂对比:扩展新产品时,简单工厂要工厂类内部代码;工厂方法是新增一个 Creator 子类和一个 Product 子类,原有代码不动,符合开闭原则。


3)抽象工厂模式(Abstract Factory,GoF)

定义:为创建一组相关或相互依赖的产品提供一个接口,而不指定具体类。每个具体工厂负责生产一整族产品(如“现代风椅子+现代风桌子”),不同工厂生产不同族(如“古典风椅子+古典风桌子”)。

核心特征

  • 产品族:多个抽象产品(如 Chair、Table),每个抽象产品有多个具体实现(ModernChair、ClassicChair…)。抽象工厂接口中为每个产品提供一个创建方法(如 createChair()createTable())。
  • 一族一起换:ConcreteFactory1 生产 ModernChair + ModernTable,ConcreteFactory2 生产 ClassicChair + ClassicTable;客户端依赖抽象工厂与抽象产品,通过切换具体工厂即可切换整族风格。
  • 解决“系列产品”的创建:适合 UI 主题、跨平台控件族、数据库/连接池族等“多产品、多风格/多实现”的场景。

结构示意

flowchart TB
  subgraph 调用方
    Client
  end
  subgraph 抽象工厂与产品
    AF["AbstractFactory\n+ createChair()\n+ createTable()"]
    Chair["Chair"]
    Table["Table"]
  end
  subgraph 具体工厂与产品族
    CF1["ConcreteFactory1\n→ ModernChair, ModernTable"]
    CF2["ConcreteFactory2\n→ ClassicChair, ClassicTable"]
    MCh[ModernChair]
    MTable[ModernTable]
    CCh[ClassicChair]
    CTable[ClassicTable]
  end
  Client --> AF
  AF --> CF1
  AF --> CF2
  CF1 --> MCh
  CF1 --> MTable
  CF2 --> CCh
  CF2 --> CTable
  MCh --> Chair
  MTable --> Table
  CCh --> Chair
  CTable --> Table

伪代码

// 抽象产品族
interface Chair { void sit(); }
interface Table { void put(); }
class ModernChair : Chair { ... }
class ModernTable : Table { ... }
class ClassicChair : Chair { ... }
class ClassicTable : Table { ... }

// 抽象工厂:一族产品的创建接口
interface AbstractFactory {
    Chair createChair();
    Table createTable();
}

// 具体工厂:生产一族产品
class ModernFactory : AbstractFactory {
    Chair createChair() { return new ModernChair(); }
    Table createTable() { return new ModernTable(); }
}
class ClassicFactory : AbstractFactory {
    Chair createChair() { return new ClassicChair(); }
    Table createTable() { return new ClassicTable(); }
}

// 调用方:通过换工厂切换整族
AbstractFactory f = new ModernFactory();
Chair c = f.createChair();
Table t = f.createTable();

与工厂方法对比:工厂方法通常是“一个方法生产一种产品”;抽象工厂是“一个工厂接口里多个方法,每个方法生产一种产品,且这一组产品是相关的一族”。抽象工厂可理解为多产品族的工厂方法组合


4)三种模式对比表
维度 简单工厂 工厂方法 抽象工厂
工厂形态 一个具体工厂类,无子类 抽象 Creator + 多个 ConcreteCreator 子类 抽象 AbstractFactory + 多个 ConcreteFactory 子类
创建方式 同一方法内根据参数 if/switch 分支 子类重写工厂方法,各返回一种产品 子类实现多个创建方法,各返回一族中的一种产品
产品数量 可多种产品,由参数决定 通常一厂一种产品 一厂一族产品(多个相关产品)
扩展方式 新增产品需工厂类内部 新增产品 = 新增 Creator 子类 + Product 子类 新增产品族 = 新增 Factory 子类 + 该族各 Product 子类
开闭原则 对扩展不友好(需改工厂) 对扩展开放(加子类即可) 对扩展开放(加新工厂子类与产品族)
典型场景 产品种类少、变化少、图简单 框架/插件:由子类决定具体产品 主题/风格/平台:整族产品一起换

5)Masonry 与三种模式的关系
  • Masonry 的 Maker:只有一个具体类 MASConstraintMaker,根据“请求的属性”(left、top、edges、size…)在同一类内部分支,创建 MASViewConstraintMASCompositeConstraint,并统一以 MASConstraint 抽象返回。形态上最接近简单工厂(一个工厂类、多种产品、参数即“布局属性”)。
  • 为何不是典型工厂方法:没有“抽象 Maker + 多个 ConcreteMaker 子类”,也没有“一个子类只生产一种约束”。创建逻辑集中在 Maker 内部,没有把“创建哪种约束”推迟到子类。
  • 为何不是抽象工厂:Masonry 不涉及“一族多产品”的切换(如多套 UI 主题、多平台控件族)。只有一类“产品”——约束描述对象(单条/复合),只是根据属性不同产生不同具体类,不涉及多产品族的抽象工厂接口。

结论:Masonry 采用的主要是简单工厂的形态(集中在一个 Maker 内、按属性分支创建),同时吸收了工厂方法的“调用方只依赖抽象产品(MASConstraint)”的优点,便于阅读和扩展约束类型时在 Maker 内增加分支或复合封装,而无需引入 Maker 子类。


3.1 工厂模式在 Masonry 中的完整映射

3.1.1 工厂方法模式(Factory Method)回顾

上文扩展小节已给出简单工厂、工厂方法、抽象工厂三种模式的定义与对比;§3.2 给出 GoF 工厂方法的标准定义与优缺点。此处仅列出 Masonry 中“工厂”角色的直接对应

GoF 角色

  • Product(抽象产品):约束对象的抽象,对应 MASConstraint 协议。
  • ConcreteProduct(具体产品):单条约束 → MASViewConstraint;复合约束 → MASCompositeConstraint
  • Creator(创建者):负责“生产”约束的工厂,对应 MASConstraintMaker
  • Factory Method(工厂方法):Creator 中根据“请求类型”创建具体产品的方法;在 Masonry 中体现为 addConstraintWithLayoutAttribute: 及复合属性的封装(如 edgessize)。

Masonry 并未采用“抽象 Creator + 多个 ConcreteCreator 子类”的经典工厂方法结构,而是在一个 Maker 类内根据请求的布局属性(left、top、edges、size 等)决定创建“单条约束”还是“组合约束”,因此更贴近简单工厂 + 工厂方法思想的融合:创建逻辑集中在 Maker 内部,对外只暴露 make.leftmake.edges 等统一入口,调用方完全依赖 MASConstraint 抽象,不关心具体是 MASViewConstraint 还是 MASCompositeConstraint

3.1.2 Masonry 中的“工厂”是谁、生产什么
角色 Masonry 中的对应 说明
工厂 / 创建者 MASConstraintMaker Block 中的 make,持有 view 和约束数组;根据访问的属性创建约束。
工厂方法 addConstraintWithLayoutAttribute:constraint:addConstraintWithLayoutAttribute: 根据 NSLayoutAttribute(Left、Top、Width、Height…)或复合键(edges、size、center)创建并返回 MASConstraint
抽象产品 MASConstraint 协议 对外统一接口:equalTooffsetpriorityinstall 等,调用方只依赖该协议。
具体产品(单条) MASViewConstraint 对应一条 NSLayoutConstraint,如 make.leftmake.width
具体产品(复合) MASCompositeConstraint 内部持有多条 MASViewConstraint,如 make.edgesmake.size

创建时机:调用方写 make.left 时,Maker 并不立刻创建 NSLayoutConstraint,而是先创建一条“约束描述对象”(MASViewConstraint),加入 Maker 的约束数组;等 Block 执行完毕、执行 [maker install] 时,再遍历这些描述对象,逐个生成并激活 NSLayoutConstraint。因此“工厂”生产的是约束描述对象,真正的系统约束在 install 阶段 才生成。

3.1.3 工厂流程示意(从 make.left 到约束对象)
flowchart LR
  A[make.left] --> B[MASConstraintMaker]
  B --> C{单属性 or 复合?}
  C -->|单属性 Left| D[addConstraintWithLayoutAttribute: Left]
  C -->|复合 edges| E[创建 left/right/top/bottom 四条]
  D --> F[新建 MASViewConstraint]
  E --> G[新建 MASCompositeConstraint]
  F --> H[加入 maker.constraints]
  G --> H
  H --> I[返回 MASConstraint 给调用方]

单属性源码级逻辑(伪代码)

// MASConstraintMaker
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)attr {
    return [self constraint:nil addConstraintWithLayoutAttribute:attr];
}

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)attr {
    MASViewAttribute *firstViewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:attr];
    if (!constraint) {
        // 当前无“正在组装的约束”,创建新的 MASViewConstraint 并加入数组
        MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:firstViewAttribute];
        [self.constraints addObject:newConstraint];
        newConstraint.delegate = self;
        return newConstraint;  // 返回给调用方,继续链式 .equalTo(...).offset(...)
    }
    // 已有约束(如 make.top 返回的),再链 .left:创建复合约束并替换
    // ... 创建 MASCompositeConstraint,用 composite 替换数组中原来的 constraint
}

复合属性“edges”的工厂行为(伪代码)

// MASConstraintMaker
- (MASConstraint *)edges {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]
        .addConstraintWithLayoutAttribute(NSLayoutAttributeRight)
        .addConstraintWithLayoutAttribute(NSLayoutAttributeTop)
        .addConstraintWithLayoutAttribute(NSLayoutAttributeBottom);
    // 内部会创建 MASCompositeConstraint,包含 left/right/top/bottom 四条 MASViewConstraint
}

因此:工厂思想在 Masonry 中的体现 = Maker 根据“请求的属性”创建相应类型的约束对象(单条或复合),调用方只通过 make.xxx 获取 MASConstraint,不直接 alloc/init 任何具体约束类,符合“将对象创建推迟到专门工厂、调用方依赖抽象”的思想 [[12]]。

3.1.4 工厂模式与“简单工厂”的对比
对比项 经典工厂方法模式 Masonry 的 Maker
创建者 抽象 Creator + 多个 ConcreteCreator 子类 单一类 MASConstraintMaker,无子类
工厂方法 子类重写 createProduct,返回抽象 Product 同一类内根据 layoutAttribute 分支,返回 MASViewConstraint 或 MASCompositeConstraint
扩展方式 新增产品时新增 ConcreteCreator 子类 新增布局语义时在 Maker 内增加属性或复合封装(如 edges、size)
客户端 依赖抽象 Product,不依赖具体类 同样只依赖 MASConstraint 协议,不依赖 MASViewConstraint / MASCompositeConstraint

Masonry 把“创建哪种约束”的逻辑收口在 Maker 的 addConstraintWithLayoutAttribute: 及复合属性里,没有为每种约束单独建工厂子类,因此更接近**简单工厂(Simple Factory)**的“一个工厂类、多种产品”的形态;同时返回的是抽象类型 MASConstraint,又具备工厂方法模式“依赖抽象”的优点。


3.2 GoF 工厂方法模式标准定义(对照理解)

工厂方法模式(Factory Method Pattern)

实质:定义一个用于创建对象的接口(或抽象方法),但让实现该接口的子类来决定实例化哪一个类。工厂方法模式将对象的实例化过程**推迟(defer)**到了子类中。

核心解决的问题: 它解决了客户端代码与具体产品类之间的耦合问题。当系统在编译时无法确定需要创建哪个具体类的对象,或者希望将具体类的实例化逻辑封装在子类中时,该模式尤为适用。

设计优势

  1. 符合开闭原则(Open-Closed Principle):系统对扩展开放,对修改关闭。当需要引入新的具体产品时,只需创建一个新的具体工厂子类,而无需修改现有的客户端代码或工厂接口
  2. 统一接口编程:客户端仅依赖于产品的抽象接口(或抽象基类),而不依赖具体实现。这确保了无论工厂返回哪种具体产品,客户端都能以一致的方式处理。

结论: 相比于在客户端直接使用 new 关键字硬编码具体类,工厂方法模式提供了一种更灵活、更易维护的对象创建策略,特别适用于框架开发或产品族经常变化的场景。

工厂方法模式通过将实例化逻辑推迟到子类,实现创建者与使用者的解耦。要点如下:

3.3 ✅ 主要优点

  • 开闭原则:新增产品时只需新增具体工厂子类与产品子类,无需修改现有客户端与抽象接口。
  • 单一职责:创建逻辑与业务逻辑分离,客户端只关心“用产品”,不关心“如何造”。
  • 低耦合:客户端依赖抽象 Creator 与 Product,便于替换具体实现(如切换数据库驱动)。
  • 统一入口:所有创建经工厂方法,便于做日志、权限、缓存等集中控制。

3.4 ❌ 主要缺点

  • 类数量增加:每增加一种产品通常需增加一个具体工厂类,产品线大时易产生“类爆炸”。
  • 抽象层次加深:调用链变长(客户端 → 具体工厂 → 抽象工厂 → 具体产品),理解成本上升。
  • 多参数/多产品族:若需根据多参数动态选产品,或需一次创建一族产品,更适合用抽象工厂或建造者。

3.5 ⚖️ 总结与适用场景建议

维度 评价
灵活性 ⭐⭐⭐⭐⭐ (极高,易于扩展新产品)
可维护性 ⭐⭐⭐⭐ (高,职责分离清晰)
复杂度 ⭐⭐ (较低,类数量随产品线性增长)
性能开销 ⭐⭐⭐ (中等,主要是类加载开销,运行时影响小)
3.5.1 💡 什么时候应该使用?
  1. 当你不知道确切需要哪个具体类的对象时:例如,框架开发中,框架本身不知道用户会具体使用哪种控件,由用户子类化框架来指定。
  2. 当你希望将对象的创建逻辑委托给专门的子类时:不同子类可能需要不同的初始化逻辑或上下文环境。
  3. 当系统需要遵循开闭原则,频繁增加新产品时:这是最典型的场景。
3.5.2 💡 什么时候应该使用?
  1. 产品种类非常固定,且几乎不会变化:此时引入工厂模式是过度设计(Over-engineering),直接 new 更简单。
  2. 一个工厂需要负责创建多种差异巨大的产品:此时可能更适合使用抽象工厂模式(Abstract Factory)或建造者模式(Builder)。
  3. 项目规模很小,追求极致的代码简洁性:简单的脚本或小型工具类应用中,工厂模式带来的类膨胀可能弊大于利。
3.5.3 代码视角对比

不用工厂:客户端用 if/switch + new 具体类,每增加一种产品都要改此处,违反开闭原则。用工厂方法:客户端依赖抽象工厂与产品,factory.createShape() 由具体工厂子类决定实例化哪种产品;新增产品时只需加新子类,客户端不变。详见上文扩展小节伪代码。

3.6 链式语法(Fluent Interface)完整解析

学习三、链式语法

实现的核心:重写Block属性的Get方法,在Block里返回对象本身

#import "ChainProgramVC.h"

@class ChainAnimal;
typedef void(^GeneralBlockProperty)(int count);
typedef ChainAnimal* (^ChainBlockProperty)(int count);

@interface ChainAnimal : NSObject
@property (nonatomic, strong) GeneralBlockProperty eat1;
@property (nonatomic, strong) ChainBlockProperty eat2;
@end
@implementation ChainAnimal
/**
 函数返回一个block,block返回void
 */
-(GeneralBlockProperty)eat1 {
    return ^(int count) {
        NSLog(@"%s count = %d", __func__, count);
    };
}
/**
 函数返回一个block,block返回ChainAnimal对象
 */
- (ChainBlockProperty)eat2 {
    return ^(int count){
        NSLog(@"%s count = %d", __func__, count);
        return self;
    };
}
@end

@interface ChainProgramVC ()
@property (nonatomic, strong) ChainAnimal *dog;
@end
@implementation ChainProgramVC
- (ChainAnimal *)dog {
    if (!_dog) {
        _dog = [[ChainAnimal alloc] init];
    }
    return _dog;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    [super viewDidLoad];
    self.dog.eat1(1);
    self.dog.eat2(2).eat2(3).eat2(4).eat1(5);
}
@end

学习四、接口简洁

把复杂留给自己,把简单留给别人

学习五、抽象方法小技巧

#define MASMethodNotImplemented() \
    @throw [NSException exceptionWithName:NSInternalInconsistencyException \
                                   reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
                                 userInfo:nil]

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {
    MASMethodNotImplemented();
}

自己实现类似需求的时候,可以采用这个技巧阻止直接使用抽象方法。

实践:实现一个自定义转场动画的基类
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface BaseAnimatedTransiton : NSObject<UIViewControllerAnimatedTransitioning>
@property (nonatomic, assign) NSTimeInterval p_transitionDuration;
+(instancetype)initWithTransitionDuration:(NSTimeInterval)transitionDuration;
-(instancetype)initWithTransitionDuration:(NSTimeInterval)transitionDuration NS_DESIGNATED_INITIALIZER;
@end

#pragma mark - (Abstract)
@interface BaseAnimatedTransiton (Abstract)
// 子类实现,父类NSException
-(void)animate:(nonnull id<UIViewControllerContextTransitioning>)transitionContext;
@end

NS_ASSUME_NONNULL_END
#import "BaseAnimatedTransiton.h"

@implementation BaseAnimatedTransiton
+(instancetype)initWithTransitionDuration:(NSTimeInterval)transitionDuration {
    BaseAnimatedTransiton* obj = [[BaseAnimatedTransiton alloc] init];
    obj.p_transitionDuration = transitionDuration;
    return obj;
}
-(instancetype)initWithTransitionDuration:(NSTimeInterval)transitionDuration {
    if (self = [super init]) {
        self.p_transitionDuration = transitionDuration;
    }
    return self;
}
-(instancetype)init {
    return [self initWithTransitionDuration:0.25];
}
-(void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
    [self animate:transitionContext];
}
-(NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext {
    return self.p_transitionDuration;
}
-(void)animate:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
    [self throwException:_cmd];
}
/**
 在Masonry的源码中使用的是宏(感觉宏不是很直观)

 @param aSelector 方法名字
 */
-(void)throwException:(SEL)aSelector {
    @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                   reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(aSelector)]
                                 userInfo:nil];
}
@end

学习六、包装任何值类型为一个对象

我们添加约束的时候使用equalTo传入的参数只能是id类型的,而mas_equalTo可以任何类型的数据。

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.size.mas_equalTo(CGSizeMake(100, 100));
    make.center.equalTo(self.view);
    // 下面这句效果与上面的效果一样
    //make.center.mas_equalTo(self.view);
}];
#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
/**
 *  Given a scalar or struct value, wraps it in NSValue
 *  Based on EXPObjectify: https://github.com/specta/expecta
 */
static inline id _MASBoxValue(const char *type, ...) {
    va_list v;
    va_start(v, type);
    id obj = nil;
    if (strcmp(type, @encode(id)) == 0) {
        id actual = va_arg(v, id);
        obj = actual;
    } else if (strcmp(type, @encode(CGPoint)) == 0) {
        CGPoint actual = (CGPoint)va_arg(v, CGPoint);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(CGSize)) == 0) {
        CGSize actual = (CGSize)va_arg(v, CGSize);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
        MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(double)) == 0) {
        double actual = (double)va_arg(v, double);
        obj = [NSNumber numberWithDouble:actual];
    } else if (strcmp(type, @encode(float)) == 0) {
        float actual = (float)va_arg(v, double);
        obj = [NSNumber numberWithFloat:actual];
    } else if (strcmp(type, @encode(int)) == 0) {
        int actual = (int)va_arg(v, int);
        obj = [NSNumber numberWithInt:actual];
    } else if (strcmp(type, @encode(long)) == 0) {
        long actual = (long)va_arg(v, long);
        obj = [NSNumber numberWithLong:actual];
    } else if (strcmp(type, @encode(long long)) == 0) {
        long long actual = (long long)va_arg(v, long long);
        obj = [NSNumber numberWithLongLong:actual];
    } else if (strcmp(type, @encode(short)) == 0) {
        short actual = (short)va_arg(v, int);
        obj = [NSNumber numberWithShort:actual];
    } else if (strcmp(type, @encode(char)) == 0) {
        char actual = (char)va_arg(v, int);
        obj = [NSNumber numberWithChar:actual];
    } else if (strcmp(type, @encode(bool)) == 0) {
        bool actual = (bool)va_arg(v, int);
        obj = [NSNumber numberWithBool:actual];
    } else if (strcmp(type, @encode(unsigned char)) == 0) {
        unsigned char actual = (unsigned char)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedChar:actual];
    } else if (strcmp(type, @encode(unsigned int)) == 0) {
        unsigned int actual = (unsigned int)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedInt:actual];
    } else if (strcmp(type, @encode(unsigned long)) == 0) {
        unsigned long actual = (unsigned long)va_arg(v, unsigned long);
        obj = [NSNumber numberWithUnsignedLong:actual];
    } else if (strcmp(type, @encode(unsigned long long)) == 0) {
        unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
        obj = [NSNumber numberWithUnsignedLongLong:actual];
    } else if (strcmp(type, @encode(unsigned short)) == 0) {
        unsigned short actual = (unsigned short)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedShort:actual];
    }
    va_end(v);
    return obj;
}

#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))

其中@encode()是一个编译时特性,其可以将传入的类型转换为标准的OC类型字符串

学习七、Block避免循环应用

Masonry中,Block持有View所在的ViewController,但是ViewController并没有持有Blcok,因此不会导致循环引用。

[self.view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.centerY.equalTo(self.otherView.mas_centerY);
}];

源码:仅仅是block(constrainMaker),没有被self持有

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

参考资料

读 SnapKit 和 Masonry 自动布局框架源码

iOS开发之Masonry框架源码解析

Masonry 源码解读

Masonry源码解析

链式语法使“多步配置”可以写成一行连贯的调用,如 make.left.equalTo(superview).offset(20).priorityHigh(),读起来接近自然语言。下面从构成要素、实现原理、与 Builder 的关系、多属性链式四方面展开。

3.6.1 链式语法的三要素
要素 说明 在 Masonry 中的体现
统一返回类型 每一步方法返回的类型与“可继续调用的对象”一致,通常是 self 或协议类型。 equalTooffsetpriority 等均返回 MASConstraint *(或 id<MASConstraint>),调用方可持续 .xxx
返回 self 或当前对象 方法内部完成“设置”后,返回当前对象本身,而不是 void 或无关类型。 offset(CGFloat) 内部设置 layoutConstant,然后 return selfequalTo(id) 设置 secondViewAttributereturn self
可选的 Block 封装 若参数需要延迟求值或复杂逻辑,可用 Block 作为 getter 的返回值,Block 内再 return self。 offsetmultipliedBy 等用“返回 Block 的 getter”,调用方写 .offset(20) 即调用该 Block(20),Block 内设置后 return self。

因此链式语法的实现核心可归纳为:Getter 返回 Block 或直接返回 self;Block 的返回值是当前对象,使每次调用后仍可继续点语法调用。

3.6.2 链式调用与 Builder / 流式接口

链式 API 在《领域驱动设计》等文献中常被称为 流式接口(Fluent Interface):通过方法链使调用读起来像一句“句子”,降低认知负担。与 建造者模式(Builder) 的关系:

  • Builder:通常有一个“最终步骤”(如 build()install()),前面步骤只配置内部状态,不产生最终产品;链式调用用于配置。
  • Masonry:前面步骤(leftequalTooffsetpriority)都是配置,最终“产出”发生在 install 阶段(Block 执行完后由 Maker 统一 install)。因此 Masonry 的链式 + 两阶段(描述 → install)与 Builder 的思想一致。

区别在于:Masonry 的“产品”是约束描述对象(MASConstraint),真正的 NSLayoutConstraint 在 install 时由 Maker 遍历描述对象再生成;Builder 模式里通常是 Director 调用 Builder 的 build 得到产品。共同点都是:链式写配置,最后一步才真正“构建”

3.6.3 完整调用链示意(一步一返回)

make.left.equalTo(superview).offset(20).priorityHigh() 为例,每一步的“谁在返回”如下:

sequenceDiagram
  participant C as 调用方
  participant M as MASConstraintMaker
  participant V as MASViewConstraint

  C->>M: make.left
  M->>M: addConstraintWithLayoutAttribute(Left)
  M->>V: 创建并加入 constraints
  M-->>C: 返回 V (MASConstraint)

  C->>V: .equalTo(superview)
  V->>V: 设置 secondViewAttribute
  V-->>C: return self (V)

  C->>V: .offset(20)
  V->>V: 设置 layoutConstant = 20
  V-->>C: return self (V)

  C->>V: .priorityHigh()
  V->>V: 设置 priority
  V-->>C: return self (V)

因此:make.left 返回的是 MASViewConstraint(单条约束描述);之后的 equalTooffsetpriorityHigh 都是这条 MASViewConstraint 的方法,每次返回 self,形成链。

3.6.4 多属性链式(make.top.left)与委托机制

当写成 make.top.left 时,表示“两条独立约束”:top 一条、left 一条。流程是:

  1. make.top:Maker 创建一条 MASViewConstraint(top),加入 constraints 数组,返回这条 MASViewConstraint
  2. 调用方继续 .left:此时是 MASViewConstraint 的 .left 被调用(因为 MASConstraint 协议也声明了 left、right、top 等属性)。
  3. MASViewConstraint 的 left 实现:在自身再绑一条 left,而是委托回 Maker[self.delegate constraint:self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]。Maker 发现传入的 constraint 非 nil(即当前已有一条 top),会创建 MASCompositeConstraint,把“原来的 top”和“新的 left”包在一起,并在 constraints 数组里用 composite 替换原来的 single constraint

因此 make.top.left 在 Maker 内部表现为:数组里有一条 MASCompositeConstraint,其内部有两条 MASViewConstraint(top、left)。这样既满足“链式写法”,又保证语义是“两条约束”而不是“一条约束有两个属性”。

3.6.5 链式语法的实现核心(代码级)

核心思路:Getter 返回一个 Block,Block 的返回值是当前对象(或约束对象),从而形成链。

// 概念示例:链式 Block 属性
typedef MASConstraint * (^ChainBlock)(CGFloat value);

- (ChainBlock)offset {
    return ^MASConstraint *(CGFloat value) {
        self.layoutConstant = value;
        return self;  // 返回自身,支持继续 .priority(...) 等
    };
}

调用顺序示例:make.left.equalTo(superview).offset(20).priority(High) → 先确定“左、等于、目标”,再设 constant,再设优先级,每一步返回可链式对象。

与“非链式”的对比(同一语义):

// 非链式:每步无返回值或返回 void,无法连续写
[constraint setSecondViewAttribute:...];
[constraint setLayoutConstant:20];
[constraint setPriority:MASLayoutPriorityDefaultHigh];

// 链式:每步返回 self,可连续写
[[[constraint equalTo:superview] offset:20] priorityHigh];
// 或写成点语法:constraint.equalTo(superview).offset(20).priorityHigh();
3.6.6 自实现简易链式 API 模板(Objective-C)

若在业务中需要类似 Masonry 的链式配置,可参考以下模板(思想与 Masonry 一致):

// 1. 协议或抽象类型:所有“可链”方法返回自身类型
@protocol Chainable <NSObject>
- (id<Chainable>)offset:(CGFloat)value;
- (id<Chainable>)priority:(UILayoutPriority)priority;
@end

// 2. 实现类:每个方法设置后 return self
@interface MyConstraint : NSObject <Chainable>
@end
@implementation MyConstraint
- (id<Chainable>)offset:(CGFloat)value {
    self.layoutConstant = value;
    return self;
}
- (id<Chainable>)priority:(UILayoutPriority)priority {
    self.priorityValue = priority;
    return self;
}
@end

// 3. 使用:链式调用
MyConstraint *c = [[MyConstraint alloc] init];
[[c offset:20] priority:UILayoutPriorityDefaultHigh];
// 或若用 Block 属性:c.offset(20).priority(High);

3.7 equalTo / offset 的链式返回原理(源码级)

链式得以成立的前提是:每一步方法返回的都是“可继续调用的对象”。在 Masonry 中:

  • equalTo(id):在 MASViewConstraint 中,会设置 secondViewAttribute(目标视图与属性),并 return self(即当前 MASConstraint),因此可继续写 .offset(20)
  • offset(CGFloat):内部设置 constraint 的 layoutConstant,同样 return self,故可再写 .priority(...)
  • priority(...):设置优先级后仍 return self,便于需要时再链其他修饰。

因此 make.left 返回的是一条“未完成”的 MASViewConstraint;.equalTo(superview) 补全“关系与目标”并仍返回这条约束;.offset(20) 补全 constant 并仍返回同一条约束。同一条约束对象在 Block 执行过程中被逐步“填满”,最后在 Maker 的 install 阶段统一生成 NSLayoutConstraint。若 secondItem 为 nil(如 make.width.mas_equalTo(100)),则对应系统约束的 toItem 为 nil、secondAttribute 为 NSLayoutAttributeNotAnAttribute,表示“与常量比较”。


4. 约束的生成与安装

4.1 安装流程(泳道图)

sequenceDiagram
  participant U as 开发者
  participant V as View
  participant M as MASConstraintMaker
  participant C as MASConstraint
  participant S as 系统 Auto Layout

  U->>V: mas_makeConstraints:
  V->>V: translatesAutoresizingMaskIntoConstraints = NO
  V->>M: 创建 Maker(view)
  V->>M: 执行 block(maker)
  loop 每条约束描述
    U->>M: make.xxx.equalTo(...).offset(...)
    M->>C: 添加/创建 MASConstraint
  end
  M->>C: install
  loop 每条 MASConstraint
    C->>S: 创建并激活 NSLayoutConstraint
  end
  S-->>V: 布局更新

4.2 约束收集与安装算法(伪代码)

阶段一:收集(Block 执行过程中不立即创建 NSLayoutConstraint,只记录描述)

// UIView+MASAdditions
function mas_makeConstraints(block):
    self.translatesAutoresizingMaskIntoConstraints = NO
    maker = [[MASConstraintMaker alloc] initWithView:self]
    block(maker)   // 执行过程中,make.left 等向 maker 内部数组追加 MASConstraint
    return [maker install]

// MASConstraintMaker -install
function install:
    constraints = 本 Maker 已收集的 MASConstraint 列表(单条 + 复合展开后的叶子)
    for each constraint in constraints:
        constraint.install   // 复合约束递归调用子约束的 install
    return constraints

阶段二:安装(将每条 MASViewConstraint 转为系统约束并激活)

// MASViewConstraint -install
function install:
    if alreadyInstalled then return
    layoutConstraint = [NSLayoutConstraint constraintWithItem: firstViewAttribute.view
        attribute: firstViewAttribute.layoutAttribute
        relatedBy: self.layoutRelation
        toItem: secondViewAttribute.view
        attribute: secondViewAttribute.layoutAttribute
        multiplier: self.layoutMultiplier
        constant: self.layoutConstant]
    layoutConstraint.priority = self.priority
    layoutConstraint.active = YES   // 或 addConstraint: 到公共 ancestor
    self.installedConstraint = layoutConstraint

说明:复合约束(如 edges)在 install 时遍历其子 MASViewConstraint 并逐一执行上述安装逻辑,保证与单条约束同一套路径,符合组合模式“统一接口”的语义。

4.3 mas_updateConstraints 只更新 constant 的原理

mas_updateConstraints:mas_makeConstraints: 共用同一个 Maker 类型,但行为不同:

  • make:每次在 Block 里调用 make.xxx 都会新增一条 MASConstraint 并加入列表,install 时全部新建 NSLayoutConstraint 并激活。
  • update:Masonry 会为当前视图维护“已由 Masonry 安装的约束”的引用;执行 update 的 Block 时,对 make.xxx 的调用会匹配到已有约束(按布局属性等匹配),仅修改该约束的 constant(以及 multiplier/priority 等可写字段),而再创建新的 NSLayoutConstraint。

因此“只改 constant”的语义在源码层体现为:根据 Block 中访问的属性(如 make.top)找到之前 install 时生成的那条 MASViewConstraint,调用其 setLayoutConstant: 或等价方法,并同步到已存在的 NSLayoutConstraint 的 constant 属性。若 Block 里写了之前 make 时从未出现过的属性,部分版本会新建一条约束(行为以官方实现为准)。这也解释了为何“布局结构不变、只改间距或动画”时推荐用 update,可避免重复约束或多余约束对象。

4.4 与系统 Auto Layout 的衔接

Masonry 不实现自己的布局引擎,而是 生成并激活 NSLayoutConstraint(或其子类 MASLayoutConstraint),完全依赖系统 Auto Layout(及底层 Cassowary 求解器)。约束在 install 时会被添加到合适的视图上:若约束涉及两个视图(firstItem、secondItem),通常添加到二者的公共祖先或 firstItem 的父视图上,以便布局引擎正确参与计算。因此与 Interface Builder、手写约束可混用;约束冲突、无法满足等仍由系统报错。调试时可为约束设置 identifier,在 Xcode 的约束列表与控制台报错中会显示该标识,便于定位冲突约束。

4.5 约束挂载视图与 install 细节(据掘金等源码分析)

结合掘金文章 [[19]] 与源码,install 阶段还有以下要点,便于理解“约束到底加在哪个 view 上”。

Maker 的 install 入口

  • 若为 remake(removeExisting = YES),会先通过 [MASViewConstraint installedConstraintsForView:self.view] 取出该视图上已由 Masonry 安装的约束,逐个 uninstall,再执行后续 install。
  • 遍历 maker 的 constraints 数组,对每条 MASConstraint 调用 constraint.install;install 完成后会清空 maker 的数组,避免重复使用。

MASViewConstraint 的 install:决定 installedView

  • 仅尺寸约束(width/height):约束只涉及当前视图自身,没有 secondItem。此时将 当前视图的父视图 作为约束的“关联视图”(secondLayoutItem),以便系统正确解析;约束会添加到当前视图或父视图上(源码中 firstViewAttribute.isSizeAttribute 时 installedView = firstViewAttribute.view)。
  • 存在相对视图(如 equalTo(otherView.mas_top)):会求两个视图的 最近公共父视图(closestCommonSuperview),把 NSLayoutConstraint 添加在该公共祖先 上,这样布局引擎才能同时约束到两个子视图。
  • 其他情况(如只与 superview 某边对齐):通常将约束添加在 firstViewAttribute.view.superview 上。

伪代码(installedView 的选取逻辑) [[19]]:

if (self.secondViewAttribute.view != nil) {
    installedView = [firstView mas_closestCommonSuperview:secondView];
    NSAssert(installedView, @"couldn't find a common superview for %@ and %@", firstView, secondView);
} else if (firstViewAttribute.isSizeAttribute) {
    installedView = firstViewAttribute.view;
} else {
    installedView = firstViewAttribute.view.superview;
}
// 最后将创建的 NSLayoutConstraint 添加到 installedView,并记录到 mas_installedConstraints

update 与 add:若是更新已有约束(updateExisting = YES),会先查找已安装的约束中匹配的那条,只修改其 constant(或 multiplier/priority 等),不新增;否则创建新的 NSLayoutConstraint 并 add 到 installedView,同时记录到视图的 mas_installedConstraints 以便后续 update/uninstall 使用。


5. 关键实现技巧

5.1 包装标量与结构体:mas_equalTo 与 MASBoxValue

系统 API 的 equalTo: 等往往需要 id 类型;而开发中常需传入 CGFloat、CGSize、CGPoint 等。Masonry 通过 mas_equalTo(...) 宏将标量/结构体装箱为 NSValue/NSNumber,再交给内部 equalTo:

#define mas_equalTo(...)  equalTo(MASBoxValue((__VA_ARGS__)))

MASBoxValue 利用 @encode(__typeof__(value)) 获取类型编码,再根据类型将 C 标量或结构体包装为 NSNumber/NSValue,从而统一走 id 接口。这样即可写出:

make.size.mas_equalTo(CGSizeMake(100, 100));
make.center.mas_equalTo(CGPointZero);

5.2 Block 与循环引用

Masonry 的 Block 会捕获外部变量(如 selfotherView),但 Block 本身并未被 self 长期持有:仅在 mas_makeConstraints: 执行期间调用一次 block(maker),执行完毕即结束,因此不会形成 self → Block → self 的循环引用 [[13]]。

// 源码中仅是 block(constraintMaker),没有被 self 持有
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

5.3 抽象方法小技巧:MASMethodNotImplemented

基类中“必须由子类实现”的方法,若直接空实现容易导致静默错误。Masonry 使用宏在未重写时抛异常,明确约定子类必须重写:

#define MASMethodNotImplemented() \
    @throw [NSException exceptionWithName:NSInternalInconsistencyException \
        reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
        userInfo:nil]

三、设计模式与延伸

模式/技巧 在 Masonry 中的体现
组合模式 MASConstraint 协议 + MASViewConstraint(叶子)+ MASCompositeConstraint(组合),形成约束树。详见 §2。
工厂思想 Maker 根据属性(left/edges/…)创建对应约束对象,调用方不直接 new;角色映射、单属性/复合创建流程见 §3.1;与简单工厂对比见 §3.1.4。
链式/流式接口 Block 属性 getter 返回“带返回值的 Block”,Block 内 return self,形成链式调用;三要素、多属性链式与自实现模板见 §3.6。
装箱(BoxValue) 标量/结构体通过 @encode 与 va_arg 统一装箱为 id,供 equalTo 使用。
抽象方法 MASMethodNotImplemented 宏在基类中抛异常,强制子类重写。

提炼与串联:上述模式与思想在 Masonry 中的协作关系、伪代码模板及“按目标选模式”的清单,见 §五、编程思想与设计模式提炼总结(思维导图、流程图、可复用伪代码)。


四、Masonry 中的优秀编程思想

Masonry 在 API 设计与源码实现中体现了一系列可复用的编程思想,理解这些思想有助于在业务代码或自研 DSL 中借鉴其设计。

1. 流式接口(Fluent Interface):把复杂留给自己,把简单留给调用方

思想:每次调用返回“可继续操作的对象”,使多步操作在调用方看来像一句连贯的“句子”,读起来接近自然语言,写起来不易漏参数、不易顺序错。

在 Masonry 中的体现make.left.equalTo(superview).offset(20).priorityHigh() 中,每一步都返回 MASConstraint(或 self),从而可以持续链下去。链式语法的三要素、完整调用链与多属性链式(如 make.top.left)的委托机制详见 §3.6 链式语法完整解析

代码案例:自实现简易链式 API(思想与 Masonry 一致)

// 思想:getter 返回 Block,Block 内完成“设置 + 返回 self”,调用方即可继续链
@interface MyConstraint : NSObject
@property (nonatomic, assign) CGFloat constant;
- (MyConstraint * (^)(CGFloat))offset;
@end
@implementation MyConstraint
- (MyConstraint * (^)(CGFloat))offset {
    return ^MyConstraint *(CGFloat value) {
        self.constant = value;
        return self;  // 返回自身,支持 .priority(...) 等后续调用
    };
}
@end
// 使用方式与 Masonry 一致:make.left.equalTo(sv).offset(20).priority(High);

2. 领域特定语言(DSL):用“业务语言”描述约束

思想:不暴露底层概念(如 NSLayoutAttribute、multiplier、constant),而是提供贴近“布局意图”的词汇(left、equalTo、offset),让代码即文档。

在 Masonry 中的体现:开发者写的是“左边等于某视图”“偏移 20”“优先级高”,而不是“item1.attributeLeft relation item2.attributeLeft multiplier 1 constant 20”。

代码案例:Masonry 写法 vs 系统写法

// 系统 API:意图被冗长参数淹没
[NSLayoutConstraint constraintWithItem:subview
                             attribute:NSLayoutAttributeLeft
                             relatedBy:NSLayoutRelationEqual
                                toItem:superview
                             attribute:NSLayoutAttributeLeft
                            multiplier:1.0
                              constant:20];

// Masonry DSL:意图一目了然
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(superview).offset(20);
}];

3. 组合模式统一接口:单条与复合用同一套 API

思想:调用方不区分“单条约束”还是“多条约束的集合”,都通过同一类型(MASConstraint)操作;复合约束(如 edges)在内部展开为多条,但对外呈现一致。

在 Masonry 中的体现make.left 返回 MASConstraint,make.edges 也返回 MASConstraint(实为 MASCompositeConstraint),都可继续 .equalTo(...).offset(...)。组合模式在 Masonry 中的角色与树状结构见 二、2. 组合模式与约束树;可复用伪代码见 五、5.3 伪代码 ①


4. 延迟执行与两阶段处理:先描述,再安装

思想:Block 执行阶段只“收集意图”,不立刻产生副作用(不立刻 addConstraint);等 Block 结束后再统一 install。这样便于做约束去重、批量激活、与系统 API 的对接。

在 Masonry 中的体现block(maker) 时只往 Maker 内部数组追加 MASConstraint;[maker install] 时才创建 NSLayoutConstraint 并激活。

代码案例:两阶段伪代码

// 阶段一:描述(无副作用)
- (NSArray *)mas_makeConstraints:(void (^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *maker = [[MASConstraintMaker alloc] initWithView:self];
    block(maker);   // 仅填充 maker 的约束数组,未修改视图层级
    return [maker install];  // 阶段二:统一安装
}

5. 装箱与类型擦除:统一标量与对象入口

思想:系统 API 往往只接受 id(对象),而业务中大量使用 CGFloat、CGSize、CGPoint 等值类型。通过“装箱”把值类型包成对象,对外提供统一接口(如 mas_equalTo),内部再根据类型解码。

在 Masonry 中的体现mas_equalTo(100)mas_equalTo(CGSizeMake(80, 80)) 通过 MASBoxValue 转为 NSNumber/NSValue,再走 equalTo:。

代码案例:MASBoxValue 思想简化版

// 宏:任意类型都先装箱再交给 equalTo
#define mas_equalTo(...)  equalTo(MASBoxValue((__VA_ARGS__)))

// 使用:调用方无需区分“传对象”还是“传标量”
[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.size.mas_equalTo(CGSizeMake(100, 100));  // 结构体
    make.width.mas_equalTo(200);                   // 标量
    make.center.equalTo(otherView);                // 对象
}];

6. 抽象基类与“必须重写”的明确约定

思想:基类定义模板方法,子类必须实现某一步;若子类未实现就调用,应立刻失败并给出清晰原因,而不是静默错误或未定义行为。

在 Masonry 中的体现:MASConstraint 的抽象方法用 MASMethodNotImplemented 宏,在未重写时抛异常并指明“必须在子类中重写 xxx”。

代码案例:自实现基类中的“必须重写”

#define MASMethodNotImplemented() \
    @throw [NSException exceptionWithName:NSInternalInconsistencyException \
        reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
        userInfo:nil]

@interface MASAbstractConstraint : NSObject
- (void)install;  // 子类实现
@end
@implementation MASAbstractConstraint
- (void)install {
    MASMethodNotImplemented();  // 若子类未重写,调用此处即崩溃并提示
}
@end

7. 编程思想小结(可复用清单)

思想 核心要点 可复用于
流式接口 每步返回 self/可链对象,形成连贯调用 构建器、配置 API、链式校验
DSL 用领域词汇封装底层概念,代码即文档 配置、查询、布局、路由
组合统一接口 单元素与集合同一类型,透明展开 树形结构、批量操作
两阶段 先收集描述再统一执行,便于优化与扩展 批量网络请求、事务、布局
装箱/类型擦除 值类型统一为对象接口,内部再解码 跨类型容器、序列化、API 兼容
抽象方法显式失败 未重写时抛异常并说明,避免静默错误 模板方法、插件、子类契约

五、编程思想与设计模式提炼总结

本节对 Masonry 中使用的编程思想设计模式做统一提炼:用思维导图总览、用流程图串联协作关系、用伪代码与模板固化“可迁移”的写法,便于在其它 DSL、配置类 API 或自研框架中复用。


5.1 思维导图:Masonry 编程思想与设计模式总览

mindmap
  root((Masonry 思想与模式))
    设计模式
      组合模式
        Component: MASConstraint 协议
        Leaf: MASViewConstraint
        Composite: MASCompositeConstraint
        统一接口 单条与复合一致
      工厂思想
        Creator: MASConstraintMaker
        Product: MASConstraint
        工厂方法: addConstraintWithLayoutAttribute
        按需创建 调用方不 new
      建造者思想
        两阶段: 描述 → install
        链式配置 最后统一构建
    编程思想
      流式接口
        每步 return self
        Block 返回自身 形成链
      领域特定语言 DSL
        业务词汇 隐藏底层概念
        left equalTo offset
      两阶段处理
        阶段一 收集描述
        阶段二 统一安装
      装箱与类型擦除
        mas_equalTo MASBoxValue
        标量/结构体 → id
      抽象方法显式失败
        MASMethodNotImplemented
        未重写即抛异常
    协作关系
      入口: mas_makeConstraints
      Maker 工厂 生产 Constraint
      Constraint 链式 配置 再 install

5.2 流程图:从 API 调用到约束生效(模式协作)

下图展示“一次完整布局”中,各模式与思想如何串联:入口工厂创建链式配置两阶段 install组合展开系统约束

flowchart TB
  subgraph 入口与两阶段
    A[开发者 mas_makeConstraints block]
    A --> B[阶段一: block maker]
    B --> C[阶段二: maker install]
  end

  subgraph 工厂与产品
    B --> D[Maker 工厂]
    D --> E{请求属性?}
    E -->|单属性 left/width| F[创建 MASViewConstraint]
    E -->|复合 edges/size| G[创建 MASCompositeConstraint]
    F --> H[返回 MASConstraint]
    G --> H
  end

  subgraph 链式与组合
    H --> I[链式 equalTo offset priority]
    I --> J[每步 return self]
    J --> C
    C --> K[遍历 constraints]
    K --> L{当前项类型?}
    L -->|Leaf| M[单条 install → NSLayoutConstraint]
    L -->|Composite| N[递归子约束 逐一 install]
    N --> M
  end

  subgraph 系统层
    M --> O[添加到公共祖先 / view]
    O --> P[Auto Layout 引擎]
    P --> Q[布局生效]
  end

提炼要点

  • 两阶段:描述(block)与执行(install)分离,便于批量、去重、与系统 API 对接。
  • 工厂:Maker 根据“请求”生产单条或组合约束,调用方只依赖 MASConstraint
  • 链式:配置过程每步返回 self,形成一句“句子”。
  • 组合:install 时对 Leaf 与 Composite 统一调用 install,Composite 内部递归子约束。

5.3 设计模式与编程思想提炼表(含伪代码)

下表将每种模式/思想抽象为:解决的问题核心做法Masonry 对应可复用伪代码适用场景,便于直接迁移到其它项目。

模式/思想 解决的问题 核心做法 Masonry 对应 伪代码骨架 适用场景
组合模式 单条与集合使用方式不一致 定义统一 Component 接口,Leaf 与 Composite 都实现;Composite 持有子节点,操作时递归 MASConstraint / MASViewConstraint / MASCompositeConstraint 见下文伪代码 ① 树形结构、批量操作、配置项分组
工厂思想 调用方与具体产品类耦合 由“工厂”根据请求创建具体产品,调用方只依赖抽象产品 Maker + addConstraintWithLayoutAttribute 见下文伪代码 ② 多种产品、按参数/类型创建、隐藏构造细节
流式接口 多步配置冗长、易漏参数 每步方法返回 self(或可链对象),形成链式调用 equalTo / offset / priority 均 return self 见下文伪代码 ③ 构建器、配置 API、校验链、DSL
两阶段处理 边描述边执行难以优化、易产生重复副作用 阶段一仅收集描述(不执行),阶段二统一执行 block(maker) 只填充数组;install 时再创建并添加 见下文伪代码 ④ 批量请求、事务、布局、表单校验
DSL 底层概念暴露、意图不直观 用领域词汇封装底层 API,让“写什么像什么” left、equalTo、offset、edges 见下文伪代码 ⑤ 配置、查询、布局、路由、规则引擎
装箱/类型擦除 系统 API 只接受 id,业务多用值类型 将标量/结构体装箱为对象,统一入口,内部再解码 mas_equalTo、MASBoxValue 见下文伪代码 ⑥ 跨类型容器、序列化、多态参数
抽象方法显式失败 子类未重写导致静默错误 基类“必须重写”的方法内抛异常并说明 MASMethodNotImplemented 见下文伪代码 ⑦ 模板方法、插件接口、子类契约

伪代码 ① 组合模式

protocol Component { func install() }
class Leaf: Component { func install() { /* 执行单条逻辑 */ } }
class Composite: Component {
    var children: [Component]
    func install() { children.forEach { $0.install() } }
}
// 调用方:component.install(),不关心是 Leaf 还是 Composite

伪代码 ② 工厂思想

class Maker {
    func left() -> Product { return create(.left) }
    func edges() -> Product { return composite([.left, .right, .top, .bottom]) }
    private func create(_ attr: Attribute) -> Product {
        let p = ConcreteProduct(attr)
        constraints.append(p)
        return p
    }
}
// 调用方:let c = maker.left(); 不 new ConcreteProduct

伪代码 ③ 流式接口

func offset(_ value: T) -> Self {
    self.value = value
    return self
}
func priority(_ p: P) -> Self {
    self.priority = p
    return self
}
// 调用:obj.offset(20).priority(high)

伪代码 ④ 两阶段处理

func make(block: (Maker) -> Void) -> Result {
    let maker = Maker()
    block(maker)      // 阶段一:只填充 maker 内部结构
    return maker.build()  // 阶段二:统一执行、产生副作用
}

伪代码 ⑤ DSL 封装

// 底层:setAttribute(Left, relation: Equal, to: view, attribute: Left, constant: 20)
// DSL:make.left.equalTo(view).offset(20)
// 实现:left 返回约束描述对象,equalTo 设目标,offset 设 constant,均 return self

伪代码 ⑥ 装箱

func box(_ value: Any) -> Id {
    if value is CGFloat { return NSNumber(value) }
    if value is CGSize { return NSValue(value) }
    // ...
}
func equalTo(_ id: Id) { /* 内部根据类型解码 */ }

伪代码 ⑦ 抽象方法显式失败

func mustOverride() {
    throw Exception("You must override \(method) in a subclass.")
}
// 基类中:func install() { mustOverride() }

5.4 流程图:六大思想在“一句话布局”中的分工

以一句 make.left.equalTo(superview).offset(20) 为例,下图标出每一步对应的思想或模式,便于记忆与迁移。

flowchart LR
  A[make] --> B[left]
  B --> C[equalTo]
  C --> D[offset]
  D --> E[install]

  subgraph 对应思想
    A1[两阶段入口]
    B1[工厂: 按 left 创建约束]
    C1[DSL: 业务语汇]
    D1[流式: return self]
    E1[两阶段: 统一 install]
  end

  A -.-> A1
  B -.-> B1
  C -.-> C1
  D -.-> D1
  E -.-> E1

5.5 可复用设计清单(按“想实现什么”选模式)

若要在业务中实现类似 Masonry 的体验,可按目标选择对应模式与伪代码模板。

目标 推荐模式/思想 参考伪代码
让“单条”与“一组”用同一套 API 组合模式 §5.3 伪代码 ①
根据“请求类型”创建不同对象,调用方不 new 工厂思想 §5.3 伪代码 ②
多步配置写成一句链式调用 流式接口 §5.3 伪代码 ③
先收集再统一执行(批量、事务、布局) 两阶段处理 §5.3 伪代码 ④
用业务词汇隐藏底层 API DSL §5.3 伪代码 ⑤
值类型与对象统一入口 装箱/类型擦除 §5.3 伪代码 ⑥
基类要求子类必须实现某方法 抽象方法显式失败 §5.3 伪代码 ⑦

5.6 小结:提炼后的编程思想一句话

  • 组合:单条与复合同一接口,操作时递归子节点。
  • 工厂:谁要谁造,调用方只拿抽象产品。
  • 流式:每步 return self,链成一句“话”。
  • 两阶段:先描述后执行,便于优化与扩展。
  • DSL:用领域词汇说话,代码即文档。
  • 装箱:值类型进“盒子”,统一走对象接口。
  • 显式失败:该子类实现的没实现,立刻报错不隐瞒。

上述思想与模式在 Masonry 中同时存在、相互配合:入口用两阶段,Maker 用工厂,约束用流式与组合,标量用装箱,基类用显式失败。理解并提炼后,可在任意“配置型、构建型、DSL 型”的 API 设计中按需复用。


参考文献

[1] SnapKit. Masonry. GitHub. github.com/SnapKit/Mas…

[2] SnapKit. SnapKit. GitHub. github.com/SnapKit/Sna…

[3] Apple. Auto Layout Guide. Developer Documentation.

[4] Sarunw. History of Auto Layout constraints. sarunw.com/posts/histo…

[5] Wikipedia. Cassowary (software). en.wikipedia.org/wiki/Cassow…

[6] Larder. What's in your Larder: iOS layout DSLs. larder.io/blog/larder…

[7] Cassowary. Solving constraint systems. cassowary.readthedocs.io/en/latest/t…

[8] University of Washington. Cassowary Constraint Solving Toolkit. constraints.cs.washington.edu/cassowary/

[9] Badros, G. J., Borning, A., & Marriott, K. (1997). Solving Linear Arithmetic Constraints for User Interface Applications. Proceedings of the 1997 ACM Symposium on User Interface Software and Technology (UIST).

[10] University of Washington. Cassowary TOCHI. constraints.cs.washington.edu/solvers/cas…

[11] 设计模式:组合模式(Composite Pattern). Runoob. www.runoob.com/design-patt…

[12] 设计模式:工厂方法. Runoob. www.runoob.com/design-patt…

[13] 读 SnapKit 和 Masonry 自动布局框架源码. 戴铭. ming1016.github.io/2018/04/07/…

[14] Masonry:iOS AutoLayout的革命性简化框架. CSDN. blog.csdn.net/gitblog_005…

[15] 源码解读——Masonry. 楚权的世界. chuquan.me/2019/10/02/…

[16] iOS中Masonry的使用总结. 星星的博客. smileasy.github.io/2019/04/01/…

[17] iOS自动布局框架之Masonry. 腾讯云开发者社区. cloud.tencent.com/developer/a…

[18] 浅析Masonry. HelloBit. www.hellobit.com.cn/doc/2020/6/…

[19] Mcyboy. Masonry实现原理并没有那么可怕. 掘金. juejin.cn/post/684490…

[20] 掘金. Masonry 相关文章. juejin.cn/post/684490…


延伸阅读

  • SnapKit:Masonry 的 Swift 继任者,本系列《04-SnapKit框架:从使用到源码解析》可对照学习。
  • Auto Layout 内在尺寸:Content Hugging 与 Compression Resistance 在 Apple《Auto Layout Guide》中的说明。
  • Cassowary 论文:深入理解约束层次与增量求解,便于分析复杂布局冲突与性能。
  • iOS 设计模式 Swift 实现(组合模式、工厂模式):可参考开源仓库如 iOS_Design_Patterns_Swift 等。
  • Masonry 官方源码github.com/SnapKit/Mas… ,建议结合本文“源码解析”章节对照阅读 MASConstraintMaker、MASViewConstraint、MASCompositeConstraint 等实现。
  • 掘金《Masonry实现原理并没有那么可怕》 [[19]]:从 makeConstraints、make(Maker)、install、equalTo 四条线梳理原理,含链式多属性(make.top.left)的委托与复合替换、约束挂载视图(closestCommonSuperview)等,可与本文 §1.3、§4.5 对照阅读。
❌