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

Objective-C 消息传递机制详解

2024-07-303.0k 阅读

一、Objective-C 消息传递机制基础概念

在 Objective-C 编程中,消息传递(Messaging)是其核心机制之一。它与传统面向对象语言如 C++ 的函数调用有着本质区别。在 C++ 中,函数调用在编译期就确定了要执行的代码地址,而 Objective-C 的消息传递是在运行时动态决定的。

当我们向一个对象发送消息时,例如 [object method],这里的 object 是接收者(receiver),method 是选择子(selector)。选择子实际上是一个指向方法的唯一标识符,它在编译期就确定了,但具体执行哪个方法实现,是在运行时根据接收者的实际类型来决定的。

这种机制赋予了 Objective-C 极大的灵活性,使得程序能够在运行时根据实际情况动态调整行为,这在诸如框架设计、插件化开发等场景中有着重要应用。

二、消息传递的基本流程

  1. 编译期处理 在编译阶段,编译器会将 [object method] 这样的消息发送表达式转化为一个 objc_msgSend 函数调用。例如,假设有如下代码:
#import <Foundation/Foundation.h>

@interface Person : NSObject
- (void)sayHello;
@end

@implementation Person
- (void)sayHello {
    NSLog(@"Hello!");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person sayHello];
    }
    return 0;
}

编译器会将 [person sayHello] 转化为类似 objc_msgSend(person, @selector(sayHello)) 的形式。这里的 @selector(sayHello) 就是选择子,它是一个 SEL 类型的对象,用于唯一标识 sayHello 方法。

  1. 运行期查找 运行时系统接收到 objc_msgSend 调用后,会开始查找方法的实现。它首先会在接收者对象的类的方法缓存(method cache)中查找。方法缓存是为了提高查找效率而存在的,它存储了最近使用过的方法。如果在缓存中找到了对应的方法,就直接调用该方法实现。

如果在缓存中未找到,运行时系统会在类的方法列表(method list)中查找。类的方法列表包含了该类及其所有父类(从最近的父类开始)定义的方法。如果在类的方法列表中找到了方法,就将其加入到方法缓存中,然后调用该方法实现。

如果在类的方法列表中也未找到,运行时系统会进入动态方法解析阶段。

三、动态方法解析

  1. 动态方法解析机制 当运行时系统在方法缓存和方法列表中都未找到方法实现时,会触发动态方法解析。它首先会调用类的 + (BOOL)resolveInstanceMethod:(SEL)sel 方法(对于实例方法)或 + (BOOL)resolveClassMethod:(SEL)sel 方法(对于类方法)。

在这个方法中,我们可以动态地为类添加方法实现。例如,我们可以在运行时为 Person 类动态添加一个方法:

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

@interface Person : NSObject
- (void)sayGoodbye;
@end

@implementation Person

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

void sayGoodbyeIMP(id self, SEL _cmd) {
    NSLog(@"Goodbye!");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person performSelector:@selector(sayGoodbye)];
    }
    return 0;
}

在上述代码中,当 [person sayGoodbye] 消息发送时,由于 Person 类原本没有 sayGoodbye 方法,运行时会调用 + (BOOL)resolveInstanceMethod:(SEL)sel 方法。我们在这个方法中通过 class_addMethod 动态添加了 sayGoodbye 方法的实现 sayGoodbyeIMP

  1. 动态方法解析的应用场景 动态方法解析在很多框架中都有应用,例如在 Core Data 框架中,它可以根据模型动态生成访问属性的方法。在一些插件化开发中,也可以利用动态方法解析在运行时加载插件的方法,实现功能的动态扩展。

四、备用接收者(Fast Forwarding)

  1. 备用接收者机制 如果动态方法解析阶段没有成功添加方法实现,运行时系统会进入备用接收者阶段。它会调用 -(id)forwardingTargetForSelector:(SEL)aSelector 方法。

在这个方法中,我们可以返回一个备用的接收者对象。如果返回了非 nil 的对象,运行时系统会将消息转发给这个备用接收者来处理。例如:

#import <Foundation/Foundation.h>

@interface Helper : NSObject
- (void)printMessage;
@end

