iOS 高频面试题(10 道)

原创文章
声明:作者声明此文章为原创,未经作者同意,请勿转载,若转载,务必注明本站出处,本平台保留追究侵权法律责任的权利。
上班墨鱼
没有什么缺点,大家低调点

iOS 高频面试题(10 道)

1. OC 中 Category(分类)和 Extension(扩展)的区别?Category 为什么能给类添加属性却不能直接生成成员变量?

面试题png

(2)Category 不能直接生成成员变量的原因

  • OC 的类结构(struct objc_class)在编译期已确定内存布局,Category 是运行时动态加载的,无法修改原类的内存布局,因此不能直接添加成员变量;

  • Category 中用 @property 声明属性时,编译器只会生成 getter/setter 方法的声明,不会自动生成实现和成员变量;

  • 解决方法:通过 关联对象(Associated Object) 间接实现属性效果:

oc 复制代码
// Category 中添加属性
@interface UIView (Extension)
@property (nonatomic, copy) NSString *identifier;
@end

@implementation UIView (Extension)
// 关联对象的 key
static const void *kIdentifierKey = &kIdentifierKey;
- (void)setIdentifier:(NSString *)identifier {
    objc_setAssociatedObject(self, kIdentifierKey, identifier, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)identifier {
    return objc_getAssociatedObject(self, kIdentifierKey);
}
@end

2. iOS 中的内存管理方式是什么?ARC 原理是什么?MRC 下的引用计数规则?

(1)核心内存管理方式
iOS 基于引用计数(Reference Counting) 管理内存,分为 MRC(手动引用计数)和 ARC(自动引用计数),核心原则:谁创建谁释放,谁持有谁释放。

(2)ARC 原理
ARC 是编译器特性(非运行时),编译器在编译期自动插入 retain/release/autorelease 代码,无需手动管理,但底层仍基于引用计数:

  • 编译期分析代码上下文,在合适的位置插入内存管理方法;
  • ARC 遵循 “强引用持有对象,弱引用不持有” 的规则;
  • 限制:不能手动调用 retain/release/autorelease,不能使用 NSAutoreleasePool(改用 @autoreleasepool)。

(3)MRC 引用计数规则
mrc png
示例(MRC):

oc 复制代码
NSString *str = [[NSString alloc] initWithString:@"test"]; // 引用计数=1
[str retain]; // 引用计数=2
[str release]; // 引用计数=1
[str release]; // 引用计数=0,对象销毁

3. UIKit 中 RunLoop 的作用是什么?RunLoop 与线程的关系?常用的 RunLoop Mode 有哪些?

(1)RunLoop 核心作用
RunLoop 是 “事件循环器”,核心功能是让线程在有任务时处理任务,无任务时休眠(避免占用 CPU),主要职责:

  • 处理事件(触摸、定时器、网络请求);
  • 管理线程生命周期(如主线程 RunLoop 一直运行,子线程默认无 RunLoop);
  • 处理 UI 刷新(CATransaction 提交后触发 RunLoop 刷新 UI);
  • 延迟执行(performSelector:withObject:afterDelay: 依赖 RunLoop)。

(2)RunLoop 与线程的关系

  • 一一对应:一个线程对应一个 RunLoop,RunLoop 存储在全局字典中,线程为 key,RunLoop 为 value;
  • 懒加载:RunLoop 不会随线程创建而创建,调用 [NSRunLoop currentRunLoop] 时才创建;
  • 主线程 RunLoop 自动创建且一直运行,子线程 RunLoop 需要手动启动,且运行后需手动停止。

(3)常用 RunLoop Mode
RunLoop Mode 是 “事件过滤器”,同一时间 RunLoop 只能处于一个 Mode,仅处理该 Mode 下的事件:

  • kCFRunLoopDefaultMode:默认模式,处理 UI 事件、定时器、常规任务(主线程默认处于该 Mode);
  • UITrackingRunLoopMode:跟踪模式,处理滑动 / 滚动事件(如 UIScrollView 滑动时,RunLoop 切换到该 Mode,默认 Mode 事件暂停);
  • kCFRunLoopCommonModes:伪模式,是 Default + Tracking 的集合(设置该 Mode 后,两种场景都能处理事件)。

实战场景:UIScrollView 滑动时定时器暂停,可将定时器加入 CommonModes 解决:

oc 复制代码
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"定时器执行");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

