MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

深入探索Objective-C消息发送与接收机制

2023-01-264.9k 阅读

消息发送与接收基础概念

在Objective-C编程中,消息发送与接收是其核心机制。Objective-C是一门动态语言,对象之间的交互通过发送和接收消息来实现。这与静态语言(如C++)中函数调用的机制有本质区别。

在Objective-C中,当向一个对象发送消息时,编译器并不会在编译时确定要调用的具体函数。相反,它会在运行时根据对象的实际类型来查找对应的方法实现。例如,考虑以下简单的代码:

#import <Foundation/Foundation.h>

@interface Animal : NSObject
- (void)makeSound;
@end

@implementation Animal
- (void)makeSound {
    NSLog(@"Generic animal sound");
}
@end

@interface Dog : Animal
- (void)makeSound {
    NSLog(@"Woof!");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Animal *animal = [[Dog alloc] init];
        [animal makeSound];
    }
    return 0;
}

在上述代码中,animal变量被声明为Animal类型,但实际指向的是Dog类型的对象。当发送makeSound消息时,运行时会根据animal实际指向的Dog对象,调用Dog类中实现的makeSound方法,输出“Woof!”。这体现了动态绑定的特性,即方法的调用基于对象的运行时类型,而非编译时类型。

消息发送的底层实现 - objc_msgSend

汇编层面剖析

消息发送的核心函数是objc_msgSend。在底层,当向一个对象发送消息时,实际调用的就是objc_msgSend函数。这个函数的原型大致如下(简化版):

id objc_msgSend(id self, SEL op, ...);

其中,self是接收消息的对象,SEL是方法选择器,它是一个指向方法名称的唯一标识符,...表示可变参数列表,用于传递方法的参数。

从汇编层面来看,objc_msgSend的实现非常复杂,因为它需要快速且高效地查找方法实现。在ARM架构下,其实现大致如下步骤:

  1. 首先,将接收者对象(self)和方法选择器(SEL)作为参数传递给objc_msgSend
  2. 函数开始时,会先检查self是否为nil。如果selfnil,在非ARC环境下,objc_msgSend会直接返回nil;在ARC环境下,会触发运行时错误。
  3. 然后,通过对象的isa指针找到对象所属的类。isa指针是每个对象都有的一个指针,它指向对象的类。
  4. 在类的方法列表中查找与SEL对应的方法实现。如果在类的方法列表中没有找到,会沿着继承链向上查找,直到找到NSObject类。如果最终都没有找到,就会进入动态方法解析阶段。

缓存机制

为了提高消息发送的效率,objc_msgSend使用了缓存机制。每个类都有一个方法缓存(cache_t),当一个方法被调用时,objc_msgSend会首先在缓存中查找对应的方法实现。如果缓存命中,就可以直接调用方法,避免了在方法列表中进行线性查找。

缓存的结构通常是一个哈希表,SEL作为键,方法实现的指针作为值。当向一个对象发送消息时,objc_msgSend会先计算SEL的哈希值,然后在缓存中查找对应的方法实现。如果缓存未命中,才会在类的方法列表和继承链中查找。

以下代码示例可以帮助理解缓存机制的作用:

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
- (void)frequentMethod;
@end

@implementation MyClass
- (void)frequentMethod {
    NSLog(@"Frequent method called");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        for (int i = 0; i < 10000; i++) {
            [obj frequentMethod];
        }
    }
    return 0;
}

在上述代码中,frequentMethod被频繁调用。由于缓存机制的存在,每次调用时大部分情况下都能在缓存中命中,大大提高了方法调用的效率。

动态方法解析

objc_msgSend在类的方法列表和继承链中都没有找到与SEL对应的方法实现时,会进入动态方法解析阶段。动态方法解析允许在运行时为类添加方法实现,而不需要在编译时就确定所有方法。

类方法的动态解析

对于类方法,运行时会调用+ (BOOL)resolveClassMethod:(SEL)sel方法。开发者可以在这个方法中动态添加类方法的实现。例如:

#import <Foundation/Foundation.h>

@interface DynamicClass : NSObject
+ (void)dynamicClassMethod;
@end

