中高端 iOS 面试题(OC)

429 阅读10分钟

以下是整理的中高端 iOS 面试题答案

2025-06-05更新一下: 1、需要了解 UIView 和 CALayer 的关系。 2、需要了解堆和栈的关系。对象创建后是在堆上还是栈上,什么时候发生变化。


内存管理

1. 什么是引用计数?Objective-C 是如何通过引用计数管理内存的?

引用计数是一种内存管理技术,每个对象维护一个计数器,表示有多少引用指向它。当计数降为 0 时,系统自动释放对象。Objective-C 使用 retain 和 release 方法来增加或减少引用计数,ARC(Automatic Reference Counting)则自动插入这些调用,简化了内存管理。

2. ARC 和 MRC 的区别是什么?ARC 的工作原理是什么?

ARC(自动引用计数)由编译器在编译时插入 retain、release 和 autorelease,开发者无需手动管理。而 MRC(手动引用计数)需要显式调用这些方法。ARC 不改变引用计数的基本原理,而是让编译器替你完成了繁琐的内存管理工作。

3. 在 ARC 环境下,什么情况下需要使用 __weak 和 __strong?

__strong 是默认的修饰符,表示强引用,适合一般情况。__weak 用于打破循环引用,例如在 block 内引用 self 时,避免 block 和 self 互相持有导致内存泄漏。

4.  __autoreleasing 和 __strong 的区别是什么?

__strong 表示一个强引用,自动管理生命周期,直到引用置 nil 时对象才会释放;__autoreleasing 用于函数中传递返回值,表示临时引用,返回后由 autoreleasepool 自动释放。

5. 什么是 __block 修饰符?它在 ARC 环境下有什么作用?

__block 允许 block 内部修改外部变量的值,在 ARC 环境下,它会自动强引用该变量并在 block 完成后释放,从而避免内存问题。

6. 如何判断对象是否已经释放?会不会有野指针问题?

ARC 环境下,__weak 指针在目标对象释放时会被自动置为 nil,因此一般不会出现野指针问题。如果是 unsafe_unretained 或 assign 修饰的对象,则可能出现野指针,需要谨慎使用。

7. assign、retain、copy 和 weak 的区别是什么?

• assign:直接赋值,适用于基本数据类型。

• retain:强引用,引用计数 +1(MRC 中使用)。

• copy:创建对象副本,用于不可变对象。

• weak:弱引用,目标释放时自动置 nil。

8. copy 和 mutableCopy 的实现原理是什么?它们的区别有哪些?

copy 是浅拷贝,对于不可变对象,返回的是指向相同内容的副本;对于可变对象,copy 返回不可变副本。mutableCopy 总是返回一个可变副本。它们的实现依赖于对象的类实现的 copyWithZone: 和 mutableCopyWithZone: 方法。

9. 如何避免循环引用(retain cycle)?在什么情况下会发生?

循环引用通常发生在对象相互持有时,如 A 持有 B,B 也强引用 A。可以通过将其中一个引用改为 weak,或者使用 __block修饰符来打破循环。

10. Autoreleasepool 的作用是什么?什么时候需要手动创建一个 Autoreleasepool?

Autoreleasepool 延迟释放 autorelease 的对象。在批量创建对象时,手动创建 Autoreleasepool 可降低内存峰值:

@autoreleasepool {
    for (int i = 0; i < 1000; i++) {
        NSString *string = [[NSString alloc] initWithFormat:@"%d", i];
    }
}

Runtime

11. 什么是 Runtime?它的主要功能有哪些?

Runtime 是 Objective-C 的运行时库,提供消息发送、动态方法解析、类和对象的动态创建与销毁等功能。它实现了面向对象语言的动态特性,使得方法调用、消息转发和类型信息查询等操作可以在运行时进行。

12. 如何使用 Objective-C Runtime 动态添加一个方法?

可以使用 class_addMethod() 动态添加一个方法。例如:

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

通过为 cls 添加 name 对应的 imp(即函数实现),在运行时为类添加新方法。