4. Swift 中值类型(Struct/Enum)和引用类型(Class)的区别?各自的适用场景?

(1)核心区别

(2)适用场景

  • 值类型:
    适合存储独立、不可共享的数据(如模型数据、基础类型、工具类):
    基础数据结构:Int/String/Array/Dictionary(Swift 中均为 Struct);
    业务模型:如 UserModel(存储用户 ID、姓名等,避免多地方修改导致数据不一致);
    轻量工具:如 Size(存储宽高)、Point(存储坐标)。
  • 引用类型:
    适合需要共享状态、继承、生命周期管理的场景:
  • UI 组件:UIViewController/UIView(需继承、共享实例);
  • 业务管理器:如 NetworkManager(单例,全局共享网络请求逻辑);
  • 需析构的对象:如 FileManager(需在 deinit 中关闭文件句柄)。

5. iOS 中 tableView/collectionView 的性能优化方式有哪些?

5.1 复用机制优化:

  • 必须使用 dequeueReusableCell(withIdentifier:) 复用单元格,避免重复创建;
  • 提前注册单元格(register(_:forCellWithReuseIdentifier:)),避免运行时查找 nib 文件;
  • 自定义单元格时,将子视图封装为属性,避免重复调用 viewWithTag:(耗时)。

5.2 布局计算优化:

  • 提前计算并缓存单元格高度(如在模型中存储 cellHeight,避免 heightForRowAt 中重复计算);
  • iOS 8+ 启用 estimatedRowHeight + automaticDimension 自动计算高度时,需固定约束,减少布局遍历;
  • 避免在 cellForRowAt/willDisplayCell 中执行复杂计算(如数据转换、正则匹配)。

5.3 渲染优化:

  • 图片优化:使用异步加载(SDWebImage)、缓存图片、压缩图片尺寸(匹配单元格大小);
  • 减少离屏渲染:避免设置 cornerRadius+masksToBounds(改用贝塞尔曲线绘制)、避免阴影(shadowPath 优化);

5.4 数据加载优化:

  • 分页加载:滚动到底部时加载下一页数据,避免一次性加载大量数据;
  • 预加载:提前加载下一页数据(如滚动到倒数 10 行时);
  • 异步处理:数据解析、模型转换放在子线程,主线程仅负责渲染。

5.5 其他优化:

  • 关闭不必要的动画:如 cell.selectionStyle = .none(无选中样式);
  • 使用 UITableViewCell 的 prepareForReuse 方法重置单元格状态(避免数据残留);
  • 大列表使用 UICollectionView 替代 UITableView(布局更灵活,性能更优);
  • 启用 shouldRasterize(光栅化):对复杂单元格,将其渲染为位图缓存,减少重复绘制(注意:仅适合静态单元格)。

6. iOS 中的代理(Delegate)和通知(Notification)的区别?各自的适用场景?

(1)核心区别
delegate png
(2)适用场景

  • Delegate:
    适合需要双向通信、实时响应、类型安全的场景:
  • 控件交互:UITableViewDelegate(点击单元格、返回高度)、UIScrollViewDelegate(滚动监听);
  • 页面传值:子控制器通过代理向父控制器传递数据(如修改后的数据);
  • 回调结果:网络请求类通过代理返回请求结果(同步处理)。
  • Notification:
    适合跨层级、解耦、一对多的通信场景:
  • 全局状态变更:如用户登录 / 退出、网络状态变化(通知所有相关页面刷新);
  • 跨组件通信:如首页、个人中心同时监听购物车数量变化;
  • 系统事件监听:如键盘弹出 / 收起(UIKeyboardWillShowNotification)。

注意:通知必须在 deinit 中注销,避免崩溃:

