iOS 面试的原理题有几个经典得不能再经典的考点。本文逐题给出标准答法与追问陷阱:消息发送与转发三阶段、weak 自动置 nil 的 SideTable 实现、Block 循环引用、RunLoop 的 Mode 机制,以及 SwiftUI 的五个属性包装器怎么选。
iOS 面试的原理题有几个”经典得不能再经典”的考点:Runtime、RunLoop、内存管理(ARC)、Block、响应链、SwiftUI。这些常年不变,但考察深度差异极大——同样问”weak 的实现原理”,有人只答”自动置 nil”,有人能讲清 SideTable 与弱引用表。
本文逐题给出标准答法、追问陷阱和那句”面试官想听的话”。版本背景:iOS 27 处于发布窗口,Swift 与 SwiftUI 的迭代节奏已趋稳。

一、Runtime:Objective-C 的动态性从哪来
Q1:什么是 Runtime?消息发送的完整过程?
Objective-C 的方法调用不是直接的函数地址跳转,而是消息发送。编译器会把 [obj doSomething] 转成:
objc_msgSend(obj, @selector(doSomething))
// 其执行路径(简化):
// 1. 快速查找:到 receiver 所属类的 cache(方法缓存哈希表)里找 IMP
// → 命中则直接跳转
// 2. 慢速查找:缓存没命中,走 lookUpImpOrForward
// → 在本类的 method_list 里二分/线性查找
// → 没找到则沿 superclass 链一路向上,直到 NSObject
// 3. 仍未找到 → 进入"消息转发"三阶段
关键点:方法解析发生在运行时,不是编译时。这就是 OC 动态性的来源——你可以在运行时给类加方法、交换实现(Method Swizzling)、甚至在消息转发阶段把消息转给别的对象。
Q2:消息转发的三个阶段?
- 动态方法解析:
+resolveInstanceMethod:/+resolveClassMethod:。给一次机会动态添加方法实现(class_addMethod)。常用于@dynamic属性的惰性实现。 - 备援接收者:
-forwardingTargetForSelector:。返回另一个能处理该消息的对象。这是三个步骤里代价最低的,只是重定向,不创建 NSInvocation。 - 完整转发:
-methodSignatureForSelector:返回方法签名,-forwardInvocation:拿到NSInvocation做任意处理(转发给多个对象、记录日志、吞掉)。
// 阶段 2:快速转发(性能最好)
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(heavyCompute)) {
return self.computeEngine; // 交给别人干
}
return [super forwardingTargetForSelector:aSelector];
}
// 阶段 3:完整转发(可应对未实现崩溃的兜底)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
NSLog(@"未实现的方法:%@", NSStringFromSelector(invocation.selector));
}
加分点:很多”防崩溃”方案就是在阶段 3 兜底。但要主动说明代价——
NSInvocation的创建开销较大,且掩盖问题不等于解决问题,线上应当配合上报,不能默默吞掉。
Q3:isKindOfClass 和 isMemberOfClass 的区别?
isKindOfClass:判断对象是否为该类或其子类的实例(沿继承链比较)。isMemberOfClass:只判断是否恰好是该类的实例(只比较当前类)。
有个经典陷阱题:对类对象调用时会走元类(meta class)链。答这类题时要分清“实例方法版本走类链,类方法版本走元类链”。
Q4:Category 和 Extension 的区别?
追问”Category 的同名方法会怎样“——后编译的 Category 会”覆盖”先编译的(实际是方法列表顺序靠前,查找时先命中)。这个行为是不确定的,不要依赖它。这也是为什么给系统类写 Category 时方法名必须加前缀。
二、内存管理:ARC 到底做了什么
Q5:ARC 下还有内存泄漏吗?
有,而且很常见。ARC 只是自动管理”引用计数”,它解决不了循环引用。三类典型泄漏:
- 循环引用:对象互相强引用,或形成了环(A→B→C→A)。
- Block 捕获 self:Block 强引用 self,self 又持有这个 Block。
- 定时器 / 通知未清理:
NSTimer(iOS 10 前的 target-action 形式)强引用 target,NotificationCenter的 block 式观察者持有 self。
// 循环引用:self -> completion -> self
self.completion = ^{
[self doSomething]; // Block 强捕获 self
};
// 解法:weak-strong dance
__weak typeof(self) weakSelf = self;
self.completion = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return; // 已释放则直接返回
[strongSelf doSomething]; // 保证执行期间不会被释放
};
// NSTimer:iOS 10+ 用 block 版 API,避免 target 被强引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer *t) {
[weakSelf tick];
}];
// 并且要在 dealloc / viewWillDisappear 里 invalidate
Q6:weak 为什么会在对象释放后自动变成 nil?
这是 Runtime 层面的一张表在起作用。简化后的结构是:
- 系统维护若干张
SideTable(全局哈希表,按对象地址分桶,带锁保证线程安全)。 - 每个 SideTable 里有
weak_table_t:以对象地址为 key,value 是所有指向它的 weak 指针地址的集合(weak_entry_t)。 - 对象释放时,Runtime 走
dealloc → objc_destructInstance → clearDeallocating,查出该对象对应的所有 weak 指针地址,把它们逐个置为 nil,然后从表中移除记录。
所以 weak 的代价是额外的一次查表与置 nil 操作,比 assign(不置 nil,会产生悬垂指针)安全但略慢。
Q7:strong、copy、assign、weak 怎么选?
strong:默认,持有对象,引用计数 +1。copy:建立不可变副本。NSString、NSArray、NSDictionary属性必须用 copy——否则一个NSMutableString赋进来后,外部改动会意外影响属性值。assign:只做简单赋值,不置 nil。用于基本数据类型(int、CGFloat、结构体)。修饰对象会产生悬垂指针。weak:不持有,对象释放后自动置 nil。用于 delegate、IBOutlet 子视图、Block 中打破循环引用。
高频追问:”delegate 为什么用 weak 而不用 strong?“——因为典型结构是
Controller持有View,View的delegate指回Controller。若 delegate 是 strong,就形成了循环引用,两者都释放不掉。
Q8:AutoreleasePool 什么时候释放?
autorelease 的对象被加入当前的自动释放池,池子在一次 RunLoop 迭代结束时(即将进入休眠前)被 pop,池内对象收到一次 release。
实践中真正需要注意的是循环内创建大量临时对象:
// 反例:百万次循环产生的临时对象堆积到循环结束才释放,内存峰值飙升
for (int i = 0; i < 1000000; i++) {
NSString *s = [NSString stringWithFormat:@"item-%d", i];
// ...
}
// 正例:手动加自动释放池,及时释放
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *s = [NSString stringWithFormat:@"item-%d", i];
// ...
}
}
三、RunLoop:事件循环的骨架
Q9:RunLoop 是什么?和线程什么关系?
RunLoop 是一个事件处理循环:有事件就处理,没事件就休眠。每条线程最多一个 RunLoop,主线程默认开启,子线程默认不开启(需要手动 [[NSRunLoop currentRunLoop] run] 或用 CFRunLoopRun()),且必须至少有一个 Source/Timer/Observer 才能维持运行。
一次循环的核心流程:
- 通知 Observer:即将进入 Loop
- 通知 Observer:即将处理 Timer / Source0
- 处理 Source0(触摸事件、
performSelector:onThread:) - 若有 Source1(基于 mach port 的端口消息),跳到第 9 步
- 通知 Observer:即将休眠
- 休眠等待(内核态,通过 mach_msg 等待唤醒,不占 CPU)
- 被某个事件唤醒(Timer 到点、Source1 消息、GCD 回主线程等)
- 处理唤醒时收到的消息,回到第 2 步
- 通知 Observer:即将退出 Loop
Q10:RunLoop 有哪些实际应用?
- NSTimer 精度问题:Timer 依赖 RunLoop,若当前正在处理耗时任务,Timer 会被顺延;滚动 scrollView 时 RunLoop 切到
UITrackingRunLoopMode,默认 mode 下的 Timer 不触发——解法是把 Timer 加到NSRunLoopCommonModes。 - 常驻线程:给子线程加一个
NSPort并启动 RunLoop,让它不退出(AFNetworking 旧版本用这个模式保活网络线程)。 - 卡顿监控:向主线程 RunLoop 注册 Observer,监听
kCFRunLoopBeforeSources与kCFRunLoopAfterWaiting之间的耗时,超时即判定为一次卡顿。 - 滑动时延迟加载大图:把任务放进
NSDefaultRunLoopMode,滚动结束(切回默认 mode)后再执行,避免影响滚动帧率。
Q11:RunLoop 的 Mode 有什么用?
Mode 是一组「Source / Timer / Observer 的集合」,RunLoop 同一时刻只能运行在某一个 Mode 下,只会处理当前 Mode 里的事件。这种设计是为了隔离不同场景的事件源——滑动时只关心触摸,就不去处理默认模式下的定时器与网络回调,从而保证滚动优先级的帧率。
常见 Mode:kCFRunLoopDefaultMode、UITrackingRunLoopMode、NSRunLoopCommonModes(注意:CommonModes 不是一个真正的 Mode,而是一组”标记为 common”的 Mode 的同步机制)。
四、Block 与 GCD
Q12:Block 有哪几种类型?
__NSGlobalBlock__:没有捕获任何外部变量,存在全局区。__NSStackBlock__:捕获了外部变量,分配在栈上(MRC 下)。__NSMallocBlock__:栈上的 Block 被copy到堆上。ARC 下大部分情况会自动 copy。
追问”为什么 Block 要捕获变量的值而不是引用?“——因为 Block 可能在其定义的作用域之外执行(比如异步回调),栈上的局部变量届时已经失效,所以普通局部变量是按值捕获(const 拷贝)。需要修改它时,必须用 __block,编译器会把它包装成一个结构体,让内外共享同一份存储。
Q13:__block 和 __weak 的区别?
__block 解决”能不能改”,__weak 解决”要不要持有”,两者维度不同,经常一起用。
__block int counter = 0; // 让 Block 内部能修改外部变量
__weak typeof(self) weakSelf = self; // 让 Block 不强持有 self
Q14:GCD 的队列与死锁?
关键区分:dispatch_get_main_queue() 是串行队列。在主队列里同步向主队列派发任务,就会死锁——因为同步派发要求”立即执行”,而主队列正在执行当前的同步派发语句,任务永远排不到。
// 死锁:主线程执行,sync 到主队列
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"永远不会执行");
});
// 安全:判断当前是否已在主队列
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) ==
dispatch_queue_get_label(dispatch_get_main_queue())) {
block(); // 直接执行
} else {
dispatch_async(dispatch_get_main_queue(), block);
}
五、SwiftUI:2026 年的必考区
Q15:SwiftUI 和 UIKit 的根本区别?
- 声明式:描述”UI 应该长什么样”,而不是”怎么一步步改成那样”。
- 值类型视图:
View是struct而非class,轻量、无继承,靠组合而非继承扩展。 - 状态驱动:
@State变化触发 body 重新求值,框架 diff 出真实变更。 - 与 UIKit 可互操作:通过
UIViewRepresentable/UIViewControllerRepresentable双向桥接,老项目可以渐进迁移。
Q16:@State、@Binding、@ObservedObject、@EnvironmentObject 怎么选?
高频陷阱:”
@ObservedObject和@StateObject的区别?“——@ObservedObject不持有对象,视图重建时可能被意外重置(比如在父视图 body 里ViewModel()现场创建);@StateObject只在视图首次创建时初始化一次,重建时保持。在视图内部创建的模型必须用@StateObject。
Q17:SwiftUI 的 body 会被频繁求值,怎么优化?
- 拆分视图:把大 body 拆成小组件,让状态变化只影响真正依赖它的子树(缩小失效范围)。
- 减少依赖:视图只声明它真正用到的状态,多余的依赖会导致无谓重算。
- 给模型加粒度:使用
@Observable(Observation 框架)后,只有真正被读取的属性才会建立依赖,比ObservableObject的”整体发布”更精准。 - 耗时计算外移:body 应当是纯函数且开销极小,重活放进 model 或异步任务。
六、小结
iOS 原理题的核心主线是“动态性 + 内存模型 + 事件循环”:Runtime 支撑了 OC 的一切动态能力,ARC 与 weak 表解决的是引用计数自动化的边界问题,RunLoop 是主线程响应用户与系统事件的心跳,SwiftUI 则代表着从命令式到声明式的范式转移。
准备建议:每条主线配一个真实案例——”Block 循环引用导致页面不释放,用 weak-strong dance 解决””滚动时 Timer 不触发,改到 CommonModes 才正常”。具体经历比完整背诵源码更能说明你真的做过。
参考:Apple 开发者文档、SwiftUI 文档、swift-corelibs-foundation。API 行为随系统版本演进,涉及生命周期与后台行为的请以目标版本的官方文档为准。
#iOS 面试题#Runtime#RunLoop#面试