13. 什么是 Method Swizzling?它的应用场景有哪些?

Method Swizzling 是交换两个方法实现的一种技术。常见应用场景包括:

• 为系统类添加行为(如统计 UIViewController 的 viewDidLoad 调用次数)。

• 替换系统默认实现(如替换 UILabel setText: 方法添加自定义逻辑)。

14. KVO 的底层实现原理是什么?为什么需要动态生成子类?

KVO 通过 Runtime 动态生成一个子类,并将观察的对象的 isa 指针指向这个子类。子类会重写属性的 setter 方法,在值变更时调用 willChangeValueForKey: 和 didChangeValueForKey:,从而通知观察者。

15. KVC 和 KVO 有什么关系?它们各自的工作原理是什么?

KVC(Key-Value Coding)是直接通过键访问对象的属性。KVO(Key-Value Observing)依赖 KVC,当某个键的值通过 KVC 修改时,会触发 KVO 的通知机制。

16. 如何动态添加属性到一个类中?实现原理是什么?

可以通过 Runtime 的关联对象(Associated Objects)实现:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);

实质是在对象外部维护一个关联表,将键值对和对象关联起来。

17. isa 指针的作用是什么?为什么 isa 指针需要优化?

isa 指针指向对象的类,表明该对象的类型信息。64 位系统中,isa 被优化为一个 bit field,能够存储更多的元信息,如引用计数、是否正在 dealloc、弱引用清理标记等。这种优化提高了内存利用率和访问效率。

18. Objective-C 中的消息发送流程是怎样的?objc_msgSend 是如何工作的?

objc_msgSend 是方法调用的核心。当发送消息时,Runtime:

• 通过 isa 找到类。

• 在类的方法缓存中查找方法。

• 找不到时沿继承链向父类查找。

• 最终通过 IMP(方法实现的指针)执行方法。

19. 如果方法找不到,Runtime 会做什么?如何实现动态方法解析?

如果方法未实现,Runtime 会:

1. 调用 resolveInstanceMethod: 让类动态添加方法。

2. 调用 forwardingTargetForSelector: 转发给其他对象。

3. 进入完整的消息转发流程:methodSignatureForSelector: + forwardInvocation:

20. 如何遍历一个类的所有方法和属性?需要用到哪些 Runtime 函数?

可以通过 Runtime 提供的函数遍历:

• 获取方法列表:class_copyMethodList

• 获取属性列表:class_copyPropertyList

• 获取实例变量列表:class_copyIvarList

21. 动态方法解析与消息转发的区别是什么?

动态方法解析(Dynamic Method Resolution)发生在方法调用失败后,通过 resolveInstanceMethod: 或 resolveClassMethod: 尝试动态添加方法实现。

消息转发(Message Forwarding)则是当动态方法解析失败后,通过 forwardingTargetForSelector: 或 forwardInvocation: 将消息转发给其他对象或自定义处理。

22. 如何用 Runtime 创建一个新类?

使用 objc_allocateClassPair 创建类,使用 class_addMethod、class_addIvar 添加方法和实例变量,最后调用 objc_registerClassPair注册该类。


RunLoop

23. RunLoop 的工作原理是什么?

RunLoop 是一个事件处理循环,它会等待输入事件(如触摸、定时器、网络事件),并以适当的方式分发事件。它通过多次检查事件源(Source)、观察者(Observer)、定时器(Timer),然后调用对应的回调来处理。

24. 为什么 NSTimer 默认会受 RunLoop 的模式影响?

NSTimer 默认运行在 NSDefaultRunLoopMode,当 RunLoop 切换到 UITrackingRunLoopMode(如滑动 UIScrollView 时),NSDefaultRunLoopMode 下的 Timer 就不会被触发。

25. 如何在后台线程中使用 RunLoop?

子线程的 RunLoop 不会自动启动。需要手动启动:

[[NSRunLoop currentRunLoop] run];

同时至少添加一个输入源(如 Timer 或 Port)以让 RunLoop 保持运行。


