-
问题:你对 Runtime 是如何理解的?
-
问题:介绍一下 isa 指针?对象、类、元类之间是什么关系?
-
三者关系:这是一个经典的三层结构。
-
问题:为什么要设计 Metaclass(元类)?
-
问题:
class_rw_t和class_ro_t有什么区别?
消息传递与转发
-
问题:OC 中向一个对象发送消息,底层的流程是怎样的?
-
问题:什么是消息转发机制?它在什么情况下会被触发?一共有哪些步骤?
-
完整流程分为三步:
- 动态方法解析:首先调用
+resolveInstanceMethod:(或+resolveClassMethod:),允许开发者在这个阶段动态地添加方法实现。如果返回YES,Runtime 会重新发送消息。 - 快速转发:如果上一步未处理,会调用
-forwardingTargetForSelector:,允许开发者将消息转发给另一个对象处理。这个步骤效率最高,适合简单的消息转发。 - 完整消息转发:这是最后的机会。首先调用
-methodSignatureForSelector:获取方法的方法签名(参数和返回值类型),如果返回不为nil,则再调用-forwardInvocation:,将一个NSInvocation对象封装给开发者,可以修改其调用目标和方法实现。如果这一步也没处理,最终会调用-doesNotRecognizeSelector:抛出unrecognized selector sent to instance异常。
- 动态方法解析:首先调用
-
问题:向
nil对象发送消息会发生什么?
动态特性与应用
-
问题:Category(分类) 和 Extension(扩展) 有什么区别?
-
问题:Runtime 是如何实现
weak变量自动置nil的? -
问题:能否向编译后得到的类中增加实例变量?为什么?
💡 一道经典"超纲题"
这是网上流传的一道 Sunnyxx 的考题,可以很好地检验你对 Runtime 底层和内存的掌握程度。
objectivec
// Person类
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)print;
@end
@implementation Person
- (void)print {
NSLog(@"my name = %@", self.name);
}
@end
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
}
问:这段代码会输出什么?会崩溃吗?为什么?
点击查看答案
**答案**:不会崩溃,会打印出当前 `ViewController` 对象的信息,例如 `my name = `。核心原因:
- 栈内存分配:
obj是一个指向栈空间的指针。栈空间的内存地址是从高到低分配的。viewDidLoad中有cls和obj两个局部变量,它们的地址是连续的。 isa指针的奥秘:obj指向了cls的地址。当对obj发送print消息时,Runtime 会从obj指向的内存开始,将其当作一个对象的isa指针。巧合的是,cls本身就是一个指向Person类的指针,它恰好可以充当这个"假对象"的isa,所以 Runtime 能正确地找到Person类并调用print方法。- 访问成员变量:关键在于
print方法中访问了self.name。name属性的 getter 方法本质上是通过self指针,跳过isa指针的长度(8字节),来读取内存中的值。在这个例子中,self就是obj,obj指向的是cls的地址。那么obj + 8会指向哪里?由于cls和obj是连续的栈变量,cls占8字节,obj + 8正好指向了cls变量下方的内存。而在调用[super viewDidLoad]时,系统会生成一个临时结构体(包含self和ViewController类),这个结构体恰好位于栈上,其地址覆盖了cls下方的内存。因此,self.name实际上读取到了self(即当前的ViewController实例)的地址,所以打印出了ViewController的信息。
希望这份总结对你的面试准备有所帮助。除了概念本身,这些底层原理之间的关联性(比如 isa 如何连接消息发送和 weak 如何影响对象生命周期)往往是面试官考察的重点。祝你面试顺利!