面试题答案
一键面试Objective-C方法的动态派发过程
-
消息发送(Message Sending)
- 在Objective-C中,当向一个对象发送消息时,比如
[object method]
,编译器并不会直接生成调用该方法的机器码。而是会转化为objc_msgSend
函数调用。 objc_msgSend
首先会根据对象的isa指针找到它的类对象。类对象中有一个方法列表(method list),该列表存储了方法的selector(方法名的一种表示形式)和对应的实现(IMP,即函数指针)。objc_msgSend
会在方法列表中查找与传入的selector相匹配的方法实现。如果在本类中没有找到,就会沿着继承链向上查找,直到找到匹配的方法或者到达根类NSObject
。
- 在Objective-C中,当向一个对象发送消息时,比如
-
动态方法解析(Dynamic Method Resolution)
- 如果在消息发送阶段没有找到方法的实现,运行时系统会启动动态方法解析。
- 对于类方法,会调用
+ (BOOL)resolveClassMethod:(SEL)sel
;对于实例方法,会调用+ (BOOL)resolveInstanceMethod:(SEL)sel
。 - 在这些方法中,我们可以动态地添加方法的实现。例如:
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(myDynamicMethod)) {
class_addMethod(self, sel, (IMP)myDynamicMethodImplementation, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void myDynamicMethodImplementation(id self, SEL _cmd) {
// 方法实现代码
}
@end
- 备用接收者(Fast Forwarding)
- 如果动态方法解析没有处理消息,运行时会进入备用接收者阶段。
- 会调用
-(id)forwardingTargetForSelector:(SEL)aSelector
。在这个方法中,我们可以返回另一个对象来处理这个消息。例如:
@implementation MyClass
-(id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(otherObjectMethod)) {
return otherObject;
}
return nil;
}
@end
- 完整的消息转发流程(Normal Forwarding)
- 如果备用接收者也没有处理消息,就会进入完整的消息转发流程。
- 首先会调用
-(void)forwardInvocation:(NSInvocation *)anInvocation
。在这里,我们可以手动构建一个新的Invocation对象,将消息转发到其他对象上。 - 还会调用
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
,用于获取方法的签名信息,以便forwardInvocation
能够正确地构建Invocation对象。
对程序运行逻辑、性能以及内存管理方面的影响
-
运行逻辑
- 属性存取方法与自定义方法结合:属性存取方法(
getter
和setter
)也是通过动态派发机制来调用的。这意味着我们可以在运行时改变属性的存取逻辑。例如,我们可以在resolveInstanceMethod
中动态地为属性的setter
方法添加额外的逻辑,比如日志记录或者数据验证。 - 灵活性:这种机制使得程序在运行时能够根据不同的情况动态地处理方法调用,增加了程序的灵活性。例如,在开发插件系统时,可以在运行时动态地添加方法实现,实现插件的功能扩展。
- 属性存取方法与自定义方法结合:属性存取方法(
-
性能
- 开销:动态派发机制相比静态调用会有一定的性能开销。因为消息发送阶段需要查找方法列表,动态方法解析和消息转发流程也会消耗额外的时间。在性能敏感的代码段,频繁的动态方法调用可能会影响程序的性能。
- 缓存优化:运行时系统为了提高性能,会缓存方法的调用结果。当相同的消息发送给同一个对象时,会直接从缓存中获取方法的实现,而不需要再次查找方法列表。
-
内存管理
- 内存泄漏风险:在动态方法解析和消息转发过程中,如果处理不当,可能会引入内存泄漏。例如,在
forwardInvocation
中,如果不正确地管理对象的引用计数,可能会导致对象无法释放。 - 对象生命周期管理:由于动态派发机制允许在运行时改变方法的实现,我们需要更加小心地管理对象的生命周期。例如,在动态添加方法实现时,要确保新添加的方法不会导致对象被过度保留或者提前释放。
- 内存泄漏风险:在动态方法解析和消息转发过程中,如果处理不当,可能会引入内存泄漏。例如,在
举例说明如何利用或避免这些影响
- 利用影响
- 动态添加日志功能:
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(setMyProperty:)) {
IMP originalIMP = class_getMethodImplementation(self, sel);
IMP newIMP = imp_implementationWithBlock(^(id self, id value) {
NSLog(@"Setting myProperty to %@", value);
((void (*)(id, SEL, id))originalIMP)(self, sel, value);
});
class_addMethod(self, sel, newIMP, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
- 这里通过动态方法解析为属性的
setter
方法添加了日志记录功能。
- 避免影响
- 性能优化:在性能敏感的地方,尽量使用静态调用。例如,对于一些频繁调用且逻辑固定的方法,可以将其声明为
final
方法,这样编译器可以进行优化,直接生成静态调用的机器码。 - 内存管理:在消息转发过程中,确保正确地管理对象的引用计数。例如,在
forwardInvocation
中,对转发的对象进行正确的retain
和release
操作,以避免内存泄漏。
- 性能优化:在性能敏感的地方,尽量使用静态调用。例如,对于一些频繁调用且逻辑固定的方法,可以将其声明为
@implementation MyClass
-(void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
id target = [self forwardingTargetForSelector:sel];
if (target) {
[anInvocation retainArguments];
[anInvocation setTarget:target];
[anInvocation invoke];
[anInvocation releaseArguments];
} else {
[super forwardInvocation:anInvocation];
}
}
@end