@implementation DynamicClass
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(dynamicClassMethod)) {
        class_addMethod(object_getClass(self), sel, (IMP)dynamicClassMethodIMP, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

void dynamicClassMethodIMP(id self, SEL _cmd) {
    NSLog(@"Dynamic class method implemented at runtime");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [DynamicClass dynamicClassMethod];
    }
    return 0;
}

在上述代码中,DynamicClass类在编译时并没有dynamicClassMethod的实现。当发送dynamicClassMethod消息且未找到实现时,会调用+ (BOOL)resolveClassMethod:(SEL)sel方法。在这个方法中,通过class_addMethod函数动态添加了dynamicClassMethod的实现,从而使消息能够正确处理。

实例方法的动态解析

对于实例方法,运行时会调用+ (BOOL)resolveInstanceMethod:(SEL)sel方法。其原理与类方法的动态解析类似,开发者可以在这个方法中动态添加实例方法的实现。例如:

#import <Foundation/Foundation.h>

@interface DynamicInstanceClass : NSObject
- (void)dynamicInstanceMethod;
@end

@implementation DynamicInstanceClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicInstanceMethod)) {
        class_addMethod(self, sel, (IMP)dynamicInstanceMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void dynamicInstanceMethodIMP(id self, SEL _cmd) {
    NSLog(@"Dynamic instance method implemented at runtime");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DynamicInstanceClass *obj = [[DynamicInstanceClass alloc] init];
        [obj dynamicInstanceMethod];
    }
    return 0;
}

在这个例子中,DynamicInstanceClass类的实例对象在发送dynamicInstanceMethod消息且未找到实现时,通过+ (BOOL)resolveInstanceMethod:(SEL)sel方法动态添加了方法实现,使得消息能够正确处理。

备用接收者

如果动态方法解析没有成功添加方法实现,运行时会进入备用接收者阶段。在这个阶段,运行时会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,允许对象指定一个备用的接收者来处理该消息。

例如,假设有两个类ClassAClassBClassA没有实现某个方法,但ClassB实现了,ClassA可以通过备用接收者机制将消息转发给ClassB

#import <Foundation/Foundation.h>

@interface ClassB : NSObject
- (void)sharedMethod;
@end

@implementation ClassB
- (void)sharedMethod {
    NSLog(@"Method implemented in ClassB");
}
@end

@interface ClassA : NSObject
- (void)sharedMethod;
@end

@implementation ClassA
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sharedMethod)) {
        return [[ClassB alloc] init];
    }
    return nil;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ClassA *a = [[ClassA alloc] init];
        [a sharedMethod];
    }
    return 0;
}

在上述代码中,ClassA没有实现sharedMethod,但通过- (id)forwardingTargetForSelector:(SEL)aSelector方法返回了ClassB的实例作为备用接收者。当向ClassA的实例a发送sharedMethod消息时,消息会被转发给ClassB的实例,从而正确执行方法。

完整的消息转发

如果备用接收者也没有找到合适的处理对象,运行时会进入完整的消息转发阶段。在这个阶段,运行时会创建一个NSInvocation对象,该对象封装了消息的所有信息,包括接收者、选择器和参数。

首先,运行时会调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法。开发者可以在这个方法中手动处理消息转发,例如将消息转发给其他对象。例如:

#import <Foundation/Foundation.h>

@interface TargetClass : NSObject
- (void)targetMethod:(NSString *)param;
@end

@implementation TargetClass
- (void)targetMethod:(NSString *)param {
    NSLog(@"Target method called with param: %@", param);
}
@end

@interface SourceClass : NSObject
- (void)sourceMethod:(NSString *)param;
@end

@implementation SourceClass
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    if ([TargetClass instancesRespondToSelector:sel]) {
        TargetClass *target = [[TargetClass alloc] init];
        [anInvocation invokeWithTarget:target];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sig = [TargetClass instanceMethodSignatureForSelector:aSelector];
    return sig;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SourceClass *source = [[SourceClass alloc] init];
        [source performSelector:@selector(sourceMethod:) withObject:@"Hello"];
    }
    return 0;
}

在上述代码中,SourceClass没有实现sourceMethod:,但通过- (void)forwardInvocation:(NSInvocation *)anInvocation方法将消息转发给了TargetClass。同时,- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法需要返回正确的方法签名,以便NSInvocation对象能够正确封装消息。

如果- (void)forwardInvocation:(NSInvocation *)anInvocation方法也没有正确处理消息,运行时会调用- (void)doesNotRecognizeSelector:(SEL)aSelector方法,抛出NSInvalidArgumentException异常,提示对象无法识别该选择器。

消息发送与接收机制的应用场景

多态性的实现

消息发送与接收机制是Objective-C实现多态性的基础。通过动态绑定,不同类的对象可以对相同的消息做出不同的响应。例如,在图形绘制的应用中,可以定义一个Shape类作为基类,CircleRectangle类继承自Shape类。每个子类都可以实现自己的draw方法。当向不同的Shape对象发送draw消息时,会根据对象的实际类型调用相应的draw方法,实现多态效果。

#import <Foundation/Foundation.h>

@interface Shape : NSObject
- (void)draw;
@end

@implementation Shape
- (void)draw {
    NSLog(@"Drawing a shape");
}
@end

@interface Circle : Shape
- (void)draw {
    NSLog(@"Drawing a circle");
}
@end

