阅读视图

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

# iOS 动态库与静态库全面解析

iOS 动态库与静态库全面解析

一、基本概念

静态库 (Static Library)

同一个库,动态库 vs 静态库,谁更大?绝大多数情况下,动态库会让包体积更大。 原因如下:

核心差异:能不能 Strip 未使用代码,静态库 — 链接器只拉入你实际用到的 .o,没用到的直接丢弃: 静态库是在编译链接阶段被完整拷贝到可执行文件中的代码集合。链接完成后,静态库文件本身不再被需要。

文件格式:

  • .a — 传统静态库(archive 文件,本质是 .o 目标文件的打包)
  • .framework — 可以是静态 framework(Xcode 从 iOS 8 起支持)

动态库 (Dynamic Library)

动态库在运行时由动态链接器(dyld)加载到进程地址空间中,不会被拷贝到可执行文件里,而是以独立文件形式存在于 App Bundle 中。

文件格式:

  • .dylib — 传统动态库(系统库使用,第三方不可提交 App Store)
  • .tbd — 动态库的文本描述文件(text-based stub),Xcode 链接系统库时使用
  • .framework — 可以是动态 framework(iOS 8+ 支持嵌入式动态 framework)

注意.framework 本身只是一种打包格式(目录结构),它既可以是静态的也可以是动态的,取决于内部二进制的 Mach-O 类型。


二、编译链接原理

静态库的链接过程

源代码 (.m/.swift)
       ↓ 编译器 (clang/swiftc)
目标文件 (.o)
       ↓ 归档工具 (ar)
静态库 (.a)
       ↓ 链接器 (ld) 将用到的 .o 拷贝进最终二进制
可执行文件 (Mach-O executable)

关键点:

  • 链接器做符号解析,只将被引用到的 .o 文件链接进来(粒度是 .o,不是函数)
  • 使用 -ObjC flag 时会链接所有包含 ObjC 类的 .o(解决 Category 不生效的问题)
  • 使用 -all_load 会强制链接所有 .o
  • 使用 -force_load <path> 可以对特定静态库强制全部链接
  • 静态库的代码最终融合进主二进制,运行时已不存在"库"的概念

动态库的链接过程

源代码 (.m/.swift)
       ↓ 编译 + 链接
动态库 (.dylib / .framework)
       ↓ 嵌入 App Bundle 的 Frameworks/ 目录
       ↓ 运行时 dyld 加载
进程地址空间

关键点:

  • 编译时只做符号检查,不拷贝代码,主二进制只记录"我依赖了哪个动态库"
  • dyld 在 App 启动时(或按需 dlopen)将动态库映射到进程地址空间
  • 动态库有独立的 install_name,指示 dyld 去哪里找它
  • 嵌入式 framework 的 install_name 通常是 @rpath/XXX.framework/XXX
  • 动态库在运行时保持独立,拥有自己的符号表和地址空间

核心区别图示

┌─────────────────────────────────────────────────────┐
│                    编译期                             │
│                                                     │
│  静态库:代码被拷贝 ──────→ 合并到主二进制               │
│  动态库:只记录依赖关系 ──→ 主二进制仅保存引用            │
│                                                     │
├─────────────────────────────────────────────────────┤
│                    运行期                             │
│                                                     │
│  静态库:不存在了,代码已在主二进制中                     │
│  动态库:dyld 加载 → rebase → bind → 映射到进程空间     │
│                                                     │
└─────────────────────────────────────────────────────┘

三、Mach-O 文件结构

无论静态库还是动态库,最终都与 Mach-O 格式密切相关。

Mach-O 文件结构:
┌──────────────────────┐
│      Header          │  ← 魔数、CPU 类型、文件类型
│                      │     MH_EXECUTE (可执行文件)
│                      │     MH_DYLIB   (动态库)
│                      │     MH_OBJECT  (目标文件,静态库内的 .o)
├──────────────────────┤
│   Load Commands      │  ← 描述 segment 布局、依赖的动态库列表、入口点等
│                      │     LC_LOAD_DYLIB 记录依赖的动态库
│                      │     LC_RPATH 指定运行时搜索路径
├──────────────────────┤
│   __TEXT Segment      │  ← 只读:机器码、字符串常量、Swift metadata
├──────────────────────┤
│   __DATA Segment      │  ← 可读写:全局变量、ObjC 元数据、GOT (全局偏移表)
├──────────────────────┤
│   __LINKEDIT Segment  │  ← 符号表、字符串表、代码签名信息
└──────────────────────┘

静态库(.a)的本质:不是 Mach-O 文件,而是多个 .o(Mach-O Object)的归档包。链接器从中提取需要的 .o 合并到最终的 Mach-O 可执行文件中。

动态库的本质:是一个完整的 Mach-O 文件(类型为 MH_DYLIB),有自己的 Header、Load Commands、Segments,运行时被 dyld 独立加载。


四、全面对比

维度 静态库 动态库
链接时机 编译期,链接器完成 运行期,dyld 完成
代码位置 拷贝进主 Mach-O 独立文件,位于 .app/Frameworks/
主二进制大小 更大(包含库代码) 更小(只记录依赖引用)
App Bundle 总大小 通常更小(Strip 掉未用代码) 可能更大(整个库都打包)
启动速度 快,无额外加载开销 慢,dyld 需要 load → rebase → bind
内存 每个引用者各有一份拷贝 系统库多进程共享;嵌入式库不共享
符号可见性 合并到主二进制的全局符号表 保持独立符号表,符号隔离
符号冲突风险 高,容易 duplicate symbols 低,各库符号空间独立
ObjC Category -ObjC flag 才能加载 自动加载
链接时优化 (LTO) 支持,编译器可跨库优化 不支持,库边界是优化屏障
增量编译 改库需重新链接整个 App 改库只需重编该库
代码签名 无需单独签名 每个动态库需独立签名
Xcode 配置 Do Not Embed Embed & Sign

五、优缺点详解

静态库的优点

  1. 启动速度快 — 不增加 dyld 加载数量,pre-main 阶段零额外开销
  2. 链接时优化 (LTO) — 编译器可以跨静态库边界做死代码消除、函数内联、常量折叠
  3. 包体积可控 — 链接器只拉入被引用的 .o,未用代码不会进入最终二进制
  4. 部署简单 — 最终只有一个 Mach-O,不需要管 Embed & Sign
  5. 无运行时依赖 — 不会出现 dylib not found / image not found 崩溃

静态库的缺点

  1. 代码重复 — 若主 App 和 Extension 都静态链接同一个库,代码各存一份
  2. 符号冲突 — 多个静态库包含同名符号时报 duplicate symbol 错误
  3. 编译链接耗时 — 主二进制越大,链接阶段越慢;改库后需重新链接整个 App
  4. 无法独立更新 — 库的任何改动都要重新编译发版

动态库的优点

  1. 系统库共享内存 — UIKit、Foundation 等系统动态库被所有 App 共享,节省内存
  2. 符号隔离 — 各动态库有独立命名空间,同名符号不冲突
  3. 增量编译友好 — 修改动态库只需重编该库,不影响主二进制链接
  4. 跨 Target 共享 — 主 App 和 Extension 可共用同一份动态 framework,避免代码重复
  5. 热替换理论可行 — 替换 .framework 文件即可更新逻辑(App Store 不允许,仅企业包/调试可用)

动态库的缺点

  1. 启动变慢 — 每个动态库在 pre-main 阶段都增加 dyld 加载耗时
  2. 包体积膨胀 — 动态库无法 Strip 未使用符号,整个库的代码都会打入 Bundle
  3. 签名复杂 — 每个动态 framework 需独立代码签名
  4. 沙盒限制 — iOS 不允许 dlopen App Bundle 外的动态库
  5. 运行时崩溃风险 — 库缺失或版本不匹配,启动时直接 crash
  6. 无法跨库 LTO — 编译器优化止步于动态库边界

六、启动性能原理 (dyld)

dyld 加载全流程

App 进程创建
  ↓
1. Load dylibs           递归加载主二进制依赖的所有动态库(及其传递依赖)
  ↓                       每个库:mmap 到虚拟内存 → 验证签名 → 注册
2. Rebase                 ASLR (地址空间布局随机化) 导致实际加载地址与编译地址不同
  ↓                       遍历所有内部指针,加上随机偏移量 (slide)
3. Bind                   解析跨库的外部符号引用
  ↓                       lazy binding: 首次调用时才解析(大部分函数)
  ↓                       non-lazy binding: 启动时立即解析(ObjC 元数据、C++ 虚表)
4. ObjC Runtime Setup     注册所有 ObjC 类到 runtime
  ↓                       插入 Category 的方法到类的方法列表
  ↓                       确保 selector 唯一性
5. Initializers           执行 +load 方法
  ↓                       执行 C/C++ __attribute__((constructor))
  ↓                       执行 Swift 全局变量的初始化器
  ↓
main() 被调用

每一步为什么耗时

阶段 耗时原因 与动态库数量的关系
Load 磁盘 I/O + 签名验证 线性正相关,库越多越慢
Rebase 遍历 __DATA 段所有内部指针 与库的数据段大小相关
Bind 符号查找(哈希表查询) 与跨库符号引用数量相关
ObjC Setup 类注册 + Category 合并 与 ObjC 类/Category 总数相关
Initializers 执行用户代码 +load 和 constructor 数量相关

dyld 2 vs dyld 3

特性 dyld 2 (iOS 12 及以前) dyld 3 (iOS 13+)
解析时机 每次启动都在进程内完整解析 首次解析后缓存为 launch closure
安全性 在 App 进程内解析(可被攻击) 解析移到进程外守护进程
缓存 closure 缓存后,后续启动跳过解析
冷启动 首次略慢(多了写缓存),后续显著加速
热启动 中等 直接读取 closure,非常快

性能数据参考

动态库数量 大致额外 pre-main 耗时 (iPhone 8 级别)
1-5 个 ~5-20ms
10-20 个 ~50-150ms
50+ 个 ~300ms+
100+ 个 可能超过 400ms watchdog 阈值 (冷启动)

Apple 官方建议:嵌入式动态 framework 控制在 6 个以内。

静态库为什么不影响启动

静态库的代码在编译期已经合并进主二进制:

  • 不增加 Load dylibs 的数量
  • 不增加跨库 Bind 的符号数量
  • ObjC 类直接注册在主二进制中,无额外开销
  • 唯一的影响:主二进制变大 → mmap 主二进制的时间微增(可忽略)

七、符号解析原理

静态链接的符号解析

主程序引用 _doSomething (未定义符号 U)
         ↓
链接器在静态库中搜索
         ↓
找到 MyModule.o 中定义了 _doSomething (符号类型 T)
         ↓
将整个 MyModule.o 拷贝进主二进制
         ↓
符号变为已定义 (resolved)
  • 粒度是 .o 文件:即使只用了 .o 中的一个函数,整个 .o 都会被链接
  • 这就是为什么 SDK 开发者会把每个函数/类放在单独的 .m 文件中,以减少无用代码

动态链接的符号解析

主程序引用 _doSomething (标记为 external, lazy)
         ↓
编译时:链接器确认动态库中存在该符号 → 通过
         ↓
运行时:首次调用 _doSomething
         ↓
dyld 在动态库的符号表中查找 → 写入 GOT/lazy pointer
         ↓
后续调用直接走 GOT,无需再次查找
  • Lazy Binding:大部分函数调用使用,首次调用时才解析,分散了启动开销
  • Non-Lazy Binding:ObjC 类引用、__DATA 段指针等在启动时立即解析

符号冲突对比

静态库:同名符号 → duplicate symbol 编译错误(严格)

ld: duplicate symbol '_MyFunction' in:
    libA.a(module.o)
    libB.a(module.o)

动态库:同名符号 → 运行时 "先加载者胜"(flat namespace)或各自独立(two-level namespace,iOS 默认)

Two-Level Namespace (iOS 默认):
  调用 libA 的 _MyFunction → 解析到 libA 内部
  调用 libB 的 _MyFunction → 解析到 libB 内部
  不会混淆

八、内存与体积影响

内存模型对比

┌─────────── 静态库场景 ──────────────┐
│                                    │
│  App 进程内存:                      │
│  ┌──────────────────┐              │
│  │ 主二进制 (__TEXT)   │ ← 含库A代码  │
│  │ 主二进制 (__DATA)   │ ← 含库A数据  │
│  └──────────────────┘              │
│                                    │
│  Extension 进程内存:                │
│  ┌──────────────────┐              │
│  │ Extension (__TEXT) │ ← 又一份库A  │
│  │ Extension (__DATA) │ ← 又一份库A  │
│  └──────────────────┘              │
│                                    │
│  → 库A代码存在两份 (磁盘 + 内存)       │
└────────────────────────────────────┘

┌─────────── 动态库场景 ──────────────┐
│                                    │
│  App 进程内存:                      │
│  ┌──────────────────┐              │
│  │ 主二进制           │              │
│  │ 库A.framework     │ ←──┐ __TEXT  │
│  └──────────────────┘    │ 页共享   │
│                          │         │
│  Extension 进程内存:      │         │
│  ┌──────────────────┐    │         │
│  │ Extension         │    │         │
│  │ 库A.framework     │ ←──┘ 同一物理页│
│  └──────────────────┘              │
│                                    │
│  → __TEXT 段可跨进程共享物理内存页      │
│  → __DATA 段每个进程各自 copy-on-write│
└────────────────────────────────────┘

体积影响对比

因素 静态库 动态库
未使用代码 链接器丢弃未引用的 .o 整个库都打进 Bundle
LTO 死代码消除 支持,可消除未使用的函数 不支持跨库消除
多 Target 场景 代码重复(每个 Target 一份) 代码只存一份
Strip 链接后可全局 Strip 只能 Strip 库自身的调试符号
压缩 (App Thinning) 主二进制参与整体压缩 每个 framework 独立压缩

九、总结

维度 胜出方 说明
启动速度 静态库 不增加 dyld 加载开销
包体积 (单 Target) 静态库 死代码消除 + LTO 优化
包体积 (多 Target) 动态库 代码共享避免重复
编译速度 动态库 增量编译不影响主二进制
符号安全 动态库 Two-Level Namespace 隔离
运行时稳定性 静态库 无 image not found 风险
部署复杂度 静态库 无需管签名和 Embed
代码优化程度 静态库 支持跨库 LTO

核心原则:除非有明确的跨 Target 代码共享需求(如 App Extension),否则优先选择静态库。iOS 嵌入式动态库不具备系统级共享优势,带来的启动开销往往得不偿失。

Matrix获取卡顿堆栈 (Point Stack)

matrix 是腾讯微信团队开源的一套移动端性能监控与分析框架,核心目标是帮助开发者定位、解决移动端(iOS/Android)应用的性能问题,是微信内部大规模验证过的成熟工具,本文通过阅读源码,详细介绍了针对卡顿日志获取的核心原理。

Matrix 通过周期性采集主线程堆栈并保存在循环数组中,在检测到卡顿时,使用 Point Stack 算法找出最有可能导致卡顿的堆栈。

核心思想

时间流逝
   ↓
每 50ms 采集一次主线程堆栈
   ↓
保存到循环数组(固定大小,如 20 个)
   ↓
检测到卡顿时
   ↓
分析循环数组,找出 Point Stack(最可能导致卡顿的堆栈)
   ↓
生成卡顿报告

设计目标

目标 实现方式
及时性 每 50ms 采集一次,不错过卡顿过程
完整性 保存一个周期内的所有堆栈(通常 20 个)
准确性 通过算法找出真正导致卡顿的堆栈
高效性 固定大小循环数组,避免内存膨胀
低开销 CPU 占用 < 3%,不影响用户体验

核心时间参数

三个关键参数

// 1. RunLoop 超时阈值(卡顿判定阈值)
static useconds_t g_RunLoopTimeOut = 2000000;  // 2000ms = 2秒
// 作用:超过此时间判定为卡顿

// 2. 检查周期(单次采集周期)
static useconds_t g_CheckPeriodTime = 1000000;  // 1000ms = 1秒
// 作用:一轮堆栈采集的总时间,通常为超时阈值的一半

// 3. 堆栈采集间隔
static useconds_t g_PerStackInterval = 50000;  // 50ms
// 作用:每次堆栈采集之间的时间间隔

参数关系

┌────────────────────────────────────────────────┐
│ g_RunLoopTimeOut (2秒) - 卡顿判定阈值          │
└────────────────────────────────────────────────┘
                    │
                    ├─ 一半
                    ↓
┌────────────────────────────────────────────────┐
│ g_CheckPeriodTime (1秒) - 检查周期             │
└────────────────────────────────────────────────┘
                    │
                    ├─ 除以
                    ↓
┌────────────────────────────────────────────────┐
│ g_PerStackInterval (50ms) - 堆栈间隔           │
└────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────┐
│ g_MainThreadCount = 1000ms / 50ms = 20         │
│ (循环数组大小 = 一个周期内采集的堆栈数量)      │
└────────────────────────────────────────────────┘

时间轴示意

时间线(以2秒卡顿为例):

T=0ms      开始监控
|
T=50ms     采集第1个堆栈 ← S0
|
T=100ms    采集第2个堆栈 ← S1
|
T=150ms    采集第3个堆栈 ← S2
|
...
|
T=950ms    采集第19个堆栈 ← S18
|
T=1000ms   采集第20个堆栈 ← S19  ← 完成一轮采集
|          ↓
|          检查是否卡顿(检查RunLoop执行时间)
|          如果未卡顿,进入下一轮采集
|
T=1050ms   采集第21个堆栈 ← S20(覆盖S0)
|
...
|
T=2000ms   检查发现 RunLoop 执行超过 2秒
|          ↓
|          触发卡顿检测
|          ↓
|          分析循环数组中的 20 个堆栈
|          ↓
|          找出 Point Stack(最可能导致卡顿的堆栈)
|          ↓
|          生成卡顿报告

堆栈获取流程

整体流程图

┌──────────────────────────────────────────┐
│ 监控线程主循环                            │
│ while (YES) {                            │
│   check();            // 检测卡顿         │recordCurrentStack(); // 采集堆栈       │
│ }                                        │
└──────────────────────────────────────────┘
                    ↓
┌──────────────────────────────────────────┐
│ recordCurrentStack() 方法                │
│                                          │
│ 外层循环:遍历检查周期                    │
│   nTotalCnt = m_nIntervalTime /         │
│               g_CheckPeriodTime         │
│   通常 = 1000ms / 1000ms = 1次          │
│                                          │
│   内层循环:在一个周期内多次采集          │
│     intervalCount = g_CheckPeriodTime / │
│                     g_PerStackInterval  │
│     通常 = 1000ms / 50ms = 20次         │
│                                          │
│     每次循环:                            │
│       1. usleep(50ms)  // 等待          │2. 获取主线程堆栈                   │
│       3. 添加到循环数组                   │
└──────────────────────────────────────────┘

详细步骤

步骤 1:初始化
// 在 WCBlockMonitorMgr 的 start 方法中
- (void)start {
    // 计算循环数组大小
    g_MainThreadCount = g_CheckPeriodTime / g_PerStackInterval;
    // 例如:1000ms / 50ms = 20
    
    // 创建主线程堆栈处理器
    m_pointMainThreadHandler = [[WCMainThreadHandler alloc] 
                                 initWithCycleArrayCount:g_MainThreadCount];
    
    // g_MainThreadCount = 20
    // 意味着循环数组可以保存 20 个堆栈
}
步骤 2:周期性采集
- (void)recordCurrentStack {
    // ================================================================
    // 外层循环:决定执行几个检查周期
    // ================================================================
    // 正常情况:m_nIntervalTime = 1000ms
    // 退火情况:m_nIntervalTime = 2000ms, 3000ms, 5000ms...
    unsigned long nTotalCnt = m_nIntervalTime / g_CheckPeriodTime;
    
    for (int nCnt = 0; nCnt < nTotalCnt && !m_bStop; nCnt++) {
        // 记录本轮开始时间(用于检测系统挂起)
        gettimeofday(&m_recordStackTime, NULL);
        
        if (g_MainThreadHandle) {
            // ========================================================
            // 内层循环:在一个检查周期内多次采集
            // ========================================================
            // intervalCount = 1000ms / 50ms = 20
            int intervalCount = g_CheckPeriodTime / g_PerStackInterval;
            
            for (int index = 0; index < intervalCount && !m_bStop; index++) {
                // 1️⃣ 等待 50ms
                usleep(g_PerStackInterval);  // 50000 微秒 = 50ms
                
                // 2️⃣ 分配内存
                size_t stackBytes = sizeof(uintptr_t) * g_StackMaxCount;
                uintptr_t *stackArray = (uintptr_t *)malloc(stackBytes);
                if (stackArray == NULL) {
                    continue;  // 内存分配失败,跳过本次
                }
                
                // 3️⃣ 初始化
                __block size_t nSum = 0;
                memset(stackArray, 0, stackBytes);
                
                // 4️⃣ 获取主线程堆栈
                [WCGetMainThreadUtil
                 getCurrentMainThreadStack:^(NSUInteger pc) {
                     stackArray[nSum] = (uintptr_t)pc;  // 保存程序计数器
                     nSum++;
                 }
                 withMaxEntries:g_StackMaxCount        // 最大100个栈帧
                 withThreadCount:g_CurrentThreadCount];
                
                // 5️⃣ 添加到循环数组
                [m_pointMainThreadHandler addThreadStack:stackArray 
                                           andStackCount:nSum];
                // 注意:stackArray 的所有权转移给 m_pointMainThreadHandler
            }
        }
        
        // ============================================================
        // 检测是否被系统挂起
        // ============================================================
        struct timeval tvCur;
        gettimeofday(&tvCur, NULL);
        unsigned long long diff = [WCBlockMonitorMgr diffTime:&m_recordStackTime 
                                                      endTime:&tvCur];
        
        if (diff > DETECTION_THREAD_JUDGE_SUSPEND_THRESHOLD) {
            // 实际消耗时间 > 10秒,说明被挂起了
            gettimeofday(&g_tvRun, NULL);  // 更新时间,避免误报
            MatrixInfo(@"挂起后运行,差值 %llu", diff);
            return;
        }
    }
}
步骤 3:获取主线程堆栈(底层实现)
// WCGetMainThreadUtil 内部使用 backtrace
+ (void)getCurrentMainThreadStack:(StackCallback)callback 
                   withMaxEntries:(size_t)maxEntries
                  withThreadCount:(NSUInteger)threadCount {
    // 1. 获取主线程
    thread_t mainThread = pthread_mach_thread_np(pthread_main_thread_np());
    
    // 2. 暂停主线程(非常短暂,微秒级)
    thread_suspend(mainThread);
    
    // 3. 获取线程状态
    _STRUCT_MCONTEXT machineContext;
    mach_msg_type_number_t state_count = THREAD_STATE_COUNT;
    kern_return_t kr = thread_get_state(mainThread,
                                        THREAD_STATE,
                                        (thread_state_t)&machineContext.__ss,
                                        &state_count);
    
    // 4. 回溯堆栈
    if (kr == KERN_SUCCESS) {
        uintptr_t backtraceBuffer[maxEntries];
        size_t backtraceLength = ksbt_backtraceLength(&machineContext);
        
        // 遍历堆栈帧
        for (size_t i = 0; i < backtraceLength && i < maxEntries; i++) {
            uintptr_t pc = ksbt_framePointer(&machineContext, i);
            callback(pc);  // 回调传递每个栈帧的地址
        }
    }
    
    // 5. 恢复主线程
    thread_resume(mainThread);
}

循环数组存储机制

数据结构设计

@interface WCMainThreadHandler {
    // ================================================================
    // 循环数组配置
    // ================================================================
    int m_cycleArrayCount;  // 数组大小,例如 20
    
    // ================================================================
    // 循环数组(核心存储结构)
    // ================================================================
    uintptr_t **m_mainThreadStackCycleArray;  // 二维数组
    // 第一维:堆栈索引 [0, 19]
    // 第二维:堆栈地址数组 uintptr_t[]
    
    size_t *m_mainThreadStackCount;  // 每个堆栈的深度
    // 例如:[50, 48, 52, ..., 45]
    
    uint64_t m_tailPoint;  // 尾指针,指向下一个写入位置
    
    // ================================================================
    // 分析数据(用于 Point Stack 算法)
    // ================================================================
    size_t *m_topStackAddressRepeatArray;  // 栈顶地址连续重复次数
    // 例如:[0, 1, 2, 0, 1, 0, ...]
    
    int *m_mainThreadStackRepeatCountArray;  // Point Stack 地址总重复次数
    // 动态分配,在找到 Point Stack 后创建
}

循环数组可视化

初始化状态(m_cycleArrayCount = 5):

索引:    0     1     2     3     4
       ┌─────┬─────┬─────┬─────┬─────┐
数组:   │NULL │NULL │NULL │NULL │NULL │
       └─────┴─────┴─────┴─────┴─────┘
          ↑
     m_tailPoint = 0


添加第 1 个堆栈(S0):

索引:    0     1     2     3     4
       ┌─────┬─────┬─────┬─────┬─────┐
数组:   │ S0  │NULL │NULL │NULL │NULL │
       └─────┴─────┴─────┴─────┴─────┘
               ↑
          m_tailPoint = 1


添加第 2-5 个堆栈:

索引:    0     1     2     3     4
       ┌─────┬─────┬─────┬─────┬─────┐
数组:   │ S0  │ S1  │ S2  │ S3  │ S4  │
       └─────┴─────┴─────┴─────┴─────┘
          ↑
     m_tailPoint = 0 (回绕)


添加第 6 个堆栈(S5,覆盖 S0):

索引:    0     1     2     3     4
       ┌─────┬─────┬─────┬─────┬─────┐
数组:   │ S5  │ S1  │ S2  │ S3  │ S4  │
       └─────┴─────┴─────┴─────┴─────┘
               ↑
          m_tailPoint = 1

时间顺序:S1 → S2 → S3 → S4 → S5(最新)

