面试题答案
一键面试1. 消息发送(Message Sending)
在Objective-C中,方法调用实际上是向对象发送消息。当编译器遇到形如[object message]
这样的表达式时,它会将其转化为objc_msgSend
函数的调用。objc_msgSend
函数的第一个参数是接收者对象(即object
),第二个参数是消息的选择器(即message
对应的SEL
),后面可以跟消息的参数。其大致流程如下:
- 缓存查找:首先,运行时系统会在接收者对象的类的方法缓存(
cache
)中查找与选择器对应的方法实现。方法缓存是为了提高查找效率,最近使用的方法会被缓存起来。如果在缓存中找到了对应的方法实现,就直接调用该方法并返回。 - 类方法列表查找:如果在缓存中未找到,就会在接收者对象的类的方法列表(
method list
)中查找。类的方法列表包含了该类及其超类(通过继承链向上查找)中所有的实例方法。如果找到了匹配的方法,就将其添加到方法缓存中,并调用该方法。 - 父类方法列表查找:如果在本类的方法列表中未找到,就沿着继承链向上在父类的方法列表中查找,重复上述过程,直到找到匹配的方法或者到达
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. 实际开发中的应用
- 方法替换:
可以利用动态方法解析来实现方法替换。例如,我们想要在运行时替换
UIViewController
的viewDidLoad
方法。首先创建一个分类,在分类的+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。