第一战:iOS 底层原理深度剖析 (Runtime & Memory)

image.png

1️⃣ Runtime(Objective-C 心脏)

核心考点:消息发送机制、类结构、方法缓存、动态特性


mindmap root((P7/P8 iOS 底层面试全景知识地图)) Runtime(Objective-C 心脏) 消息发送机制(objc_msgSend) 快速路径(汇编) 慢速路径(C++) 深挖题 Q1: 为什么用汇编写 详细解答: 性能优化, 灵活性, 平台适配 类对象结构 isa 链 class_data_bits_t 深挖题 Q3: isa_t 位域结构 详细解答: 位域字段说明 方法缓存(cache_t) 扩容机制 淘汰策略 深挖题 Q2: cache_t 扩容机制 详细解答: 触发条件, 扩容过程, 哈希冲突处理 类加载过程(+load / +initialize) Category 底层实现 内存管理(ARC 背后的机制) 引用计数存储 Tagged Pointer isa.extra_rc SideTables Weak 引用销毁流程 深挖题 Q4: Weak 引用销毁流程 详细解答: dealloc 流程, clearDeallocating AutoreleasePool 原理 深挖题 Q5: AutoreleasePool 原理 详细解答: 结构, push, autorelease, pop, RunLoop Associated Objects 原理 深挖题 Q6: Associated Objects 原理 详细解答: 存储结构, 释放流程 Block(闭包与捕获) Block 类型(Global / Stack / Malloc) 捕获机制 __block 原理 深挖题 Q7: __block 原理 详细解答: __Block_byref 结构体, forwarding 指针 Block 内存布局 深挖题 Q8: Block 内存布局 详细解答: Block_layout, descriptor 循环引用 深挖题 Q9: Block 循环引用检测与解决 详细解答: 检测方法, 解决方案 延伸底层机制 KVO 原理 深挖题 Q10: KVO 底层原理 详细解答: 动态子类, setter 重写, 释放恢复 GCD 底层 深挖题 Q11: dispatch_once 底层 详细解答: pthread_once, CAS 内存对齐与对象布局 深挖题 Q12: 内存对齐规则 详细解答: 对齐原则, 性能提升

1.1 消息发送机制(objc_msgSend)

  • 调用流程
    [obj foo] → objc_msgSend(obj, @selector(foo))
  • 快速路径(汇编)
    1. 检查 obj 是否为 nil(nil 消息安全)。
    2. 通过 isa 掩码(ISA_MASK)找到 Class。
    3. 在 Cache(Buckets)中哈希查找 IMP。
    4. 哈希冲突解决:开放寻址法(向后找,回到头部)。
  • 慢速路径(C++)
    • lookUpImpOrForward → 二分查找 method_list → 递归查找父类。
    • 动态方法决议:resolveInstanceMethod
    • 消息转发:forwardingTargetForSelector → methodSignatureForSelector → forwardInvocation

💣 深挖题 Q1: 为什么 objc_msgSend 用汇编写?
详细解答

  1. 性能优化
    • C 语言函数调用需要建立栈帧,而 objc_msgSend 只是查找 IMP 并跳转,不需要额外栈帧。
    • 汇编可直接操作寄存器(ARM64 的 x0, x1, x2…),避免参数搬运到栈上。
    • 消息参数是变长的,C 语言处理变长参数需要额外开销,汇编可直接按寄存器协议传递。
  2. 灵活性
    • 汇编可直接 jmp 到 IMP,减少一次函数返回开销。
    • 可内联快速路径,避免函数调用层层嵌套。
  3. 平台适配
    • Apple 为不同架构分别写了高度优化的汇编版本。

1.2 类对象结构

  • isa 链

    Instance → Class → MetaClass → RootMetaClass → RootMetaClass(自指)
    
  • class_data_bits_t

    • 存储 methods、properties、protocols。
  • isa 位域(non-pointer isa)

    • 包含引用计数标记、是否有 weak 引用、是否正在析构等信息。

