一. KVO 和 KVC 底层面试题
1. KVO 是如何实现的?
- KVO全称为Key-Value Observing,也称为“键值监听”,主要用于监听某个对象的属性值的改变。
- 利用Runtime动态生成一个子类,并且让
instance对象的isa指向这个全新的子类,并且这个全新的子类重写了setter方法、class方法、dealloc方法、_isKVOA方法。当修改instance对象的属性时,重写的setter方法会调用Foundation的_NSSetXXXValueAndNotify函数,在_NSSetXXXValueAndNotify函数内部会先调用willChangeValueForKey:方法,然后调用父类原来的setter方法,最后调用didChangeValueForKey:方法,并在didChangeValueForKey方法内部触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
2. KVC 是如何工作的?
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性。- 首先会按照顺序依次查找
setKey:方法和_setKey:方法(getKey:、key、isKey、_key:),只要找到这两个方法当中的任何一个就直接传递参数,调用方法;没有找到的话会调用accessInstanceVariablesDirectly来判断是否支持访问成变量,如果是则是调用对应的成员变量,如果返回是NO,则是调用setValue:forUndefineKey:(valueforUndefineKey)方法,并抛出异常
3. KVO 的 isKindOfClass 为什么变成 NSKVONotifying_XX?
4. 如何手动触发 KVO?
- 手动调用
willChangeValueForKey:和didChangeValueForKey:这两个方`即可。 - 直接修改成员变量也不会触发对应的方法。
5. KVO 是否可以监听 struct 变量?
-不能,因为是值对象,没有继承于NSObject
二. Runtime 面试题
1. objc_msgSend 的调用流程?
- 其核心代码为
void objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);其中参数1为方法接收对象,参数二为SEL,参数三、四···为SEL的参数值 - 过程分为三个部分
-
消息发送:
其实就是先找接收者,没有的话直接退出,有的话在接收者的Class的Cache去查找方法,如果存在直接调用方法结束查找,如果没有则是调用Class的class_rw_t对象里面去查对应的方法,如果能找到方法,则是调用方法,并将方法存储到cache中。如果没有则是寻找SuperClass的Cache中是否有对应方法,如果有方法,调用该方法,并存到Cache中。如果没有则是继续向上查找SuperClass的class_rw_t结构中是否有该方法。一直找到最顶层的MetaClass,如果此时依然没有找到对应的方法,我们将进入下一个阶段动态解析。
-
动态解析:所谓动态解析就是调用
+(BOOL)resolveInstanceMethod:(SEL)sel; +(BOOL)resolveClassMethod:(SEL)sel;两个方法去执行通过内部动态添加方法函数class_addMethod添加的实例方法和类方法。并增加标记Try Resolve =YES,然后走消息发送的流程。如果没有的话没有找到对应的方法,则进入下一个阶段:消息转发 -
消息转发:上面的消息发送和动态解析都没有实现方法的调用的话,我们就开始考虑是不是要为这个方法指定一个接收者。通过
+/- (id)forwardingTargetForSelector:(SEL)sel获取方法接收者,如果方法的接收者为nil或者没有实现,则我们进行下一个步骤,对该方法进行签名+/- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector返回一个NSMethodSignature对象,其内部封装返回值类型,参数类型等类型信息。拿到方法名之后直接调用- (void)forwardInvocation:(NSInvocation *)anInvocation在该方法内部重新指定或者构造接收函数。否则认为不要需要处理该方法,应该放任异常抛出。
-
2. method swizzling 如何工作?
Method Swizzle的本质是在运行时交换方法实现(IMP),一般是在原有的方法中,插入自己的业务需求。Objective-C中调用一个方法, 实际上是在底层通过objc_msgSend()发送一个消息。 而查找消息的唯一依据是selector的方法名。每一个OC实例对象都保存有isa指针和实例变量,其中isa指针所属类,类维护一个运行时可接收的方法列表(MethodLists); 方法列表(MethodLists)中保存selector & IMP的映射关系。在运行时,通过selecter找到匹配的IMP,从而找到的具体的实现函数。利用Objective-C的动态特性,在运行时替换selector对应的方法实现(IMP),达到给hook的目的。
3. class_rw_t 结构体是什么
- 一个结构体保存类对象在运行时所使用属性、方法还有遵循的协议等信息。要区别于class_ro_t,后者保存了在编译期确定的属性、方法还有遵循的协议。前者的结构之中有一个指针指向后者。相当于前者将后者信息都拷贝到自身中,方便在使用的时候直接掉调用;
4. IMP 指针是什么?
- (1)它是指向一个方法具体实现的指针,每一个方法都有一个对应的IMP,所以,我们可以直接调用方法的IMP指针,来避免方法调用死循环的问题。
- (2)当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由IMP这个函数指针指向了这个方法实现的。
三. RunLoop 面试题
1. RunLoop 的作用是什么?
- 基本概念
- RunLoop 是一个事件处理循环,用于监听和处理输入事件(如触摸、网络数据、定时器等)。
- 它确保线程在有任务时执行任务,无任务时休眠,避免资源浪费。
- 线程与 RunLoop 的关系
- 主线程的 RunLoop 默认启动,其他线程的 RunLoop 需手动启用(通过
run方法)。 - 每个线程对应唯一的 RunLoop(通过
NSRunLoop.current或CFRunLoopGetCurrent()获取)。
- 主线程的 RunLoop 默认启动,其他线程的 RunLoop 需手动启用(通过
2. RunLoop 的数据结构是怎样的,如何执行的?
- 是一个结构体
3. RunLoop 应用场景?
1. 主线程事件处理
- 主线程 RunLoop 负责处理所有 UI 事件(触摸、绘图等)。
- 例如:滚动
UIScrollView时,RunLoop 切换到UITrackingRunLoopMode,优先处理滚动事件。
2. 子线程保活
class BackgroundThread {
var thread: Thread?
func start() {
thread = Thread {
// 添加 Port 防止 RunLoop 退出
let port = NSMachPort()
RunLoop.current.add(port, forMode: .default)
RunLoop.current.run()
}
thread?.start()
}
}
3. 定时器(Timer)的正确使用
在子线程中使用 Timer 需手动启动 RunLoop。
```
DispatchQueue.global().async {
let timer = Timer(timeInterval: 1, repeats: true) { _ in
print("Timer fired")
}
RunLoop.current.add(timer, forMode: .default)
RunLoop.current.run() // 启动 RunLoop
}
```
4. 性能优化
- 避免阻塞主线程:将耗时操作放到子线程,防止主线程 RunLoop 延迟 UI 响应。
- 合理使用 Mode:在滚动时暂停非关键任务(如将定时器注册到
commonModes)。
四. OC 消息机制
1.SEL 和 IMP 的区别?
- 一个类(
Class)持有一个分发表,在运行期分发消息,表中的每一个实体代表一个方法(Method),它的名字叫做选择子(SEL),对应着一种方法实现(IMP);SEL:定义:typedef struct objc_selector *SEL,代表方法的名称。仅以名字来识别。翻译成中文叫做选择子或者选择器,选择子代表方法在Runtime期间的标识符。为SEL类型,虽然SEL是objc_selector结构体指针,但实际上它只是一个C字符串。在类加载的时候,编译器会生成与方法相对应的选择子,并注册到Objective-C的Runtime运行系统。不论两个类是否存在依存关系,只要他们拥有相同的方法名,那么他们的SEL都是相同的。IMP:定义:typedef id (*IMP)(id, SEL, ...),代表函数指针,即函数执行的入口。该函数使用标准的C调用。第一个参数指向self(它代表当前类实例的地址,如果是类则指向的是它的元类),作为消息的接受者;第二个参数代表方法的选择子;... 代表可选参数,前面的id代表返回值。
objc_msgSend的底层原理?
- 参考
objc_msgSend调用过程。
5. Swift 属性修饰符面试题
weak和unowned的区别?
- weak 的变量时,它的引用计数不会被改变。而且当这个弱引用变量所引用的对象被释放时,这个变量将被自动设为 nil。这也是弱引用必须被声明为 Optional 的原因
- unowned 标记的变量,即使它的原来引用已经被释放,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不是 Optional ,也不会被指向 nil。所以,当我们试图访问这样的 unowned 引用时,程序就会发生错误
copy和strong的区别?
strong表示对对象的强引用(增加引用计数),赋值时直接持有对象的引用。适用于可变对象(如NSMutableString)或需要共享同一实例的场景copy在赋值时会对对象调用copy方法,生成一个不可变副本(如NSString)。用于保护不可变对象的封装性(避免外部可变对象被修改)