修饰符与内存语义
属性修饰符解决“谁拥有对象、怎么暴露 API”,变量修饰符解决“闭包捕获、生命周期和置空行为”。面试里要把 property attribute 和 ownership qualifier 分开讲。
对象属性默认用 strong,delegate 和反向引用用 weak,NSString/集合/Block 属性优先 copy。基础类型用 assign。Block 里避免循环引用用 __weak,需要修改局部变量才用 __block。
| 修饰符 | 语义 | 适用场景 | 补充与坑点 |
|---|---|---|---|
strong / retain |
强引用,拥有对象。 | 模型、服务对象、普通对象属性。 | ARC 下常写 strong;retain 多见于 MRC 或历史代码。 |
weak |
弱引用,不增加引用计数,目标释放后自动置 nil。 |
delegate、dataSource、父子/回调反向引用。 | 只能用于对象类型;比 assign 安全,因为不会悬垂。 |
assign |
直接赋值,不做对象生命周期管理。 | BOOL、NSInteger、CGFloat 等标量。 |
不要修饰 Objective-C 对象,否则对象释放后会留下野指针。 |
copy |
保存副本,通常是不可变副本或堆上的 Block。 | NSString、NSArray、NSDictionary、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 是线程安全。”更准确的说法是:它只保护单次存取,复合操作仍需要锁、队列或其他同步方案。
Block
Block 的高频点是对象本质、三种存储位置、变量捕获方式、循环引用和 __block 底层结构。
Block 是带函数指针和捕获环境的对象。全局 Block 不捕获自动变量;栈 Block 在栈上;拷贝后是堆 Block。自动变量默认按值捕获,对象默认强捕获;需要修改局部变量用 __block,破循环引用用 __weak。
捕获规则
- 局部自动变量默认按值捕获,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。
Runtime
Runtime 的主线是“对象如何找到方法”:对象通过 isa 找类,类查 cache 和方法列表,找不到再解析和转发。
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 信息,把动态结构映射到静态对象上。
RunLoop
RunLoop 是线程的事件循环:有事处理,没事休眠,被事件唤醒后继续。主线程默认开启,子线程要长期存活时需要手动配置 source/timer 并启动。
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 的 BeforeWaiting 和 Exit 附近提交事务,因此布局和绘制问题会反映到主线程周期里。
RunLoop 阶段演示
kCFRunLoopEntry
RunLoop 即将进入一轮循环。Observer 可以在这个阶段记录状态。
主线程 RunLoop 不断重复这个过程,UI 事件、定时器、布局提交都被放进周期里处理。
while(1)。更合理的模型是:创建线程,给 RunLoop 添加 Source/Port 或 Timer,运行 RunLoop,任务通过 Source 或队列唤醒线程。
UIKit
UIKit 高频题集中在事件命中、响应链、UIView/CALayer 边界、AutoLayout 和滚动性能。
UIView 管交互和层级,CALayer 管显示和动画。事件传递从 window 向下 hit-test 找目标 view,响应链从目标 view 向上找 responder。卡顿主要来自主线程重任务、布局求解、图片处理和 GPU 渲染压力。
Hit-Test
- 从
UIWindow开始向子视图递归查找。 - 先判断
hidden、userInteractionEnabled、alpha、pointInside。 - 子视图通常按从前到后的顺序检查,命中最上层可响应视图。
Responder Chain
响应链从命中的 view 开始,沿 superview、viewController、window、application 向上传递。事件传递是“找谁”,响应链是“谁处理”。
| 问题 | 更准确的描述 | 优化建议 |
|---|---|---|
| AutoLayout 慢 | 不是“不能用”,而是复杂约束系统在频繁更新时求解成本高。 | 减少嵌套和约束数量,列表里缓存高度,必要时局部使用 frame。 |
| 离屏渲染 | mask、阴影、group opacity、复杂圆角等可能触发额外渲染路径。 | 设置 shadowPath,预渲染图片,避免滚动中动态绘制。 |
| UITableView vs UICollectionView | 单列标准列表下 UITableView 更专用;UICollectionView 更灵活但布局系统更通用。 | 按需求选型,不为简单单列列表引入复杂布局。 |
多线程
先分清 CPU、线程、RunLoop、队列的层级,再讨论同步/异步、串行/并行、锁和死锁。
线程是 CPU 调度单位,GCD 队列是任务调度抽象。串行队列一次执行一个任务,并行队列可同时执行多个任务。同步会等待任务完成,异步不等待。死锁常见于当前串行队列同步派发到自己。
| 组合 | 行为 | 面试提醒 |
|---|---|---|
| 串行 + 同步 | 当前线程等待任务执行完。 | 在当前串行队列里 sync 到自己会死锁。 |
| 串行 + 异步 | 任务按顺序执行,调用方不等待。 | 可能新开线程,也可能复用线程;队列不等于线程。 |
| 并行 + 同步 | 当前线程等待,任务可在当前线程或其他线程执行。 | 同步限制的是调用方,不是队列并发能力。 |
| 并行 + 异步 | 多个任务可并发执行,调用方立即返回。 | 并发数由系统调度,不是无限开线程。 |
死锁推演
主线程正在执行
当前代码已经运行在主线程,而主队列也是串行队列。
队列里的下一个任务必须等当前任务结束才有机会执行。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"不会执行到这里");
});
栅栏函数
dispatch_barrier 只对自建并发队列有意义。它等待前面的任务完成,再独占执行,然后放行后续任务。
线程安全
互斥锁、信号量、串行队列、读写锁、NSCondition 都是工具。关键是保护共享可变状态。
单例
dispatch_once 保证初始化只执行一次,适合全局唯一实例。不要把单例当成全局状态垃圾桶。
网络与缓存
把 HTTP 流程、现代 TLS、NSURLSession 结构、缓存协商和断点续传串起来,答案会比单背步骤更稳。
HTTP 请求通常经过 DNS、TCP、TLS、请求响应和连接复用。HTTPS 用证书验证身份,用密钥协商生成对称密钥。缓存由 Cache-Control、ETag、Last-Modified 等共同决定;断点续传依赖 Range 和服务端支持。
现代 TLS 说法
TLS 1.2/1.3 主流使用 ECDHE 做密钥协商,证书用于身份验证和签名校验,后续数据用对称加密保护。
旧式简化说法
“客户端生成随机密钥,用服务器公钥加密发过去”更像旧 RSA key exchange 的简化描述。面试中最好补一句现代 TLS 的 ECDHE。
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 失效。 |
启动优化与稳定性
启动优化要拆成 pre-main 和 main 后;稳定性要按崩溃类型定位:野指针、越界、KVC/KVO、线程、生命周期和资源异常。
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 和计算。 |