swift 复制代码
// Swift 注销通知
deinit {
    NotificationCenter.default.removeObserver(self)
}

7. iOS 中 Block 的原理是什么?Block 的循环引用如何解决?

(1)Block 原理
Block 是 OC 中的 “闭包”,本质是封装了函数调用和函数环境的 OC 对象(底层是 struct __block_impl 结构体):

  • Block 存储在栈 / 堆 / 全局区:
    全局 Block:捕获不到外部变量,存储在全局区(如 ^{NSLog(@"test");});
    栈 Block:捕获外部变量,存储在栈区(生命周期由系统管理,易销毁);
    堆 Block:栈 Block 调用 copy 后,转移到堆区(手动管理生命周期)。
  • Block 捕获外部变量:
    值捕获:基本数据类型(int/NSString)捕获值,Block 内修改不影响外部;
    引用捕获:对象类型捕获指针,需注意循环引用;
    __block 修饰:让变量变为可修改(栈变量转为堆变量)。

(2)Block 循环引用原因及解决

  • 原因:Block 会强引用捕获的对象,若对象同时强引用 Block(如控制器持有 Block,Block 捕获 self),则形成循环引用,导致对象无法释放。
  • 解决方法:
  1. 弱引用 + 强引用(常用):
oc 复制代码
// OC
__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf; // 避免self提前释放
    if (strongSelf) {
        [strongSelf doSomething];
    }
};
swift 复制代码
// Swift
weak var weakSelf = self
self.block = { [weak self] in
    guard let strongSelf = self else { return }
    strongSelf.doSomething()
}
  1. 使用 __unsafe_unretained:类似 weak,但不会自动置空(慎用,可能野指针);
  2. 打破循环:使用完成后将 Block 置为 nil(如网络请求完成后,self.block = nil);
  3. 捕获局部变量:若 Block 仅在方法内使用,捕获局部变量(非 self 属性),避免循环。

8. iOS 中的多线程方案有哪些?GCD 的核心概念和常用方法?

(1)iOS 多线程方案对比
thread png

(2)GCD 核心概念

  • 队列(Queue):存储任务的容器,分为:
    串行队列(Serial):任务按顺序执行,只有一个线程;
    并发队列(Concurrent):任务同时执行,多个线程(由系统管理);
    主队列(Main Queue):串行队列,运行在主线程,处理 UI 操作。

  • 任务(Task):分为同步(sync)和异步(async):
    同步任务:阻塞当前线程,任务执行完才返回;
    异步任务:不阻塞当前线程,任务后台执行。

(3)GCD 常用方法
1.异步并发(常用,非 UI 任务):

swift 复制代码
// Swift
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
concurrentQueue.async {
    // 子线程执行耗时任务(如网络请求、数据解析)
    print("任务1:\(Thread.current)")
}
concurrentQueue.async {
    print("任务2:\(Thread.current)")
}
  1. 异步串行(顺序执行任务):
swift 复制代码
let serialQueue = DispatchQueue(label: "com.example.serial")
serialQueue.async { print("任务1") }
serialQueue.async { print("任务2") } // 任务1执行完才执行
  1. 主线程更新 UI:
swift 复制代码
DispatchQueue.main.async {
    self.label.text = "更新UI" // 必须在主线程执行
}
  1. 延时执行:
swift 复制代码
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    print("2秒后执行")
}
  1. 栅栏函数(控制并发队列的任务顺序):
swift 复制代码
concurrentQueue.async { print("任务1") }
concurrentQueue.async { print("任务2") }
// 栅栏:等待前面任务执行完,执行自己,再执行后面任务
concurrentQueue.async(flags: .barrier) { print("栅栏任务") }
concurrentQueue.async { print("任务3") }

9. iOS 中 App 的启动流程是什么?如何优化 App 启动速度?

