loader

Nerio News Magazine brings you trusted, timely and thought-provoking stories from around the globe.

Follow Us

iOS 面试高频 25 问:Runtime、RunLoop、内存管理与 SwiftUI

Share This Article:

iOS 面试的原理题有几个经典得不能再经典的考点。本文逐题给出标准答法与追问陷阱:消息发送与转发三阶段、weak 自动置 nil 的 SideTable 实现、Block 循环引用、RunLoop 的 Mode 机制,以及 SwiftUI 的五个属性包装器怎么选。

iOS 面试高频 25 问:Runtime、RunLoop、内存管理与 SwiftUI
关键词iOS 面试、Objective-C Runtime、消息转发、RunLoop、ARC、SwiftUI

iOS 面试的原理题有几个”经典得不能再经典”的考点:Runtime、RunLoop、内存管理(ARC)、Block、响应链、SwiftUI。这些常年不变,但考察深度差异极大——同样问”weak 的实现原理”,有人只答”自动置 nil”,有人能讲清 SideTable 与弱引用表

本文逐题给出标准答法、追问陷阱和那句”面试官想听的话”。版本背景:iOS 27 处于发布窗口,Swift 与 SwiftUI 的迭代节奏已趋稳。

消息转发的三个阶段
OC 消息转发:动态方法解析 → 备援接收者 → 完整转发

一、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:消息转发的三个阶段?

  1. 动态方法解析+resolveInstanceMethod: / +resolveClassMethod:。给一次机会动态添加方法实现class_addMethod)。常用于 @dynamic 属性的惰性实现。
  2. 备援接收者-forwardingTargetForSelector:。返回另一个能处理该消息的对象。这是三个步骤里代价最低的,只是重定向,不创建 NSInvocation。
  3. 完整转发-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:isKindOfClassisMemberOfClass 的区别?

  • isKindOfClass: 判断对象是否为该类或其子类的实例(沿继承链比较)。
  • isMemberOfClass: 只判断是否恰好是该类的实例(只比较当前类)。

有个经典陷阱题:对类对象调用时会走元类(meta class)链。答这类题时要分清“实例方法版本走类链,类方法版本走元类链”

Q4:Category 和 Extension 的区别?

维度 Category(分类) Extension(扩展/匿名分类)
编译时机 运行时合并进类 编译时
能否加属性 只能加方法声明,属性需关联对象 可以加实例变量与属性
需要源码 不需要 需要(写在 .m 里)
典型用途 给系统类加方法、拆分大类 隐藏私有方法与属性

追问”Category 的同名方法会怎样“——后编译的 Category 会”覆盖”先编译的(实际是方法列表顺序靠前,查找时先命中)。这个行为是不确定的,不要依赖它。这也是为什么给系统类写 Category 时方法名必须加前缀

二、内存管理:ARC 到底做了什么

Q5:ARC 下还有内存泄漏吗?

有,而且很常见。ARC 只是自动管理”引用计数”,它解决不了循环引用。三类典型泄漏:

  1. 循环引用:对象互相强引用,或形成了环(A→B→C→A)。
  2. Block 捕获 self:Block 强引用 self,self 又持有这个 Block。
  3. 定时器 / 通知未清理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:strongcopyassignweak 怎么选?

  • strong:默认,持有对象,引用计数 +1。
  • copy建立不可变副本NSStringNSArrayNSDictionary 属性必须用 copy——否则一个 NSMutableString 赋进来后,外部改动会意外影响属性值。
  • assign:只做简单赋值,不置 nil。用于基本数据类型(int、CGFloat、结构体)。修饰对象会产生悬垂指针。
  • weak:不持有,对象释放后自动置 nil。用于 delegate、IBOutlet 子视图、Block 中打破循环引用。

高频追问:”delegate 为什么用 weak 而不用 strong?“——因为典型结构是 Controller 持有 ViewViewdelegate 指回 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 才能维持运行。

一次循环的核心流程:

  1. 通知 Observer:即将进入 Loop
  2. 通知 Observer:即将处理 Timer / Source0
  3. 处理 Source0(触摸事件、performSelector:onThread:
  4. 若有 Source1(基于 mach port 的端口消息),跳到第 9 步
  5. 通知 Observer:即将休眠
  6. 休眠等待(内核态,通过 mach_msg 等待唤醒,不占 CPU)
  7. 被某个事件唤醒(Timer 到点、Source1 消息、GCD 回主线程等)
  8. 处理唤醒时收到的消息,回到第 2 步
  9. 通知 Observer:即将退出 Loop

Q10:RunLoop 有哪些实际应用?

  • NSTimer 精度问题:Timer 依赖 RunLoop,若当前正在处理耗时任务,Timer 会被顺延;滚动 scrollView 时 RunLoop 切到 UITrackingRunLoopMode,默认 mode 下的 Timer 不触发——解法是把 Timer 加到 NSRunLoopCommonModes
  • 常驻线程:给子线程加一个 NSPort 并启动 RunLoop,让它不退出(AFNetworking 旧版本用这个模式保活网络线程)。
  • 卡顿监控:向主线程 RunLoop 注册 Observer,监听 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 之间的耗时,超时即判定为一次卡顿。
  • 滑动时延迟加载大图:把任务放进 NSDefaultRunLoopMode,滚动结束(切回默认 mode)后再执行,避免影响滚动帧率。

Q11:RunLoop 的 Mode 有什么用?

Mode 是一组「Source / Timer / Observer 的集合」,RunLoop 同一时刻只能运行在某一个 Mode 下,只会处理当前 Mode 里的事件。这种设计是为了隔离不同场景的事件源——滑动时只关心触摸,就不去处理默认模式下的定时器与网络回调,从而保证滚动优先级的帧率。

常见 Mode:kCFRunLoopDefaultModeUITrackingRunLoopModeNSRunLoopCommonModes(注意: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 应该长什么样”,而不是”怎么一步步改成那样”。
  • 值类型视图Viewstruct 而非 class,轻量、无继承,靠组合而非继承扩展。
  • 状态驱动@State 变化触发 body 重新求值,框架 diff 出真实变更。
  • 与 UIKit 可互操作:通过 UIViewRepresentable / UIViewControllerRepresentable 双向桥接,老项目可以渐进迁移。

Q16:@State@Binding@ObservedObject@EnvironmentObject 怎么选?

属性包装器 所有权 适用场景
@State 拥有 视图私有的简单值(开关、输入文本)
@Binding 借用 子视图读写父视图的状态,双向传递
@ObservedObject 外部持有 引用类型的外部模型,需自己保证生命周期
@StateObject 拥有 视图首次创建时初始化的模型,生命周期与视图绑定
@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#面试

Related Post

发表回复

Your email address will not be published.