💣 深挖题 Q3: isa_t 位域结构?
详细解答

union isa_t {     
    uintptr_t bits;  // 视角 A:原始数据(看作一个 64 位的整数)
    struct {         // 视角 B:精细化数据(看作 64 个独立的开关)
        // ... 里面的位域 ...
    }; 
};
  • nonpointer:是否启用位域优化。

  • shiftcls:类指针(位移存储)。

  • extra_rc:引用计数。

  • has_sidetable_rc:是否有 SideTable 引用计数。

  • weakly_referenced:是否有 weak 引用。

  • deallocating:是否正在析构。

  • union (共用体):这意味着 bits 和下面的 struct 共用同一块 8 字节(64位)的内存

  • 互通性

    • 当你写 isa.bits = 0x… 时,你是在一次性操作整个 64 位数据。

    • 当你读 isa.extra_rc 时,你是在按当前编译目标的位域定义读取其中一段。它是否位于最高位、占多少位都与架构和 objc4 版本有关。

  • uintptr_t:在 64 位系统下,等同于 unsigned long long(无符号长整型),占 64 bit。

  • : 1 (位域):冒号后面的数字表示该成员占用几个二进制位 (bit)。加起来刚好 64 位。


1.3 方法缓存(cache_t)

  • 扩容:方法缓存会根据容量和占用情况扩容或重建;具体阈值、是否保留旧桶和增长策略随 Runtime 版本变化。
  • 冲突处理:常见实现采用开放寻址探测,不是通用的 LRU 缓存,也不能概括成“冲突时直接覆盖旧值”。

💣 深挖题 Q2: cache_t 扩容机制?
详细解答

  1. 触发条件:达到当前实现设定的容量或占用条件。
  2. 扩容过程
    • 常见策略会增长容量并重新建立缓存;增长倍数和旧桶回收方式属于版本细节。
  3. 哈希冲突处理:开放寻址法,冲突时向后找空位,末尾回到头部继续找。

1.4 类加载过程

  • +load
    • 在 image 加载时调用,按依赖顺序,线程安全。
  • +initialize
    • 在类第一次接收消息时调用,线程安全,可被继承。

1.5 Category 底层实现

  • 方法插入到类的 method_list(优先于原类方法)。
  • 不能添加实例变量(内存布局已固定)。

2️⃣ 内存管理(ARC 背后的机制)

核心考点:引用计数存储、SideTables、Weak 引用、AutoreleasePool


2.1 引用计数存储

  • Tagged Pointer:小对象直接存指针,无引用计数。
  • isa.extra_rc:位域存储引用计数,溢出后进入 SideTable。
  • SideTables
    • 全局 StripeMap(分段锁哈希表)。
    • 包含 RefcountMap 和 WeakTable。

2.2 Weak 引用销毁流程

💣 深挖题 Q4: Weak 引用销毁流程?
详细解答

  1. 对象 dealloc → _objc_rootDealloc
  2. 检查 isa.weakly_referenced 位标记。
  3. 如果有 weak 引用:
    • 调用 object_dispose → objc_destructInstance → clearDeallocating
  4. clearDeallocating
    • 获取对象地址对应的 SideTable。
    • 在 WeakTable 中查找该对象的 entry。
    • 遍历 entry 中的所有 weak 指针地址,将它们置为 nil。
    • 移除 entry,释放锁。

简单来说,SideTables 使用分段锁(Lock Striping)在锁竞争与结构开销之间取平衡。具体分片数不是 Objective-C 语义的一部分,也不应作为面试时必须背诵的固定结论。

以下是适合“高级工程师/架构师”面试的深度回答逻辑:


1. 核心设计模式:分段锁 (Lock Striping)

面试官问:“为什么不直接用一个全局的大表?或者给每个对象配一个表?”

