iOS Interview Playbook

从零散笔记到可复述的知识模型

新版把每个知识点整理成同一套阅读节奏:先看 30 秒答法,再看核心模型、易错点和小演示。目标不是背一堆术语,而是能在面试里把原因讲顺。

知识地图

内存语义strong / weak / copy / atomic
Block捕获、堆栈、循环引用
Runtimeisa、cache、转发链
RunLoopMode、Observer、休眠唤醒
UIKit事件、渲染、布局性能
稳定性启动、KVC/KVO、崩溃治理
01 / Ownership

修饰符与内存语义

属性修饰符解决“谁拥有对象、怎么暴露 API”,变量修饰符解决“闭包捕获、生命周期和置空行为”。面试里要把 property attribute 和 ownership qualifier 分开讲。

30 秒答法

对象属性默认用 strong,delegate 和反向引用用 weak,NSString/集合/Block 属性优先 copy。基础类型用 assign。Block 里避免循环引用用 __weak,需要修改局部变量才用 __block

修饰符 语义 适用场景 补充与坑点
strong / retain 强引用,拥有对象。 模型、服务对象、普通对象属性。 ARC 下常写 strongretain 多见于 MRC 或历史代码。
weak 弱引用,不增加引用计数,目标释放后自动置 nil delegate、dataSource、父子/回调反向引用。 只能用于对象类型;比 assign 安全,因为不会悬垂。
assign 直接赋值,不做对象生命周期管理。 BOOLNSIntegerCGFloat 等标量。 不要修饰 Objective-C 对象,否则对象释放后会留下野指针。
copy 保存副本,通常是不可变副本或堆上的 Block。 NSStringNSArrayNSDictionary、Block。 集合 copy 是浅拷贝;元素本身不会递归复制。
atomic / nonatomic 控制 getter/setter 是否具备原子访问语义。 iOS 项目中通常写 nonatomic atomic 不等于业务线程安全,只保证单次 getter/setter 的同步。
readonly / readwrite 控制是否对外生成 setter。 公开头文件只读,类扩展里改为可读写。 readwrite 是默认值。
__block 让局部变量以“可变盒子”的形式被 Block 捕获。 Block 内需要修改外部局部变量。 ARC 下 __block 对对象变量仍可能强持有,不能用它破循环引用。
__weak / __unsafe_unretained 弱持有或不安全不持有。 Block 捕获 self;兼容极旧代码。 __unsafe_unretained 不会置空,释放后访问就是野指针。
@synthesize / @dynamic 控制属性访问器的生成或承诺运行时提供。 自定义 ivar、Core Data、动态派发属性。 @dynamic 只是让编译器别报警,运行时没有实现仍会崩。

修饰符选择演示

delegate

delegate 通常是反向引用,不应该被持有者强引用。

推荐:weak。如果写成 strong,很容易形成控制器和子对象之间的循环引用。

@property (nonatomic, weak) id<UITableViewDelegate> delegate;

推荐答法

copy 不是因为 NSString 一定可变,而是为了隔离外部传入的 NSMutableString,保证属性语义稳定。”

容易误答

atomic 是线程安全。”更准确的说法是:它只保护单次存取,复合操作仍需要锁、队列或其他同步方案。

02 / Closure Object

Block

Block 的高频点是对象本质、三种存储位置、变量捕获方式、循环引用和 __block 底层结构。

30 秒答法

Block 是带函数指针和捕获环境的对象。全局 Block 不捕获自动变量;栈 Block 在栈上;拷贝后是堆 Block。自动变量默认按值捕获,对象默认强捕获;需要修改局部变量用 __block,破循环引用用 __weak

NSGlobalBlock 不捕获自动变量,生命周期全局存在。
NSStackBlock 捕获自动变量且未逃逸,位于栈上。
NSMallocBlock 被 copy 到堆上,能安全逃逸出当前栈帧。