@implementation Helper
- (void)printMessage {
    NSLog(@"This is from Helper.");
}
@end

@interface Person : NSObject
@end

@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(printMessage)) {
        return [[Helper alloc] init];
    }
    return nil;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person performSelector:@selector(printMessage)];
    }
    return 0;
}

在上述代码中,Person 类没有 printMessage 方法,当消息发送时,forwardingTargetForSelector: 方法返回了一个 Helper 对象,于是消息就转发给了 Helper 对象来处理。

  1. 备用接收者的使用场景 备用接收者机制在代理模式的实现中有很好的应用。例如,在一些视图控制器之间的通信中,一个视图控制器可以作为另一个视图控制器的备用接收者,处理其未实现的消息,实现功能的解耦和复用。

五、完整转发(Full Forwarding)

  1. 完整转发机制 如果备用接收者阶段没有找到合适的接收者,运行时系统会进入完整转发阶段。它首先会调用 -(void)forwardInvocation:(NSInvocation *)anInvocation 方法。

在这个方法中,我们可以创建一个新的 NSInvocation 对象,将其发送给其他对象来处理消息。同时,运行时系统还会调用 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法来获取方法的签名信息,以便创建正确的 NSInvocation 对象。例如:

#import <Foundation/Foundation.h>

@interface Helper : NSObject
- (void)printMessageWithArg:(NSString *)arg;
@end

@implementation Helper
- (void)printMessageWithArg:(NSString *)arg {
    NSLog(@"Message: %@", arg);
}
@end

@interface Person : NSObject
@end

@implementation Person
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(printMessageWithArg:)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    Helper *helper = [[Helper alloc] init];
    if ([helper respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:helper];
    }
}
@end

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

在上述代码中,Person 类没有 printMessageWithArg: 方法。在完整转发阶段,methodSignatureForSelector: 方法返回了正确的方法签名,forwardInvocation: 方法将消息转发给了 Helper 对象来处理。

  1. 完整转发的应用场景 完整转发在一些框架的适配和扩展中有重要应用。例如,当我们需要兼容旧版本的 API 或者对系统类进行功能扩展时,可以利用完整转发机制,将消息转发给自定义的实现类,实现功能的定制化。

六、消息传递中的 SEL 和 IMP

  1. SEL(选择子) SEL 是一种数据类型,它用于唯一标识一个方法。在 Objective-C 中,@selector 表达式会返回一个 SEL 对象。例如 @selector(sayHello) 就是一个 SELSEL 对象在程序启动时就被注册到运行时系统中,不同类中相同名字的方法会对应同一个 SEL

SEL 的优点是占用内存小,因为它本质上是一个指针,而且在比较两个 SEL 是否相等时,速度非常快,因为只需要比较指针的值。例如,我们可以这样比较两个 SEL

SEL sel1 = @selector(sayHello);
SEL sel2 = @selector(sayHello);
BOOL isEqual = sel1 == sel2;
  1. IMP(实现) IMP 也是一种数据类型,它是一个指向方法实现的函数指针。当运行时系统找到方法的实现后,IMP 就指向这个具体的实现函数。例如,对于 Person 类的 sayHello 方法,其实现函数可能被 IMP 指向如下:
void sayHelloIMP(id self, SEL _cmd) {
    NSLog(@"Hello!");
}

在运行时,objc_msgSend 最终会通过 IMP 来调用实际的方法实现。IMP 与具体的类和方法实现紧密相关,不同类中相同名字的方法,其 IMP 可能不同。

七、消息传递机制的性能优化

  1. 缓存的利用 由于消息传递首先会在方法缓存中查找,我们应该尽量让常用的方法能够被缓存。在设计类的方法调用逻辑时,尽量让频繁调用的方法集中在少数几个选择子上。例如,在一个视图控制器中,如果某个视图的更新方法被频繁调用,可以将相关的逻辑封装在一个方法中,这样可以提高缓存命中率。

  2. 减少动态方法解析和转发 动态方法解析和转发过程相对复杂,会带来一定的性能开销。尽量在设计阶段就确定好类的方法,避免在运行时大量使用动态方法解析和转发。如果确实需要动态功能,可以在程序启动时进行预加载和初始化,将可能用到的动态方法提前添加好,减少运行时的动态操作。

