前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础进阶

  • 精选模块

    • 精选篇

      • 1 HTML模块
      • 2 CSS模块
      • 3 JS模块
      • 4 ES6模块
      • 5 浏览器模块
      • 6 React模块
      • 7 Vue模块
      • 8 Node模块
      • 9 前端工程相关
      • 10 移动多端开发
      • 11 小程序模块
      • 12 Uniapp模块
      • 13 前端安全模块
      • 14 性能优化相关
      • 15 HTTP模块
      • 16 常用设计模式
      • 17 框架通识
      • 18 排序算法
      • 19 计算机通识
      • 20 React Native模块
      • 21 桌面端模块
      • 22 Flutter模块
      • 23 Android模块
      • 24 iOS模块
    • 进阶篇

# iOS 原生面试题精选

覆盖语言基础、系统原理、UI、并发、架构、性能、安全、测试与发布。每题都有 30 秒速记、代码示例、详细解析和面试官追问,流程类题目配了时序图。

# 1 Objective-C 语言基础

# 1. [obj foo] 这一行在运行时到底发生了什么?找不到方法时还有哪几次补救机会?

30 秒速记

  • [obj foo] 编译后是 objc_msgSend(obj, @selector(foo)),调用哪个函数到运行时才定 = 动态绑定
  • 查找顺序:isa 找到类 → 方法缓存 cache → 本类方法列表 → 沿 superclass 一路往上查,找到就写进缓存
  • 找不到有三次机会:+resolveInstanceMethod: 动态加方法 → -forwardingTargetForSelector: 换个接收者 → -methodSignatureForSelector: + -forwardInvocation: 完整转发
  • 三步都没接住 → -doesNotRecognizeSelector: → unrecognized selector sent to instance 崩溃
  • 给 nil 发消息不会崩,直接返回 0 / nil;这是特性,也是很多「怎么没反应」的 问题来源

[obj foo] 在编译期只是变成一次 objc_msgSend(obj, @selector(foo)) 调用,真正执行哪段代码,是运行时顺着 isa 去类里现查的。 先查方法缓存,命中就直接跳过去,这也是消息发送没想象中慢的原因;没命中再查本类方法列表、父类,一路查到 NSObject,找到后写进缓存。全链路都没找到,运行时还给三次机会:先问类要不要动态补一个方法,再问能不能把消息丢给别的对象,最后打包成 NSInvocation 让你自己处理。三次都不接,就是大家熟悉的 unrecognized selector 崩溃。

# 编译器把方法调用变成了什么

Objective-C 的方法调用不是 C++ 那种编译期就定好的虚表跳转,而是「发消息」。比如下面这行:

NSString *s = [user nameWithPrefix:@"Mr."];
// 编译后大致等价于:
NSString *s = ((NSString *(*)(id, SEL, NSString *))objc_msgSend)(user, @selector(nameWithPrefix:), @"Mr.");
@前端进阶之旅: 代码已经复制到剪贴板

objc_msgSend 是汇编写的,它拿到接收者后读 isa(arm64 上是带标志位的 non-pointer isa,要掩码取出类指针),先在类的 cache_t 里按 SEL 做哈希查找。命中就尾调用跳到 IMP,整个过程只比普通函数调用多几条指令。没命中才进 C 实现的慢路径 lookUpImpOrForward:查本类方法列表(排好序的列表用二分),再逐级查父类的缓存和方法列表,找到后把结果填进当前类的缓存,下次同样的调用就走快路径了。

每个方法本质上是一个 C 函数,前两个隐藏参数就是 self 和 _cmd,这也是为什么在方法里能直接用 _cmd 拿到当前选择子。

# 三次补救机会怎么用