捕获规则

  • 局部自动变量默认按值捕获,Block 内看到的是创建时的值。
  • static / 全局变量不复制值,Block 直接访问存储位置。
  • 对象变量会被捕获为对象引用;在 ARC 下通常表现为强持有。
  • self.property 会捕获 self,不是只捕获那个属性。

__block 的 forwarding

__block 变量会被包装为结构体,结构体里有 forwarding 指针。Block 从栈复制到堆时,栈上的变量和堆上的变量都会通过 forwarding 指向唯一有效实例。

循环引用拆解

self -> block -> self

对象强持有 Block,Block 又强捕获 self,就形成环。

解决重点不是“让变量可修改”,而是打断所有权关系。

@property (nonatomic, copy) void (^completion)(void);

self.completion = ^{
    [self reloadData];
};
__block 在 ARC 下不能当成破循环引用的工具。面试里如果说“用 __block 防止循环引用”,要补充这是 MRC 时代的历史语境,ARC 下应该使用 __weak
03 / Message Dispatch

Runtime

Runtime 的主线是“对象如何找到方法”:对象通过 isa 找类,类查 cache 和方法列表,找不到再解析和转发。

30 秒答法

id 是对象指针,Class 是类对象指针。实例方法存在类对象中,类方法存在元类中。消息发送先查 cache,再查方法列表和父类;仍找不到就动态解析、快速转发、完整转发,最后才崩溃。

对象 / 类 / 元类

id = objc_object *Class = objc_class *。对象的 isa 指向类;类的 isa 指向元类;元类保存类方法。

方法缓存

cache 是 Runtime 的快路径。方法第一次查找后会缓存 IMP,后续同一 selector 命中 cache 就直接调用。

分类

分类方法和主类同名时存在覆盖风险,加载顺序相关,不要依赖这种行为。分类的 +load 都会执行。

objc_msgSend 流程演示

1. isa 查找

接收者通过 isa 指针找到它的类对象。实例方法从类对象开始找;类方法从元类开始找。

这一步解释了为什么对象、类和元类是 Runtime 的入口。

转发链说法

完整顺序是动态方法解析、快速转发、方法签名、forwardInvocation:。快速转发只能换接收者,完整转发可以改参数、返回值,也能分发给多个对象。

respondsToSelector: 误区

它不会真正执行完整消息转发。若对象依赖转发链处理消息,需要同步重写 respondsToSelector:,否则外部判断可能不准确。

关联对象、Swizzling、字典转模型怎么串起来

关联对象通过 Runtime 的关联表给对象挂额外 key-value,常用于分类“加属性”。Swizzling 交换 selector 和 IMP,常用于埋点、修复、AOP,但要用 dispatch_once 控制时机和幂等。字典转模型则是读取属性列表、类型编码和 setter 信息,把动态结构映射到静态对象上。

04 / Event Loop

RunLoop

RunLoop 是线程的事件循环:有事处理,没事休眠,被事件唤醒后继续。主线程默认开启,子线程要长期存活时需要手动配置 source/timer 并启动。

30 秒答法

RunLoop 和线程一一对应。Mode 决定当前能处理哪些 source/timer;Common Modes 不是独立模式,而是一组模式标记。卡顿监控通常观察主线程 RunLoop 状态,线程保活则给子线程添加 source 并运行 RunLoop。

Mode

Default 处理普通事件,Tracking 用于滚动等追踪场景,Common 是模式集合标签,不是实际运行模式。

Source / Timer / Observer

Source 负责输入事件,Timer 负责定时,Observer 监听状态变化。卡顿监控通常基于 Observer 和独立线程信号。

Core Animation 提交

UIKit/CA 会在 RunLoop 的 BeforeWaitingExit 附近提交事务,因此布局和绘制问题会反映到主线程周期里。

RunLoop 阶段演示

kCFRunLoopEntry

RunLoop 即将进入一轮循环。Observer 可以在这个阶段记录状态。