多线程

26. 为什么主线程需要 RunLoop,而全局并发队列不需要?

主线程需要处理 UI 事件、用户交互和定时器等,需要一个运行循环来等待和分发事件。全局并发队列的线程通常用于一次性执行任务,不需要持续运行,因此无需 RunLoop。

27. 在主线程上使用 dispatch_sync 会发生什么?

如果在主线程调用 dispatch_sync(dispatch_get_main_queue(), ^{ ... }) 会导致死锁。因为 dispatch_sync 会等待 block 完成,但 block 必须在当前队列完成其他任务后执行,造成循环等待。

28. 如何检测和避免死锁?

• 避免同步调用自身队列。

• 使用 Xcode 的 Thread Sanitizer 工具检测死锁。

• 将耗时操作移到其他队列,减少主线程阻塞的可能性。

29. NSLock、pthread_mutex 和 dispatch_semaphore 的区别是什么?

• NSLock 是基于 Objective-C 的封装,使用简单,但性能略低于其他选项。

• pthread_mutex 是 POSIX 标准的底层锁,性能高,适合高频使用。

• dispatch_semaphore 不是真正的锁,而是通过信号量控制并发访问,更轻量。

30. 在 Objective-C 中如何实现线程安全的懒加载?

使用 dispatch_once 确保初始化代码只执行一次:

static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    sharedInstance = [[self alloc] init];
});
return sharedInstance;

UI 和动画

31. UIView 的动画是如何在底层实现的?

UIView 的动画是基于 Core Animation 实现的。

当调用 UIView 的动画方法(如 animateWithDuration:animations:)时,实际是对 Layer 的属性做了隐式动画。Core Animation 会处理动画帧、时间曲线、渲染等细节,UIView 只是在高层提供了易用的接口。

32. 如何提高复杂动画的性能?

• 避免频繁调用 setNeedsDisplay;

• 使用 rasterization 缓存复杂图层;

• 减少透明度图层的数量;

• 使用 shouldRasterize 让静态内容提前渲染成位图。

33. 如何在 UITableView 上实现流畅的滚动性能?

• 减少 cellForRowAtIndexPath: 中的复杂逻辑;

• 复用 cell 减少内存分配开销;

• 异步加载和缓存图片资源;

• 将图像解码放在后台队列完成,避免主线程卡顿。


性能优化

34. 如何找到内存泄漏并解决?

• 使用 Instruments 的 Leaks 工具;

• 检查循环引用;

• 检查未释放的强引用;

• 使用 Xcode 的 Memory Graph 调试器查看对象引用链。

35. App 启动时间过长的原因可能有哪些?

• 在 didFinishLaunchingWithOptions: 中执行了耗时任务;

• 初始化过多的类或加载过多的资源;

• 网络请求阻塞了启动流程。

36. 如何优化 App 的冷启动时间?

• 延迟初始化不必要的组件;

• 减少动态库加载数量;

• 缩小资源的初始加载量;

• 优化数据解码和解析流程。


综合问题

37. 如何为 Objective-C 项目设计一个模块化架构?

将不同的功能模块拆分成独立的框架或动态库,每个模块只负责自己的逻辑。通过公共协议或接口定义模块间的通信,确保模块之间的依赖最小化。

38. 如何保证数据模型与视图的解耦?

使用 MVC 或 MVVM 架构。将数据逻辑(Model)独立出来,视图只负责展示,通过 Controller 或 ViewModel 将两者连接起来。

39. 如何管理一个大型项目中的依赖?

使用 CocoaPods 或 Carthage 管理第三方库;

通过模块化拆分功能,每个模块的依赖独立管理;

明确依赖的版本和来源,确保团队一致性。

40. 如何在 Objective-C 项目中引入 Swift?

• 添加 Swift 文件时,Xcode 会自动生成 Bridging Header;

• 在 Bridging Header 中引入需要在 Swift 中使用的 Objective-C 头文件;

• 使用 @objc 暴露 Swift 方法给 Objective-C 调用。