你的回答

  • 方案 A:全局一张表 (One Global Table)

    • 如果有成千上万个对象同时进行 retain/releaseweak 操作,所有线程都要抢同一把锁

    • 结果:严重的锁竞争(Lock Contention),多核 CPU 变成串行执行,性能崩塌。

  • 方案 B:每对象一表 (One Table per Object)

    • 给每个对象分配独立的锁和表。

    • 结果:内存爆炸。大部分对象可能根本不需要存 weak 指针,这是极大的浪费。

  • 方案 C:SideTables (分段锁)

    • Runtime 选择了折中方案:将对象按地址散列到若干桶(Buckets)。不同对象只有落在不同分片、且没有其他共享瓶颈时,相关表操作才有机会并行。

2. 为什么分片数量不能写死?

分片数通常由 Runtime 的模板参数、目标平台和编译配置决定。你在某一版源码里可能看到 8、64 或其他取值,但它只能说明那一版实现。面试时更有价值的是解释分段锁为何能降低竞争,以及同一分片内仍需要串行保护,而不是替 Apple 猜一个数字背后的硬件公式。


3. 🔥 进阶考点:伪共享 (False Sharing) 与 缓存行 (Cache Line)

这是 P8 / 专家级 的回答点。

你的回答:

“除了减少锁竞争,选 64 这个数字还可能隐含了对 CPU 缓存行(Cache Line) 的考量。

现代 CPU 的 L1 Cache Line 通常是 64 Bytes。

如果锁的数量太少且内存排列太紧密,可能会发生 伪共享 (False Sharing)

  • 核心 A 修改了 SideTable[0] 的锁。

  • 核心 B 修改了 SideTable[1] 的锁。

  • 如果这两个锁在同一个 Cache Line 里,CPU 必须通过总线强制同步,导致两个核心互相等待,虽然逻辑上它们锁的是不同的表。

是否存在 padding、锁是否独占缓存行,要看具体结构定义和对齐结果。仅凭“数量是 64”无法推导出避免伪共享,这类内容适合拿源码和性能计数器验证,不适合写成既定设计动机。”

SideTable 里存储啥

SideTable 里存储的不仅仅是弱引用对象的地址。它主要存储两个完全不同的东西,它们共用一个锁(这就是为什么分段锁非常关键):

  1. 外部引用计数 (External Reference Count Map)

  2. 弱引用表 (Weak Reference Map)

以下是 SideTable 的详细构成和每个部分的作用:


1. 外部引用计数表 (RefcountMap)

这部分通常由 RefcountMap 结构体实现,用来存储对象的 非内嵌 引用计数。

🔹 作用:解决 isa 空间的不足

现代 Objective-C(64 位架构)的内存优化非常激进:

  • 内嵌引用计数:对于大部分对象,它们的 retain 计数是直接嵌入在 isa 指针中的(有专门的位域存储)。

  • 外部存储:当 nonpointer isa 的内联计数空间不足,或对象使用不带内联引用计数的 isa 路径时,Runtime 会借助 SideTableRefcountMap。Tagged Pointer 本身不是一个需要把引用计数迁移到 SideTable 的普通对象;“锁状态占用 isa”也不是这里可以泛化的原因。

结论: 并非所有对象的引用计数都在 SideTable 里,只有那些 溢出或特殊状态 的对象才会在此存储。

2. 弱引用表 (WeakPointerMap)

这部分通常由 WeakPointerMap 结构体实现,用于存储所有指向某个对象的弱引用指针。

🔹 作用:实现弱引用归零 (Zeroing Out)

这是 SideTable 存在的另一个主要原因:

  1. 存储关系:它存储了从 对象地址弱引用指针列表 的映射关系。

    • Key:被弱引用的对象的内存地址。

    • Value:一个列表,里面存了所有 __weak 变量的地址。

  2. 对象销毁时:当对象发生 dealloc 时,Runtime 会去它对应的 SideTable 中查找 WeakPointerMap

  3. 归零:Runtime 拿到弱引用指针列表后,会遍历这个列表,将所有指向该对象的 __weak 变量的值全部设为 nil,从而避免野指针。

3. SideTable 的结构模型 (总结)