主线程 RunLoop 不断重复这个过程,UI 事件、定时器、布局提交都被放进周期里处理。

子线程保活不能只写 while(1)。更合理的模型是:创建线程,给 RunLoop 添加 Source/Port 或 Timer,运行 RunLoop,任务通过 Source 或队列唤醒线程。
主线程 Timer 加到 Default Mode 时,滚动进入 Tracking Mode 后可能不触发。需要滚动中继续触发时,把 Timer 加到 Common Modes。
05 / UI Pipeline

UIKit

UIKit 高频题集中在事件命中、响应链、UIView/CALayer 边界、AutoLayout 和滚动性能。

30 秒答法

UIView 管交互和层级,CALayer 管显示和动画。事件传递从 window 向下 hit-test 找目标 view,响应链从目标 view 向上找 responder。卡顿主要来自主线程重任务、布局求解、图片处理和 GPU 渲染压力。

Hit-Test

  • UIWindow 开始向子视图递归查找。
  • 先判断 hiddenuserInteractionEnabledalphapointInside
  • 子视图通常按从前到后的顺序检查,命中最上层可响应视图。

Responder Chain

响应链从命中的 view 开始,沿 superview、viewController、window、application 向上传递。事件传递是“找谁”,响应链是“谁处理”。

主线程JSON 解析、归档、文件 IO、图片解码都要避免挤在滚动时执行。
布局约束越多、层级越深,AutoLayout 求解成本越高。
渲染阴影、mask、复杂圆角和过大图片会增加 GPU/离屏压力。
列表复用、预取、异步解码、缓存高度、滚动时降级非关键任务。
问题 更准确的描述 优化建议
AutoLayout 慢 不是“不能用”,而是复杂约束系统在频繁更新时求解成本高。 减少嵌套和约束数量,列表里缓存高度,必要时局部使用 frame。
离屏渲染 mask、阴影、group opacity、复杂圆角等可能触发额外渲染路径。 设置 shadowPath,预渲染图片,避免滚动中动态绘制。
UITableView vs UICollectionView 单列标准列表下 UITableView 更专用;UICollectionView 更灵活但布局系统更通用。 按需求选型,不为简单单列列表引入复杂布局。
06 / Concurrency

多线程

先分清 CPU、线程、RunLoop、队列的层级,再讨论同步/异步、串行/并行、锁和死锁。

30 秒答法

线程是 CPU 调度单位,GCD 队列是任务调度抽象。串行队列一次执行一个任务,并行队列可同时执行多个任务。同步会等待任务完成,异步不等待。死锁常见于当前串行队列同步派发到自己。

组合 行为 面试提醒
串行 + 同步 当前线程等待任务执行完。 在当前串行队列里 sync 到自己会死锁。
串行 + 异步 任务按顺序执行,调用方不等待。 可能新开线程,也可能复用线程;队列不等于线程。
并行 + 同步 当前线程等待,任务可在当前线程或其他线程执行。 同步限制的是调用方,不是队列并发能力。
并行 + 异步 多个任务可并发执行,调用方立即返回。 并发数由系统调度,不是无限开线程。

死锁推演

主线程正在执行

当前代码已经运行在主线程,而主队列也是串行队列。

队列里的下一个任务必须等当前任务结束才有机会执行。

dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"不会执行到这里");
});

栅栏函数

dispatch_barrier 只对自建并发队列有意义。它等待前面的任务完成,再独占执行,然后放行后续任务。

线程安全

互斥锁、信号量、串行队列、读写锁、NSCondition 都是工具。关键是保护共享可变状态。

单例

dispatch_once 保证初始化只执行一次,适合全局唯一实例。不要把单例当成全局状态垃圾桶。

07 / Network

网络与缓存

把 HTTP 流程、现代 TLS、NSURLSession 结构、缓存协商和断点续传串起来,答案会比单背步骤更稳。

30 秒答法