(1)App 启动流程(冷启动)
冷启动是 App 从完全关闭到打开的过程,分为 3 个阶段:

  • dyld 阶段(动态链接器):
    加载可执行文件(Mach-O);
    加载动态库(系统库如 UIKit、自定义库);
    重定位符号,绑定依赖。
  • runtime 阶段:
    初始化 OC 运行时(objc_init);
    加载类、分类、方法(+load 方法执行);
    初始化全局变量、静态变量。
  • 主线程初始化阶段:
    执行 UIApplicationMain;
    加载 AppDelegate,执行 application:didFinishLaunchingWithOptions:;
    加载根控制器,渲染首屏 UI。

(2)启动速度优化
核心思路:“减少加载项、延迟执行、异步处理”:

  • dyld 阶段优化:
    减少动态库数量(合并静态库,移除无用动态库);
    启用 bitcode(编译器优化二进制);
    减少符号表(strip 无用符号)。

  • runtime 阶段优化:
    避免在 +load 方法中执行耗时操作(改用 +initialize 或延迟执行);
    减少类 / 分类数量(合并无用分类);
    优化全局变量 / 静态变量(避免初始化耗时)。

  • 主线程初始化阶段优化:
    延迟加载非首屏资源(如广告、统计、第三方 SDK);
    异步初始化第三方 SDK(如埋点、推送,放在子线程);
    首屏 UI 简化:使用骨架屏替代复杂视图,首屏只加载核心控件;
    懒加载:控制器的子视图、数据请求等延迟到 viewDidAppear 执行。

  • 其他优化:
    启用启动优化(Xcode 的 Build Settings 中开启 Optimization Level);
    压缩资源(图片、字体,减少加载时间);
    使用 Instruments 的 Time Profiler 分析启动耗时,定位瓶颈。

10. iOS 中常见的崩溃类型有哪些?如何捕获和排查崩溃?

(1)常见崩溃类型

  • 野指针崩溃(EXC_BAD_ACCESS):
    访问已释放的对象(如通知未注销、Block 循环引用导致对象提前释放);
    访问空指针(如 nil 对象调用不存在的方法)。

  • 数组越界(NSRangeException):
    访问数组索引超出范围(如 array[10],但数组只有 5 个元素)。

  • 字典键值异常:
    字典设置 nil 键 / 值(OC 中字典键值不能为 nil,Swift 可);
    访问不存在的键(虽不崩溃,但返回 nil,可能导致后续逻辑异常)。

  • 死锁(Deadlock):
    主线程同步等待子线程,子线程又等待主线程(如 GCD 同步任务入主队列)。

  • 内存溢出(OOM):
    加载大量图片 / 数据,内存占用超过系统限制,被系统杀死。

  • 类型转换失败(NSInvalidArgumentException):
    如将 NSString 强制转为 NSNumber。

(2)崩溃捕获与排查

  • 开发阶段排查:
    Xcode 控制台:直接查看崩溃日志(含崩溃类型、调用栈);
    Instruments:Zombies 工具检测野指针,Leaks 工具检测内存泄漏;
    开启 Zombie Objects(Xcode -> Edit Scheme -> Diagnostics -> Enable Zombie Objects),定位野指针。

  • 线上崩溃捕获:
    第三方工具:集成 Crashlytics(Fabric)、Bugly、友盟统计,自动捕获崩溃日志;
    自定义崩溃捕获:通过 NSSetUncaughtExceptionHandler 捕获 OC 异常,signal 捕获信号量崩溃:

oc 复制代码
// 捕获 OC 异常
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
void UncaughtExceptionHandler(NSException *exception) {
    // 记录崩溃信息:调用栈、异常原因
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    // 上传到服务器
}
  • 崩溃排查步骤:

查看崩溃日志的 Exception Type(崩溃类型)和 Exception Message(原因);

分析调用栈(Thread 0 Crashed 是崩溃线程),定位崩溃代码行;

复现崩溃场景(如特定操作、特定设备),逐步排查;

针对野指针:检查内存管理(ARC 引用、代理是否为 weak);

针对逻辑崩溃:添加断言(NSAssert)、边界检查(数组 / 字典访问前判断范围)。

以上。

暂无评论,快来发表第一条评论吧