八、与其他语言机制的对比

  1. 与 C++ 函数调用对比 如前文所述,C++ 的函数调用是静态绑定的,在编译期就确定了函数的地址。而 Objective-C 的消息传递是动态绑定的,在运行时才确定方法的实现。这使得 Objective-C 在灵活性上更具优势,但也带来了一定的性能开销。例如,在 C++ 中:
class Animal {
public:
    void say() {
        std::cout << "Animal says" << std::endl;
    }
};

class Dog : public Animal {
public:
    void say() override {
        std::cout << "Dog says woof" << std::endl;
    }
};

int main() {
    Animal *animal = new Dog();
    animal->say();
    return 0;
}

这里 animal->say() 的调用在编译期就确定了要调用 Dog 类的 say 方法(通过虚函数表)。而在 Objective-C 中,类似的操作是动态的:

#import <Foundation/Foundation.h>

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

@implementation Animal
- (void)say {
    NSLog(@"Animal says");
}
@end

@interface Dog : Animal
- (void)say;
@end

@implementation Dog
- (void)say {
    NSLog(@"Dog says woof");
}
@end

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

这里 [animal say] 的实际调用是在运行时根据 animal 的实际类型(Dog)来确定的。

  1. 与 Java 方法调用对比 Java 的方法调用在大部分情况下是静态绑定的,但对于被声明为 virtual(Java 中默认所有非 final 方法都是 virtual)的方法,会采用动态绑定。然而,Java 的动态绑定是基于类继承体系和虚函数表的,相对 Objective-C 的消息传递机制,灵活性稍逊一筹。例如,Java 中不能在运行时动态为类添加方法,而 Objective-C 可以通过动态方法解析来实现这一点。

九、消息传递机制在框架中的应用

  1. UIKit 框架中的应用UIKit 框架中,消息传递机制无处不在。例如,当用户点击一个按钮时,系统会向按钮对应的视图控制器发送 action 消息。视图控制器通过实现相应的 action 方法来处理用户的点击事件。这里的 action 消息发送就是基于 Objective-C 的消息传递机制。
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@end

@implementation ViewController

- (IBAction)buttonTapped:(id)sender {
    NSLog(@"Button tapped!");
}

@end

在上述代码中,buttonTapped: 方法就是接收按钮点击消息的方法。

  1. Foundation 框架中的应用Foundation 框架中,NSNotificationCenter 也利用了消息传递机制。当一个通知被发布时,NSNotificationCenter 会向注册了相应通知的对象发送消息。这些对象通过实现特定的选择子方法来处理通知。例如:
#import <Foundation/Foundation.h>

@interface Observer : NSObject
@end

@implementation Observer

- (void)handleNotification:(NSNotification *)notification {
    NSLog(@"Received notification: %@", notification.name);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Observer *observer = [[Observer alloc] init];
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserver:observer selector:@selector(handleNotification:) name:@"SomeNotification" object:nil];
        [center postNotificationName:@"SomeNotification" object:nil];
    }
    return 0;
}

在上述代码中,Observer 对象通过 handleNotification: 方法接收并处理通知消息。

十、总结消息传递机制的重要性

Objective-C 的消息传递机制是其面向对象编程的核心特性之一。它的动态性使得程序能够在运行时根据实际情况灵活调整行为,这在复杂的框架设计、插件化开发以及系统的可扩展性方面都有着不可替代的作用。

虽然消息传递机制带来了灵活性,但也需要开发者在使用过程中注意性能问题,合理利用缓存,减少不必要的动态操作。同时,深入理解消息传递机制也有助于我们更好地阅读和编写 Objective-C 代码,尤其是在处理复杂的框架和类库时,能够更清晰地把握程序的运行逻辑。通过与其他语言机制的对比,我们也能更深刻地认识到 Objective-C 消息传递机制的独特之处和优势所在。在实际项目开发中,充分利用消息传递机制的特性,可以编写出更具扩展性和灵活性的高质量代码。