添加堆栈的实现

- (void)addThreadStack:(uintptr_t *)stackArray 
         andStackCount:(size_t)stackCount {
    if (stackArray == NULL) {
        return;
    }
    
    pthread_mutex_lock(&m_threadLock);
    
    // ================================================================
    // 1. 将堆栈写入循环数组
    // ================================================================
    
    // 如果当前位置已有堆栈,先释放旧的
    if (m_mainThreadStackCycleArray[m_tailPoint] != NULL) {
        free(m_mainThreadStackCycleArray[m_tailPoint]);
    }
    
    // 保存新堆栈
    m_mainThreadStackCycleArray[m_tailPoint] = stackArray;
    m_mainThreadStackCount[m_tailPoint] = stackCount;
    
    // ================================================================
    // 2. 统计栈顶地址连续重复次数(核心!)
    // ================================================================
    
    // 计算上一个位置的索引
    uint64_t lastTailPoint = (m_tailPoint + m_cycleArrayCount - 1) % m_cycleArrayCount;
    
    // 获取上一个堆栈的栈顶地址
    uintptr_t lastTopStack = 0;
    if (m_mainThreadStackCycleArray[lastTailPoint] != NULL) {
        lastTopStack = m_mainThreadStackCycleArray[lastTailPoint][0];
    }
    
    // 获取当前堆栈的栈顶地址
    uintptr_t currentTopStackAddr = stackArray[0];
    
    // 比较栈顶地址
    if (lastTopStack == currentTopStackAddr) {
        // 栈顶地址相同,累加重复次数
        size_t lastRepeatCount = m_topStackAddressRepeatArray[lastTailPoint];
        m_topStackAddressRepeatArray[m_tailPoint] = lastRepeatCount + 1;
    } else {
        // 栈顶地址不同,重置重复次数
        m_topStackAddressRepeatArray[m_tailPoint] = 0;
    }
    
    // ================================================================
    // 3. 移动尾指针
    // ================================================================
    
    m_tailPoint = (m_tailPoint + 1) % m_cycleArrayCount;
    
    pthread_mutex_unlock(&m_threadLock);
}

栈顶地址重复次数统计示例

假设连续采集到以下堆栈(简化为栈顶地址):

时间: T0   T50  T100 T150 T200 T250 T300 T350 T400
索引:  0    1    2    3    4    5    6    7    8
堆栈: S0   S1   S2   S3   S4   S5   S6   S7   S8
栈顶: A    B    C    C    C    C    C    D    D

m_topStackAddressRepeatArray 的值:
     [0,   0,   0,   1,   2,   3,   4,   0,   1]
      ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑
      A    B    C   CCCCD   D重
     首次  首次 首次  复2345  首次  复2