在 Runtime 源码中,每个 SideTable 桶的结构通常包含这三个字段:

字段名称类型存储内容保护范围
lockspinlock_tmutex用于保护本 SideTable 桶内的数据结构。整个 SideTable
refcntsRefcountMap存储溢出对象的引用计数。refcnts (受 lock 保护)
weak_tableWeakPointerMap存储弱引用指针的映射关系。weak_table (受 lock 保护)

🔑 架构总结

SideTable 的设计体现了 职责划分性能平衡

  • 分段锁SideTables 通过分片技术,将外部引用计数弱引用管理的压力分散到多个锁域,避免所有对象共享一把全局锁。

  • 职责合并:引用计数和弱引用都是对象生命周期管理的一部分,将它们放在同一个 SideTable 内的两个 Map 中,可以共用同一把锁,减少了对内存的反复访问,提高了局部性。


4. 它们是怎么映射的?(哈希算法)

为了证明你看过源码,可以简单提一下映射逻辑:

$Index = Hash(Object_Address) % StripeCount$

  • 哈希实现通常会忽略对齐造成的恒定低位并混合地址位,但具体右移量和混合算法应以目标 objc4 源码为准。

  • 再映射到当前实现的分片范围。哈希只能改善分布,不能保证绝对均匀。


总结:面试话术

SideTables 采用的是**分段锁(Lock Striping)**机制。

  1. 解决并发瓶颈:如果只有 1 个表,所有相关操作都会争用同一把锁;增加分片可以降低无关对象之间的竞争,但会增加结构开销。

  2. 实现细节:分片数与哈希函数随平台和 Runtime 版本变化,业务代码不依赖具体数字。

  3. 边界:落到同一分片的对象仍会争用分片锁;是否存在伪共享要看实际内存布局,不能只靠分片数量推断。”

flowchart TD subgraph Object[对象内存 & ISA] ISA_Count["isa.extra_rc (随身钱包)"] ISA_Flags["isa 标记位
(has_sidetable_rc / weakly_referenced)"] end subgraph SideTableZone[SideTable 分段锁银行] Lock["🔒 Lock"] RefMap[("RefcountMap
(存款单)")] WeakMap[("WeakRefMap
(遗嘱/通讯录)")] end %% Retain 流程 Start_Retain(Retain +1) --> CheckISA_Full{isa 满了吗?} CheckISA_Full -- 没满 (Fast) --> IncISA[isa.extra_rc++] CheckISA_Full -- 满了 (Overflow) --> MoveHalfToBank[1 批量转移计数
2. 标记 has_sidetable_rc = 1] MoveHalfToBank --> LockRetain[加锁] LockRetain --> StoreInMap[存入 RefcountMap] StoreInMap --> UnlockRetain[解锁] --> IncISA %% Release 流程 Start_Release(Release -1) --> CheckISA_Empty{isa 空了吗?} CheckISA_Empty -- 没空 (Fast) --> DecISA[isa.extra_rc--] CheckISA_Empty -- 空了 (Underflow) --> CheckBankFlag{有银行存款标记?} CheckBankFlag -- No --> Dealloc[触发 Dealloc] CheckBankFlag -- Yes --> LockRel[加锁] LockRel --> BorrowHalf[从 RefcountMap
取出 一半 计数] BorrowHalf --> UnlockRel[解锁] UnlockRel --> RefillISA[回填 isa] --> DecISA %% Weak 流程 Start_Weak(Weak 赋值) --> SetWeakFlag[标记 isa.weakly_referenced = 1] SetWeakFlag --> LockWeak[加锁] LockWeak --> RegisterWeak[在 WeakRefMap 注册
Key: Obj, Value: &ptr] RegisterWeak --> UnlockWeak[解锁] %% Dealloc 流程详细 Dealloc --> CheckWeakFlag{被 Weak 引用过?} CheckWeakFlag -- No --> FreeMem(free 内存) CheckWeakFlag -- Yes --> LockDealloc[加锁] LockDealloc --> ClearWeak[1 查 WeakRefMap
2. 将所有 weakPtr 设为 nil
3. 删除记录] ClearWeak --> UnlockDealloc[解锁] --> FreeMem style Object fill:#e3f2fd,stroke:#2196f3 style SideTableZone fill:#fff3e0,stroke:#ff9800 style Start_Retain fill:#c8e6c9,stroke:#4caf50 style Start_Release fill:#ffcdd2,stroke:#f44336 style Start_Weak fill:#e1bee7,stroke:#9c27b0 style Dealloc fill:#263238,stroke:#000,color:#fff

