MST

星途 面试题库

面试题:Objective-C的消息机制原理及应用

解释Objective-C的消息机制是如何工作的,包括消息发送、动态方法解析、备用接收者、完整的消息转发流程。并举例说明在实际开发中如何利用消息机制实现一些特殊功能,比如方法替换、AOP(面向切面编程)等。
23.4万 热度难度
编程语言Objective-C

知识考点

AI 面试

面试题答案

一键面试

1. 消息发送(Message Sending)

在Objective-C中,方法调用实际上是向对象发送消息。当编译器遇到形如[object message]这样的表达式时,它会将其转化为objc_msgSend函数的调用。objc_msgSend函数的第一个参数是接收者对象(即object),第二个参数是消息的选择器(即message对应的SEL),后面可以跟消息的参数。其大致流程如下:

  1. 缓存查找:首先,运行时系统会在接收者对象的类的方法缓存(cache)中查找与选择器对应的方法实现。方法缓存是为了提高查找效率,最近使用的方法会被缓存起来。如果在缓存中找到了对应的方法实现,就直接调用该方法并返回。
  2. 类方法列表查找:如果在缓存中未找到,就会在接收者对象的类的方法列表(method list)中查找。类的方法列表包含了该类及其超类(通过继承链向上查找)中所有的实例方法。如果找到了匹配的方法,就将其添加到方法缓存中,并调用该方法。
  3. 父类方法列表查找:如果在本类的方法列表中未找到,就沿着继承链向上在父类的方法列表中查找,重复上述过程,直到找到匹配的方法或者到达NSObject类(如果还未找到,则进入动态方法解析阶段)。

2. 动态方法解析(Dynamic Method Resolution)

当在继承链上都未找到匹配的方法时,运行时系统会给类一次动态添加方法实现的机会,即进入动态方法解析阶段。分为两个步骤:

  • 实例方法解析:运行时系统会调用类的+ (BOOL)resolveInstanceMethod:(SEL)sel类方法。在这个方法中,类可以通过class_addMethod函数动态地为该选择器添加一个实例方法实现。如果添加成功,返回YES,此时消息发送流程重新开始,从缓存查找开始;如果返回NO,则进入备用接收者阶段。
  • 类方法解析:如果是类方法调用未找到,运行时系统会调用类的+ (BOOL)resolveClassMethod:(SEL)sel类方法,同样可以在这个方法中动态添加类方法的实现。处理逻辑与实例方法解析类似。

3. 备用接收者(Forwarding Target For Selector)

如果动态方法解析没有处理该消息,运行时系统会调用接收者对象的- (id)forwardingTargetForSelector:(SEL)sel实例方法。在这个方法中,对象可以返回另一个能够处理该消息的对象。如果返回了一个非nil对象,那么消息就会被转发给这个备用接收者,消息发送流程重新开始;如果返回nil,则进入完整的消息转发阶段。

4. 完整的消息转发(Full Forwarding)

如果备用接收者也没有处理该消息,就进入完整的消息转发阶段。分为两步:

  • 方法签名和消息转发:运行时系统会调用接收者对象的- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel实例方法,该方法需要返回一个描述方法参数和返回值类型的NSMethodSignature对象。如果返回nil,运行时系统会发出unrecognized selector的错误。如果返回了有效的方法签名,运行时系统会调用- (void)forwardInvocation:(NSInvocation *)invocation方法,在这个方法中,开发者可以手动指定消息的最终接收者并发送消息。
  • 异常处理:如果在forwardInvocation:方法中没有处理消息,运行时系统会调用- (void)doesNotRecognizeSelector:(SEL)sel方法,抛出NSInvalidArgumentException异常,提示该对象无法识别这个选择器。

5. 实际开发中的应用

  • 方法替换: 可以利用动态方法解析来实现方法替换。例如,我们想要在运行时替换UIViewControllerviewDidLoad方法。首先创建一个分类,在分类的+load方法中进行方法替换:
#import "UIViewController+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (MethodSwizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod(self, @selector(viewDidLoad));
        Method swizzledMethod = class_getInstanceMethod(self, @selector(xx_viewDidLoad));
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)xx_viewDidLoad {
    // 可以在这里添加自定义逻辑,然后调用原方法
    [self xx_viewDidLoad];
    NSLog(@"viewDidLoad method has been swizzled");
}
@end

这样,当任何UIViewController及其子类调用viewDidLoad方法时,实际上会先执行我们替换后的逻辑。

  • AOP(面向切面编程): 利用消息转发机制可以实现AOP。比如,我们想在某个类的所有方法调用前后添加日志记录。可以创建一个代理类,在代理类的forwardInvocation:方法中添加日志记录逻辑,然后将原对象的消息转发给代理类。示例代码如下:
@interface AOPProxy : NSObject
@property (nonatomic, strong) id target;
@end

@implementation AOPProxy
- (instancetype)initWithTarget:(id)target {
    self = [super init];
    if (self) {
        _target = target;
    }
    return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = invocation.selector;
    NSLog(@"Before calling %@", NSStringFromSelector(sel));
    [invocation invokeWithTarget:self.target];
    NSLog(@"After calling %@", NSStringFromSelector(sel));
}
@end

使用时,创建代理对象并将原对象传入:

SomeClass *originalObject = [[SomeClass alloc] init];
AOPProxy *proxy = [[AOPProxy alloc] initWithTarget:originalObject];
[proxy someMethod]; // 调用代理对象的方法,会执行AOP逻辑并转发给原对象

通过这种方式,可以在不修改原类代码的情况下,为其添加通用的横切逻辑,实现AOP。