分析:
- S6(索引6)的栈顶地址 C 连续重复了 4 次(从 S2S6- 说明主线程在函数 C 上停留了 5 × 50ms = 250ms
- S6 就是最有可能导致卡顿的堆栈(Point Stack

什么是 Point Stack?

Point Stack(关键堆栈) 是指在一个检查周期内,最有可能导致卡顿的主线程堆栈

一、核心数据结构

1. 循环数组配置

变量名 类型 说明
m_cycleArrayCount int 循环数组大小(例如:20)
m_tailPoint uint64_t 循环数组尾指针,指向下一个写入位置
pthread_mutex_t m_threadLock 线程锁,保护循环数组的并发访问

循环数组原理:

数组大小 = 检查周期 / 堆栈间隔
例如:1000ms / 50ms = 20

索引:    0    1    2    3    4    ...   19
       ┌────┬────┬────┬────┬────┬ ─ ─ ┬────┐
堆栈:   │ S0 │ S1 │ S2 │    │    │     │    │
       └────┴────┴────┴─▲──┴────┴ ─ ─ ┴────┘
                        │
                   m_tailPoint

当数组满时,从头开始覆盖(FIFO)

2. 堆栈存储数组(二维数组)

变量名 类型 维度 说明
m_mainThreadStackCycleArray uintptr_t ** [cycleArrayCount][stackDepth] 堆栈地址二维数组
m_mainThreadStackCount size_t * [cycleArrayCount] 每个堆栈的深度数组

数据结构示意:

m_mainThreadStackCycleArray:
  [0] → [0x1000, 0x2000, 0x3000, ...]  // 第0个堆栈,深度=3
  [1] → [0x1000, 0x2000, 0x3000, ...]  // 第1个堆栈,深度=3
  [2] → [0x1000, 0x2000, 0x4000, ...]  // 第2个堆栈,深度=3
  ...
  [19] → NULL                          // 尚未写入

m_mainThreadStackCount:
  [0] = 3   // 第0个堆栈深度
  [1] = 3   // 第1个堆栈深度
  [2] = 3   // 第2个堆栈深度
  ...
  [19] = 0  // 尚未写入

3. 栈顶重复次数数组

变量名 类型 说明
m_topStackAddressRepeatArray size_t * 每个堆栈的栈顶地址连续重复次数

用途: 找出 Point Stack(栈顶重复次数最多的堆栈)

数据示例:

假设连续采集的堆栈栈顶地址:
索引:    0      1      2      3      4
栈顶:    A      A      A      B      B

m_topStackAddressRepeatArray:
       [0]    [1]    [2]    [3]    [4]
       0      1      2      0      1

解释:
- 索引0: 第一次出现A,重复0- 索引1: 第二次出现A(与前一个相同),重复1- 索引2: 第三次出现A(与前一个相同),重复2- 索引3: 出现B(改变了),重复0- 索引4: 第二次出现B(与前一个相同),重复1次

结果:索引2的重复次数最多(2次),所以索引2Point Stack

4. Point Stack地址重复次数数组

变量名 类型 说明
m_mainThreadStackRepeatCountArray int * Point Stack中每个地址的总重复次数(动态分配)

用途: 统计 Point Stack 中每个地址在所有堆栈中的总出现次数,识别热点函数

数据示例:

假设有4个堆栈,Point Stack是索引2Stack 0:        Stack 1:        Stack 2(Point):  Stack 3:
0x1000          0x1000          0x1000          0x1000
0x2000          0x2000          0x2000          0x2000
0x3000          0x3000          0x3000          0x4000
0x4000          0x5000          0x6000

Point Stack (索引2) 的地址:
  [0] = 0x1000
  [1] = 0x2000
  [2] = 0x3000

统计结果 m_mainThreadStackRepeatCountArray:
  [0] = 4   // 0x1000 在4个堆栈中都出现
  [1] = 4   // 0x2000 在4个堆栈中都出现
  [2] = 3   // 0x3000 在3个堆栈中出现

符号化后:
  [0] main           (4次) ← 所有堆栈都有,基础函数
  [1] viewDidLoad    (4次) ← 所有堆栈都有,入口函数
  [2] heavyWork      (3次) ← 75%的时间在这里,瓶颈!⚠️

image.png

算法流程

总体流程图

开始
  ↓
1. 查找最大重复次数
  ↓
2. 按时间顺序找出第一个等于最大值的堆栈索引
  ↓
3. 复制 Point Stack
  ↓
4. 计算 Point Stack 中每个地址的总重复次数
  ↓
5. 创建 KSStackCursor 并返回
  ↓
结束

步骤 1:查找最大重复次数

目的: 找出 m_topStackAddressRepeatArray 中的最大值。

size_t maxValue = 0;
BOOL trueStack = NO;

// 第一次遍历:只找最大值(不记录索引)
for (int i = 0; i < m_cycleArrayCount; i++) {
    size_t currentValue = m_topStackAddressRepeatArray[i];
    if (currentValue >= maxValue) {
        maxValue = currentValue;
        trueStack = YES;
    }
}

if (!trueStack) {
    return NULL;  // 没有有效堆栈
}

步骤 2:找出 Point Stack 的索引

目的: 按时间顺序(从新到旧)找第一个重复次数等于 maxValue 的堆栈。

size_t currentIndex = (m_tailPoint + m_cycleArrayCount - 1) % m_cycleArrayCount;

// 第二次遍历:按时间顺序(从新到旧)
for (int i = 0; i < m_cycleArrayCount; i++) {
    // 计算真实索引
    int trueIndex = (m_tailPoint + m_cycleArrayCount - i - 1) % m_cycleArrayCount;
    
    // 找到第一个等于最大值的
    if (m_topStackAddressRepeatArray[trueIndex] == maxValue) {
        currentIndex = trueIndex;
        break;  // 找到最新的,立即停止
    }
}

索引计算公式:

trueIndex = (m_tailPoint + m_cycleArrayCount - i - 1) % m_cycleArrayCount

参数说明:
- m_tailPoint: 下一个要写入的位置
- i: 遍历变量(0 = 最新,1 = 次新,...)
- m_cycleArrayCount: 数组大小(如20)

例子:
假设 m_tailPoint = 1, m_cycleArrayCount = 5

i=0: trueIndex = (1+5-0-1) % 5 = 0  → 最新堆栈
i=1: trueIndex = (1+5-1-1) % 5 = 4  → 次新堆栈
i=2: trueIndex = (1+5-2-1) % 5 = 3  → 第三新堆栈
i=3: trueIndex = (1+5-3-1) % 5 = 2  → 第四新堆栈
i=4: trueIndex = (1+5-4-1) % 5 = 1  → 最旧堆栈(空)

步骤 3:复制 Point Stack

size_t stackCount = m_mainThreadStackCount[currentIndex];
size_t pointThreadSize = sizeof(uintptr_t) * stackCount;
uintptr_t *pointThreadStack = (uintptr_t *)malloc(pointThreadSize);

// 复制堆栈地址
for (size_t idx = 0; idx < stackCount; idx++) {
    pointThreadStack[idx] = m_mainThreadStackCycleArray[currentIndex][idx];
}

步骤 4:计算地址总重复次数

三层循环统计:

// 分配重复次数数组
m_mainThreadStackRepeatCountArray = (int *)malloc(stackCount * sizeof(int));
memset(m_mainThreadStackRepeatCountArray, 0, stackCount * sizeof(int));

// 外层循环:遍历 Point Stack 的每个地址
for (size_t i = 0; i < stackCount; i++) {
    uintptr_t targetAddress = m_mainThreadStackCycleArray[currentIndex][i];
    
    // 中层循环:遍历循环数组中的每个堆栈
    for (int innerIndex = 0; innerIndex < m_cycleArrayCount; innerIndex++) {
        size_t innerStackCount = m_mainThreadStackCount[innerIndex];
        
        // 内层循环:遍历当前堆栈的每个地址
        for (size_t idx = 0; idx < innerStackCount; idx++) {
            // 比较是否匹配
            if (targetAddress == m_mainThreadStackCycleArray[innerIndex][idx]) {
                m_mainThreadStackRepeatCountArray[i] += 1;
            }
        }
    }
}

算法分析:

  • 时间复杂度:O(n × m × k)
    • n = Point Stack 深度(通常 < 100)
    • m = 循环数组大小(通常 20)
    • k = 平均堆栈深度(通常 < 50)
  • 实际数据量很小,性能可接受

步骤 5:创建 KSStackCursor

KSStackCursor *pointCursor = (KSStackCursor *)malloc(sizeof(KSStackCursor));
kssc_initWithBacktrace(pointCursor, pointThreadStack, (int)stackCount, 0);
return pointCursor;

作用: 将原始堆栈数组包装成 KSCrash 能使用的标准格式。


至于堆栈的获取,可以参考我的另一篇文章ARM64 调用栈回溯原理

移动应用上架到应用商店的完整指南:原理与详细步骤

随着智能手机的普及,移动应用程序(App)已经成为人们日常生活中必不可少的一部分。而将自己的App上架到应用商店则是许多开发者的梦想,因为这意味着他们的作品可以被更多人看到、下载和使用。本文将介绍App上架到应用商店的原理和详细步骤。

一、App上架的原理

App上架到应用商店的原理可以简单概括为:开发者将开发好的App上传到应用商店,应用商店审核通过后将App发布到应用商店。在这个过程中,开发者需要遵守应用商店的规定和要求,以确保App能够通过审核并成功上架。

具体来说,开发者需要准备好以下内容:

  1. 应用商店账号:开发者需要在目标应用商店注册一个账号,并遵守该应用商店的规定和要求。

  2. App信息:开发者需要提供App的名称、描述、图标、版本号、支持的设备类型等信息。

  3. App安装包:开发者需要将App打包成符合应用商店要求的安装包,并上传到应用商店。对于iOS应用,可以使用AppUploader等工具在Windows、Linux或Mac系统中上传IPA文件到App Store,无需Mac电脑即可操作,比传统方法更高效。

  4. 证书和签名:开发者需要使用证书和签名对App进行加密和验证,以确保App的安全性和可靠性。使用工具如AppUploader可以简化iOS证书的申请和签名过程,支持多电脑协同,无需钥匙串助手。

  5. 测试和调试:开发者需要对App进行测试和调试,以确保App的质量和稳定性。

二、App上架的详细步骤

  1. 注册应用商店账号

开发者需要在目标应用商店注册一个账号,以便上传App和管理App的信息。不同的应用商店可能有不同的注册流程和要求,开发者需要仔细阅读应用商店的注册指南,并提供必要的信息和证明文件。

  1. 准备App信息

开发者需要准备好App的名称、描述、图标、版本号、支持的设备类型等信息。这些信息将在应用商店中展示,并影响用户对App的印象和选择。

  1. 打包App安装包

开发者需要将App打包成符合应用商店要求的安装包,并上传到应用商店。不同的应用商店可能有不同的安装包要求,开发者需要仔细阅读应用商店的指南,并使用合适的工具和方法进行打包。AppUploader支持快速上传IPA文件,并内置工具查看和编辑相关文件内容。

  1. 证书和签名

开发者需要使用证书和签名对App进行加密和验证,以确保App的安全性和可靠性。证书和签名的获取和使用也可能因应用商店的不同而有所差异,开发者需要仔细阅读应用商店的指南,并按照要求进行操作。利用AppUploader,开发者可以直接创建和管理iOS证书,简化流程。

  1. 测试和调试

开发者需要对App进行测试和调试,以确保App的质量和稳定性。测试和调试的过程可能会涉及多个设备和操作系统,开发者需要尽可能模拟用户的使用场景,并记录和解决问题。AppUploader提供USB和二维码安装测试功能,方便在iOS设备上验证应用。

  1. 提交审核

开发者需要将准备好的App信息、安装包、证书和签名上传到应用商店,并提交审核。审核的过程可能需要几天甚至几周的时间,开发者需要耐心等待,并及时响应应用商店的反馈和要求。

  1. 上架发布

审核通过后,应用商店会将App发布到应用商店,供用户下载和使用。开发者需要及时更新App的信息和版本,并处理用户的反馈和问题。

总之,将App上架到应用商店需要开发者投入大量时间和精力,需要遵守应用商店的规定和要求,并保证App的质量和安全性。只有经过认真准备和审核,才能让自己的App在应用商店中脱颖而出,成为用户喜爱的产品。

Xcode SPM 太慢/报错?代理 + 缓存修复

SPM 加速:终端代理

在终端执行(端口如 7890 按自己改):

export https_proxy=http://127.0.0.1:7890
export http_proxy=http://127.0.0.1:7890
cd /path/to/your/project
xcodebuild -resolvePackageDependencies

报 fatalError 时

错误里会带类似 FloatingPanel-f92b491a 的路径,删掉该缓存再重试:

rm -rf ~/Library/Caches/org.swift.swiftpm/repositories/FloatingPanel-f92b491a

多个包都报错就清空整个缓存:

rm -rf ~/Library/Caches/org.swift.swiftpm/repositories/*

然后重新执行 xcodebuild -resolvePackageDependencies

isa 指针、元类、继承链


一、isa 不只是一个指针

在 64 位设备上,指针只需要 36~40 位就能表示所有内存地址。苹果觉得剩下的位浪费了,于是把 isa 设计成了一个 union(联合体) ,把类指针和一堆标志位都塞进了这 64 位里。

这叫 Tagged Pointer / Non-pointer ISA 技术。


二、isa_t 的完整源码

// 文件:objc-private.h
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;         // 原始的 64 位值

private:
    Class cls;              // 类指针(只在 non-pointer isa 关闭时使用)

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;       // 展开后是一堆位域定义
    };
    ...
};

ISA_BITFIELD 展开(ARM64,iOS 真机)

// 这是 ARM64 的位域定义
uintptr_t nonpointer        : 1;   // bit 0
uintptr_t has_assoc         : 1;   // bit 1
uintptr_t has_cxx_dtor      : 1;   // bit 2
uintptr_t shiftcls          : 33;  // bit 3~35  ← 类指针在这里!
uintptr_t magic             : 6;   // bit 36~41
uintptr_t weakly_referenced : 1;   // bit 42
uintptr_t unused            : 1;   // bit 43
uintptr_t has_sidetable_rc  : 1;   // bit 44
uintptr_t extra_rc          : 19;  // bit 45~63

三、每一位的含义逐个解释

bit 0:nonpointer

uintptr_t nonpointer : 1;

含义: 这个 isa 是不是 "non-pointer isa"(优化过的 isa)。

  • 0:纯指针,整个 64 位就是类地址(老设备/某些特殊情况)
  • 1:non-pointer isa,64 位里藏了很多信息

现代 iOS 设备全是 1


bit 1:has_assoc

uintptr_t has_assoc : 1;

含义: 这个对象是否有关联对象(Associated Object)。

关联对象就是你用 objc_setAssociatedObject 给对象动态绑定的数据。

为什么需要这一位?

  • 对象 dealloc 时,runtime 需要清理关联对象
  • 用这一位做快速判断:has_assoc == 0 → 跳过关联对象清理,直接释放,更快

bit 2:has_cxx_dtor

uintptr_t has_cxx_dtor : 1;

含义: 这个类(或它的父类)是否有 C++ 析构函数,或者 OC 的 .cxx_destruct 方法。

.cxx_destruct 是编译器自动生成的方法,用来清理带有 __strong 修饰的成员变量(ARC 下自动 release)。

为什么需要这一位?

  • 对象 dealloc 时,如果没有需要清理的 C++ 对象,就跳过 .cxx_destruct 调用
  • 优化释放速度

bit 3~35:shiftcls(33位)

uintptr_t shiftcls : 33;

含义: 这 33 位才是真正的类指针(右移 3 位存储,取的时候左移 3 位还原)。

为什么只用 33 位?因为 ARM64 的内存对齐保证类地址的低 3 位永远是 0,可以省掉。

如何取出类指针?

// runtime 内部的取法
Class getClass() const {
    return (Class)(shiftcls << 3);  // 左移3位还原真实地址
}

bit 36~41:magic(6位)

uintptr_t magic : 6;

含义: 固定的魔数,值是 0b011010(十进制 26)。

用途: 调试用。当你看到一个 isa,如果 magic 值不对,说明这个对象已经被释放或内存被踩了(野指针)。Xcode 和 runtime 的断言会检查这个值。


bit 42:weakly_referenced

uintptr_t weakly_referenced : 1;

含义: 这个对象是否被弱引用__weak 指针)指向过。

为什么需要这一位?

  • 对象 dealloc 时,如果有弱引用指向它,需要去 SideTable(全局散列表)里把那些弱引用都清零(避免 dangling pointer)
  • 用这一位快速判断:weakly_referenced == 0 → 跳过 SideTable 查找,直接释放

bit 43:unused

uintptr_t unused : 1;

含义: 目前未使用,预留位。


bit 44:has_sidetable_rc

uintptr_t has_sidetable_rc : 1;

含义: 引用计数是否溢出到了 SideTable。

正常情况下,引用计数存在 isa 的 extra_rc 里(19位,最大能存 2^19 - 1 = 524287)。如果引用计数超过了这个值,has_sidetable_rc = 1,多出来的部分存在全局的 SideTable 里。


bit 45~63:extra_rc(19位)

uintptr_t extra_rc : 19;

含义: 存储对象的引用计数 - 1

为什么是减 1?因为对象存活时引用计数至少为 1,存 0 代表计数是 1,节省一点空间。

实际的引用计数 = extra_rc + 1(如果 has_sidetable_rc == 0)


四、如何取出 isa 里的类指针(实际代码)

// objc-object.h
inline Class objc_object::getIsa() {
    if (fastpath(!isTaggedPointer())) {
        return ISA();
    }
    // ... TaggedPointer 的特殊处理
}

inline Class objc_object::ISA(bool authenticated) {
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    // 某些架构用索引
    ...
#else
    // ARM64 主路径:取 shiftcls 位,左移3位还原地址
    return (Class)(isa.bits & ISA_MASK);
#endif
}

其中 ISA_MASK 在 ARM64 是 0x0000000ffffffff8ULL,作用就是取 bit 3~35。


五、元类(Metaclass)是什么?

这是 OC 最难理解的概念之一,但其实逻辑非常自洽。

问题的由来

在 OC 里,"一切皆对象"——包括类本身也是对象。

[NSString class]  // 这返回的是一个对象
[NSString stringWithString:@"hello"]  // 这是给"类对象"发消息

既然类也是对象,那类对象的 isa 指向哪里?

答案就是:元类(metaclass)

元类的定义

元类是"类的类"。它存储的是类方法+ 方法),就像普通类存储实例方法(- 方法)一样。

对比:类 vs 元类

普通类(Class) 元类(Metaclass)
本质 objc_class 结构体 也是 objc_class 结构体
方法列表里存的 实例方法(- 类方法(+
isa 指向 元类 根元类(NSObject 的元类)
superclass 指向 父类 父类的元类

六、完整的 isa + 继承链图

这是 OC 里最经典的一张图,一定要理解它:

                 isa                  isa               isa
实例对象(inst) --------→ 类(MyClass) --------→ 元类(Meta-MyClass) ──→ 根元类
                                                                          │
              superclass              superclass             superclass    │ isa(自指)
         MyClass ───────→ NSObject     Meta-MyClass ──────→ Meta-NSObject─┘
                               │                                  │
                               │ superclass = nil                 │ superclass
                               ↓                                  ↓
                             (nil)                             NSObject(不是元类!)

用文字描述:

  1. 实例对象.isaMyClass(类)
  2. MyClass.isaMeta-MyClass(元类)
  3. Meta-MyClass.isaMeta-NSObject(根元类)
  4. Meta-NSObject.isaMeta-NSObject自指!根元类的 isa 指向自己

继承链:

  1. MyClass.superclassNSObject
  2. NSObject.superclassnil
  3. Meta-MyClass.superclassMeta-NSObject(元类也有继承链)
  4. Meta-NSObject.superclassNSObject元类继承链的终点是 NSObject 类,不是 nil!

七、为什么元类的继承链终点是 NSObject?

这个设计让你可以在任何类方法里调用 NSObject 的实例方法(比如 respondsToSelector:)。

// 这为什么能工作?
[MyClass respondsToSelector:@selector(doSomething)];

+respondsToSelector: 是 NSObject 的实例方法(- 方法),存在 NSObject 类里。
当你给 MyClass 发这个消息,runtime 查找路径:

Meta-MyClass(没有)
    → Meta-NSObject(没有)
        → NSObject(在这找到了!)

因为 Meta-NSObject.superclass = NSObject,所以元类链最终能访问到 NSObject 的实例方法。优雅!


八、TaggedPointer:特殊的对象

不是所有"对象"都是真正的对象(有 isa 的结构体)。

什么是 TaggedPointer?

对于一些小值对象(比如 NSNumberNSDate、小字符串),苹果直接把值编码进指针本身,不分配堆内存。

NSNumber *num = @42;
// 在 64 位下,这个指针可能长这样:
// 0xb000000000000162  (不是真实的堆地址!)
// 最高位 1 = TaggedPointer 标志
// 低位存了 42 这个值

判断是否是 TaggedPointer

static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
// ARM64: _OBJC_TAG_MASK = (1UL<<63),最高位为1就是 TaggedPointer

TaggedPointer 的好处

  • 不需要堆分配:直接在指针里存值,alloc 时不走 malloc
  • 不需要引用计数:也不需要 release,直接丢弃
  • 更快:少了内存分配和释放的开销

九、SideTable:引用计数和弱引用的大本营

当 isa 的 extra_rc 不够用,或者有弱引用时,数据存在 SideTable 里。

struct SideTable {
    spinlock_t slock;           // 自旋锁,保证线程安全
    RefcountMap refcnts;        // 引用计数表(散列表)
    weak_table_t weak_table;    // 弱引用表
};

全局有 8 个(或 64 个)SideTable,通过对象地址取模来分配,减少锁竞争。

weak_table_t 弱引用表

struct weak_table_t {
    weak_entry_t *weak_entries;  // 弱引用条目数组
    size_t num_entries;
    ...
};

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;  // 被指向的对象
    // 指向该对象的所有 __weak 指针地址的集合
    union {
        struct { weak_referrer_t *referrers; ... };
        struct { weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; };
    };
};

__weak 置零的过程

对象 dealloc
    ↓
检查 isa.weakly_referenced
    ↓(== 1)
去 SideTable 找 weak_entry_t
    ↓
遍历所有指向该对象的 __weak 指针
    ↓
全部置 nil
    ↓
从 weak_table 删除该条目

这就是为什么 __weak 指针在对象释放后自动变成 nil,而不会变成野指针——runtime 帮你清零了。


十、小结

概念 本质 存在哪里
isa 64位 union,含类指针+引用计数+标志位 每个对象的第一个字段
元类 存类方法的 objc_class 全局静态区
TaggedPointer 值直接编码进指针,无堆对象 栈/寄存器
extra_rc 引用计数(-1)的快速存储 isa 的高19位
SideTable 溢出引用计数 + 弱引用表 全局散列表

下一篇:延伸问题 Q&A——消息发送、方法查找、Swizzle、dealloc 全流程等

objc_class 结构体逐行解析

前言

objc_class 开始,是因为它是整个 Runtime 的基础数据结构。Runtime 管的事很多——消息发送、方法查找、内存管理、Category 加载……但这些行为最终都要落在"类长什么样"上面。搞清楚 objc_class,后面的东西才能接得上。

一、源码全貌(先看完整结构)

下面是从 Apple 开源的 objc4 里提取的核心结构体,我做了适度精简,保留所有关键字段。

建议先整体扫一遍,有个印象,后面逐个解释。

// ============================================================
// 文件:objc-runtime-new.h(objc4-818.2)
// 源码地址:https://opensource.apple.com/source/objc4/
// ============================================================

// -------------------- 1. objc_object --------------------
// 所有 OC 对象的基类,只有一个字段:isa
struct objc_object {
private:
    isa_t isa;  // 64位,包含类指针+引用计数+标志位

public:
    Class ISA(bool authenticated = false);
    Class getIsa();
    // ... 省略其他方法
};


// -------------------- 2. objc_class --------------------
// 这就是"类"的底层结构,继承自 objc_object
struct objc_class : objc_object {
    // 注意:isa 字段继承自 objc_object,这里不重复写
    
    Class superclass;           // 父类指针
    cache_t cache;              // 方法缓存(哈希表)
    class_data_bits_t bits;     // 指向 class_rw_t 的指针+标志位

    // 取出真正的数据
    class_rw_t *data() const {
        return bits.data();
    }
    // ... 省略其他方法
};


// -------------------- 3. class_data_bits_t --------------------
// 这是 objc_class.bits 的类型,用来存储指向 class_rw_t 的指针 + 几个标志位
struct class_data_bits_t {
private:
    uintptr_t bits;   // 就是一个 64 位整数,低位藏标志位,高位存指针

public:
    // 用掩码取出真正的 class_rw_t 指针
    class_rw_t *data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

    // 各种标志位的读取方法
    bool isSwiftLegacy() const {
        return getBit(FAST_IS_SWIFT_LEGACY);
    }
    bool isSwiftStable() const {
        return getBit(FAST_IS_SWIFT_STABLE);
    }
    // ... 其他方法
};

// ARM64 下的掩码和标志位定义:
// FAST_DATA_MASK      = 0x00007ffffffffff8UL  (取 bit 3~46,即真正的指针)
// FAST_IS_SWIFT_LEGACY = 1 << 0  (bit 0: 是否是旧版 Swift 类)
// FAST_IS_SWIFT_STABLE = 1 << 1  (bit 1: 是否是新版 Swift 类)
// FAST_HAS_DEFAULT_RR  = 1 << 2  (bit 2: 是否有默认的 retain/release)


// -------------------- 4. cache_t --------------------
// 方法缓存,加速方法查找
struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;  // 桶数组地址
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;   // 桶数量-1(用于哈希取模)
            uint16_t                   _flags;
            uint16_t                   _occupied;    // 已使用的桶数
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
public:
    // ... 省略查找、插入方法
};

// 单个缓存桶
struct bucket_t {
private:
    explicit_atomic<SEL> _sel;       // 方法名(选择子)
    explicit_atomic<uintptr_t> _imp; // 函数指针(方法实现地址)
};


// -------------------- 5. class_rw_t --------------------
// 运行时可读写数据(Category 方法会合并到这里)
struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
    uint16_t index;

    explicit_atomic<uintptr_t> ro_or_rw_ext;  // 指向 class_ro_t 或扩展数据

    Class firstSubclass;       // 第一个子类
    Class nextSiblingClass;    // 兄弟类(形成链表)

    // 获取方法/属性/协议列表
    const method_array_t methods() const;
    const property_array_t properties() const;
    const protocol_array_t protocols() const;

    // 获取只读数据
    const class_ro_t *ro() const;
};


// -------------------- 6. class_ro_t --------------------
// 编译期只读数据(源码里写死的方法、变量、属性)
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;      // 实例变量起始偏移
    uint32_t instanceSize;       // sizeof(实例),对象占多少字节

    const uint8_t * ivarLayout;  // 强引用 ivar 的内存布局

    const char * name;           // 类名字符串,如 "NSString"
    
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;  // 方法列表
    protocol_list_t * baseProtocols;     // 协议列表
    const ivar_list_t * ivars;           // 实例变量列表
    
    const uint8_t * weakIvarLayout;      // 弱引用 ivar 的内存布局
    property_list_t *baseProperties;     // 属性列表
};


// -------------------- 7. method_t --------------------
// 单个方法的描述
struct method_t {
    SEL name;              // 方法名(选择子),本质是 const char *
    const char *types;     // 类型编码,如 "v16@0:8"
    IMP imp;               // 函数指针(真正的代码地址)
};


// -------------------- 8. ivar_t --------------------
// 单个实例变量的描述
struct ivar_t {
    int32_t *offset;       // 偏移量指针(Non-Fragile ABI 用)
    const char *name;      // 变量名,如 "_name"
    const char *type;      // 类型编码,如 "@"NSString""
    uint32_t alignment_raw;// 对齐方式
    uint32_t size;         // 占多少字节
};


// -------------------- 9. isa_t --------------------
// isa 的真正定义(union,64位里塞了很多信息)
union isa_t {
    uintptr_t bits;        // 原始64位值

    // ARM64 位域展开(iOS 真机):
    struct {
        uintptr_t nonpointer        : 1;   // bit 0:  是否是优化过的 isa
        uintptr_t has_assoc         : 1;   // bit 1:  有关联对象?
        uintptr_t has_cxx_dtor      : 1;   // bit 2:  有 C++ 析构?
        uintptr_t shiftcls          : 33;  // bit 3-35:  类指针(右移3位存储)
        uintptr_t magic             : 6;   // bit 36-41: 固定值 0x1a,调试用
        uintptr_t weakly_referenced : 1;   // bit 42: 被弱引用?
        uintptr_t unused            : 1;   // bit 43: 未使用
        uintptr_t has_sidetable_rc  : 1;   // bit 44: 引用计数溢出到 SideTable?
        uintptr_t extra_rc          : 19;  // bit 45-63: 引用计数-1
    };
};

二、结构关系图

objc_class(一个类在内存里的样子)
┌─────────────────────────────────────┐
│  isa (继承自 objc_object)           │ ← isa_t union,64位
├─────────────────────────────────────┤
│  superclass                         │ ← 指向父类的 objc_class
├─────────────────────────────────────┤
│  cache                              │ ← cache_t 结构体
│    └── bucket_t[] 数组              │     每个桶存 { SEL, IMP }
├─────────────────────────────────────┤
│  bits                               │ ← class_data_bits_t(指针+标志位)
│    └── data() ───────────────────────────→ class_rw_t(运行时可写)
│                                     │        ├── methods()
│                                     │        ├── properties()
│                                     │        ├── protocols()
│                                     │        └── ro() ────────→ class_ro_t(只读)
│                                     │                             ├── name
│                                     │                             ├── baseMethods
│                                     │                             │     └── method_t[]
│                                     │                             ├── ivars
│                                     │                             │     └── ivar_t[]
│                                     │                             └── baseProperties
└─────────────────────────────────────┘

三、逐结构体解析

接下来按源码出现的顺序,逐个讲解每个结构体、每个字段的含义。


3.1 objc_object —— 所有对象的祖宗

struct objc_object {
private:
    isa_t isa;
};

这是什么?

这是 OC 里所有对象的底层表示。不管是 NSStringUIView、还是你自定义的 MyClass 实例,底层都是 objc_object

字段解析

字段 类型 含义
isa isa_t "is a" 的缩写,标识"这个对象是什么类型"。是一个 64 位的 union,里面藏了类指针 + 引用计数 + 各种标志位。isa_t 的详细结构会在第二篇展开讲解。

为什么只有一个字段?

因为 objc_object最小公共祖先。每个对象只需要知道"我是什么类型"(isa),其他的成员变量由具体的类定义,紧跟在 isa 后面存储。

内存布局示意

一个 MyClass 实例的内存:
┌────────────────┐ ← 对象起始地址
│     isa        │   8 字节(objc_object 的字段)
├────────────────┤
│    _name       │   8 字节(MyClass 自己的 ivar)
├────────────────┤
│    _age        │   4 字节(MyClass 自己的 ivar)
└────────────────┘

3.2 objc_class —— 类的完整定义

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;

    class_rw_t *data() const {
        return bits.data();
    }
};

这是什么?

这是 OC 里的底层表示。每个 @interface MyClass 在运行时都对应一个 objc_class 结构体实例。

注意它继承自 objc_object,所以"类也是对象"——类对象有自己的 isa(指向元类)。

字段逐个解析

字段 类型 含义
isa isa_t(继承来的) 类对象的 isa 指向它的元类(metaclass)。isa_t 的详细结构见第二篇。
superclass Class 父类指针。Classobjc_class * 的 typedef,即指向另一个 objc_class 的指针。NSObject 的 superclass 是 nil。;
cache cache_t 方法缓存,哈希表结构。最近调用的方法会缓存在这里,加速后续调用。
bits class_data_bits_t 一个 64 位整数,低 3 位是标志位,高位是 class_rw_t 指针

Class 是什么类型?

// objc.h
typedef struct objc_class *Class;

Class 就是 objc_class * 的别名,一个指向类对象的指针。你代码里写的所有 Class 都只是这个指针,没有额外结构:

Class cls = [MyClass class];       // 拿到 MyClass 的 objc_class * 指针
Class superCls = [cls superclass]; // 拿到父类的 objc_class * 指针

同理,id 也是:

typedef struct objc_object *id;    // id = objc_object *,指向任意实例对象

superclass 有什么用?

实现继承。当在当前类找不到方法时,runtime 会沿着 superclass 链往上找。

调用 [myObj doSomething]
    ↓
在 MyClass 的方法列表里找
    ↓ 找不到
通过 superclass 到 NSObject 里找
    ↓ 还找不到
触发消息转发

3.3 class_data_bits_t —— 指针 + 标志位的混合体

struct class_data_bits_t {
private:
    uintptr_t bits;   // 64 位整数

public:
    class_rw_t *data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

这是什么?

就是 objc_class.bits 的类型。它不是简单的指针,而是把 class_rw_t 指针几个标志位 打包进同一个 64 位整数里。

为什么能这样做?

因为 class_rw_t 在内存里是 8 字节对齐的,所以它的地址的低 3 位永远是 000。苹果就把这 3 位拿来存标志位,不浪费。

64 位的布局

class_data_bits_t.bits(64位)

 63                                3  2  1  0
┌────────────────────────────────┬──┬──┬──┐
│     class_rw_t 指针 (bit 3~63) │ 210│
└────────────────────────────────┴──┴──┴──┘
                                   │  │  │
                                   │  │  └─ FAST_IS_SWIFT_LEGACY (是旧版Swift类?)
                                   │  └──── FAST_IS_SWIFT_STABLE (是新版Swift类?)
                                   └─────── FAST_HAS_DEFAULT_RR  (有默认retain/release?)

取指针的掩码

// ARM64
#define FAST_DATA_MASK 0x00007ffffffffff8UL

// 二进制:...11111111111111111111111111111111111111000
// 作用:与运算后,低 3 位清零,剩下的就是真正的 class_rw_t 地址

data() 方法做了什么?

class_rw_t *data() const {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
    // bits & 掩码 → 把低 3 位标志位清掉 → 得到纯净的 class_rw_t 指针
}

一句话总结

class_data_bits_tisa_t 的设计思路一样——充分利用内存对齐带来的空闲位,一个 64 位整数里塞多种信息,省内存


3.4 cache_t —— 方法缓存

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t> _maybeMask;
            uint16_t _flags;
            uint16_t _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
};

struct bucket_t {
private:
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
};

为什么需要缓存?

每次调用方法都去 class_rw_t 的方法列表里遍历查找,太慢了。cache_t 是一个哈希表,把最近调用过的方法缓存起来。

字段解析

字段 含义
_bucketsAndMaybeMask 哈希桶数组的起始地址
_maybeMask 桶数量 - 1,用于哈希取模(hash & mask
_occupied 当前已使用的桶数量

bucket_t 是什么?

单个缓存条目,存储 SEL(方法名)和 IMP(函数指针)的映射。

字段 类型 含义
_sel SEL 方法选择子(方法名),如 @selector(viewDidLoad)
_imp uintptr_t 方法实现的函数地址

查找流程

[obj doSomething]
    ↓
计算 @selector(doSomething) 的哈希值
    ↓
hash & _maybeMask → 得到桶的索引
    ↓
取出 bucket_t,比较 _sel 是否等于 @selector(doSomething)
    ↓
相等 → 直接调用 _imp,结束(命中缓存,极快)
不相等 → 去 class_rw_t 里慢速查找

缓存什么时候会失效?

  • 调用 method_exchangeImplementations(Method Swizzle)后
  • 动态添加方法后
  • 类第一次加载时

失效时 runtime 会调用 flushCaches() 清空缓存。


3.5 class_rw_t —— 运行时可读写数据

struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
    uint16_t index;

    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;

    const method_array_t methods() const;
    const property_array_t properties() const;
    const protocol_array_t protocols() const;
    const class_ro_t *ro() const;
};

这是什么?

rw = read-write(可读写)。这里存放运行时可以修改的数据,比如 Category 添加的方法会合并到这里。

字段解析

字段 含义
flags 各种标志位(是否已初始化、是否有 C++ 构造函数等)
ro_or_rw_ext 指向 class_ro_t(只读数据),或扩展数据
firstSubclass 指向第一个子类,形成子类链表
nextSiblingClass 指向下一个兄弟类(同一个父类的其他子类)

获取方法/属性/协议

const method_array_t methods() const;     // 返回方法列表(含 Category 方法)
const property_array_t properties() const; // 返回属性列表
const protocol_array_t protocols() const;  // 返回协议列表

这些方法返回的是合并后的列表——源码里写的 + Category 加进来的。

ro() 方法

返回 class_ro_t 指针,取出编译期确定的只读数据。


3.6 class_ro_t —— 编译期只读数据

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;

    const uint8_t * ivarLayout;
    const char * name;
    
    WrappedPtr<method_list_t, ...> baseMethods;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

这是什么?

ro = read-only(只读)。这里存放编译时就确定的数据,运行时不能修改。

字段逐个解析

字段 类型 含义
flags uint32_t 标志位
instanceStart uint32_t 实例变量在对象内存中的起始偏移(通常是 8,跳过 isa)
instanceSize uint32_t 一个实例对象占多少字节(sizeof
ivarLayout const uint8_t * 描述哪些 ivar 是强引用(ARC 用)
name const char * 类名字符串,如 "UIViewController"
baseMethods method_list_t * 源码里定义的方法列表(不含 Category)
baseProtocols protocol_list_t * 源码里遵循的协议列表
ivars ivar_list_t * 实例变量列表
weakIvarLayout const uint8_t * 描述哪些 ivar 是弱引用
baseProperties property_list_t * 源码里定义的属性列表

class_ro_t vs class_rw_t 对比

class_ro_t class_rw_t
全称 read-only read-write
什么时候确定 编译期(写进 Mach-O 二进制文件) 运行时(启动时构造)
能修改吗 ❌ 不能 ✅ 能
存什么 源码里写死的方法、属性、变量 动态添加的方法、Category 合并的方法

为什么要分两层?

因为 Category 是运行时加载的。编译期不知道会有哪些 Category,所以:

  1. 编译期:把源码里写的方法存进 class_ro_t
  2. 运行时:遍历所有 Category,把它们的方法合并class_rw_t

查找方法时,先查 class_rw_t(含 Category),它内部会访问 class_ro_t


3.7 method_t —— 单个方法

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

字段解析

字段 类型 含义 例子
name SEL 方法选择子(方法名) @selector(viewDidLoad)
types const char * 类型编码(返回值+参数的类型) "v16@0:8"
imp IMP 函数指针,指向方法的真正实现 0x100001234(代码段地址)

SEL 是什么?

typedef struct objc_selector *SEL;

本质是一个唯一化的 C 字符串。同名方法在整个程序里 SEL 值相同(指针相等),所以比较方法名只需要比较指针,极快。

SEL sel1 = @selector(doSomething);
SEL sel2 = @selector(doSomething);
// sel1 == sel2(指针相等,不是字符串比较)

IMP 是什么?

typedef void (*IMP)(id, SEL, ...);

函数指针,前两个参数固定是:

  • id self:消息接收者
  • SEL _cmd:方法选择子

这解释了为什么 OC 方法里能直接用 self_cmd——它们是函数的隐藏参数。

// 你写的:
- (void)doSomething {
    NSLog(@"%@", self);
}

// 编译器眼里的:
void doSomething(id self, SEL _cmd) {
    NSLog(@"%@", self);
}

types 字符串怎么读?

- (NSString *)nameWithPrefix:(NSString *)prefix 为例,types 是 @24@0:8@16

@    → 返回值是 id(对象)
24   → 所有参数总共占 24 字节
@    → 第1个参数是 id(self)
0    → 从第 0 字节开始
:    → 第2个参数是 SEL(_cmd)
8    → 从第 8 字节开始
@    → 第3个参数是 id(prefix)
16   → 从第 16 字节开始

这套编码叫 Type Encoding,runtime 靠它做方法签名校验。


3.8 ivar_t —— 单个实例变量

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

字段解析

字段 类型 含义 例子
offset int32_t * 偏移量的指针(不是值!) 指向存储偏移量的内存
name const char * 变量名 "_name"
type const char * 类型编码 "@"NSString""
alignment_raw uint32_t 内存对齐方式 通常是 3(2^3 = 8 字节对齐)
size uint32_t 占多少字节 指针占 8 字节

为什么 offset 是指针而不是值?

这是 Non-Fragile ABI(非脆弱 ABI)的设计。

假设父类 NSObject 有 8 字节的 isa,子类 MyClass_name 变量在 offset 8。

如果 Apple 在新系统里给 NSObject 加了一个成员变量(变成 16 字节),按老 ABI,MyClass_name 还在 offset 8,就会和 NSObject 新增的变量重叠——程序崩溃。

Non-Fragile ABI 的解决方案:

  1. offset 是指针,不是值
  2. App 启动时,runtime 检查父类大小是否变化
  3. 如果变化了,自动调整所有子类 ivar 的 offset 值
  4. 子类不需要重新编译
旧系统:NSObject 8字节,MyClass._name 在 offset 8
    ↓ Apple 升级系统
新系统:NSObject 16字节
    ↓ runtime 自动修正
MyClass._name 的 offset 从 8 改成 16

访问 ivar 的过程

// 伪代码
id value = *(id *)((char *)obj + *ivar->offset);
// 1. 取出 offset 指针指向的偏移值
// 2. 对象地址 + 偏移值 = ivar 的内存地址
// 3. 解引用得到 ivar 的值

3.9 isa_t —— 64 位里藏了很多东西

union isa_t {
    uintptr_t bits;

    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33;
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t unused            : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
};

为什么不直接存指针?

在 64 位系统上,指针只需要约 40 位就能表示所有内存地址。剩下的位"浪费"了,苹果就把引用计数和各种标志位塞进去,省内存。

这叫 Non-pointer ISA(优化过的 isa)。

每一位的含义

位域 位数 含义
nonpointer 1 是否是优化过的 isa(现代设备都是 1)
has_assoc 1 对象是否有关联对象(objc_setAssociatedObject
has_cxx_dtor 1 是否有 C++ 析构函数或 ARC 的 .cxx_destruct
shiftcls 33 类指针(右移 3 位存储,取时左移还原)
magic 6 固定值 0x1a,调试用(值不对说明内存被踩了)
weakly_referenced 1 是否被 __weak 指针指向过
unused 1 未使用,预留
has_sidetable_rc 1 引用计数是否溢出到 SideTable
extra_rc 19 存储引用计数 - 1(最大 2^19 - 1 = 524287)

如何取出类指针?

Class cls = (Class)(isa.bits & ISA_MASK);
// ISA_MASK = 0x0000000ffffffff8ULL
// 掩码取出 bit 3~35,然后隐含左移还原

四、完整内存布局示意

把所有结构体串起来,一个类在内存里长这样:

objc_class 实例(代表 MyClass 这个类)
┌─────────────────────────────────────────────────────┐
│  isa (64位 isa_t)                                   │ → 指向 Meta-MyClass(元类)
├─────────────────────────────────────────────────────┤
│  superclass (8字节)                                 │ → 指向 NSObject
├─────────────────────────────────────────────────────┤
│  cache (cache_t)                                    │
│    _bucketsAndMaybeMask → [ bucket_t, bucket_t... ] │ 每个桶: { SEL, IMP }
│    _maybeMask = N-1                                 │
│    _occupied = 已用桶数                              │
├─────────────────────────────────────────────────────┤
│  bits (class_data_bits_t)                           │ ← 低3位是标志位,高位是指针
│    data() ──────────────────────────────────────────│──→ class_rw_t
│                                                     │      ├── methods()   → [method_t, ...]
│                                                     │      ├── properties()→ [property_t, ...]
│                                                     │      ├── protocols() → [protocol_t, ...]
│                                                     │      └── ro() ───────→ class_ro_t
│                                                     │              ├── name = "MyClass"
│                                                     │              ├── instanceSize = 24
│                                                     │              ├── baseMethods
│                                                     │              │     ├── method_t { SEL, types, IMP }
│                                                     │              │     └── method_t { ... }
│                                                     │              └── ivars
│                                                     │                    ├── ivar_t { offset*, "_name", "@", 3, 8 }
│                                                     │                    └── ivar_t { offset*, "_age",  "i", 2, 4 }
└─────────────────────────────────────────────────────┘

五、小结

结构体 可否运行时修改 存放什么
objc_class 不直接改 类的容器,持有 superclass/cache/bits
isa_t 部分可改(引用计数位) 类指针 + 引用计数 + 标志位,全塞在 64 位里
class_data_bits_t 不直接改 class_rw_t 指针 + 3 个标志位,又一个"指针+标志"混合体
cache_t 是(每次调用方法后更新) 最近调用的方法 SEL → IMP 映射
class_rw_t 运行时合并后的方法、属性、协议
class_ro_t 编译期确定的方法、变量、属性,写死在二进制里
method_t IMP 可以换(Swizzle) 一个方法的名字、类型编码、实现地址
ivar_t offset 可改(Non-Fragile ABI) 一个实例变量的名字、类型、偏移量

下一篇:isa 指针深度解析、元类体系、完整继承链图

50 岁的苹果和 51 岁的我 -- 肘子的 Swift 周报 #127

issue127.webp

50 岁的苹果和 51 岁的我

再有不到半个月,Apple 将迎来 50 岁生日。Tim Cook 也发表了一篇短文,致敬过去半个世纪的历程。不过,由于苹果一直以来始终引领潮流的形象,很多人并没有意识到它已经是 IT 产业中名副其实的元老。与它年龄相当的 IT 巨头,如今仍留在一线牌桌上的寥寥无几。

作为一个只比苹果大一岁的科技爱好者,从 Apple II 到如今的 iPhone、MacBook,苹果的产品几乎伴随我走过了大半人生。严格来说,我并不算真正的果粉——不会因为没能第一时间买到新品而遗憾,也几乎不再熬夜看发布会,更说不出新产品的具体参数。但回顾过去,在每一个人生节点上,我都会很自然地选择苹果的产品,并在近几年成为了苹果开发生态中的一员。

其实我也没有完全想明白,苹果对我持久的吸引力究竟来自哪里。是因为很早就开始使用它的产品?是它的创新、体验和气质?还是 Jobs 的人格魅力?说实话,如今的选择已经完全出于习惯和本能,就像老友间的默契,早已不需要什么特别的理由。

当然,苹果的成长之路并非一帆风顺,其间也有过低谷。但有一点必须承认:它在过去 50 年间的企业定位几乎没变——为个人和社会创造强大的工具。即便在最新一轮 AI 浪潮中,苹果看似失去了先机,但作为连接人与数字世界的“最后一厘米”的核心参与者,它仍然具备在 AI 时代留在牌桌中央的资本。毕竟,我们生活在物质世界中,需要实打实的硬件设备和个人化服务来享受技术进步的成果。

50 岁的苹果或许能给更多企业带来启示:与其模仿它“炫酷”、“创新”的外表,不如学习它的专注与坚持。成为与用户长久互相陪伴的伙伴,或许才是它成功的真正密码。

大概率再过十年,当苹果 60 岁、我 61 岁的时候,我仍然用着一台苹果电脑。

生日快乐,苹果!

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

原创

2026 年,为什么我仍在思考 Core Data

到 2026 年,Core Data 已经问世 21 年,尽管仍有不少开发者在使用它,但在今天的 Swift 项目里,它越来越像个“时代遗留”。并发得靠 perform 一层层套,模型声明堆满样板代码,字符串谓词随时等你踩坑。这篇文章不是要为 Core Data 辩护,也不是要说服新的开发者回到 Core Data。它更像是一篇问题整理:在 2026 年,为什么仍有人坚持使用 Core Data;而如果要继续使用它,我们今天真正需要解决的问题又是什么。

近期推荐

原生 AI 聊天应用 — 极速、隐私优先、100+ 专业功能

一个原生应用,100+ AI 模型,支持 Mac、iOS 和 Android。极速响应、键盘驱动、非 Electron。使用码 FATBOBMAN25 立享 25% OFF。


苹果工程师谈应用安全与内存保护 (Fortify Your App: Essential Strategies to Strengthen Security Q&A)

在苹果开发者中心举办的一场安全专题活动中,多位苹果工程师围绕应用安全与内存安全进行了近六小时的分享与问答,内容涵盖现代应用面临的安全挑战,以及 Apple 平台提供的一系列防护技术。Anton Gubarenko 将这场活动中的大量开发者问答整理成文,讨论了第三方库安全评估、UserDefaults 与 plist 数据存储的风险、Keychain 与文件保护策略、Swift unsafe API 的使用边界,以及如何在 Xcode 中启用 Enhanced Security 等能力。对于希望了解 Apple 平台安全机制与实践建议的开发者来说,这是一份信息密度很高的问答整理,其中包含不少来自苹果工程师的一手信息。


用 CLI 与 MCP 自动化配置 iOS 订阅 (Faster iOS Subscriptions with ASC CLI and RevenueCat MCP)

为应用添加订阅功能本身并不复杂,但在 App Store Connect 与 RevenueCat 两个后台之间来回配置,过程往往相当繁琐。Rudrank Riyam 介绍了一种更高效的做法:使用 ASC CLI 在终端中一次性创建订阅产品,再让 AI 代理通过 RevenueCat 的 MCP Server 自动完成 entitlements、offerings 与 paywall 的配置,从而将原本依赖控制台点击的流程迁移到 CLI + AI Agent 的自动化工作流中。


JetBrains 面向 Swift 开发者的调查 (JetBrains Swift Developers Survey)

JetBrains 最近发布了一份面向 Swift 开发者的调研问卷,邀请开发者分享当前使用的开发工具、工作流程以及在 Swift 生态中的痛点。尽管官方并未说明调研的具体用途,但社区中已经出现不少猜测:这项调查可能与 JetBrains 重新评估 Swift 开发工具支持有关。

在 JetBrains 于 2022 年宣布停止维护 AppCode 之后,Swift 开发者基本回到了以 Xcode 为核心的工具链。此次调研也引发了一些讨论——有人期待 JetBrains 重新探索 Swift tooling 的可能性,也有人认为这更可能与 Kotlin Multiplatform 或 Swift 构建工具链相关。如果你对 Swift 开发工具生态的未来方向感兴趣,不妨参与这份调查。


不依赖编译器识别 Swift Protocol 的方法 (How Well Can You Detect a Swift Protocol Without the Compiler?)

在 Swift 项目中,Protocol 几乎无处不在,但如果不依赖编译器或完整构建环境,仅通过源码文本判断一个文件是否定义或使用了协议,结果会有多可靠?Xiangyu Sun 在这篇文章中系统评估了多种检测策略,例如使用 SourceKit/LSP、SwiftSyntax AST、关键字正则匹配,以及通过 extension Foo: Barany / some 等语法信号进行启发式判断,并对这些方法的准确率与适用场景进行了比较。

文章最有意思的部分在于作者发现:简单的命名约定可以显著提升静态分析效果。如果团队统一使用 *Protocol 后缀命名协议类型(如 PaymentServiceProtocol),很多原本存在歧义的检测方法都会变得更加可靠。作者还进一步讨论了这种约定在 AI 辅助开发中的价值:通过在文件级别预分类协议文件,可以在向 LLM 提供上下文时显著减少 token 消耗,并提高分析效率,这是一个颇具启发性的视角。


迁移到 Swift Concurrency 前需要注意的细节 (What you should know before Migrating from GCD to Swift Concurrency)

从 GCD 迁移到 Swift Concurrency 并非简单的语法替换。在这篇文章中,Soumya Ranjan Mahunt 指出:Swift Concurrency 在任务调度、执行顺序以及并发语义上与 GCD 存在一些关键差异,例如 Task 的调度并不保证与 GCD 相同的 FIFO 执行顺序,而 actor 也并不是 DispatchQueue 的直接替代,其执行行为可能受到任务优先级和调度策略的影响。此外,文中还讨论了一些在实际迁移过程中容易被忽视的问题,例如 DispatchGroup 在 Swift Concurrency 中并没有完全等价的 API,以及在旧系统版本中使用 assumeIsolated 可能遇到的兼容性问题。


选择 AI Agent Skill 的九步框架 (A 9-Step Framework for Choosing the Right Agent Skill)

随着 AI Agent 在开发工作流中的应用越来越广泛,如何为 Agent 设计合适的“技能”(Skill / Tool)也逐渐成为一个新的工程问题。Antoine van der Lee 提出了一个用于判断何时应该为 Agent 创建技能的九步框架,帮助开发者在自动化能力、可维护性以及系统复杂度之间取得平衡。Antoine 指出,并非所有任务都适合直接交给 LLM,也并非所有能力都需要实现为 Agent 工具。文章从任务确定性、执行成本、可复用性以及安全性等角度出发,提供了一套相对系统的评估思路。

工具

DataStoreKit

这是一个很有意思的开源项目,由 Anferne Pineda 开发。它基于 SwiftData 的自定义 store 能力,在保留 SwiftData 上层开发体验的同时,重新实现了一套面向 SQLite 的底层存储后端,包括从 SwiftData 模型、谓词到 SQLite schema、SQL、快照与持久化历史的映射和执行。

DataStoreKit 提供了一些值得关注的特性,例如支持对数组、字典等集合类型数据进行谓词查询,底层以 JSON 形式映射到 SQLite;同时也提供了 SQL 直通能力,让开发者在 #Predicate 之外,能够直接利用 SQLite 的能力完成查询或维护操作。

这是目前为数不多、且实现深度较高的 SwiftData DataStore 自定义实践,展示了 SwiftData 作为数据表现层而非完整持久化引擎的另一种可能性。项目目前仍处于较早期阶段,API 和能力边界可能还会继续调整,但已经非常值得持续关注。


Playwright for Swift

Miguel Piedrafita 开发的 swift-playwright,将 Playwright 这套成熟的浏览器自动化能力带入了 Swift 生态。开发者可以直接使用 Swift 代码驱动 Chromium、Firefox 和 WebKit,完成页面导航、点击、输入、截图、执行 JavaScript 等常见操作,整体 API 风格也尽量贴近官方 Playwright。

从实现方式上看,它并不是重新实现一套浏览器自动化框架,而是在 Swift 侧封装了 Playwright 协议,底层依然通过 Node.js 的 Playwright driver 与浏览器通信。对于希望使用 Swift 构建测试工具、CLI,甚至 AI Agent 的开发者来说,这个项目提供了一个颇具吸引力的切入点。

活动

LET'S VISION 2026 -- Born to Create · Powered by AI

  • 👀 70+ 展商现场体验
  • 🤖 AI 创新产品 / AI Agent
  • 🥽 XR / 空间计算沉浸体验
  • 🎤 创作者与开发者分享

如果你是开发者、设计师、产品经理、创作者,还是对 AI 和未来科技感兴趣的探索者,都很值得来逛逛。

  • 📅 2026.3.28 – 3.29
  • 📍 上海 · 漕河泾会议中心

15% OFF 门票 👇

往期内容

💝 支持与反馈

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

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

🚀 拓展 Swift 视野

iOS 学习笔记 - 创建第一个APP

创建项目

现在已经完成了对 Xcode 的安装,所以开发环境已经安装完成了,接下来就需要开始真正的 iOS 开发了。对于 iOS 开发,这里使用了目前苹果比较推荐初学者入门的方式,使用 Swift 语言进行编程,并且使用苹果推荐的 SwiftUI

1. 新建项目: 首先点击 Xcode 图标打开 Xcode,这时候会出现 Xcode 的初始界面,在这里点击 Create New Project... 创建一个新的项目。 截屏2026-03-15 23.15.00.png

2. 创建一个 iOS 项目: 之后需要选择开发什么类型的项目,需要开发的是 iOS 平台的 App,所以在对话框中,选择 Applocation 中的 App 选项,再点击 Next 按钮。 截屏2026-03-15 23.33.34.png

3. 选择项目的详细信息: 点击 Next 之后,就能看到配置具体的项目信息的选项,填写项目的基础信息。

  • Product Name: TestDemo
  • Organization Identifier: com.meohao 这个对则
  • Interface: 选择默认的 Swift UI
  • Language: 开发语言选择 Swift

然后点击 Next,之后选择保存的目录,就能创建出一个最基础的 iOS App 的项目了。 截屏2026-03-15 23.39.16.png

项目分析

完成了最基础的项目创建,可以得到一个简单的 iOS 的项目代码和目录结构,并且右侧会展示我们当前页面的预览。这是一个基于 Swift UIiOS App 项目。

image.png

下面可以分析一下创建出来的这个新项目项目。

目录结构

Xcode 中,左侧显示了项目目录结构,可以看到,我们的项目中包含三部分:

  1. Assets: 这里放了 iOS App 的相关资源文件,有图标,色彩定义等。后续使用到了会详细介绍。
  2. CotentView: 页面文件,整个 iOS 项目都由一个个 View 组成,每个 View 也能作为组件放到其他 View 中,这就很想前端现在的组件化。
  3. TestDemoApp: 这是整个项目的入口文件,这个文件里的代码定义了入口。

代码文件

接下来进代码可以分析一下这两个文件都做了些什么:

import SwiftUI // 导入 Swift UI 包

@main // 这里表示这里是main,作为入库,相当于我们其他编程中的 main 函数
struct TestDemoApp: App { // 定义了一个结构体
    var body: some Scene {
        WindowGroup {
            ContentView() // 把 ContentView 放进来执行
        }
    }
}

这个入口文件,定义使用 @main 定义了入口,并且调用了 ContentView。那接下来看看 ContentView 的代码。

import SwiftUI

struct ContentView: View { // 定义结构体
    var body: some View {
        VStack { // 使用布局方式 竖排布局
            Image(systemName: "globe") // 展示图片
                .imageScale(.large) // 设置大小
                .foregroundStyle(.tint) // 设置样式
            Text("Hello, world!") // 展示文字
        }
        .padding() // 这个布局外加上边距
    }
}

#Preview { // 通过设个代码,可以再右侧看到当前的预览
    ContentView()
}

这两段代码中,可以认识到 Swift 结构和一些知识。

Swift 结构体

Swift 结构体是一种通用且灵活的构造体,我们可以理解为一种组织代码的方式,如果有其他编程经验,相信对结构体并不陌生。

Swift结构体可以定义属性方法,这个和其他编程语言中的 class—— 很像。 Swift 中,也存在数据类型的定义,结构体有部分相同之处,也有一些不一样的地方,后续使用到的时候再详细介绍。

从这里可以看出,结构体可以定义一些变量,同时也能定义一些常量和方法。(这里不太懂的同学可以先不急着了解,下一节会详细介绍 Swift 语言编程的基础知识)例如,定义一个结构体:

struct TestStruct {
    var number: Int
}

这就定义了一个有属性结构体,同时,定义属性的时候,也可以对其初始化。

struct TestStruct {
    var number: Int = 1
}

定义好了结构体,就可以对结构体进行实例化,可以看到示例代码中,已经有对结构体进行实例化的例子,在 #Preview处。对于已经初始化变量的结构体,我们可以再初始化的时候,不带参数:

struct TestStruct {
    var number: Int = 1
}

TestStruct()

然而,在未初始化的结构体中,则必须在实例化的时候,带上参数,不然会发生报错:

struct TestStruct {
    var number: Int
}

TestStruct(number: 1)

结尾

这里,就完成创建出了一个最简单的 iOS App 项目。同时也对 Swift 的结构体有了一定的了解。

大型 iOS 工程单元测试 — 变更驱动测试与跨模块 Mock

一、定位

本文是 为大型 iOS 工程补充单元测试方法论补充篇。前文提供了"画链路 → 选节点 → 写测试 → 融入迭代"的完整框架,覆盖了基础的 Mock 策略和用例设计原则。

本文聚焦以下问题:

  • 当测试目标不是"从零覆盖"而是"验证一次代码变更"时,应如何设计用例?
  • 当被测方法依赖的实验开关位于另一个模块、通过 Service Locator 解析时,如何 Mock?
  • 当同一份数据有两个来源、且实验开关决定是否去重时,如何构造测试场景?
  • 当方法的聚合语义不是"求和"而是"计数"时,如何防止未来开发者误改?

二、变更驱动测试设计(Change-Driven Test Design)

2.1 原则

传统的"从零覆盖"思路是:遍历方法的所有分支,为每个分支写用例。而在实际业务迭代中,更常见的场景是你刚修改了一段逻辑,需要快速验证变更的正确性

变更驱动测试的核心思路:

识别本次变更引入的新分支或行为差异
为新分支写正向用例(验证新行为)
为旧分支写回归用例(验证未被破坏)
如果变更引入了新的数据源排除/包含逻辑,为排除和包含各写至少一条用例

2.2 双态特性开关测试

当一次变更由特性开关(Feature Flag)控制时,同一个方法在 flag=true 和 flag=false 下有不同行为。此时必须成对测试

用例类型 目的 示例
flag=true 正向 验证新行为生效 shouldCountUnreadByCell=true 时 notice groups 被跳过,unreadCount 仅含 interactor 贡献
flag=false 回归 验证旧行为未被破坏 shouldCountUnreadByCell=false 时 notice groups 仍参与累加
flag=true 复合 新行为 + 多种过滤条件叠加 flag=true + muted groups + shop groups + redPoint groups → 全部被跳过,仅剩 interactor

关键原则:回归用例的重要性不低于正向用例。开发者常犯的错误是只测了新路径而遗漏了旧路径的回归验证,有可能改坏了旧路径的功能而未及时发现。

2.3 案例

BizScenarioItemInboxTabNumber.checkHasNoticeTabbarUnreadCount: 在引入 shouldCountUnreadByCell 分支后,新增了 4 条用例:flag=true 跳过 notice groups、flag=true 时 cellCount 不受影响、flag=false 包含 notice groups(回归)、flag=true 复合场景。每条用例的 assertion message 都明确标注了 flag 状态和预期计算过程。


三、Service Center Protocol Mock(跨模块协议依赖的 Mock 策略)

3.1 问题场景

在 Service Locator 架构中,模块间的依赖通过协议(Protocol)解耦。被测代码通过 GET_CLASS(IMModuleService) 获取另一个模块提供的 Class,再调用其类方法。这带来了一个 Mock 难题:

  • IMModuleService协议,不是类,无法直接 swizzle
  • 协议的实现类位于另一个模块,测试工程可能没有链接该模块
  • 在测试环境中,Service Center 默认为空,GET_CLASS 返回 nil

3.2 解法:注册 Mock Class 到测试 Service Center

SwiftTestCaseserviceBehavior = .newCenter 会为每个测试创建隔离的 Service Center。利用基类提供的 mockGetStatelessProtocolService(_:andReturn:) 方法,将一个轻量 Mock 类注册到该 Center 中:

// 定义只实现所需类方法的 Mock 类
private class MockIMModuleServiceTrue: NSObject {
    @objc class func shouldCountUnreadByCell() -> Bool { return true }
}

// 在测试中注册
if let proto = NSProtocolFromString("IMModuleService") {
    mockGetStatelessProtocolService(proto, andReturn: MockIMModuleServiceTrue.self)
}

工作原理

被测代码: [GET_CLASS(IMModuleService) shouldCountUnreadByCell]
         ↓
GET_CLASS 查询 Service Center → 返回 MockIMModuleServiceTrue.class
         ↓
[MockIMModuleServiceTrue shouldCountUnreadByCell] → YES

3.3 与 Runtime Swizzle 的对比

维度 Runtime Swizzle Service Center Mock
适用场景 目标类已知且已链接 目标是协议,实现类不可见或位于其他模块
隔离性 全局替换,需手动恢复 仅在测试的隔离 Service Center 内生效,自动还原
tearDown 负担 必须手动调用 restore() 无需手动清理
限制 需要已知类名和方法签名 仅适用于通过 GET_CLASS / Service Locator 解析的依赖

3.4 适用准则

当被测方法通过以下宏/方式获取依赖时,优先使用 Service Center Mock:

  • GET_CLASS(Protocol) / GET_PROTOCOL(Protocol)
  • ServiceCenter.defaultCenter.getStatelessProtocolService()
  • 任何通过 Service Locator 模式解析的跨模块协议依赖

四、Fake Environment 模式(Context Protocol Mock)

4.1 问题场景

某些组件通过一个宽接口的 "Context" 协议获取运行时环境(数据字典、配置管理器、事件分发器等)。直接构造真实 Context 需要初始化整个管理器链路,测试成本极高。

4.2 解法:实现仅含测试所需数据的 Fake Context

private class MockUnreadCountContext: NSObject, UnreadCountContext {
    var mockEntranceCountModelDict: [String: InboxEntranceUnreadCountModel] = [:]
    var checkNeedUpdateCalled = false
    var lastCheckScene: BizScenarioItemCheckScene = []

    func entranceCountModelDict() -> [String: InboxEntranceUnreadCountModel]? {
        return mockEntranceCountModelDict
    }
    func checkNeedUpdate(_ scene: BizScenarioItemCheckScene) {
        checkNeedUpdateCalled = true
        lastCheckScene = scene
    }
    // 其余方法空实现
}

4.3 设计要点

要点 说明
只实现被测路径依赖的方法 非必需方法留空实现,降低维护成本
var 暴露可控数据 测试通过直接修改 mockEntranceCountModelDict 来控制输入
Spy 能力 添加 checkNeedUpdateCalled / lastCheckScene 等标记,验证被测方法是否正确触发了 Context 上的副作用
弱引用安全 Context 属性通常为 weak,确保 Mock 对象在测试期间被持有(存为实例属性)

4.4 与协议 Mock 对象的区别

前文的"协议 Mock 对象"聚焦于数据提供者(如 NoticeUnreadCountItemProtocol),每个 Mock 只需返回数值。Fake Environment 聚焦于运行时环境,需要同时提供数据字典、触发副作用(如 checkNeedUpdate:)、并可能被多个被测方法共享。


五、聚合语义测试(Aggregation Semantics Testing)

5.1 问题

聚合方法有两种常见语义,外部签名几乎相同,但行为差异大:

语义 含义 示例方法
Sum 将所有符合条件的项的值相加 countForNumber: → 返回 5 + 3 = 8
Count 统计符合条件且值 > 0 的项的个数 countForUnreadCell: → 返回 2(有 2 项非零)

如果未来开发者误将 Count 语义改为 Sum 语义(或反过来),逻辑上仍然"能跑通",但业务含义错误。

5.2 方法:用数据设计锁定语义

构造让 Sum 和 Count 结果必然不同的测试数据,使得任何语义变更都会导致断言失败:

// count 为 5 和 3 → Sum = 8, Count = 2
// 如果断言 Count == 2,则改为 Sum 后结果变为 8,测试失败
func test_countForUnreadCell_countsEntrancesNotSumsCount() {
    mockContext.mockEntranceCountModelDict = [
        combinedKey(1): makeEntranceModel(entranceID: 1, count: 5),
        combinedKey(2): makeEntranceModel(entranceID: 2, count: 3),
    ]
    XCTAssertEqual(item.count(forUnreadCell: nil), 2,
                   "Cell count = number of entrances with unread, not sum of counts")
}

关键:选择每项 count > 1 的数据。如果所有 count 都为 1,则 Sum 和 Count 结果相同,无法区分语义。

5.3 推广

此方法适用于所有存在语义歧义的聚合操作:

  • Max vs Sum:确保数据中有多项,且各项值不同
  • Any vs All:确保数据中有 true 和 false 的混合
  • Distinct count vs Total count:确保数据中有重复项

六、数据源去重测试(Deduplication Testing)

6.1 问题

当同一份业务数据通过两个独立渠道到达聚合点时(如 notice_countentrance_count 都包含通知未读数),需要在特定条件下去重,否则会出现重复计算。

6.2 测试策略

构造"两个渠道都有数据"的场景,验证在去重开关开启时只有一路生效:

dataSource (notice groups) = { group100: 10, group200: 3 }
interactor (entrance items) = countForNumber: 5

shouldCountUnreadByCell=true  → result = 5  (只用 interactor,跳过 dataSource)
shouldCountUnreadByCell=false → result = 18 (dataSource 13 + interactor 5)

设计要点

  • 两路数据都给非零值,使得去重与不去重的结果有明显差异
  • 对去重路径和非去重路径各写至少一条用例
  • Assertion message 中明确标注"哪一路被跳过"以及"预期计算过程"

七、变更传播测试(Mutation-Aggregation Vertical Slice)

7.1 问题

底层数据的变更(标记已读、静音)需要正确传播到上层的聚合结果中。仅测试"model.count 被置 0"是不够的,因为聚合层可能因为缓存、过滤条件等原因未感知到变更。

7.2 方法:跨层断言

在一个测试用例中同时操作底层和观察上层,形成"垂直切片":

func test_updateMuteStatus_muteExcludesFromCount() {
    // 底层:构造 model
    let model = makeEntranceModel(entranceID: 1, count: 5)
    setUpEntranceModels([combinedKey(1): model])

    // 上层:构造 aggregation item,共享同一个 model
    let countItem = InboxEntranceUnreadCountItem()
    let ctx = MockUnreadCountContext()
    ctx.mockEntranceCountModelDict = [combinedKey(1): model]
    countItem.context = ctx

    XCTAssertEqual(countItem.count(forNumber: nil), 5, "Before mute")

    // 执行变更
    service.updateMuteStatus(true, forEntranceID: 1, subEntranceKey: nil)

    // 验证传播:上层聚合结果反映了底层变更
    XCTAssertEqual(countItem.count(forNumber: nil), 0, "After mute, excluded from count")
}

7.3 适用场景

  • 标记已读 → 未读数归零
  • 静音 → 从聚合计算中排除
  • 归档 → 从聚合计算中排除
  • 任何"底层状态变更应影响上层输出"的链路

7.4 与纯单元测试的关系

垂直切片测试严格来说介于单元测试和集成测试之间。在大型工程中,它的性价比很高:不需要启动完整的 Service 链路,但能验证两层之间的契约是否正确。推荐在以下情况使用:

  • 两层通过共享可变对象(同一个 model 实例)交互
  • 上层的聚合逻辑包含过滤条件(muted、archived 等),变更后的状态可能被过滤

八、短路行为测试(Short-Circuit Testing)

8.1 问题

某些遍历方法在找到第一个匹配项后会 stop*stop = YES),不再继续遍历。如果去掉 stop,方法签名和大部分行为不变,但在存在多个同类型 item 时会错误地累加。

8.2 方法:构造多个同类型 item,验证只取第一个

func test_countForNumberWithType_stopsAfterFirstMatch() {
    addMockItem(showType: .number, countForNumber: 10, itemType: .entranceCountItem)
    addMockItem(showType: .number, countForNumber: 5, itemType: .entranceCountItem)
    XCTAssertEqual(
        interactor.count(forNumber: nil, withUnreadCountItemType: .entranceCountItem), 10,
        "Should stop after first matching item"
    )
}

如果 stop 被移除,结果会变为 15,测试失败。

8.3 适用场景

  • 按类型过滤的方法(预期每种类型只有一个活跃实例)
  • 优先级查找方法(返回第一个满足条件的结果)
  • 任何使用 enumerateObjectsUsingBlock: + *stop = YES 的 ObjC 代码

九、总结:何时使用哪种模式

场景 推荐模式 本文章节
验证一次代码变更 变更驱动测试 + 双态 Flag 测试
被测方法依赖跨模块协议(通过 Service Locator) Service Center Protocol Mock
被测对象通过宽接口 Context 获取环境 Fake Environment 模式
聚合方法的 Sum/Count 语义容易被误改 聚合语义测试
同一数据有两个来源、需条件性去重 数据源去重测试
底层变更需传播到上层聚合结果 Mutation-Aggregation 垂直切片
遍历方法有 stop/短路行为 短路行为测试

这些模式与前文的基础方法论互补使用。基础方法论解决"测什么"和"怎么 Mock"的问题,本文解决"改了代码后怎么精准验证"和"复杂依赖场景下怎么构造可控环境"的问题。

Swift 从入门到精通-终篇

第14章:SwiftUI 进阶

14.1 动画

SwiftUI 动画非常简单,只需添加 .animation


struct AnimationView: View {

    @State private var isExpanded = false

    

    var body: some View {

        VStack {

            RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)

                .fill(isExpanded ? Color.blue : Color.red)

                .frame(

                    width: isExpanded ? 300 : 100,

                    height: isExpanded ? 300 : 100

                )

                .animation(.spring(response: 0.3, dampingFraction: 0.5), value: isExpanded)

            

            Button("切换") {

                isExpanded.toggle()

            }

        }

    }

}

**动画类型: **

  • .default - 默认动画

  • .linear - 线性

  • .easeIn / .easeOut / .easeInOut - 缓动

  • .spring() - 弹性动画

  • .interactiveSpring() - 交互式弹性

14.2 手势


struct GestureView: View {

    @State private var offset: CGSize = .zero

    @State private var scale: CGFloat = 1.0

    @State private var rotation: Angle = .zero

    

    var body: some View {

        Image(systemName: "star.fill")

            .font(.system(size: 100))

            .foregroundColor(.yellow)

            .offset(offset)

            .scaleEffect(scale)

            .rotationEffect(rotation)

            .gesture(

                DragGesture()

                    .onChanged { gesture in

                        offset = gesture.translation

                    }

                    .onEnded { _ in

                        withAnimation {

                            offset = .zero

                        }

                    }

            )

            .gesture(

                MagnificationGesture()

                    .onChanged { scale = $0 }

            )

            .gesture(

                RotationGesture()

                    .onChanged { rotation = $0 }

            )

    }

}

14.3 数据持久化


import SwiftUI

  


struct TodoItem: Identifiable, Codable {

    let id: UUID

    var title: String

    var isCompleted: Bool

    var createdAt: Date

}

  


class TodoStore: ObservableObject {

    @Published var todos: [TodoItem] = []

    

    private let saveKey = "todos"

    

    init() {

        load()

    }

    

    func add(_ title: String) {

        let todo = TodoItem(

            id: UUID(),

            title: title,

            isCompleted: false,

            createdAt: Date()

        )

        todos.append(todo)

        save()

    }

    

    func toggle(_ todo: TodoItem) {

        if let index = todos.firstIndex(where: { $0.id == todo.id }) {

            todos[index].isCompleted.toggle()

            save()

        }

    }

    

    func delete(at offsets: IndexSet) {

        todos.remove(atOffsets: offsets)

        save()

    }

    

    // UserDefaults 存储

    private func save() {

        if let encoded = try? JSONEncoder().encode(todos) {

            UserDefaults.standard.set(encoded, forKey: saveKey)

        }

    }

    

    private func load() {

        if let data = UserDefaults.standard.data(forKey: saveKey),

           let decoded = try? JSONDecoder().decode([TodoItem].self, from: data) {

            todos = decoded

        }

    }

}


第15章:并发编程 - async/await

15.1 为什么需要并发?

想象一个场景:

  • 用户点击按钮

  • App 需要从网络加载图片

  • 如果用同步方式,界面会卡住,直到图片加载完成

  • 用户看到"假死",体验极差

**并发让 App 能同时做多件事: **

  • 主线程:响应用户操作、更新界面

  • 后台线程:网络请求、文件读写、复杂计算

15.2 旧的方式 - GCD


// 旧的写法(仍然有效,但不推荐新代码使用)

DispatchQueue.global().async {

    // 后台线程执行耗时操作

    let data = try! Data(contentsOf: url)

    

    DispatchQueue.main.async {

        // 主线程更新 UI

        imageView.image = UIImage(data: data)

    }

}

**问题: **

  • 嵌套层级深(回调地狱)

  • 错误处理麻烦

  • 代码难以阅读和维护

15.3 async/await - 新的方式


// 定义异步函数

func fetchImage(from url: URL) async throws -> UIImage {

    let (data, _) = try await URLSession.shared.data(from: url)

    guard let image = UIImage(data: data) else {

        throw ImageError.invalidData

    }

    return image

}

  


// 调用异步函数

func loadImage() async {

    do {

        let image = try await fetchImage(from: imageURL)

        imageView.image = image

    } catch {

        print("加载失败: \(error)")

    }

}

**代码看起来像同步的,实际上是异步执行的! **

15.4 在 SwiftUI 中使用


struct AsyncImageView: View {

    @State private var image: UIImage?

    @State private var isLoading = false

    

    let url: URL

    

    var body: some View {

        Group {

            if let image = image {

                Image(uiImage: image)

                    .resizable()

                    .aspectRatio(contentMode: .fit)

            } else if isLoading {

                ProgressView()

            } else {

                Image(systemName: "photo")

                    .font(.largeTitle)

                    .foregroundColor(.gray)

            }

        }

        .onAppear {

            loadImage()

        }

    }

    

    private func loadImage() {

        isLoading = true

        

        Task {

            do {

                let (data, _) = try await URLSession.shared.data(from: url)

                await MainActor.run {

                    self.image = UIImage(data: data)

                    self.isLoading = false

                }

            } catch {

                await MainActor.run {

                    self.isLoading = false

                }

            }

        }

    }

}

**Task: **创建异步任务的环境

**MainActor: **确保代码在主线程执行(用于 UI 更新)

15.5 结构化并发


// 顺序执行(慢)

func fetchSequentially() async throws -> [User] {

    let user1 = try await fetchUser(id: 1)

    let user2 = try await fetchUser(id: 2)

    let user3 = try await fetchUser(id: 3)

    return [user1, user2, user3]

}

  


// 并行执行(快!)

func fetchConcurrently() async throws -> [User] {

    async let user1 = fetchUser(id: 1)

    async let user2 = fetchUser(id: 2)

    async let user3 = fetchUser(id: 3)

    return try await [user1, user2, user3]

}

  


// 使用 TaskGroup 处理动态数量的任务

func fetchUsers(ids: [Int]) async throws -> [User] {

    try await withThrowingTaskGroup(of: User.self) { group in

        for id in ids {

            group.addTask {

                try await fetchUser(id: id)

            }

        }

        

        var users: [User] = []

        for try await user in group {

            users.append(user)

        }

        return users

    }

}

**async let: **启动并行任务

**withTaskGroup: **管理一组动态任务

15.6 异步序列


// 异步序列

let stream = AsyncStream<Int> { continuation in

    Task {

        for i in 1...5 {

            try? await Task.sleep(for: .seconds(1))

            continuation.yield(i)

        }

        continuation.finish()

    }

}

  


// 遍历

for await number in stream {

    print("收到: \(number)")

}


第16章:Actor 与数据竞争防护

16.1 数据竞争问题


class UnsafeCounter {

    var count = 0

    

    func increment() {

        count += 1  // 非线程安全!

    }

}

  


let counter = UnsafeCounter()

  


// 从多个线程同时增加

DispatchQueue.concurrentPerform(iterations: 1000) { _ in

    counter.increment()

}

  


// 结果可能小于 1000!

print(counter.count)

原因: count += 1 实际上分三步:

  1. 读取 count 的值

  2. 加 1

  3. 写回 count

如果两个线程同时执行,可能都读到 5,都变成 6,结果只增加了 1。

16.2 Actor - 隔离状态


actor SafeCounter {

    private var count = 0

    

    func increment() {

        count += 1  // 线程安全!

    }

    

    func getCount() -> Int {

        return count

    }

}

  


let counter = SafeCounter()

  


await withTaskGroup(of: Void.self) { group in

    for _ in 0..<1000 {

        group.addTask {

            await counter.increment()

        }

    }

}

  


let finalCount = await counter.getCount()

print(finalCount)  // 1000

**Actor 的特点: **

  • 同一时间只有一个线程能访问 actor 内部

  • 自动防止数据竞争

  • 访问 actor 的属性和方法需要 await

16.3 MainActor - 主线程 Actor


@MainActor

class ViewModel: ObservableObject {

    @Published var items: [Item] = []

    

    func load() async {

        items = await fetchItems()  // await 切到后台

        // 自动回到主线程

    }

}

  


// 或者只标记某个方法

class ViewModel2: ObservableObject {

    @Published var items: [Item] = []

    

    @MainActor

    func updateUI() {

        // 确保在主线程执行

        items.append(newItem)

    }

}

16.4 Sendable 协议


// 安全的值类型,可以跨 actor 传递

struct UserData: Sendable {

    let id: UUID

    let name: String

}

  


// 类也可以遵循 Sendable,但需要是 final 且属性都是 Sendable

final class SafeClass: Sendable {

    let value: Int

    init(value: Int) {

        self.value = value

    }

}

**Swift 6 会强制检查: **如果类型不是 Sendable,不能跨 actor 传递。


第一次做分享,期待更优秀的你~

Swift 从入门到精通-第四篇

第13章:SwiftUI 基础 - 声明式 UI

13.1 SwiftUI 是什么?

SwiftUI 是 Apple 在 2019 年推出的声明式 UI 框架,用来替代 UIKit。它的核心理念是:

**告诉 SwiftUI "你想要什么界面",而不是 "怎么创建界面"。 **

对比:

  • **UIKit(命令式) **:创建视图 → 添加到父视图 → 设置约束 → 手动更新

  • **SwiftUI(声明式) **:描述界面状态,SwiftUI 自动处理更新

13.2 你的第一个 SwiftUI 视图

在 Xcode 中创建 SwiftUI 项目,替换 ContentView.swift:


import SwiftUI

  


struct ContentView: View {

    var body: some View {

        Text("Hello, World!")

            .font(.largeTitle)

            .foregroundColor(.blue)

            .padding()

    }

}

  


#Preview {

    ContentView()

}

**关键概念: **

  • View 协议:所有 UI 元素都遵循这个协议

  • body:计算属性,返回视图层级

  • 修饰符(modifier):用 . 链式调用,修改视图样式

13.3 状态管理 - @State

SwiftUI 中,视图是函数:输入相同的状态,输出相同的界面。


struct CounterView: View {

    // @State 标记可变状态

    @State private var count = 0

    

    var body: some View {

        VStack(spacing: 20) {

            Text("Count: \(count)")

                .font(.largeTitle)

            

            Button("Increment") {

                count += 1  // 修改状态,自动刷新界面

            }

            .buttonStyle(.borderedProminent)

        }

    }

}

** @State 的特点: **

  • 用于视图内部状态

  • SwiftUI 自动管理存储

  • 值改变时自动重绘视图

13.4 布局系统 - Stacks


struct LayoutDemoView: View {

    var body: some View {

        VStack(spacing: 20) {      // 垂直堆栈

            Text("上方")

            

            HStack(spacing: 20) {  // 水平堆栈

                Text("左")

                    .frame(maxWidth: .infinity)

                    .background(Color.red.opacity(0.3))

                Text("中")

                    .frame(maxWidth: .infinity)

                    .background(Color.green.opacity(0.3))

                Text("右")

                    .frame(maxWidth: .infinity)

                    .background(Color.blue.opacity(0.3))

            }

            

            ZStack {                // 重叠堆栈

                Circle()

                    .fill(Color.yellow)

                    .frame(width: 100, height: 100)

                Text("Z")

                    .font(.largeTitle)

            }

            

            Text("下方")

        }

        .padding()

    }

}

**三种 Stack: **

  • VStack - 垂直排列

  • HStack - 水平排列

  • ZStack - 前后叠加

13.5 常用控件


struct ControlsView: View {

    @State private var text = ""

    @State private var isOn = false

    @State private var sliderValue = 50.0

    @State private var selectedDate = Date()

    @State private var selectedColor = Color.red

    

    var body: some View {

        Form// 表单,自动处理滚动和布局

            Section("输入") {

                TextField("请输入", text: $text)

                SecureField("密码", text: $text// 密码输入

                TextEditor(text: $text// 多行文本

            }

            

            Section("选择") {

                Toggle("开关", isOn: $isOn)

                

                Slider(value: $sliderValue, in: 0...100) {

                    Text("滑动条")

                }

                

                DatePicker("日期", selection: $selectedDate)

                

                ColorPicker("颜色", selection: $selectedColor)

            }

            

            Section("按钮") {

                Button("普通按钮") {}

                

                Button(action: {}) {

                    Label("带图标的按钮", systemImage: "star.fill")

                }

                

                Button("主要按钮") {}

                    .buttonStyle(.borderedProminent)

                

                Button("胶囊按钮") {}

                    .buttonStyle(.bordered)

                    .controlSize(.large)

                    .tint(.green)

            }

        }

    }

}

注意 **$** 符号: $texttext 的绑定(Binding),双向数据流。

13.6 列表与导航


// 数据模型

struct Restaurant: Identifiable {

    let id = UUID()

    let name: String

    let cuisine: String

    let rating: Double

}

  


struct RestaurantRow: View {

    let restaurant: Restaurant

    

    var body: some View {

        HStack {

            VStack(alignment: .leading) {

                Text(restaurant.name)

                    .font(.headline)

                Text(restaurant.cuisine)

                    .font(.subheadline)

                    .foregroundColor(.secondary)

            }

            

            Spacer()

            

            HStack {

                Image(systemName: "star.fill")

                    .foregroundColor(.yellow)

                Text(String(format: "%.1f", restaurant.rating))

            }

        }

    }

}

  


struct RestaurantListView: View {

    let restaurants = [

        Restaurant(name: "川味轩", cuisine: "川菜", rating: 4.5),

        Restaurant(name: "金鼎轩", cuisine: "粤菜", rating: 4.2),

        Restaurant(name: "日料屋", cuisine: "日料", rating: 4.8)

    ]

    

    var body: some View {

        NavigationView {

            List(restaurants) { restaurant in

                NavigationLink(destination: RestaurantDetailView(restaurant: restaurant)) {

                    RestaurantRow(restaurant: restaurant)

                }

            }

            .navigationTitle("餐厅列表")

        }

    }

}

  


struct RestaurantDetailView: View {

    let restaurant: Restaurant

    

    var body: some View {

        VStack(spacing: 20) {

            Image(systemName: "fork.knife.circle.fill")

                .resizable()

                .frame(width: 100, height: 100)

                .foregroundColor(.orange)

            

            Text(restaurant.name)

                .font(.largeTitle)

            

            Text(restaurant.cuisine)

                .font(.title2)

                .foregroundColor(.secondary)

            

            HStack {

                ForEach(0..<Int(restaurant.rating), id: \.self) { _ in

                    Image(systemName: "star.fill")

                        .foregroundColor(.yellow)

                }

            }

            

            Spacer()

        }

        .padding()

        .navigationTitle("详情")

    }

}

**关键概念: **

  • Identifiable 协议:让数据可以被列表唯一标识

  • List:自动处理行、分隔线、滑动删除

  • NavigationView + NavigationLink:页面跳转

13.7 数据绑定 - @Binding

当子视图需要修改父视图的状态时:


// 子视图

struct ToggleView: View {

    @Binding var isOn: Bool  // 绑定,不是自己的状态

    

    var body: some View {

        Toggle("开关", isOn: $isOn)

    }

}

  


// 父视图

struct ParentView: View {

    @State private var lightOn = false

    

    var body: some View {

        ToggleView(isOn: $lightOn// 传递绑定

    }

}

13.8 可观察对象 - @StateObject 和 @ObservedObject

对于复杂数据模型,使用 ObservableObject:


import Combine

  


class TaskStore: ObservableObject {

    @Published var tasks: [Task] = []

    @Published var filter: TaskFilter = .all

    

    var filteredTasks: [Task] {

        switch filter {

        case .all: return tasks

        case .active: return tasks.filter { !$0.isCompleted }

        case .completed: return tasks.filter { $0.isCompleted }

        }

    }

    

    func addTask(_ title: String) {

        tasks.append(Task(title: title))

    }

    

    func toggleTask(_ task: Task) {

        if let index = tasks.firstIndex(where: { $0.id == task.id }) {

            tasks[index].isCompleted.toggle()

        }

    }

}

  


struct TaskListView: View {

    @StateObject private var store = TaskStore()  // 创建可观察对象

    

    var body: some View {

        List {

            ForEach(store.filteredTasks) { task in

                TaskRow(task: task) {

                    store.toggleTask(task)

                }

            }

        }

    }

}

** @StateObject vs @ObservedObject: **

  • @StateObject:创建并持有对象(用这个视图创建的数据)

  • @ObservedObject:引用外部传入的对象

13.9 环境值 - @Environment


struct ContentView: View {

    @Environment(\.colorScheme) var colorScheme

    @Environment(\.dismiss) var dismiss

    

    var body: some View {

        VStack {

            Text(colorScheme == .dark ? "Dark Mode" : "Light Mode")

            

            Button("关闭") {

                dismiss()  // 关闭当前页面

            }

        }

    }

}


第14章:SwiftUI 进阶

14.1 动画

SwiftUI 动画非常简单,只需添加 .animation


struct AnimationView: View {

    @State private var isExpanded = false

    

    var body: some View {

        VStack {

            RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)

                .fill(isExpanded ? Color.blue : Color.red)

                .frame(

                    width: isExpanded ? 300 : 100,

                    height: isExpanded ? 300 : 100

                )

                .animation(.spring(response: 0.3, dampingFraction: 0.5), value: isExpanded)

            

            Button("切换") {

                isExpanded.toggle()

            }

        }

    }

}

**动画类型: **

  • .default - 默认动画

  • .linear - 线性

  • .easeIn / .easeOut / .easeInOut - 缓动

  • .spring() - 弹性动画

  • .interactiveSpring() - 交互式弹性

14.2 手势


struct GestureView: View {

    @State private var offset: CGSize = .zero

    @State private var scale: CGFloat = 1.0

    @State private var rotation: Angle = .zero

    

    var body: some View {

        Image(systemName: "star.fill")

            .font(.system(size: 100))

            .foregroundColor(.yellow)

            .offset(offset)

            .scaleEffect(scale)

            .rotationEffect(rotation)

            .gesture(

                DragGesture()

                    .onChanged { gesture in

                        offset = gesture.translation

                    }

                    .onEnded { _ in

                        withAnimation {

                            offset = .zero

                        }

                    }

            )

            .gesture(

                MagnificationGesture()

                    .onChanged { scale = $0 }

            )

            .gesture(

                RotationGesture()

                    .onChanged { rotation = $0 }

            )

    }

}

14.3 数据持久化


import SwiftUI

  


struct TodoItem: Identifiable, Codable {

    let id: UUID

    var title: String

    var isCompleted: Bool

    var createdAt: Date

}

  


class TodoStore: ObservableObject {

    @Published var todos: [TodoItem] = []

    

    private let saveKey = "todos"

    

    init() {

        load()

    }

    

    func add(_ title: String) {

        let todo = TodoItem(

            id: UUID(),

            title: title,

            isCompleted: false,

            createdAt: Date()

        )

        todos.append(todo)

        save()

    }

    

    func toggle(_ todo: TodoItem) {

        if let index = todos.firstIndex(where: { $0.id == todo.id }) {

            todos[index].isCompleted.toggle()

            save()

        }

    }

    

    func delete(at offsets: IndexSet) {

        todos.remove(atOffsets: offsets)

        save()

    }

    

    // UserDefaults 存储

    private func save() {

        if let encoded = try? JSONEncoder().encode(todos) {

            UserDefaults.standard.set(encoded, forKey: saveKey)

        }

    }

    

    private func load() {

        if let data = UserDefaults.standard.data(forKey: saveKey),

           let decoded = try? JSONDecoder().decode([TodoItem].self, from: data) {

            todos = decoded

        }

    }

}


苹果谷歌纷纷调低官方抽成,苹果谷歌全球抽成比例汇总

一、苹果中国区抽成“紧急”下调

2026年3月12日,苹果突然宣布中国区AppStore官方抽成从 30% 改为 25%,小型开发者抽成从15% 改为 12%2026年3月15日生效来源

想必,今天大家都被这个截图刷屏了吧。

图片.png

为什么说“紧急”呢?
1、“根据与中国监管部门的沟通”,写得很清楚,是中国监管部门推动的;
2、“自3月15日起”,约等于立刻生效,对比谷歌的三个月后生效,凸显一个“急”;
3、“调整无需开发者在此之前签署新条款”,手续流程都免了,直接生效!
2、“更新版协议的简体中文版将于一个月内在 Apple开发者网站上线”,流程后面再补,先上线!

不知道苹果发生了什么,但是感觉很爽。有种苹果被工信部发了违规整改通知的感觉(DDDD),让苹果也尝尝工信部的厉害,马上整改,立刻上线!哈哈哈。

中国开发者什么都不用做,代码都不用改,就额外增(bai)加(piao) 3%~5% 的收益。

感谢那些为此做出贡献的人!

补充:有律师说出了苹果紧急“降税”的真相 ,有兴趣的可以点开看看。

二、谷歌将陆续降低全球抽成并开放三方支付

苹果紧急降低抽成除了迫于监管压力,估计也迫于竞争对手的压力。

早在3月4日,谷歌在安卓开发者网站发布了一篇博客《选择和开放的新时代》宣布将陆续在全球降低抽成,开放第三方支付,并且后续除了《小型开发者计划》外,还会新推出《应用体验计划》和《游戏升级计划》来让利开发者

《应用体验计划》《游戏升级计划》的本质:质量换费率。通过经济激励(降低费率)来引导开发者提升应用和游戏的整体品质。开发者必须达到相应的技术集成和体验标准,来满足计划条件,才能获得费率减免。举例说明,比如,游戏类必须集成 Play Games Services 功能(如成就系统、现代玩家个人资料认证)。Play Console 中的“Android Vitals”指标,确保应用在崩溃率、ANR(无响应)率等方面符合谷歌的健康度标准。

计划的具体内容,谷歌尚未公布。

谷歌将现有的抽成拆成了两部分:
Google商店服务费:标准20%、参加上述新计划15%、小型开发者10%、订阅10%(取最小值)
Google支付服务费:约5%(每个地区可能不一样)

在美国、英国和欧洲经济区 (EEA),支付服务费为 5%。其他地区的支付服务费详情谷歌后续公布。

商店服务费,只要你在谷歌商店上架就要交,不管你用谷歌支付还是三方支付;
支付服务费,用谷歌支付就要交,用三方支付不交。

谷歌最终抽成比例:
官方支付抽成:15%~25%
三方支付抽成:10%~20%

谷歌新政策全球上线后,官方支付和三方支付只差5%,三方支付还得加上3%左右的通道费,和官方支付相比,三方支付毫无竞争力,这也是为什么谷歌敢在全球开放三方支付的原因。

需要注意的是,这次费率变化并非即刻生效,而是将分时间、逐步在全球不同地区推广:

各区域的推出日期 地区 《应用体验计划》《游戏升级计划》上线地区
2026年6月30日 欧洲经济区、英国、美国  
2026年9月30日 澳大利亚 澳大利亚、欧洲经济区、英国、美国
2026年12月31日 日本、韩国 日本、韩国
2027年9月30日 世界其他地区 世界其他地区

三、苹果、谷歌全球抽成比例汇总

目前,谷歌和苹果,在全球都面临着反垄断、三方支付、三方商店的压力,革命一旦发起,就像星星之火一样会传递到全世界,一会这个国家闹,一会那个国家闹。面对这样的情况,谷歌和苹果却走出了不一样的应对路数。

1、谷歌全球统一标准

谷歌,将在2026年到2027年陆续在全球执行统一的新标准,开放三方支付、开放三方商店。

Google商店服务费:标准20%、参加上述新计划15%、小型开发者10%、订阅10%(取最小值)
Google支付服务费:约5%(每个地区可能不一样)

官方支付抽成:15%~25%
三方支付抽成:10%~20%

全球实行时间线:

各区域的推出日期 地区 《应用体验计划》《游戏升级计划》上线地区
2026年6月30日 欧洲经济区、英国、美国  
2026年9月30日 澳大利亚 澳大利亚、欧洲经济区、英国、美国
2026年12月31日 日本、韩国 日本、韩国
2027年9月30日 世界其他地区 世界其他地区

2、苹果按闹施政

从目前来看,苹果是按闹施政,谁闹我就便宜点,不闹就维持原样。但感觉不是长久之计,说不定苹果后续也会像谷歌那样统一标准。目前情况来看,谷歌还是眼光更长远一些,走在了前面,胸襟更大。

以下是苹果当前(2026.3.13)全球费率情况

地区 官方内购参考佣金 三方支付苹果抽成 备注
欧盟 13% - 20%,官方文档 15%~20% 欧盟计费很复杂,还会按安装量抽成
日本 15% - 26%,官方文档 10%~15%,外部链接购买 10%~21%,应第三方购买  
韩国 15% ~ 30% 11% ~ 26%  
美国 15% - 30% 0%,外部链接购买 海外公司可以申请;必须同时提供内购作为备选;仍然向苹果上报收入用于审计
中国 12% ~ 25%,官方文档 不允许三方支付  
其它 15% ~ 30% 不允许三方支付

和谷歌一样,苹果也把抽成拆了商店服务费+支付服务费,从上表可以看到三方支付和官方支付比也没有优势。

美国外链支付比较特殊,可以做到0%费率,但同样要满足三方支付的苛刻条件:必须接入官方内购作为备选、有苹果警告弹窗、仍然需要上报三方收入给苹果审计。

如果你对三方支付感兴趣可以看看我往期文档《三方支付真的香吗?日本iOS、Google三方支付调研报告 》,这篇虽然讲得是日本,但三方支付的接入流程和要求,全球都是一样的。

Flutter的状态管理工具

一、Provider

1.原理

Provider 本质上是基于 Flutter 的InheritedWidget 实现的,核心思想是数据自上而下传递,形成一个「数据提供者 - 消费者」的树形结构。

2、使用示例

2.1 定义可监听的状态模型(继承 ChangeNotifier) 核心:数据变化时调用 notifyListeners() 通知组件刷新


class LoginStatusModel extends ChangeNotifier {
  bool _isLogin = false;

  bool get isLogin => _isLogin;

  void updateLoginStatus(bool isLogin) {
    _isLogin = isLogin;
    notifyListeners();  // 关键:通知所有订阅的组件刷新
  }
}

2.2 使用 Provider或其子类,包裹 App实例,并将 状态模型实例作为值传递

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => LoginStatusModel(), 
      child: const MyApp()、
    ),
  );
}

2.3 使用状态数据,在需要监听数据变化的Widget中,使用Provider.of、Consumer获取数据:

  @override
  Widget build(BuildContext context) {
    // 使用 Consumer 监听 CounterModel
    return Consumer<LoginStatusModel>(
      builder: (context, loginStatus, child) {
        return Text('${loginStatus.isLogin}');
      },
    );
  }
}

// 也可以使用  Provider.of() 来获取:
Text('${Provider.of<LoginStatusModel>(context, listen: false).isLogin}')
特性 Provider.of<T>(context) Consumer<T> context.watch<T>() (推荐)
主要用途 灵活获取,常用于非 build 方法中 build 中获取并直接构建子 Widget build 中获取数据用于逻辑判断或属性赋值
是否监听变化 取决于 listen 参数 (默认 true) 是 (自动监听) 是 (自动监听)
代码位置 任意位置 (build 内/外,异步方法中) 只能在 build 方法的 return 树中 只能在 build 方法体内 (return 之前)
是否需要 builder 不需要 需要 (builder 回调) 不需要
典型场景 按钮点击事件、定时器、初始化逻辑 需要根据数据动态生成整个 Widget 时 需要根据数据决定 Widget 的属性 (颜色、文本) 时
性能优化 可设置 listen: false 避免不必要重绘 仅重建 Consumer 及其子节点 重建当前 Widget

A. Provider.of<T>(context)

这是最原始的方法。它的关键在于第二个参数 listen

  • listen: true (默认)

    • 行为:监听数据变化。如果数据变了,当前 Widget 会重建
    • 限制:只能在 build 方法中使用(因为重建需要触发 build)。
    • 缺点:如果在 build 中用默认值,会导致整个父 Widget 重绘,不够精细。
  • listen: false (常用)

    • 行为:不监听数据变化。只获取当前的实例对象。
    • 场景:在事件回调(如 onPressed)、initState、或者异步方法中调用修改数据的方法(如 increment())。
    • 优势:不会因为数据变化导致当前 Widget 无谓重绘。
Widget build(BuildContext context) {
  // ✅ 获取数据 (自动监听)
  final counter = context.watch<CounterModel>(); 
  
  // 可以在这里做逻辑处理
  final color = counter.count > 10 ? Colors.red : Colors.green;
  final text = counter.count > 10 ? '太多了!' : '正常';

  return Column(
    children: [
      // 使用处理后的数据
      Text(text, style: TextStyle(color: color)),
      
      // 按钮事件 (必须用 read 或 Provider.of(..., listen: false))
      ElevatedButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: Text('增加'),
      )
    ],
  );
}

B. Consumer 是一个 Widget。它的作用是将“获取数据”和“构建 UI”合二为一。

  • 特点:它提供了一个 builder 函数。只有当数据变化时,只有这个 Consumer 节点及其子节点会重绘,它的父兄弟节点不会重绘。
  • 场景:当你需要根据数据直接返回一个新的 Widget 结构时。

// ✅ 场景:只想让这段文字区域刷新,不影响周围的布局

Consumer<CounterModel>(
  builder: (context, counter, child) {
    // counter 就是 CounterModel 实例
    return Text(
      '当前计数: ${counter.count}',
      style: TextStyle(fontSize: 24, color: Colors.blue),
    );
  },
  // child 参数可用于优化:传递不变的子组件,避免每次重绘都重建它
  // child: Icon(Icons.star), 
)

C. context.watch<T>() —— Consumer 的语法糖 (现代推荐)

这是 provider 6.0+ 版本后最推荐的写法。它等价于 Provider.of<T>(context, listen: true),但写法更简洁。

  • 特点:直接在 build 方法体中使用,返回数据对象。
  • 场景:当你需要在 build 方法中获取数据,用来计算属性、做条件判断,或者组合多个数据源时。
  • 注意:调用 watch 的代码所在的 整个 Widget 的 build 方法 会在数据变化时重跑。如果该 Widget 很大,可能不如 Consumer 精准。
Widget build(BuildContext context) {
  // ✅ 获取数据 (自动监听)
  final counter = context.watch<CounterModel>(); 
  
  // 可以在这里做逻辑处理
  final color = counter.count > 10 ? Colors.red : Colors.green;
  final text = counter.count > 10 ? '太多了!' : '正常';

  return Column(
    children: [
      // 使用处理后的数据
      Text(text, style: TextStyle(color: color)),
      
      // 按钮事件 (必须用 read 或 Provider.of(..., listen: false))
      ElevatedButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: Text('增加'),
      )
    ],
  );
}

总结使用口诀

  • 改数据 (按钮/事件) ➡️ 用 read (或 of(..., listen: false))
  • 显数据 (局部刷新) ➡️ 用 Consumer
  • 显数据 (简单逻辑) ➡️ 用 watch
  • 初始化 (生命周期) ➡️ 用 of(..., listen: false)

二、RiverPod

1.原理

简洁表达:

  • 中心化管理:通过 ProviderContainerProviderScope)统一管理所有状态,状态封装在 Provider 中,脱离 Widget 上下文;

  • 精准订阅分发:基于 Ref 实现 Widget/Provider 对状态的订阅,状态变化时仅通知订阅者,最小化重建;

  • 无上下文 + 类型安全:解决了传统 Provider 的核心痛点,同时通过静态类型检查提升开发效率。

Ref (通常通过 WidgetRef 在 UI 中使用) 是整个状态管理系统的核心控制器上下文对象,是widget和provider,Provider和Provider沟通的唯一桥梁。

2、使用示例

  • 必须使用 ProviderScope 包裹整个应用。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    const ProviderScope( // 👈 必须包裹这里
      child: MyApp(),
    ),
  );
}
2.1 简单状态:@riverpod (替代 StateProvider)
// counter_provider.dart 文件,供后续订阅分发使用
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 使用 @riverpod 注解,运行 build_runner 后会自动生成 CounterProvider
@riverpod
class Counter extends _$Counter {
  @override
  int build() {
    // 初始值
    return 0;
  }

  // 定义修改状态的方法
  void increment() {
    state++; // 👈 直接修改 state 属性,自动通知监听者
  }

  void reset() {
    state = 0;
  }
}

2.2 复杂状态:AsyncNotifier (替代 FutureProvider + StateNotifier)

用于处理异步操作(如网络请求)并管理复杂状态。这是 Riverpod 最强大的部分。

import 'package:flutter_riverpod/flutter_riverpod.dart';

// 模拟用户模型
class User {
  final String name;
  final int age;
  User({required this.name, required this.age});
}

// 定义 AsyncNotifier
@riverpod
class CurrentUser extends _$CurrentUser {
  @override
  Future<User> build() async {
    // 模拟网络延迟
    await Future.delayed(const Duration(seconds: 2));
    
    // 模拟可能发生的错误
    // if (someCondition) throw Exception("Failed to load");

    return User(name: "Alice", age: 25);
  }

  // 修改用户信息的方法
  Future<void> updateAge(int newAge) async {
    state = const AsyncValue.loading(); // 手动设置加载状态
    
    try {
      await Future.delayed(const Duration(seconds: 1)); // 模拟 API 调用
      final user = state.value!; // 获取旧数据
      state = AsyncValue.data(User(name: user.name, age: newAge)); // 更新数据
    } catch (e, st) {
      state = AsyncValue.error(e, st); // 处理错误
    }
  }
}
2.3 组合状态:派生数据 (Derived State)

在一个 Provider 中读取另一个 Provider,实现数据联动。

@riverpod
String userNameRef(UserNameRef ref) {
  // 监听 CurrentUser Provider
  final userAsync = ref.watch(currentUserProvider);

  // 处理异步状态
  return userAsync.when(
    data: (user) => user.name,
    loading: () => "加载中...",
    error: (_, __) => "加载失败",
  );
}
A. 使用 ConsumerWidget (推荐)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 继承 ConsumerWidget
class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 1. 监听简单状态 (Counter)
    // ref.watch 会自动订阅,数据变化时重建此 Widget
    final count = ref.watch(counterProvider); 

    // 2. 监听异步状态 (CurrentUser)
    final userAsync = ref.watch(currentUserProvider);

    return Scaffold(
      appBar: AppBar(title: const Text("Riverpod Demo")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 显示异步用户数据
            userAsync.when(
              data: (user) => Text("你好, ${user.name} (年龄: ${user.age})"),
              loading: () => const CircularProgressIndicator(),
              error: (err, stack) => Text("错误: $err"),
            ),
            
            const SizedBox(height: 20),

            // 显示计数
            Text("计数: $count", style: const TextStyle(fontSize: 24)),
            
            const SizedBox(height: 20),

            // 3. 修改状态 (使用 ref.read 或 ref.notifier)
            ElevatedButton(
              onPressed: () {
                // 方式 A: 直接调用生成的 notifier 方法 (推荐)
                ref.read(counterProvider.notifier).increment();
                
                // 方式 B: 如果是 AsyncNotifier
                // ref.read(currentUserProvider.notifier).updateAge(26);
              },
              child: const Text("增加计数"),
            ),
            
            ElevatedButton(
              onPressed: () {
                // 触发异步更新
                ref.read(currentUserProvider.notifier).updateAge(30);
              },
              child: const Text("更新用户年龄 (异步)"),
            ),
          ],
        ),
      ),
    );
  }
}

B. 在非 Widget 类中使用 (Riverpod 的杀手锏)

由于不依赖 Context,你可以在任何地方(如路由守卫、服务类、甚至 main 函数之后)访问状态。

// 例如:在一个普通的 Dart 类中
class AnalyticsService {
  final Ref ref; // 注入 Ref

  AnalyticsService(this.ref);

  void logCount() {
    // 直接读取当前值,不订阅变化 (类似 listen: false)
    final currentCount = ref.read(counterProvider);
    print("当前计数是: $currentCount");
  }
  
  void subscribeToCount() {
    // 也可以手动监听变化
    ref.listen(counterProvider, (previous, next) {
      print("计数从 $previous 变成了 $next");
    });
  }
}

特性 Provider (旧) Riverpod (新)
依赖 Context ✅ 强依赖 (BuildContext) ❌ 无依赖 (WidgetRefRef)
类型安全 ⚠️ 运行时检查 (容易崩溃) ✅ 编译时检查 (配合代码生成)
异步支持 🆗 需要 FutureProvider 🚀 原生强大 (AsyncValue, when)
状态组合 😐 较难,容易嵌套地狱 🤩 极简 (ref.watch 其他 Provider)
测试难度 😫 需要 Mock Context 😃 极易 (直接创建 ProviderContainer)
代码量 多 (样板代码) 少 (配合 @riverpod 宏)
学习曲线 低 (但精通难) 中 (概念多,但逻辑清晰)
  1. 始终使用代码生成 (@riverpod) :不要手动编写 Provider(...),让宏帮你处理类型安全和样板代码。

  2. 拆分小 Provider:不要试图用一个 Provider 管理所有状态。将计数器、用户信息、主题设置拆分成不同的 Provider,然后按需组合。

  3. 善用 AsyncValue:处理异步数据时,利用 .when() 方法优雅地处理 loadingdataerror 三种状态,避免大量的 if/else 判断。

  4. 区分 watch 和 read

    • 在 build 方法中需要重建 UI时用 ref.watch
    • 事件回调(如按钮点击)中修改数据时用 ref.read(...).notifier
    • 非 build 环境(如服务类)中用 ref.read 或 ref.listen

以下是自己的理解修正:

  1. watch:是订阅者。它监听 Provider 的数据变化,一旦变化,自动触发 UI 刷新(或 Provider 重算)。

  2. read:是获取动作

    • 事件回调(如按钮点击)中,我们使用 ref.read(provider.notifier) 来获取控制器,然后调用它的方法来修改数据
    • 修改数据后,Riverpod 会自动通知所有 watch 该数据的地方进行刷新。
  3. notifier:是控制器(遥控器)。它持有修改数据的方法(如 increment)。

你的目的 应该用什么? 结果
显示数据 (Text, Image, List) ref.watch() 数据变,UI 自动刷新 ✅
按钮点击/手势 (修改数据) ref.read(...).notifier 获取控制器,修改数据 ✅
按钮点击/手势 (读取参数) ref.read() 获取当前值,用于逻辑判断 ✅
定时器/异步回调 ref.read() 获取最新值,避免闭包旧值 ✅
纯 Dart 类/服务 ref.read() 访问全局状态 ✅
Build 中显示数据 ref.read() ❌ 界面不会更新 (Bug)

Swift 从入门到精通-第三篇

第9章:协议与扩展

9.1 协议 (Protocol) - 定义接口

协议定义了一组方法、属性或其他要求的蓝图:

protocol Greetable {
    var name: String { get }  // 只读属性要求
    func greet()              // 方法要求
}

protocol Describable {
    var description: String { get }
}

遵循协议:

struct User: Greetable, Describable {
    let name: String
    let email: String
    
    // 必须实现协议要求
    func greet() {
        print("Hi, I'm (name)!")
    }
    
    var description: String {
        return "User: (name) <(email)>"
    }
}

let user = User(name: "Alice", email: "alice@example.com")
user.greet()        // Hi, I'm Alice!
print(user.description)

9.2 协议扩展 - 提供默认实现

// 在扩展中提供默认实现
extension Greetable {
    func greet() {
        print("Hello, my name is (name)")
    }
}

struct Employee: Greetable {
    let name: String
    // 不需要实现 greet(),使用默认实现
}

let employee = Employee(name: "Bob")
employee.greet()  // Hello, my name is Bob

协议扩展的强大之处:

  • 可以给协议添加默认行为
  • 遵循者可以选择使用默认实现或自定义
  • 这是"面向协议编程"的核心

9.3 协议组合

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

struct Person: Named, Aged {
    var name: String
    var age: Int
}

// 函数接受同时遵循多个协议的类型
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday (celebrator.name), you're (celebrator.age)!")
}

let person = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: person)

9.4 带关联类型的协议

protocol Container {
    associatedtype Item  // 关联类型
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

struct IntStack: Container {
    // 自动推断 Item = Int
    private var items: [Int] = []
    
    var count: Int { items.count }
    
    mutating func append(_ item: Int) {
        items.append(item)
    }
    
    subscript(i: Int) -> Int {
        return items[i]
    }
}

// 也可以显式指定
double Stack: Container {
    typealias Item = Double  // 显式指定
    // ...
}

9.5 扩展系统类型

// 给 Int 添加方法
extension Int {
    var squared: Int {
        return self * self
    }
    
    func times(_ action: () -> Void) {
        for _ in 0..<self {
            action()
        }
    }
}

print(5.squared)  // 25
3.times {
    print("Hello!")
}
// Hello!
// Hello!
// Hello!

// 给 Collection 添加方法
extension Collection {
    var isNotEmpty: Bool {
        return !isEmpty
    }
}

[1, 2, 3].isNotEmpty  // true

第10章:泛型编程

10.1 为什么要用泛型?

想象你要写交换两个值的函数:

// 只能交换整数
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

// 只能交换字符串
func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

重复代码!用泛型可以写一个通用的:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)

<T>类型参数,代表任意类型。

10.2 泛型类型

// 泛型栈
struct Stack<Element> {
    private var items: [Element] = []
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
    
    var topItem: Element? {
        return items.last
    }
    
    var isEmpty: Bool {
        return items.isEmpty
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop()!)  // 2

var stringStack = Stack<String>()
stringStack.push("hello")
stringStack.push("world")

10.3 类型约束

// 要求 T 必须遵循 Comparable 协议
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])

常用类型约束:

  • T: Equatable - 可以比较相等
  • T: Comparable - 可以比较大小
  • T: Hashable - 可以作为字典的 key
  • T: SomeProtocol - 遵循某个协议

10.4 关联类型与 where 子句

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

// 要求两个容器的 Item 相同且可比较
func allItemsMatch<C1: Container, C2: Container>(_ container1: C1, _ container2: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {
    
    if container1.count != container2.count {
        return false
    }
    
    for i in 0..<container1.count {
        if container1[i] != container2[i] {
            return false
        }
    }
    return true
}

10.5 不透明返回类型 (some)

protocol Shape {
    func draw() -> String
}

struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result = [String]()
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")
    }
}

struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        return Array(repeating: line, count: size).joined(separator: "\n")
    }
}

// 返回遵循 Shape 协议的某种类型,但隐藏具体是什么
func makeShape() -> some Shape {
    return Triangle(size: 3)
}

// 也可以返回不同的,只要都是 Shape
func makeRandomShape() -> some Shape {
    Bool.random() ? Triangle(size: 3) : Square(size: 3)
}

some 的好处:

  • 调用者不需要知道具体类型
  • 编译器可以进行类型优化
  • 在 SwiftUI 中非常常用(some View

第11章:错误处理

11.1 定义错误类型

enum VendingMachineError: Error {
    case invalidSelection                    // 选择无效
    case insufficientFunds(coinsNeeded: Int) // 金额不足,附带需要多少钱
    case outOfStock                          // 缺货
}

任何遵循 Error 协议的类型都可以表示错误。

11.2 抛出错误

class VendingMachine {
    var inventory = [
        "Candy Bar": Item(price: 12, count: 7),
        "Chips": Item(price: 10, count: 4),
        "Pretzels": Item(price: 7, count: 11)
    ]
    var coinsDeposited = 0
    
    func vend(itemNamed name: String) throws {
        guard let item = inventory[name] else {
            throw VendingMachineError.invalidSelection
        }
        
        guard item.count > 0 else {
            throw VendingMachineError.outOfStock
        }
        
        guard item.price <= coinsDeposited else {
            throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }
        
        // 执行购买逻辑
        coinsDeposited -= item.price
        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem
        
        print("Dispensing (name)")
    }
}

throws 标记表示这个函数可能抛出错误。

11.3 处理错误

do-catch

let vendingMachine = VendingMachine()
vendingMachine.depositCoins(8)

do {
    try vendingMachine.vend(itemNamed: "Candy Bar")
} catch VendingMachineError.invalidSelection {
    print("Invalid selection.")
} catch VendingMachineError.outOfStock {
    print("Out of stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("Insufficient funds. Please insert an additional (coinsNeeded) coins.")
} catch {
    print("Unexpected error: (error)")
}

try? - 转换为可选值

// 成功返回结果,失败返回 nil
if let result = try? someThrowingFunction() {
    // 使用结果
} else {
    // 处理失败
}

// 等效于
do {
    let result = try someThrowingFunction()
} catch {
    // 忽略错误
}

try! - 强制解包(危险!)

// 确定不会失败时使用
let result = try! someThrowingFunction()
// 如果失败了,程序会崩溃

11.4 defer - 清理资源

func processFile(filename: String) throws -> String {
    let file = try openFile(filename)
    
    defer {
        closeFile(file)  // 无论如何都会执行
    }
    
    if filename.isEmpty {
        throw FileError.invalidName  // 先执行 defer,再抛出错误
    }
    
    return try readFile(file)
}  // 正常返回也会执行 defer

11.5 Result 类型

enum NetworkError: Error {
    case badURL
    case noData
    case decodingError
}

func fetchUser(completion: (Result<User, NetworkError>) -> Void) {
    // 模拟网络请求
    let success = Bool.random()
    if success {
        completion(.success(User(name: "Alice")))
    } else {
        completion(.failure(.noData))
    }
}

// 使用
fetchUser { result in
    switch result {
    case .success(let user):
        print("Got user: (user.name)")
    case .failure(let error):
        print("Error: (error)")
    }
}

第12章:内存管理

12.1 ARC (自动引用计数)

Swift 使用 ARC 自动管理内存:

  • 每次创建实例,引用计数 +1
  • 每次引用消失,引用计数 -1
  • 引用计数为 0,内存被释放

你不需要手动管理,但要理解引用关系。

12.2 强引用循环问题

class Person {
    let name: String
    var apartment: Apartment?  // 强引用
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("(name) is being deinitialized")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?  // 强引用
    
    init(unit: String) {
        self.unit = unit
    }
    
    deinit {
        print("Apartment (unit) is being deinitialized")
    }
}

var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil
// 没有打印 deinit 消息!内存泄漏了!

问题: Person 持有 Apartment,Apartment 持有 Person,形成一个环,引用计数永远不会归零。

12.3 弱引用 (Weak Reference)

class Apartment {
    let unit: String
    weak var tenant: Person?  // 弱引用!
    
    init(unit: String) {
        self.unit = unit
    }
    
    deinit {
        print("Apartment (unit) is being deinitialized")
    }
}

// 现在当 john = nil,Person 实例会被释放
// 然后 Apartment 的 tenant 自动变成 nil

弱引用的特点:

  • 不增加引用计数
  • 指向的实例释放后自动变成 nil
  • 必须是可选类型(因为可能为 nil)

什么时候用弱引用?

  • 父子关系中的子(如 Apartment 和 tenant)
  • 委托模式 (Delegate pattern)

12.4 无主引用 (Unowned Reference)

class Customer {
    let name: String
    var card: CreditCard?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("(name) is being deinitialized")
    }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer  // 无主引用
    
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    
    deinit {
        print("Card #(number) is being deinitialized")
    }
}

var alice: Customer? = Customer(name: "Alice")
alice!.card = CreditCard(number: 1234_5678_9012_3456, customer: alice!)

alice = nil
// 两个实例都被释放

无主引用的特点:

  • 不增加引用计数
  • 不是可选类型
  • 指向的实例释放后变成 dangling pointer(悬挂指针)

什么时候用无主引用?

  • 确定引用的实例永远比自己活得长
  • 不会造成循环引用的强关系

12.5 闭包中的循环引用

class HTMLElement {
    let name: String
    let text: String?
    
    // 闭包捕获 self,形成循环引用!
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<(self.name)>(text)</(self.name)>"
        } else {
            return "<(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("(name) is being deinitialized")
    }
}

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "Hello")
let html = paragraph!.asHTML()
paragraph = nil
// 没有 deinit!循环引用!

解决:捕获列表

lazy var asHTML: () -> String = { [weak self] in
    guard let self = self else {
        return ""
    }
    if let text = self.text {
        return "<(self.name)>(text)</(self.name)>"
    } else {
        return "<(self.name) />"
    }
}

捕获列表语法:

  • [weak self] - 弱引用 self
  • [unowned self] - 无主引用 self
  • [x = someValue] - 捕获时复制值而不是引用

Buildable Folder & Group & Folder Reference in Xcode

深入理解代替单纯记忆

问题背景

  • 在开发iOS项目时,希望将一堆图片资源放入Main Bundle中,但又不希望资源在Bundle的最顶层目录中,希望自定义目录
  • 但一时想不到该如何解决,于是想到FolderGroup等概念
  • 经过简单搜索后,发现Xcode对于这两个概念的定义还是有些差异的
  • 于是继续查阅学习了一番,编写本文,方便后续查阅和分享

本文提到的内容,参考的Xcode版本为26.0(17A324)和26.3(17C529)

Buildable Folder

  • Buildable Folder是自Xcode 16(2024年6月)引入的概念,初衷是为了减少代码管理中的冲突问题
  • 后续新建的工程或者新建Folder时,默认都是Buildable Folder

官方原文如下:

Minimize project file changes and avoid conflicts with buildable folder references. Convert an existing group to a buildable folder with the Convert to Folder context menu item in the Project Navigator. Buildable folders only record the folder path into the project file without enumerating the contained files, minimizing diffs to the project when your team adds or removes files, and avoiding source control conflicts. To use a folder as an opaque copiable resource, the default behavior before Xcode 16, uncheck the Build Folder Contents option in the File Inspector.

Buildable Folder如何降低代码冲突

  1. 先添加1个普通Group--BuildableFolderTest,project文件的变化如下所示: image.png

  2. 然后向BuildableFolderTest Group中添加ABC.swift文件后,project文件的变化如下: image.pngimage.png

    这说明Group目录下的文件,都要在project文件中进行记录

  3. 继续,将BuildableFolderTest Group通过Convert to Folder选项转为Folder(Buildable Folder)后,project文件的变化如下: image.pngimage.png

  4. 然后再向BuildableFolderTest这个Folder中添加DEF.swift文件后,发现project文件没有任何变化

所以,project文件仅记录了Folder自身,至于目录中的文件是不会记录在project文件中,所以会减少因团队多人同时修改Project文件导致的代码冲突

Apply to Each File vs Apply Once to Folder

当创建Folder(Buildable Folder)后,选中Folder,在File inspector中会看到有个Build Rule,有两个选择:Apply to Each FileApply Once to Folder,默认是Apply to Each File

image.png

Apply Once to Folder

Apply Once to Folder开启后,project文件是什么样?

image.pngimage.pngimage.png

当开启该模式时,通过查看目录下的每个文件可以看出,文件是没有Target归属的概念的。同样,在该目录下创建新文件也不需要选择Target

再配合Xcode Buildable Folders中所提到的To use a folder as an opaque copiable resource, the default behavior before Xcode 16, uncheck the Build Folder Contents option in the File Inspector.

其实,Apply Once to Folder就是Xcode 16之前的Folder,之前叫Folder Reference (在Xcode 16之前,创建Folder时,官方名称就叫做Folder Reference)

  • Folder Reference一般是用作资源包,目录下不包含源代码
  • 另一个Folder Reference重要作用是可以在Bundle中自定义目录

Buildable Folder vs Folder Reference

Buildable Folder顾名思义,其中的内容是由编译系统参与的

  • 所以Buildable Folder中可以放源代码文件,并可以参与编译,打包到最终可执行文件中;也可以制定源文件的Target
  • Folder Reference则保留老的逻辑,不参与编译,用作资源包,即使放入源代码文件也无法选择Target,只能当做普通文件资源处理

Create Group with Folder

同样是在Xcode 16开始的另一个变化是,创建Group时由原来的不自动创建磁盘物理目录(Folder)变为自动创建。当然,仍可以创建没有FolderGroup,原文如下:

Create groups with associated folders by default when using the New Group and New Group from Selection commands in the Project Navigator. To create a group without a folder, hold the Option key in the context menu to reveal the New Group without Folder variant of the command.

[Group without Folder] vs [Group] vs [Folder(Buildable Folder)] vs [Folder Reference]

特性 Group without Folder Group Buildable Folder Folder Reference
Project Navigator 图标 image.png image.png image.png image.png
是否对应磁盘目录 ❌ 不必须 ✅ 必须 ✅ 必须 ✅ 必须
工程结构是否可与磁盘不同 ✅ 可以 ❌ 基本一致 ❌ 必须一致 ❌ 必须一致
.pbxproj 是否记录每个文件 ✅ 会 ✅ 会 ❌ 不会 ❌ 不会
新增文件是否修改 .pbxproj ✅ 会 ✅ 会 ❌ 不会 ❌ 不会
Git 冲突概率
是否参与编译系统
是否自动编译源码 ✅(自动发现目录中的源码)
Bundle 中是否保留目录结构 ❌ 通常不会 ❌ 通常不会 ❌ 通常不会 会保留(如果被加入 Bundle)
默认是否进入 Bundle ❌ 否 ❌ 否 ❌ 否 仅在选中 Target 时自动加入
典型用途 逻辑分组 常规项目结构 源码目录 资源目录
  • 当前(Xcode 26),默认的Group和Folder组合是Group with Folder + Buildable Folder。这可能也意味着这两项是日常最常用的

回答开始的问题

  • 既然是想打包资源放入Bundle,并自定义目录,那必然是Folder Reference

参考

iOS必看!Deepseek给的Runtime实现原理,通俗易懂~

iOS Runtime 消息转发机制完全解析

写在前面

在Objective-C的世界里,方法调用并不是像C++那样在编译时就确定要执行的函数地址,而是一个运行时动态绑定的过程。当我们写下 [receiver message] 这样的代码时,编译器实际上会将其转换为 objc_msgSend(receiver, @selector(message)) 的调用。这个 objc_msgSend 函数会负责在接收者所属的类及其父类的方法列表中查找对应的实现并执行。

那么问题来了:如果一直找到根类NSObject都没有找到这个方法的实现,会发生什么?

很多开发者都见过这样的崩溃信息:unrecognized selector sent to instance 0xXXXXXXXX。这正是因为消息发送失败,而Runtime也没有找到合适的方式处理这条消息,最终通过 doesNotRecognizeSelector: 抛出的异常。

但在这个崩溃发生之前,Objective-C的Runtime给了我们三次"拯救"的机会,这就是本文要详细讲解的消息转发机制


第一章:消息发送机制回顾

在深入探讨消息转发之前,有必要先回顾一下完整的消息发送流程,因为消息转发正是这个流程中处理失败情况的最后保障。

1.1 objc_msgSend的工作流程

当我们向一个对象发送消息时,Runtime系统会按照以下步骤查找方法的实现:

  1. 检查目标对象是否为nil:如果接收者为nil,Objective-C的特性是忽略该消息,程序不会崩溃(这在很多情况下简化了代码逻辑)。如果为nil且消息有返回值,基本数据类型的返回值为0,对象类型的返回值为nil。

  2. 查找缓存:每个类都有一个缓存(cache),用于存储最近使用过的方法。Runtime会首先在该类的缓存中查找方法的实现(IMP)。如果找到,直接调用该实现。

  3. 查找当前类的方法列表:如果在缓存中没有找到,Runtime会从当前类的方法列表中查找。方法列表以数组形式组织,查找过程会遍历整个列表(已排序的列表使用二分查找,否则线性查找)。

  4. 沿着继承链向上查找:如果在当前类中没有找到,Runtime会沿着继承链逐级向上查找父类的方法列表和缓存,直到根类NSObject为止。

  5. 动态方法解析:如果一直找到根类都没有找到方法的实现,Runtime会进入"动态方法解析"阶段,给类一个机会动态添加方法的实现。

  6. 消息转发:如果动态方法解析没有添加实现(或者添加后仍然无法处理),Runtime会进入"消息转发"流程。

  7. 抛出异常:如果所有转发尝试都失败,最终会调用 doesNotRecognizeSelector: 抛出异常,程序崩溃。

这个流程可以用下面的流程图清晰地展示:

flowchart TD
    A[向对象发送消息] --> B{接收者为nil?}
    B -->|是| C[忽略消息/返回0/nil]
    B -->|否| D[查找缓存]
    
    D --> E{缓存中找到IMP?}
    E -->|是| F[调用IMP]
    E -->|否| G[在当前类方法列表中查找]
    
    G --> H{当前类中找到?}
    H -->|是| F
    H -->|否| I[在父类方法列表中查找]
    
    I --> J{父类中找到?}
    J -->|是| F
    J -->|否| I
    
    J -->|一直查到NSObject仍未找到| K[动态方法解析]
    
    K --> L{动态添加了实现?}
    L -->|是| F
    L -->|否| M[消息转发流程]
    
    M --> N{转发成功?}
    N -->|是| F
    N -->|否| O[doesNotRecognizeSelector:\n抛出异常]

1.2 方法的本质:SEL、IMP与Method

要深入理解消息转发,我们需要先了解Objective-C中方法的三个核心概念:

SEL(选择器):是方法的名字,在Runtime中用 objc_selector 结构体表示。在运行时,不同类的同名方法的选择器是相同的。SEL在Runtime中会被唯一化,因此可以使用 == 来比较两个SEL是否相等。

IMP(函数指针):是方法的实现,本质上是一个函数指针,指向方法实现的首地址。它的定义如下:

typedef id (*IMP)(id self, SEL _cmd, ...);

每个IMP都至少包含两个参数:self(消息接收者)和_cmd(这个方法的SEL)。

Method(方法):是用于表示方法定义的结构体,包含三个成员:

struct method_t {
    SEL name;      // 方法名
    const char *types;  // 方法类型编码
    IMP imp;       // 方法实现
}

当我们调用一个方法时,就是从SEL到IMP的映射过程。Runtime维护了每个类的方法列表(method list),这个列表存储了该类定义的所有方法。消息转发机制本质上是在这个映射过程失败后的补救措施。


第二章:消息转发的三个阶段

当消息发送流程无法找到对应的IMP时,Runtime会启动消息转发机制。这个机制分为三个阶段,每个阶段都给开发者一次处理这条"无法识别"的消息的机会。

2.1 第一阶段:动态方法解析

这是消息转发的第一道防线。当Runtime在当前类和父类中都找不到方法的实现时,会首先调用 +resolveInstanceMethod:(对于实例方法)或 +resolveClassMethod:(对于类方法)。

2.1.1 resolveInstanceMethod的工作原理

这个方法的定义如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel

当这个方法被调用时,Runtime给了我们一个机会:可以动态地为这个SEL添加一个实现。如果添加成功并返回YES,Runtime会重新启动消息发送流程,这次就能找到方法的实现了。

这个方法最典型的应用场景是处理 @dynamic 属性。@dynamic 告诉编译器不要自动生成属性的getter和setter方法,我们会在运行时动态提供它们。

2.1.2 实战:动态添加方法实现

让我们通过一个具体的例子来理解这个过程:

#import <objc/runtime.h>

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;  // 注意:我们使用@dynamic
@end

@implementation Person
@dynamic name;  // 告诉编译器不要自动生成getter/setter

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(name)) {
        // 动态添加getter方法
        class_addMethod(self, sel, (IMP)dynamicNameGetter, "@@:");
        return YES;
    }
    else if (sel == @selector(setName:)) {
        // 动态添加setter方法
        class_addMethod(self, sel, (IMP)dynamicNameSetter, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// getter方法的实现
id dynamicNameGetter(id self, SEL _cmd) {
    // 通过关联对象获取存储的值
    return objc_getAssociatedObject(self, @selector(name));
}

// setter方法的实现
void dynamicNameSetter(id self, SEL _cmd, NSString *newName) {
    // 通过关联对象存储值
    objc_setAssociatedObject(self, @selector(name), newName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

现在,当我们调用:

Person *p = [[Person alloc] init];
[p setName:@"张三"];
NSLog(@"%@", [p name]);  // 输出:张三

尽管Person类并没有真正实现name的getter和setter方法,但在消息发送过程中,Runtime调用了 resolveInstanceMethod:,我们动态添加了这两个方法的实现,因此程序能够正常运行。

2.1.3 方法签名的类型编码

在调用 class_addMethod 时,我们需要指定方法的类型编码(types)。这个编码字符串描述了方法的返回类型和参数类型。例如:

  • "v@:" 表示返回void,有两个参数:id和SEL(即标准的实例方法)
  • "@@" 表示返回id,有两个参数:id和SEL(标准的getter方法)
  • "v@:@" 表示返回void,有三个参数:id、SEL和id(标准的setter方法)

完整的类型编码表:

编码 含义
c char
i int
s short
l long
q long long
C unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B BOOL/C++ bool
v void
* char* (字符串)
@ id (对象)
# Class (类对象)
: SEL (选择器)
^type 指向type的指针

2.1.4 类方法的动态解析

对于类方法,我们需要重写 +resolveClassMethod:

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(classMethod)) {
        // 注意:这里添加方法的目标是元类(metaclass)
        Class metaClass = objc_getMetaClass(class_getName(self));
        class_addMethod(metaClass, sel, (IMP)dynamicClassMethodImp, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

需要注意的是,类方法是存储在元类(metaclass)中的,因此我们需要获取元类来添加类方法的实现。

2.1.5 动态方法解析的时机

动态方法解析发生在消息发送流程失败之后,但在消息转发之前。如果你希望每次调用这个方法时都能走动态解析,注意这个方法只会被调用一次(因为一旦添加了实现,后续调用就能直接找到IMP了)。

2.2 第二阶段:快速消息转发

如果动态方法解析没有添加实现(或者返回NO),Runtime会进入消息转发的第二阶段:快速消息转发。

这个阶段的核心是 forwardingTargetForSelector: 方法。Runtime会调用这个方法,期望它能返回一个能够处理这条消息的对象。

2.2.1 forwardingTargetForSelector的定义

- (id)forwardingTargetForSelector:(SEL)aSelector

这个方法的职责是:当对象无法处理某个消息时,返回一个能够处理该消息的对象。Runtime会将原始消息转发给这个返回的对象,就好像它才是原始的消息接收者一样。

这个机制非常高效,因为它只是简单地改变消息的接收者,不需要创建 NSInvocation 对象,也没有复杂的参数处理。

2.2.2 实战:将消息转发给备用对象

假设我们有一个 Person 类,它不包含 run 方法,但我们有一个 Car 类实现了 run 方法:

@interface Car : NSObject
- (void)run;
@end

@implementation Car
- (void)run {
    NSLog(@"Car is running");
}
@end

@interface Person : NSObject
@end

@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        // 返回一个可以处理run消息的Car对象
        return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

现在执行以下代码:

Person *person = [[Person alloc] init];
[person run];  // 输出:Car is running

尽管 Person 对象并没有 run 方法,但通过 forwardingTargetForSelector:,我们将 run 消息转发给了 Car 对象,程序能够正常运行。

2.2.3 模拟多重继承

Objective-C不支持多重继承,但通过快速消息转发,我们可以实现类似多重继承的效果。一个对象可以将自己没有实现的方法转发给其他对象,从外部看就像这个对象继承了多个类的功能。

例如,我们可以创建一个类,它能够处理来自多个不同类的方法:

@interface MultiClass : NSObject
@property (nonatomic, strong) Car *car;
@property (nonatomic, strong) House *house;
@end

@implementation MultiClass
- (instancetype)init {
    if (self = [super init]) {
        _car = [[Car alloc] init];
        _house = [[House alloc] init];
    }
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([_car respondsToSelector:aSelector]) {
        return _car;
    } else if ([_house respondsToSelector:aSelector]) {
        return _house;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

这样,MultiClass 的实例就能同时响应 CarHouse 的方法,达到了类似多重继承的效果。

2.2.4 注意事项

使用 forwardingTargetForSelector: 时有几点需要注意:

  1. 不要返回self:如果在这个方法中返回self,会造成无限循环,因为Runtime会再次尝试向self发送消息。
  2. 这个方法主要用于转发给其他对象,不适合修改消息本身。
  3. 返回的对象不必与原始接收者有继承关系,任何对象都可以。
  4. 如果返回nil或self,则进入下一阶段:完整消息转发。

2.3 第三阶段:完整消息转发

如果前两个阶段都无法处理消息,Runtime会进入最后一个阶段:完整消息转发。这是消息转发机制中最强大、最灵活但也最复杂的阶段。

这个阶段涉及两个方法:

  • methodSignatureForSelector::获取方法的签名(参数类型和返回类型)
  • forwardInvocation::转发封装了消息的 NSInvocation 对象
flowchart TD
    A[消息转发第二阶段返回nil] --> B[调用methodSignatureForSelector:]
    
    B --> C{返回有效的方法签名?}
    C -->|否| D[调用doesNotRecognizeSelector:\n抛出异常]
    C -->|是| E[创建NSInvocation对象]
    
    E --> F[调用forwardInvocation:\n并将NSInvocation传入]
    
    F --> G{在forwardInvocation:中\n处理消息?}
    G -->|否| D
    G -->|是| H[消息处理成功]
    
    H --> I[将返回值传递给\n原始消息发送者]

2.3.1 methodSignatureForSelector: 的作用

methodSignatureForSelector: 的定义如下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

Runtime调用这个方法的目的是获取方法的签名信息,包括方法的返回类型和参数类型。有了这些信息,Runtime才能创建 NSInvocation 对象。

如果这个方法返回nil,Runtime会直接调用 doesNotRecognizeSelector: 并抛出异常,程序崩溃。因此,在实现完整消息转发时,我们必须为无法处理的消息提供一个有效的方法签名。

2.3.2 创建方法签名

方法签名可以通过多种方式创建:

// 方式1:使用字符串创建(类型编码)
NSMethodSignature *signature1 = [NSMethodSignature signatureWithObjCTypes:"v@:"];

// 方式2:从已有方法获取
NSMethodSignature *signature2 = [self methodSignatureForSelector:@selector(existingMethod)];

// 方式3:从协议获取
struct objc_method_description desc = protocol_getMethodDescription(protocol, selector, YES, YES);
NSMethodSignature *signature3 = [NSMethodSignature signatureWithObjCTypes:desc.types];

类型编码字符串的格式和之前 class_addMethod 中使用的格式一致。

2.3.3 forwardInvocation: 的核心作用

forwardInvocation: 的定义如下:

- (void)forwardInvocation:(NSInvocation *)anInvocation

methodSignatureForSelector: 返回了有效的方法签名后,Runtime会创建一个 NSInvocation 对象,该对象封装了这条消息的所有信息:

  • 消息的目标(target)
  • 消息的选择器(selector)
  • 所有的参数
  • 等待填充的返回值

然后将这个 NSInvocation 对象作为参数传递给 forwardInvocation: 方法。在这个方法中,我们可以:

  1. 将消息转发给其他对象
  2. 修改消息的选择器、参数或目标
  3. 直接处理消息并设置返回值
  4. 甚至"吃掉"消息,什么都不做(这样就不会崩溃)

2.3.4 实战:完整消息转发的实现

下面是一个完整的示例,演示如何实现完整消息转发:

@interface Person : NSObject
@end

@interface Car : NSObject
- (void)run;
@end

@implementation Car
- (void)run {
    NSLog(@"Car is running");
}
@end

@implementation Person
// 第一步:提供方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        // 返回run方法的签名:"v@:" 表示返回void,两个参数:id, SEL
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 第二步:转发调用
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    // 创建备用对象
    Car *car = [[Car alloc] init];
    
    // 检查备用对象是否能响应这个选择器
    if ([car respondsToSelector:selector]) {
        // 将消息转发给备用对象
        [anInvocation invokeWithTarget:car];
    } else {
        // 如果备用对象也不能处理,调用父类实现(最终会抛出异常)
        [super forwardInvocation:anInvocation];
    }
}
@end

执行测试代码:

Person *person = [[Person alloc] init];
[person run];  // 输出:Car is running

2.3.5 修改消息内容后转发

完整消息转发的一个强大之处在于,我们可以在转发前修改消息的内容。例如,我们可以修改方法的选择器:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL originalSelector = [anInvocation selector];
    
    if (originalSelector == @selector(run)) {
        // 修改选择器为drive
        [anInvocation setSelector:@selector(drive)];
        
        Car *car = [[Car alloc] init];
        if ([car respondsToSelector:@selector(drive)]) {
            [anInvocation invokeWithTarget:car];
            return;
        }
    }
    
    [super forwardInvocation:anInvocation];
}

我们也可以修改参数:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    if (selector == @selector(setAge:)) {
        // 获取原始参数
        int age;
        [anInvocation getArgument:&age atIndex:2]; // 前两个参数是self和_cmd
        
        // 修改参数值(例如:限制年龄范围)
        if (age < 0) age = 0;
        if (age > 150) age = 150;
        
        // 设置修改后的参数
        [anInvocation setArgument:&age atIndex:2];
    }
    
    // 转发给实际处理的对象
    if ([_realObject respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:_realObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

2.3.6 处理返回值

NSInvocation 也能处理返回值。我们可以从 anInvocation 中获取返回值,修改它,或者设置自己的返回值:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 先尝试转发给备用对象
    if ([_backup respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:_backup];
        
        // 获取返回值
        char returnType[10];
        strcpy(returnType, [[anInvocation methodSignature] methodReturnType]);
        
        if (returnType[0] == '@') { // 返回对象类型
            id result = nil;
            [anInvocation getReturnValue:&result];
            
            // 可以修改返回值
            if (result == nil) {
                result = @"Default Value";
                [anInvocation setReturnValue:&result];
            }
        }
        return;
    }
    
    [super forwardInvocation:anInvocation];
}

2.3.7 转发给多个对象

完整消息转发甚至可以将一个消息转发给多个对象。这在某些设计模式中很有用,例如观察者模式或责任链模式:

@interface MessageChain : NSObject
@property (nonatomic, strong) NSArray *handlers;
@end

@implementation MessageChain
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    BOOL handled = NO;
    
    for (id handler in self.handlers) {
        if ([handler respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:handler];
            handled = YES;
            // 可以选择是否继续转发给下一个处理器
            // break;
        }
    }
    
    if (!handled) {
        [super forwardInvocation:anInvocation];
    }
}
@end

2.4 三个阶段的关系与选择

这三个阶段是递进的关系:如果第一阶段处理了,第二阶段就不会触发;如果第二阶段处理了,第三阶段就不会触发。

选择使用哪个阶段取决于你的需求:

  • 动态方法解析:适合在运行时动态添加方法实现,例如处理 @dynamic 属性、实现轻量级的代理模式。
  • 快速消息转发:适合简单地将消息转发给另一个对象,性能最好,但不能修改消息内容。
  • 完整消息转发:最强大、最灵活,可以修改消息内容、参数、返回值,甚至可以将消息转发给多个对象,但性能开销也最大。

第三章:深入源码分析

了解理论之后,让我们深入Runtime的源码,看看消息转发机制究竟是如何实现的。这里我们基于苹果开源的objc4源码进行分析。

3.1 从消息发送到消息转发的转折点

objc_msgSend 的核心实现中,如果方法查找失败,会调用 lookUpImpOrForward 函数。这个函数的简化逻辑如下:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver) {
    IMP imp = nil;
    bool triedResolver = NO;
    
    // 尝试从缓存和方法列表中查找
    // ...
    
    // 如果没有找到实现
    if (resolver && !triedResolver) {
        // 调用动态方法解析
        _class_resolveMethod(cls, sel, inst);
        triedResolver = YES;
        // 重新尝试查找
        goto retry;
    }
    
    // 动态解析失败,返回转发IMP
    imp = (IMP)_objc_msgForward_impcache;
    
    return imp;
}

关键点在于:当动态方法解析失败后,lookUpImpOrForward 会返回一个特殊的IMP:_objc_msgForward_impcache。这个IMP指向的是消息转发的入口函数。

3.2 消息转发的入口:__objc_msgForward

_objc_msgForward_impcache 最终会调用到 __objc_msgForward 函数。在x86_64架构的汇编实现中,这个函数的逻辑大致是:

ENTRY __objc_msgForward
    // 跳转到消息转发的核心实现
    jmp __objc_forward_handler
END_ENTRY __objc_msgForward

__objc_forward_handler 是一个C函数,它会调用到CoreFoundation框架中的 __forwarding__ 函数。这就是消息转发的真正核心实现。

3.3 CoreFoundation中的__forwarding__函数

__forwarding__ 函数是消息转发机制的心脏。虽然苹果没有开源CoreFoundation的全部代码,但通过反汇编和分析,我们可以还原其大致逻辑:

int __forwarding__(void *frameStackPointer, int isStret) {
    // 获取消息的接收者和选择器
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + sizeof(id));
    
    // 尝试快速转发
    id forwardingTarget = nil;
    if ([receiver respondsToSelector:@selector(forwardingTargetForSelector:)]) {
        forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget != nil && forwardingTarget != receiver) {
            // 转发给目标对象
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }
    
    // 快速转发失败,尝试完整转发
    NSMethodSignature *signature = nil;
    if ([receiver respondsToSelector:@selector(methodSignatureForSelector:)]) {
        signature = [receiver methodSignatureForSelector:sel];
    }
    
    if (signature == nil) {
        // 没有方法签名,无法继续
        [receiver doesNotRecognizeSelector:sel];
        return 0;
    }
    
    // 创建NSInvocation对象
    NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:signature frame:frameStackPointer];
    
    // 调用forwardInvocation:
    if ([receiver respondsToSelector:@selector(forwardInvocation:)]) {
        [receiver forwardInvocation:invocation];
    } else {
        [receiver doesNotRecognizeSelector:sel];
    }
    
    // 获取返回值
    // ...
    return 0;
}

从这个伪代码可以看出,__forwarding__ 函数完整地实现了我们之前讨论的消息转发流程:

  1. 尝试快速转发
  2. 如果快速转发没有返回合适的对象,尝试获取方法签名
  3. 如果方法签名有效,创建 NSInvocation 并调用 forwardInvocation:
  4. 如果所有步骤都失败,调用 doesNotRecognizeSelector: 抛出异常

3.4 日志调试技巧

Runtime提供了一个调试函数 instrumentObjcMessageSends,可以让我们查看消息发送和转发的详细过程:

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 开启消息发送日志
        instrumentObjcMessageSends(YES);
        
        Person *person = [[Person alloc] init];
        [person run];
        
        // 关闭日志
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

运行程序后,在 /tmp/msgSends- 目录下会生成日志文件,内容类似于:

+ Person NSObject initialize
+ Person NSObject new
- Person NSObject init
- Person forwardingTargetForSelector: run
- Person methodSignatureForSelector: run
- Person forwardInvocation:
- Person doesNotRecognizeSelector: run

通过这个日志,我们可以清楚地看到消息转发的每一步调用过程,对于理解和调试消息转发非常有帮助。


第四章:消息转发的应用场景

消息转发机制不仅仅是理论上的知识点,它在实际开发中有很多实用的应用场景。

4.1 防止崩溃:安全的消息调用

一个常见的应用场景是防止因为调用未实现方法而导致的崩溃。例如,我们可以创建一个安全的代理对象,当目标对象不能响应某个消息时,不是崩溃而是返回一个默认值:

@interface SafeProxy : NSObject
@property (nonatomic, weak) id target;
@end

@implementation SafeProxy
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 如果target可以响应,直接转发
    if ([_target respondsToSelector:aSelector]) {
        return _target;
    }
    return self; // 让完整转发来处理
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 为任何方法提供默认签名(返回对象类型)
    return [NSMethodSignature signatureWithObjCTypes:"@@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 不处理消息,只设置返回值为nil
    id nilValue = nil;
    [anInvocation setReturnValue:&nilValue];
}
@end

使用这个SafeProxy,我们可以安全地调用任何方法:

Person *person = [[Person alloc] init];
SafeProxy *proxy = [[SafeProxy alloc] init];
proxy.target = person;

// 如果person实现了run方法,正常执行
[proxy run]; 

// 如果person没有实现fly方法,不会崩溃,而是返回nil
id result = [proxy fly]; // result = nil,没有崩溃

4.2 模拟多继承

如前所述,通过消息转发可以实现类似多继承的效果。这在某些设计模式中非常有用,例如"装饰器"模式或"代理"模式。

4.3 API兼容性处理

在开发中,我们经常会遇到iOS系统版本升级导致API变化的情况。通过消息转发,我们可以优雅地处理这种变化:

@interface CompatibilityHandler : NSObject
@end

@implementation CompatibilityHandler
+ (void)load {
    // 交换forwardInvocation:方法
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = [UIDevice class];
        SEL originalSelector = @selector(forwardInvocation:);
        SEL swizzledSelector = @selector(compatibility_forwardInvocation:);
        
        // 方法交换的实现...
    });
}

- (void)compatibility_forwardInvocation:(NSInvocation *)invocation {
    SEL selector = [invocation selector];
    
    if (selector == @selector(isLowPowerModeEnabled)) {
        // 低电量模式是iOS 9.0引入的
        if (@available(iOS 9.0, *)) {
            // 如果系统支持,转发给原始实现
            [invocation invoke];
        } else {
            // 如果不支持,返回默认值NO
            BOOL defaultValue = NO;
            [invocation setReturnValue:&defaultValue];
        }
    } else {
        // 其他消息正常转发
        [self compatibility_forwardInvocation:invocation];
    }
}
@end

4.4 实现AOP(面向切面编程)

通过消息转发,我们可以实现简单的AOP编程,在不修改原有类的情况下添加额外的逻辑:

@interface AspectProxy : NSObject
@property (nonatomic, strong) id target;
@property (nonatomic, copy) void (^beforeBlock)(SEL);
@property (nonatomic, copy) void (^afterBlock)(SEL);
@end

@implementation AspectProxy
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 必须返回nil才能进入完整转发
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [_target methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    // 执行前置逻辑
    if (_beforeBlock) {
        _beforeBlock(selector);
    }
    
    // 转发给目标对象
    if ([_target respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:_target];
    }
    
    // 执行后置逻辑
    if (_afterBlock) {
        _afterBlock(selector);
    }
}
@end

4.5 实现动态代理

在RxSwift等响应式编程框架中,消息转发被广泛用于实现动态代理,拦截方法调用并将它们转换为信号流:

// RxSwift中拦截方法的简化实现
@interface RXMessageSentObserver : NSObject
// ... 
@end

@implementation _RXObjCRuntime
- (void)interceptMethod:(SEL)selector ofClass:(Class)cls {
    // 1. 创建子类
    // 2. 重写forwardInvocation:
    // 3. 在forwardInvocation:中创建信号
}
@end

4.6 JSPatch等热修复框架的实现原理

热修复框架如JSPatch利用消息转发机制来实现动态替换OC方法的实现。基本原理是:

  1. 将要修复的类的 forwardInvocation: 方法替换为自己的实现
  2. 将原方法的IMP指向 _objc_msgForward,强制进入消息转发流程
  3. forwardInvocation: 中,执行JavaScript代码

第五章:性能考量与最佳实践

消息转发机制虽然强大,但使用不当可能会带来性能问题。

5.1 性能开销分析

不同阶段的消息转发性能开销不同:

阶段 性能开销 主要原因
正常消息发送 极小 直接查找IMP并调用
动态方法解析 较小 只执行一次,后续调用正常
快速消息转发 中等 需要调用Cocoa方法,但流程简单
完整消息转发 较大 需要创建NSInvocation对象,处理参数和返回值

为什么完整消息转发开销大

  1. 需要调用 methodSignatureForSelector: 获取方法签名
  2. Runtime需要根据方法签名创建 NSInvocation 对象
  3. NSInvocation 需要拷贝参数和设置返回值
  4. 整个流程涉及多次Objective-C方法调用

5.2 性能优化建议

根据性能开销,我们应遵循以下最佳实践:

  1. 优先使用快速消息转发:如果只是简单地将消息转发给另一个对象,尽量使用 forwardingTargetForSelector:,避免使用完整转发。

  2. 缓存方法签名:如果在完整转发中经常处理同一类消息,可以缓存方法签名,避免每次调用 methodSignatureForSelector:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    static NSMutableDictionary *signatureCache;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        signatureCache = [NSMutableDictionary dictionary];
    });
    
    NSString *selString = NSStringFromSelector(aSelector);
    NSMethodSignature *signature = signatureCache[selString];
    if (!signature) {
        signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        signatureCache[selString] = signature;
    }
    return signature;
}
  1. 避免频繁触发转发:如果一个方法经常被调用,最好不要依赖消息转发来处理它。考虑在 resolveInstanceMethod: 中动态添加IMP,这样后续调用就和正常方法一样快了。

5.3 调试消息转发

当遇到与消息转发相关的bug时,可以使用以下调试技巧:

  1. 使用instrumentObjcMessageSends:开启日志,查看消息转发的每一步。

  2. 添加日志输出:在转发方法中添加日志:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"📱 Forwarding %@ to another target", NSStringFromSelector(aSelector));
    // ...
}
  1. 使用断点调试:在 forwardInvocation: 中设置断点,检查 NSInvocation 的内容。

  2. 检查方法签名:常见的崩溃原因是 methodSignatureForSelector: 返回了不正确的签名。可以使用以下代码验证签名:

NSMethodSignature *sig = [self methodSignatureForSelector:@selector(someMethod:)];
NSLog(@"Signature: %s", [sig methodReturnType]); // 检查返回类型
for (NSUInteger i = 0; i < [sig numberOfArguments]; i++) {
    NSLog(@"Arg %lu: %s", i, [sig getArgumentTypeAtIndex:i]);
}

5.4 与其他动态特性的比较

消息转发与Objective-C的其他动态特性既有联系又有区别:

特性 目的 触发时机
消息转发 处理无法识别的消息 方法查找失败后
方法交换 交换两个方法的IMP 运行时主动执行
动态添加方法 为类添加新方法 运行时主动执行
KVO 监听属性变化 创建子类并重写setter

重要区别

  • 消息转发是被动的,只有在正常消息发送失败后才会触发
  • 方法交换、动态添加方法是主动的,我们可以在任何时候执行
  • KVO是利用Runtime创建子类并重写方法,本质上也是动态特性的一种应用

第六章:面试深度解析

消息转发是iOS面试中的高级话题。下面梳理一些常见的面试题和深度解析。

6.1 基础问题

Q1:OC中给nil对象发送消息会发生什么?

解析:给nil发送消息是安全的,不会崩溃。Runtime在 objc_msgSend 中会首先检查接收者是否为nil,如果是nil,直接返回。返回值的类型取决于方法声明的返回类型:

  • 如果返回对象类型,返回nil
  • 如果返回整型,返回0
  • 如果返回结构体,返回的结构体各字段都是0
  • 如果返回浮点类型,返回0.0

Q2:unrecognized selector sent to instance 这个异常是怎么产生的?

解析:当向一个对象发送它无法处理的消息,且消息转发机制也无法处理时,Runtime最终会调用 doesNotRecognizeSelector: 方法。NSObject 中该方法的默认实现就是抛出这个异常。也就是说,这个异常是消息转发流程失败的最后结果。

Q3:消息转发分哪几个阶段?每个阶段的作用是什么?

解析:消息转发分为三个阶段:

  1. 动态方法解析:调用 resolveInstanceMethod:/resolveClassMethod:,允许开发者动态添加方法实现。

  2. 快速消息转发:调用 forwardingTargetForSelector:,允许将消息转发给另一个对象。

  3. 完整消息转发:调用 methodSignatureForSelector: 获取方法签名,然后创建 NSInvocation 对象并调用 forwardInvocation:,允许修改消息内容或转发给多个对象。

6.2 进阶问题

Q4:如何在运行时动态添加方法?

解析:在 resolveInstanceMethod: 中使用 class_addMethod 函数:

void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"动态添加的方法被调用");
}

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

Q5:快速转发和完整转发有什么区别?如何选择?

解析:主要区别在于:

  1. 需要重载的方法数量:快速转发只需重载 forwardingTargetForSelector:,完整转发需要重载 methodSignatureForSelector:forwardInvocation: 两个方法。

  2. 功能强大程度:快速转发只能简单地改变消息接收者,不能修改消息内容;完整转发可以修改消息的参数、选择器、返回值等。

  3. 性能开销:快速转发性能更好,完整转发需要创建 NSInvocation 对象,开销较大。

选择建议

  • 如果只是想将消息转发给另一个对象,且不需要修改消息内容,优先使用快速转发
  • 如果需要修改消息内容、参数、返回值,或者需要将消息转发给多个对象,使用完整转发

Q6:消息转发可以用来实现多重继承吗?和真正的多重继承有什么区别?

解析:可以通过消息转发实现类似多重继承的效果。区别在于:

  • 真正的多重继承是将多个类的功能合并到一个对象中
  • 通过消息转发实现的"伪多继承",功能仍然分散在不同的对象中,只是通过转发机制让外部看起来像一个对象处理了所有消息

Q7:如果消息转发的方法本身也找不到实现会怎样?

解析:这是一个容易忽略的细节。如果消息转发的方法(如 forwardingTargetForSelector:)本身没有实现,Runtime也会按照同样的流程查找它的实现。如果找不到,同样会触发消息转发。但通常情况下,这些方法在 NSObject 中都有默认实现,所以不会出现这种情况。

Q8:如何调试消息转发过程?

解析:可以使用以下方法:

  1. 使用 instrumentObjcMessageSends(YES) 开启日志
  2. 查看 /tmp/msgSends- 目录下的日志文件
  3. 在转发方法中添加断点和日志输出
  4. 使用反汇编工具分析 __forwarding__ 函数

6.3 高级问题

Q9:消息转发和method swizzling有什么关系?能结合使用吗?

解析:消息转发和method swizzling是两种不同的动态特性,但可以结合使用。例如,可以实现一个通用的方法拦截机制:

// 1. 先将原方法的IMP替换为_objc_msgForward
Method method = class_getInstanceMethod(cls, originalSelector);
method_setImplementation(method, _objc_msgForward);

// 2. 再添加一个转发方法
class_addMethod(cls, @selector(customForward:), (IMP)customForwardIMP, "v@:@");

// 3. 交换forwardInvocation:方法
Method originalForwardMethod = class_getInstanceMethod(cls, @selector(forwardInvocation:));
Method swizzledForwardMethod = class_getInstanceMethod(cls, @selector(customForwardInvocation:));
method_exchangeImplementations(originalForwardMethod, swizzledForwardMethod);

这种技术被用于RxSwift等框架的方法拦截功能。

Q10:如何实现一个通用的消息转发中心,能够记录所有无法识别的消息?

解析:可以创建一个基类,所有需要日志功能的类都继承自这个基类:

@interface LoggingBase : NSObject
@property (nonatomic, strong) NSMutableArray *unrecognizedMessages;
@end

@implementation LoggingBase
- (instancetype)init {
    if (self = [super init]) {
        _unrecognizedMessages = [NSMutableArray array];
    }
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 记录无法识别的消息
    NSString *message = [NSString stringWithFormat:@"%@: %@", self, NSStringFromSelector(aSelector)];
    [_unrecognizedMessages addObject:message];
    
    // 可以选择转发给默认处理对象
    return [DefaultHandler sharedHandler];
}

// 可以添加一个方法来导出日志
- (void)exportUnrecognizedMessages {
    NSLog(@"Unrecognized messages: %@", _unrecognizedMessages);
}
@end

Q11:消息转发机制在ARC下有什么特别需要注意的地方?

解析:ARC下使用消息转发时需要注意:

  1. 内存管理:在 forwardInvocation: 中处理对象参数时,ARC会自动处理内存管理,但要注意不要造成循环引用。

  2. 方法签名:方法签名的类型编码必须准确,特别是在有对象参数或返回值时。错误的类型编码可能导致ARC下的内存管理错误。

  3. 返回值处理:当从 forwardInvocation: 返回时,Runtime会根据方法签名自动处理返回值的retain/release。如果方法签名不准确,可能导致内存泄漏或崩溃。

  4. 使用 __unsafe_unretained:在某些情况下,可能需要使用 __unsafe_unretained 来避免ARC自动插入的retain/release操作干扰转发逻辑。

Q12:从源码层面分析,消息转发和消息发送的性能差异主要体现在哪些方面?

解析:从源码层面看,性能差异主要体现在:

  1. 正常消息发送:汇编实现,查找缓存后直接跳转,几条指令就能完成。

  2. 动态方法解析:需要调用Objective-C方法,但只执行一次,后续调用恢复正常。

  3. 快速转发:需要调用 forwardingTargetForSelector:,这是一个完整的Objective-C方法调用,涉及消息发送流程。但无需创建复杂的对象。

  4. 完整转发

    • 需要调用 methodSignatureForSelector: 获取签名
    • Runtime需要遍历方法签名,解析每个参数的类型
    • 创建 NSInvocation 对象需要分配内存
    • NSInvocation 需要拷贝参数值
    • 调用 forwardInvocation: 方法
    • 转发后需要处理返回值

这些步骤加起来,完整转发的性能开销可能是正常消息发送的几十倍甚至上百倍。


第七章:总结与展望

7.1 消息转发机制的核心价值

Objective-C的消息转发机制是其动态性的集中体现,它给了开发者三次机会来处理无法识别的消息:

  1. 动态方法解析:让我们能够在运行时动态添加方法实现
  2. 快速消息转发:让我们能够将消息简单地转发给其他对象
  3. 完整消息转发:让我们能够完全掌控消息的处理过程

这三次机会形成了一个从简单到复杂的递进结构,开发者可以根据需求选择合适的层次进行干预。

7.2 设计思想解读

消息转发机制的设计体现了几个重要的软件工程思想:

  1. 容错性:系统提供了容错机制,允许程序在出现问题时尝试恢复,而不是直接崩溃。

  2. 渐进式干预:提供了三个层次的干预机会,每个层次都有不同的复杂度和能力,开发者可以根据需要选择。

  3. 开闭原则:通过消息转发,我们可以在不修改原有类的情况下,扩展类的功能,符合开闭原则。

  4. 责任链模式:消息转发本质上是一个责任链模式的实现,每个阶段都有机会处理消息,如果处理不了就传递给下一阶段。

7.3 与其他语言动态特性的对比

与其他动态语言相比,Objective-C的消息转发机制有独特之处:

语言 类似特性 特点
Objective-C 消息转发 分三个阶段,功能强大,与Runtime紧密结合
Ruby method_missing 类似forwardInvocation:,但更简洁
Python getattr 属性访问的fallback机制
JavaScript Proxy 可以拦截对象的各种操作

其中,Ruby的 method_missing 和Objective-C的 forwardInvocation: 最为相似。不同之处在于,Objective-C提供了更细粒度的控制(三个阶段),而Ruby只提供了一个统一的入口。

7.4 未来展望

随着Swift的兴起,Objective-C的使用场景在减少,但消息转发机制的设计思想仍然值得学习:

  1. Swift的动态特性:Swift虽然强调静态类型安全,但也提供了反射机制和 @objc 动态特性。理解消息转发有助于理解Swift中与Objective-C交互的部分。

  2. 跨平台开发:像Flutter这样的跨平台框架,在实现平台通道时也借鉴了消息转发的思想。

  3. AOP编程:面向切面编程在现代开发中越来越重要,消息转发是实现AOP的基础技术之一。

7.5 最后的思考

消息转发机制是Objective-C Runtime皇冠上的明珠,它展示了动态语言的强大能力。掌握消息转发,不仅能帮助我们写出更健壮的代码,还能让我们更深入地理解Objective-C的设计哲学。

在实际开发中,我们应当合理使用消息转发机制:

  • 在需要的地方使用,但不要滥用
  • 优先考虑性能更好的方案(如快速转发优先于完整转发)
  • 做好日志和调试,确保转发逻辑正确

最终,消息转发机制体现了编程语言设计中的一个重要思想:给予开发者更多的控制权,同时也赋予更多的责任。当我们决定使用消息转发时,我们实际上是在说:"我知道这条消息可能无法被正常处理,但我有办法解决这个问题。"

这种思想超越了具体的编程语言,是每个优秀程序员都应该具备的能力——在系统无法自动处理的情况发生时,能够提供优雅的降级方案。


参考资料

  1. Apple官方文档:forwardInvocation:
  2. Objective-C Runtime源码 (objc4-818.2)
  3. 《Effective Objective-C 2.0》 - Matt Galloway
  4. 《Objective-C Runtime Programming Guide》 - Apple Inc.

iOS NotificationCenter Observer 的隐性性能代价

引言

iOS 9 之后,Apple 为 NSNotificationCenter 的 target-action 模式引入了 zeroing weak reference。当 observer 对象被释放后,系统自动将内部的 weak reference 置为 nil,不再向其投递通知,也不会产生野指针 crash。

Apple 文档对此也有明确说明:

If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.

这条规则被广泛接受,许多团队因此在代码规范中不再严格要求管理 observer 生命周期。然而,"不会 crash"并不等于"没有性能影响"。本文通过一个真实的线上卡死案例,探讨 NotificationCenter observer 管理不当可能带来的隐性性能问题。

一个线上卡死案例

在一次线上卡死监控中,我们发现一类卡死的比例明显上升。主线程被卡住 13 秒,CPU 占用 98.8%,处于 running 状态(不是锁等待)。堆栈顶部如下:

_weak_unregister_no_lock
_objc_moveWeak
__CFXNotificationRegistrarAddObserver
SomeTimeViewModel.componentMount()     ← 调用 NotificationCenter.default.addObserver(...)

卡死发生在一个 ViewModel 的初始化阶段——调用 addObserver(self, selector:, name:, object:) 注册通知时。注册一个通知 observer 本身应该是一个非常轻量的操作,为什么会导致 13 秒的卡死?

经过排查,这个问题的根因并不是"忘记 removeObserver 导致 dead entries 累积"(事实上代码在 dealloc 链路中已经正确调用了 removeObserver),而是一个更容易被忽视的问题:同一个通知名下积累了过多的 live observers

NotificationCenter 的内部机制

要理解这个问题,需要了解 NSNotificationCenter 在 iOS 9+ 中的内部机制。

注册表结构

当调用 addObserver:selector:name:object: 时,NotificationCenter 在内部维护一个注册表(registrar),按通知名称索引,存储所有注册信息。每条注册信息大致包括:

  • 一个指向 observer 的 weak reference
  • selector
  • notification name
  • object filter

这些信息存储在 CoreFoundation 内部的数据结构(类似于哈希表 + 数组)中。

扩容与 weak reference 迁移

和 HashMap 类似,NotificationCenter 的内部存储在容量不足时会扩容——分配更大的存储空间,将现有条目迁移到新位置。

对于包含 weak reference 的条目,迁移过程需要通过 ObjC runtime 的 objc_moveWeak 将 weak reference 从旧内存地址搬迁到新地址。这个操作涉及:

  1. _weak_unregister_no_lock:从 runtime 的 side table 中注销旧地址
  2. _weak_register_no_lock:在 side table 中注册新地址

单次操作很快,但如果某个通知名下积累了大量条目,扩容时需要逐个迁移所有 live entries 的 weak reference,累积耗时就可能达到秒级。

两种问题模式

NotificationCenter 的 entries 膨胀来自两个方面,它们可以独立存在,也可以叠加:

模式一:dead entries 累积(不调 removeObserver 的短生命周期对象)

当 observer 被释放时,其 weak reference 自动置 nil,但注册条目本身不会被移除。对于频繁创建和销毁的对象(如 Feed 中的各类 Component),如果不在 deinit 中 removeObserver,NotificationCenter 会持续累积 dead entries。

模式二:live entries 过多(长生命周期对象大量注册同一通知)

即使每个 observer 都正确管理了 removeObserver,如果大量长生命周期对象同时注册同一个通知,live entries 的数量本身就可能很大。

案例分析

回到开头的卡死案例。我们排查后发现,触发卡死的通知名 .tabBarDidChangeSelectedIndex 在整个 App 中有 31 个文件 注册了 observer,涵盖 Feed、社交、个人资料页、IM、Notice、电商 等几乎所有主要模块。

在 IM 模块的 会话 列表中,架构设计如下:

  • 每个会话对应一个持久化的 ViewModel(存储在字典中,不会被频繁销毁)
  • 每个 ViewModel 在 init 时创建一棵包含 50+ 子组件的组件树
  • 其中 ViewModel 本身和 TimeViewModel 各注册了一次 .tabBarDidChangeSelectedIndex

对于一个有 200 个会话的用户,仅 会话 列表就贡献了 400 个 live observers。这些 observer 的生命周期管理是正确的(dealloc 时通过组件树的 unmount 链路移除),但它们的数量本身就是问题。

再叠加 App 其他模块的 observer(包括可能存在的 dead entries),这个通知名下的总 entries 数量相当可观。当新增一个 observer 触发内部存储扩容时,迁移所有 entries 的累积耗时就造成了 13 秒的卡死。

被忽视的关键点

这个案例有一个容易被忽视的教训:即使 observer 生命周期管理完全正确(deinit 中有 removeObserver),也不意味着没有性能风险。 问题不在于单个 observer 的正确性,而在于同一个通知名下的 observer 总量。

哪些场景容易踩坑

1. 热门通知名 + 大量模块共同注册

像 Tab 切换、App 前后台、网络状态变化这类全局通知,往往被 App 中大量模块同时监听。每个模块的注册看起来都合理,但总量可能超出预期。

2. 持久化对象在 init 阶段无差别注册

如果一个对象会存在很久(如 1:1 对应数据模型的 ViewModel),且在 init 阶段就注册通知,那么所有实例的 observer 都会持续存在。即使只有屏幕上可见的几个实例真正需要响应通知,其余实例的注册也在白白增加 entries 总量。

3. 短生命周期对象不调 removeObserver

对于频繁创建和销毁的对象(如 Feed 滑动过程中的各种 Component),如果不在 deinit 中 removeObserver,每次销毁都会留下一个 dead entry。随着用户使用时间增长,dead entries 不断累积。

4. 组件树放大效应

在 TTKC 等组件化框架中,一个容器可能包含数十个子组件,每个子组件可能独立注册通知。容器的数量 × 子组件的数量 = 总 observer 数量,放大效应显著。

建议

按需注册:只为可见的实例注册 observer

对于列表中的 ViewModel,如果通知只用于更新 UI 展示(如刷新时间文本),那么只有屏幕上可见的实例才需要注册。可以在 Cell 即将显示时注册,在不可见时移除:

override func cellWillDisplay() {
    super.cellWillDisplay()
    NotificationCenter.default.addObserver(self, selector: #selector(onTabBarChange(_:)),
                                           name: .tabBarDidChangeSelectedIndex, object: nil)
}

override func cellDidEndDisplay() {
    super.cellDidEndDisplay()
    NotificationCenter.default.removeObserver(self, name: .tabBarDidChangeSelectedIndex, object: nil)
}

对于 200 个会话的用户,这将 observer 数量从 200 减少到 ~10-20(可见 Cell 数量)。

集中式 observer:N 个独立注册 → 1 个集中处理

如果多个同类对象都需要响应同一个通知,考虑用一个集中式 observer 替代 N 个独立注册:

// 在 DataController 中注册一次
NotificationCenter.default.addObserver(self, selector: #selector(onTabBarChange(_:)),
                                       name: .tabBarDidChangeSelectedIndex, object: nil)

@objc func onTabBarChange(_ notification: NSNotification) {
    for viewModel in viewModelDict.values {
        viewModel.handleTabBarChange()
    }
}

N 个 observer 注册变为 1 个,彻底消除了这个通知名下的数量问题。

对于短生命周期对象:在 deinit 中 removeObserver

deinit {
    NotificationCenter.default.removeObserver(self)
}

这一行代码的作用不是防 crash(iOS 9+ 不需要),而是及时清理 NotificationCenter 内部的注册条目,避免 dead entries 累积。

考虑使用 block-based API + 显式 token 管理

block-based API 返回一个 opaque token,移除时通过 token 精确定位,语义更清晰:

private var observerToken: NSObjectProtocol?

func setup() {
    observerToken = NotificationCenter.default.addObserver(
        forName: .someNotification, object: nil, queue: .main
    ) { [weak self] _ in
        self?.handleNotification()
    }
}

deinit {
    if let token = observerToken {
        NotificationCenter.default.removeObserver(token)
    }
}

需要注意的是,block-based API 不使用 zeroing weak reference——如果 block 中 strong capture 了 self,会导致循环引用。block 中必须使用 [weak self],且必须在合适时机 remove token。

在 Code Review 中关注

建议在 Code Review 中对以下模式保持敏感:

  • 这个通知名在 App 中有多少处注册?是否是"热门通知"?
  • 注册 observer 的对象有多少个实例同时存在?
  • 是否在 init 阶段就注册,但实际上只在可见时才需要?
  • 短生命周期对象是否在 deinit 中调了 removeObserver?

小结

NotificationCenter 的性能问题有两个维度:

  1. 单个 observer 的生命周期管理:短生命周期对象不调 removeObserver,导致 dead entries 累积
  2. 同一通知名下的 observer 总量:即使每个 observer 都正确管理了生命周期,大量 live observers 本身就是性能风险

第一个问题比较符合直觉,容易在 Code Review 中发现。第二个问题更隐蔽——每个模块的注册看起来都合理,但当一个大型 App 中有几十个模块同时注册同一个通知时,总量就可能超出 NotificationCenter 内部数据结构的性能安全边界。

Apple 的"不需要 removeObserver"是关于正确性的保证,不是关于性能的保证。在大型 App 中,NotificationCenter observer 需要像内存一样被视为一种有限资源来管理。

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

深入理解代替单纯记忆

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

一、问题现象

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

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

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

二、场景结构

出问题的 VC 架构如下:

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

关键行为:

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

三、复现步骤

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

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

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

四、排查过程

4.1 排除数据源问题

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

4.2 怀疑 reloadData 在 off-screen 时异常

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

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

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

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

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

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

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

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

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

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

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

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

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

五、根因分析

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

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

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

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

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

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

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

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

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

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

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

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

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

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

六、修复

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

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

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

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

七、总结

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

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

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

排查建议

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

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

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

# iOS Block 深度解析

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


一、Block 的本质

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

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

一句话概括 Block 的本质:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

二、Block 的底层结构

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

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

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

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

2.1 flags 标志位详解

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

flags 位域布局 (32 bit):

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

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

2.2 descriptor 的多态结构

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

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

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

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

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

2.3 invoke 函数的隐含参数

当你写这样的代码:

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

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

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

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

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

2.4 Block 的类型编码(Type Encoding)

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

Block 签名编码规则:

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

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

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

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

三、Block 的三种类型

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

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

3.1 三种类型的判定规则

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

类比理解:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

四、变量捕获机制

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

4.1 捕获规则总览

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

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

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

int a = 10;

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

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

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

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

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

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

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

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

NSMutableArray *arr = [NSMutableArray new];

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

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

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

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

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

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

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

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

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

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

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

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

4.5 __block 修饰符的深层原理

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

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

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

__forwarding 指针的精妙设计

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

阶段一:Block 还在栈上时

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

阶段二:Block 被 copy 到堆上

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

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

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

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

__block 变量被多个 Block 捕获时

__block int a = 10;

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

底层发生了什么?

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

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

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

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

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

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

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

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

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

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

捕获对象时的引用关系:

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

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

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

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

_Block_object_assign 的内部逻辑

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

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

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

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

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

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

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

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

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

4.8 捕获变量的内存对齐

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

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

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

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

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

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

5.1 循环引用的形成

经典循环引用:

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

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

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

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

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

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

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

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

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

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

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

5.3 解决方案的原理对比

方案一:__weak(推荐)

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

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

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

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

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

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

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

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

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

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

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

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

5.4 Weak-Strong Dance 的深层理解

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

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

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

加了 __strong 后:

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

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

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

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

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

5.6 循环引用的检测方法

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

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

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

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

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

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

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

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

6.1 Block 的 copy 语义链

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

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

6.2 Block 的 dispose 语义链

Block 引用计数归零时:

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

6.3 Block 的 retain/release 实现细节

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

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

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

6.4 Block 属性用 copy 还是 strong?

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

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

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

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

七、Block 与线程安全

7.1 Block 本身的线程安全性

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

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

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

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

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

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

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

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

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

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

7.3 Block 与 GCD 的线程交互模式

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

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

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

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

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

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

8.1 GCD 中的 Block

dispatch_async(queue, block)

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

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

8.2 RunLoop 中的 Block

CFRunLoopPerformBlock(runloop, mode, block)

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

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

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

8.3 Notification/KVO 中的 Block

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

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

解决:Block 内必须用 weakSelf

8.4 Block 作为 Associated Object

objc_setAssociatedObject(self, key, block, OBJC_ASSOCIATION_COPY_NONATOMIC);

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

8.5 Block 在 KVO 中的新 API

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

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

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

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

九、Block 的性能考量

9.1 Block 的开销分析

Block 的性能开销来源:

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

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

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

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

9.2 编译器对 Block 的优化

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

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

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

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

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

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

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

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

优化策略:

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

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

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

十、Block 的调试技巧

10.1 在运行时识别 Block 类型

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

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

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

  (lldb) po [block superclass]
  → NSBlock

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

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

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

10.2 在汇编层面识别 Block 调用

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

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

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

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

10.3 排查 Block 相关的 Crash

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

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

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

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

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

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

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

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

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

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

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

十一、Block 与 Swift 的桥接

11.1 OC Block 在 Swift 中的映射

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

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

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

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

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

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

11.2 Swift 闭包在 OC 中的使用

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

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

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

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

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

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

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

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

12.1 十大常见 Block 陷阱

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

12.2 Block 的最佳实践总结

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

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

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

三种回调机制的对比:

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

选择建议:

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

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

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

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

十四、知识体系脑图

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

十五、面试高频考点速查

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