@interface Rectangle : Shape
- (void)draw {
    NSLog(@"Drawing a rectangle");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Shape *circle = [[Circle alloc] init];
        Shape *rectangle = [[Rectangle alloc] init];
        [circle draw];
        [rectangle draw];
    }
    return 0;
}

在上述代码中,circlerectangle虽然都被声明为Shape类型,但实际调用draw方法时,会根据对象的实际类型调用CircleRectangle类中各自的draw方法,展示了多态性。

代理模式的实现

代理模式是一种常用的设计模式,在Objective-C中可以通过消息发送与接收机制轻松实现。例如,在一个视图控制器中,可以设置一个代理对象来处理特定的事件。当事件发生时,视图控制器向代理对象发送消息,代理对象根据自身的实现来处理事件。

#import <Foundation/Foundation.h>

@protocol MyDelegate <NSObject>
- (void)handleEvent;
@end

@interface MyViewController : NSObject
@property (nonatomic, weak) id<MyDelegate> delegate;
- (void)triggerEvent;
@end

@implementation MyViewController
- (void)triggerEvent {
    if ([self.delegate respondsToSelector:@selector(handleEvent)]) {
        [self.delegate handleEvent];
    }
}
@end

@interface MyDelegateClass : NSObject <MyDelegate>
- (void)handleEvent {
    NSLog(@"Event handled by delegate");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyViewController *vc = [[MyViewController alloc] init];
        MyDelegateClass *delegate = [[MyDelegateClass alloc] init];
        vc.delegate = delegate;
        [vc triggerEvent];
    }
    return 0;
}

在上述代码中,MyViewController通过delegate属性持有一个实现了MyDelegate协议的对象。当triggerEvent方法被调用时,会检查代理对象是否响应handleEvent消息,如果响应则发送该消息,实现了代理模式。

方法交换

通过消息发送与接收机制,可以在运行时交换方法的实现,这在很多场景下非常有用,例如AOP(面向切面编程)。可以使用method_exchangeImplementations函数来交换两个方法的实现。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface MyClass : NSObject
- (void)originalMethod;
@end

@implementation MyClass
- (void)originalMethod {
    NSLog(@"Original method");
}
@end

void newMethodImplementation(id self, SEL _cmd) {
    NSLog(@"New method implementation");
    // 调用原始方法
    Method originalMethod = class_getInstanceMethod([self class], @selector(originalMethod));
    ((void (*)(id, SEL))objc_msgSend)(self, originalMethod);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Method originalMethod = class_getInstanceMethod([MyClass class], @selector(originalMethod));
        Method newMethod = class_getInstanceMethod([MyClass class], @selector(newMethodImplementation));
        method_exchangeImplementations(originalMethod, newMethod);

        MyClass *obj = [[MyClass alloc] init];
        [obj originalMethod];
    }
    return 0;
}

在上述代码中,通过method_exchangeImplementations函数交换了originalMethodnewMethodImplementation的实现。当调用originalMethod时,实际上会执行newMethodImplementation的代码,并且在newMethodImplementation中还可以通过objc_msgSend调用原始的originalMethod实现,实现了方法的增强。

性能优化与注意事项

性能优化

  1. 减少动态方法解析的使用:动态方法解析虽然强大,但由于涉及运行时的方法添加等操作,性能相对较低。尽量在编译时确定所有必要的方法实现,避免频繁进入动态方法解析阶段。
  2. 合理利用缓存:由于objc_msgSend使用缓存机制来提高方法查找效率,尽量让常用的方法能够在缓存中命中。可以通过合理设计类的继承结构和方法调用频率,使方法调用更集中在少量的方法上,提高缓存命中率。
  3. 避免过多的消息转发:备用接收者和完整的消息转发都涉及额外的运行时开销。如果可能,尽量在类内部实现方法,而不是依赖消息转发机制。

注意事项

  1. 空指针检查:由于objc_msgSendselfnil时的行为在不同环境下有所不同(非ARC返回nilARC触发运行时错误),在向对象发送消息时,要注意对象是否可能为nil,避免潜在的运行时错误。
  2. 方法签名一致性:在进行消息转发,特别是在- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法中,要确保返回的方法签名与实际要转发到的方法签名一致,否则可能导致NSInvocation对象封装消息错误,引发运行时异常。
  3. 内存管理:在动态添加方法实现(如class_addMethod)和使用备用接收者、消息转发等机制时,要注意相关对象的内存管理。例如,创建的备用接收者对象需要正确释放,避免内存泄漏。

通过深入理解Objective-C的消息发送与接收机制,开发者能够更好地编写高效、灵活的代码,充分发挥Objective-C作为动态语言的优势。无论是实现复杂的设计模式,还是进行性能优化,对这一核心机制的掌握都是至关重要的。