@implementation Router
// 第一次:动态解析。返回 YES 后运行时会重新走一遍查找
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(ping)) {
        class_addMethod(self, sel, (IMP)pingIMP, "v@:"); // 类型编码:返回 void,参数 self、_cmd
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 第二次:快速转发,把消息原样交给另一个对象,开销小
- (id)forwardingTargetForSelector:(SEL)sel {
    if ([self.impl respondsToSelector:sel]) return self.impl;
    return [super forwardingTargetForSelector:sel];
}

// 第三次:完整转发,能拿到参数、改参数、转发给多个对象,开销最大
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [super methodSignatureForSelector:sel] ?: [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)inv {
    NSLog(@"吞掉未实现的消息: %@", NSStringFromSelector(inv.selector)); // 输出:吞掉未实现的消息: xxx
}
@end

static void pingIMP(id self, SEL _cmd) { NSLog(@"pong"); }
@前端进阶之旅: 代码已经复制到剪贴板

第一步适合「运行时才知道要哪些方法」的场景,@dynamic 属性、Core Data 的属性访问器就是这个思路。第二步常用来做代理转发、多重继承的模拟。第三步最灵活,NSProxy 做 AOP、给 NSTimer 做弱引用代理都靠它。注意第三步的 methodSignatureForSelector: 返回 nil 就直接崩,不会进 forwardInvocation:。

# 边界和坑

给 nil 发消息返回零值:对象返回 nil,数字返回 0,结构体在现代运行时下也是零填充。好处是不用到处判空,坏处是链式调用里某一环是 nil 时不会报错,只是「什么都没发生」。另外 Swift 里纯 Swift 类的方法走虚表或静态派发,不经过 objc_msgSend,只有标了 @objc dynamic 的方法才会走消息机制,这点在 KVO、swizzling 时很关键。

面试官追问

追问 1线上报 -[NSNull length]: unrecognized selector,最常见的原因是什么?
参考回答

后端返回了 null,JSON 解析出来是 NSNull 单例,代码当成 NSString 去调 length。根治是在模型解析层把 NSNull 转成 nil,别在业务代码里到处判。

追问 2能不能统一 hook forwardInvocation: 把所有 unrecognized selector 吞掉,防止崩溃?
参考回答

能做,很多防护 SDK 就是在 NSObject 上兜底到 forwardingTargetForSelector: 返回一个桩对象。但我只接受「吞掉 + 上报」,不接受静默吞,否则本该崩出来的逻辑错误会变成数据错乱,更难查。

追问 3消息发送每次都要查表,为什么性能没问题?
参考回答

靠方法缓存,热路径基本都命中 cache_t,命中后只多几条汇编指令。真正慢的是第一次调用和完整转发,forwardInvocation: 要构造 NSInvocation,比正常调用慢一两个数量级,别放在高频路径。

追问 4respondsToSelector: 返回 YES,调用还是崩了,可能是什么情况?
参考回答

常见是转发类重写了 respondsToSelector: 说能处理,但转发目标其实没实现,或者转发目标已经被释放变成 nil。反过来,默认的 respondsToSelector: 不知道转发的存在,转发能接住的方法它会返回 NO。写转发类就要让 respondsToSelector: 和真实转发逻辑保持一致。

追问 5Swift 写的类,为什么 perform(#selector(...)) 有时候找不到方法?
参考回答

方法没暴露给 ObjC 运行时。Swift 4 起继承 NSObject 也不再自动推断 @objc,要手动标 @objc,否则 #selector 都编译不过;想被替换或被 KVO 观察还得加 dynamic。

# 2. Method Swizzling 怎么写才不出事?

30 秒速记

  • 原理:交换两个 Method 的 IMP,之后调 A 实际跑 B 的代码
  • 放在 +load 里 + dispatch_once,只交换一次;交换两次就换回去了
  • 先 class_addMethod:本类没实现、方法在父类时,直接 exchange 会把父类的实现换掉,影响所有兄弟类
  • 新方法里调 [self xxx_viewDidAppear:] 不是死循环,交换后它指向原实现
  • 坑:_cmd 变了、多个库重复 hook、Swift 纯方法 hook 不到;能用子类或组合解决的就别 swizzle

swizzling 就是在运行时把两个方法的实现对调,常用来做无侵入的埋点、AOP。 写法上有三条硬规矩:放在 +load 里用 dispatch_once 保证只换一次;交换前先 class_addMethod,防止方法其实定义在父类,你一换就把父类的实现改了,所有子类跟着遭殃;新方法里调用自己的名字其实是调原实现,这是故意的。它的问题在于全局生效、难排查,多个库 hook 同一个方法时顺序不可控,所以我只在埋点、监控这类基础设施里用,业务逻辑不碰。

# 一份能放心用的模板

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础进阶

  • 精选模块

    • 精选篇

      • 1 HTML模块
      • 2 CSS模块
      • 3 JS模块
      • 4 ES6模块
      • 5 浏览器模块
      • 6 React模块
      • 7 Vue模块
      • 8 Node模块
      • 9 前端工程相关
      • 10 移动多端开发
      • 11 小程序模块
      • 12 Uniapp模块
      • 13 前端安全模块
      • 14 性能优化相关
      • 15 HTTP模块
      • 16 常用设计模式
      • 17 框架通识
      • 18 排序算法
      • 19 计算机通识
      • 20 React Native模块
      • 21 桌面端模块
      • 22 Flutter模块
      • 23 Android模块
      • 24 iOS模块
        • 1 Objective-C 语言基础
          • 1. [obj foo] 这一行在运行时到底发生了什么?找不到方法时还有哪几次补救机会?
          • 2. Method Swizzling 怎么写才不出事?
          • 3. Category 和 Extension 有什么区别?Category 里为什么加不了实例变量?
          • 4. +load 和 +initialize 有什么区别?分类里实现了会怎样?
          • 5. strong、weak、copy、assign 怎么选?NSString 属性为什么要用 copy?
          • 6. weak 指针在对象释放后是怎么自动变成 nil 的?
          • 7. block 为什么要 copy?它是怎么捕获外部变量的,__block 又做了什么?
          • 8. block 的循环引用怎么判断、怎么解?weakSelf / strongSelf 为什么要配对用?
          • 9. ObjC 的协议和轻量级泛型怎么用?NSArray<NSString *> * 运行时真的只能放字符串吗?
        • 2 Objective-C Runtime
        • 3 Swift 类型系统
        • 4 Swift 内存安全与 ARC
        • 5 Swift Concurrency
        • 6 应用与 Scene 生命周期
        • 7 UIKit 视图生命周期
        • 8 事件、手势与响应链
        • 9 Auto Layout
        • 10 SwiftUI 状态与更新
        • 11 SwiftUI 性能与互操作
        • 12 导航与深链
        • 13 RunLoop、GCD 与 Operation
        • 14 网络与 URLSession
        • 15 数据持久化
        • 16 图片、图形与动画
        • 17 音视频与相机
        • 18 通知与后台任务
        • 19 安全与隐私
        • 20 性能、卡顿与内存
        • 21 测试与可测试架构
        • 22 工程化与模块化
        • 23 签名、打包与发布
        • 24 Objective-C 与 Swift 混编
        • 25 系统设计与实战排障
    • 进阶篇

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

24-ios

# iOS 原生面试题精选

覆盖语言基础、系统原理、UI、并发、架构、性能、安全、测试与发布。每题都有 30 秒速记、代码示例、详细解析和面试官追问,流程类题目配了时序图。

# 1 Objective-C 语言基础

# 1. [obj foo] 这一行在运行时到底发生了什么?找不到方法时还有哪几次补救机会?

30 秒速记

  • [obj foo] 编译后是 objc_msgSend(obj, @selector(foo)),调用哪个函数到运行时才定 = 动态绑定
  • 查找顺序:isa 找到类 → 方法缓存 cache → 本类方法列表 → 沿 superclass 一路往上查,找到就写进缓存
  • 找不到有三次机会:+resolveInstanceMethod: 动态加方法 → -forwardingTargetForSelector: 换个接收者 → -methodSignatureForSelector: + -forwardInvocation: 完整转发
  • 三步都没接住 → -doesNotRecognizeSelector: → unrecognized selector sent to instance 崩溃
  • 给 nil 发消息不会崩,直接返回 0 / nil;这是特性,也是很多「怎么没反应」的 问题来源

[obj foo] 在编译期只是变成一次 objc_msgSend(obj, @selector(foo)) 调用,真正执行哪段代码,是运行时顺着 isa 去类里现查的。 先查方法缓存,命中就直接跳过去,这也是消息发送没想象中慢的原因;没命中再查本类方法列表、父类,一路查到 NSObject,找到后写进缓存。全链路都没找到,运行时还给三次机会:先问类要不要动态补一个方法,再问能不能把消息丢给别的对象,最后打包成 NSInvocation 让你自己处理。三次都不接,就是大家熟悉的 unrecognized selector 崩溃。

# 编译器把方法调用变成了什么

Objective-C 的方法调用不是 C++ 那种编译期就定好的虚表跳转,而是「发消息」。比如下面这行:

NSString *s = [user nameWithPrefix:@"Mr."];
// 编译后大致等价于:
NSString *s = ((NSString *(*)(id, SEL, NSString *))objc_msgSend)(user, @selector(nameWithPrefix:), @"Mr.");
@前端进阶之旅: 代码已经复制到剪贴板

objc_msgSend 是汇编写的,它拿到接收者后读 isa(arm64 上是带标志位的 non-pointer isa,要掩码取出类指针),先在类的 cache_t 里按 SEL 做哈希查找。命中就尾调用跳到 IMP,整个过程只比普通函数调用多几条指令。没命中才进 C 实现的慢路径 lookUpImpOrForward:查本类方法列表(排好序的列表用二分),再逐级查父类的缓存和方法列表,找到后把结果填进当前类的缓存,下次同样的调用就走快路径了。

每个方法本质上是一个 C 函数,前两个隐藏参数就是 self 和 _cmd,这也是为什么在方法里能直接用 _cmd 拿到当前选择子。

# 三次补救机会怎么用

@implementation Router
// 第一次:动态解析。返回 YES 后运行时会重新走一遍查找
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(ping)) {
        class_addMethod(self, sel, (IMP)pingIMP, "v@:"); // 类型编码:返回 void,参数 self、_cmd
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 第二次:快速转发,把消息原样交给另一个对象,开销小
- (id)forwardingTargetForSelector:(SEL)sel {
    if ([self.impl respondsToSelector:sel]) return self.impl;
    return [super forwardingTargetForSelector:sel];
}

// 第三次:完整转发,能拿到参数、改参数、转发给多个对象,开销最大
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [super methodSignatureForSelector:sel] ?: [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)inv {
    NSLog(@"吞掉未实现的消息: %@", NSStringFromSelector(inv.selector)); // 输出:吞掉未实现的消息: xxx
}
@end

static void pingIMP(id self, SEL _cmd) { NSLog(@"pong"); }
@前端进阶之旅: 代码已经复制到剪贴板

第一步适合「运行时才知道要哪些方法」的场景,@dynamic 属性、Core Data 的属性访问器就是这个思路。第二步常用来做代理转发、多重继承的模拟。第三步最灵活,NSProxy 做 AOP、给 NSTimer 做弱引用代理都靠它。注意第三步的 methodSignatureForSelector: 返回 nil 就直接崩,不会进 forwardInvocation:。

# 边界和坑

给 nil 发消息返回零值:对象返回 nil,数字返回 0,结构体在现代运行时下也是零填充。好处是不用到处判空,坏处是链式调用里某一环是 nil 时不会报错,只是「什么都没发生」。另外 Swift 里纯 Swift 类的方法走虚表或静态派发,不经过 objc_msgSend,只有标了 @objc dynamic 的方法才会走消息机制,这点在 KVO、swizzling 时很关键。

面试官追问

追问 1线上报 -[NSNull length]: unrecognized selector,最常见的原因是什么?
参考回答

后端返回了 null,JSON 解析出来是 NSNull 单例,代码当成 NSString 去调 length。根治是在模型解析层把 NSNull 转成 nil,别在业务代码里到处判。

追问 2能不能统一 hook forwardInvocation: 把所有 unrecognized selector 吞掉,防止崩溃?
参考回答

能做,很多防护 SDK 就是在 NSObject 上兜底到 forwardingTargetForSelector: 返回一个桩对象。但我只接受「吞掉 + 上报」,不接受静默吞,否则本该崩出来的逻辑错误会变成数据错乱,更难查。

追问 3消息发送每次都要查表,为什么性能没问题?
参考回答

靠方法缓存,热路径基本都命中 cache_t,命中后只多几条汇编指令。真正慢的是第一次调用和完整转发,forwardInvocation: 要构造 NSInvocation,比正常调用慢一两个数量级,别放在高频路径。

追问 4respondsToSelector: 返回 YES,调用还是崩了,可能是什么情况?
参考回答

常见是转发类重写了 respondsToSelector: 说能处理,但转发目标其实没实现,或者转发目标已经被释放变成 nil。反过来,默认的 respondsToSelector: 不知道转发的存在,转发能接住的方法它会返回 NO。写转发类就要让 respondsToSelector: 和真实转发逻辑保持一致。

追问 5Swift 写的类,为什么 perform(#selector(...)) 有时候找不到方法?
参考回答

方法没暴露给 ObjC 运行时。Swift 4 起继承 NSObject 也不再自动推断 @objc,要手动标 @objc,否则 #selector 都编译不过;想被替换或被 KVO 观察还得加 dynamic。

# 2. Method Swizzling 怎么写才不出事?

30 秒速记

  • 原理:交换两个 Method 的 IMP,之后调 A 实际跑 B 的代码
  • 放在 +load 里 + dispatch_once,只交换一次;交换两次就换回去了
  • 先 class_addMethod:本类没实现、方法在父类时,直接 exchange 会把父类的实现换掉,影响所有兄弟类
  • 新方法里调 [self xxx_viewDidAppear:] 不是死循环,交换后它指向原实现
  • 坑:_cmd 变了、多个库重复 hook、Swift 纯方法 hook 不到;能用子类或组合解决的就别 swizzle

swizzling 就是在运行时把两个方法的实现对调,常用来做无侵入的埋点、AOP。 写法上有三条硬规矩:放在 +load 里用 dispatch_once 保证只换一次;交换前先 class_addMethod,防止方法其实定义在父类,你一换就把父类的实现改了,所有子类跟着遭殃;新方法里调用自己的名字其实是调原实现,这是故意的。它的问题在于全局生效、难排查,多个库 hook 同一个方法时顺序不可控,所以我只在埋点、监控这类基础设施里用,业务逻辑不碰。

# 一份能放心用的模板

@implementation UIViewController (Track)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = [self class];
        SEL origSel = @selector(viewDidAppear:);
        SEL swizSel = @selector(track_viewDidAppear:);
        Method origM = class_getInstanceMethod(cls, origSel);
        Method swizM = class_getInstanceMethod(cls, swizSel);

        // 1. 本类可能没实现 viewDidAppear:(只在父类有),先尝试给本类加一个,实现用新方法
        BOOL added = class_addMethod(cls, origSel,
                                     method_getImplementation(swizM),
                                     method_getTypeEncoding(swizM));
        if (added) {
            // 2. 加成功了:把新方法的实现换成原实现(可能是父类的),父类本身不受影响
            class_replaceMethod(cls, swizSel,
                                method_getImplementation(origM),
                                method_getTypeEncoding(origM));
        } else {
            // 3. 本类自己就有实现,直接交换
            method_exchangeImplementations(origM, swizM);
        }
    });
}

- (void)track_viewDidAppear:(BOOL)animated {
    [self track_viewDidAppear:animated]; // 交换后这里调用的是原始 viewDidAppear:,不是递归
    NSLog(@"PV: %@", NSStringFromClass(self.class)); // 输出:PV: HomeViewController
}
@end
@前端进阶之旅: 代码已经复制到剪贴板

# 为什么必须先 class_addMethod

← 23 Android模块1 高频考点 →

fe