HTTP 请求通常经过 DNS、TCP、TLS、请求响应和连接复用。HTTPS 用证书验证身份,用密钥协商生成对称密钥。缓存由 Cache-Control、ETag、Last-Modified 等共同决定;断点续传依赖 Range 和服务端支持。

现代 TLS 说法

TLS 1.2/1.3 主流使用 ECDHE 做密钥协商,证书用于身份验证和签名校验,后续数据用对称加密保护。

旧式简化说法

“客户端生成随机密钥,用服务器公钥加密发过去”更像旧 RSA key exchange 的简化描述。面试中最好补一句现代 TLS 的 ECDHE。

DNS把域名解析成 IP,可能受本地、运营商、公共 DNS 缓存影响。
TCP / QUICHTTP/1.1 和 HTTP/2 常基于 TCP;HTTP/3 基于 QUIC。
TLS证书校验、密钥协商、生成会话密钥。
Request发送请求行、Header、Body,服务端处理后返回响应。
Cache根据缓存策略决定直接用缓存、协商缓存或重新拉取。

NSURLSession

管理会话、连接复用、任务创建、Cookie、缓存和协议栈行为。

Configuration

控制超时、缓存、蜂窝网络、后台传输、Cookie、协议类等。

Task / Delegate

DataTask、DownloadTask、UploadTask 执行具体请求;Delegate 接收进度、重定向、证书验证和完成回调。

缓存头 / 策略 含义 常见结论
Cache-Control: max-age 强缓存有效期。 未过期可直接使用缓存,通常不发请求。
ETag / If-None-Match 基于资源标识的协商缓存。 服务端返回 304 表示缓存仍可用。
Last-Modified / If-Modified-Since 基于修改时间的协商缓存。 精度和可靠性弱于 ETag,但仍常见。
Range 请求资源的部分字节区间。 断点续传依赖它,服务端文件变化会导致 resumeData 失效。
08 / Launch & Stability

启动优化与稳定性

启动优化要拆成 pre-main 和 main 后;稳定性要按崩溃类型定位:野指针、越界、KVC/KVO、线程、生命周期和资源异常。

30 秒答法

pre-main 主要受 Mach-O 加载、动态库、符号绑定、+load、静态初始化影响;main 后关注首屏路径。优化方向是减少动态库和启动期工作、懒加载、二进制重排、用 Instruments/MetricKit 度量。

pre-main

  • 加载 Mach-O 和 dyld。
  • 加载依赖库,做 Rebase / Bind。
  • 执行 +load 和 C++ 静态构造。
  • 现代系统有 dyld closure,但动态库数量和启动期代码仍重要。

main 后

  • UIApplicationMain 创建应用对象和 delegate。
  • 开启主线程 RunLoop。
  • 创建 window、rootViewController、加载首屏。
  • 首屏网络、图片、布局和同步 IO 都会影响体感。

二进制重排

把启动路径热函数放得更连续,减少 Page Fault。常配合 order file、启动采样和实际设备验证。

KVC

给标量属性设置 nil 会触发 setNilValueForKey:,未知 key 会触发 setValue:forUndefinedKey:

KVO

旧式 KVO 要管理 add/remove、context 和回调线程。现代系统减少了部分 dealloc 崩溃,但工程上仍应清晰管理观察生命周期。

稳定性问题 典型原因 治理动作
数组 / 字典越界 服务端数据异常、异步状态变化、索引未校验。 输入校验、边界判断、NSNull 处理、数据快照。
野指针 assign 对象、unsafe 引用、delegate 生命周期错误。 改用 weak,明确持有关系,打开 Zombie 辅助定位。
KVO 崩溃 重复添加/移除、context 混乱、回调线程更新 UI。 用唯一 context、封装 token、主线程更新 UI。
启动慢 动态库多、+load 重、首屏同步任务多。 延迟加载、懒初始化、减少启动链路 IO 和计算。