2.3 AutoreleasePool 原理

💣 深挖题 Q5: AutoreleasePool 原理?
详细解答

  • 结构
    • AutoreleasePoolPage:4096 字节,双向链表。
    • 每个 page 存储 autorelease 对象指针。
  • push
    • 插入一个 POOL_BOUNDARY(哨兵对象)。
  • autorelease
    • 将对象指针压入当前 page。
    • 如果 page 满,新建 page 链接起来。
  • pop(token)
    • 从栈顶开始发送 release,直到遇到哨兵。
  • RunLoop 关系
    • 在 Entry / BeforeWaiting / Exit 自动 push/pop。

2.4 Associated Objects 原理

💣 深挖题 Q6: Associated Objects 原理?
详细解答

  • 存储在全局 AssociationsManager(哈希表)。
  • key:对象地址,value:关联对象列表(map)。
  • 释放对象时:
    • objc_destructInstance 会调用 _object_remove_assocations
    • 遍历并释放所有关联对象。

3️⃣ Block(闭包与捕获)

核心考点:类型、捕获机制、__block 原理、循环引用


3.1 Block 类型

  • Global:不捕获变量。
  • Stack:捕获外部变量,生命周期随栈结束。
  • Malloc:copy 到堆上,延长生命周期。

3.2 捕获机制

  • 局部变量:值拷贝。
  • 对象:指针拷贝(retain)。
  • static 变量:指针传递。
  • 全局变量:直接访问。

3.3 __block 原理

💣 深挖题 Q7: __block 原理?
详细解答

  • 编译器将 __block 变量包装成 __Block_byref 结构体:

c

复制

struct __Block_byref_var { void *__isa; struct __Block_byref_var *__forwarding; int flags; int size; int var; };

  • 栈上时 __forwarding 指向自己。
  • copy 到堆上时:
    • 创建堆副本。
    • 栈上的 __forwarding 指向堆副本。
    • 保证 Block 内外访问的是同一个变量。

3.4 Block 内存布局

💣 深挖题 Q8: Block 内存布局?
详细解答

c

复制

struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; }; struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *src); };

  • invoke:Block 执行函数指针。
  • descriptor:包含 size、copy/dispose 函数。
  • copy/dispose 用于管理捕获的对象(retain/release)。

3.5 循环引用

💣 深挖题 Q9: Block 循环引用检测与解决?
详细解答

  • 检测
    • Instruments(Leaks)
    • Xcode Memory Graph
    • 静态分析工具
  • 解决
    • __weak 打破环。
    • __strong 在 Block 内防止提前释放。

objc

复制

__weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf doSomething]; };


4️⃣ 延伸底层机制


4.1 KVO 原理

💣 深挖题 Q10: KVO 底层原理?
详细解答

  • 动态生成子类(NSKVONotifying_Class)。
  • 重写 setter:
    • 调用 willChangeValueForKey
    • 调用原 setter。
    • 调用 didChangeValueForKey
  • 释放时恢复原类。

4.2 GCD 底层

💣 深挖题 Q11: dispatch_once 底层?
详细解答

  • dispatch_once → pthread_once
  • 使用静态变量记录是否执行过,CAS 保证线程安全。

4.3 内存对齐与对象布局

💣 深挖题 Q12: 内存对齐规则?
详细解答

  • 对象内存按最大成员类型对齐。
  • 对齐提高 CPU 访问效率,减少跨字节访问。