Objective-C 消息传递机制详解
一、Objective-C 消息传递机制基础概念
在 Objective-C 编程中,消息传递(Messaging)是其核心机制之一。它与传统面向对象语言如 C++ 的函数调用有着本质区别。在 C++ 中,函数调用在编译期就确定了要执行的代码地址,而 Objective-C 的消息传递是在运行时动态决定的。
当我们向一个对象发送消息时,例如 [object method]
,这里的 object
是接收者(receiver),method
是选择子(selector)。选择子实际上是一个指向方法的唯一标识符,它在编译期就确定了,但具体执行哪个方法实现,是在运行时根据接收者的实际类型来决定的。
这种机制赋予了 Objective-C 极大的灵活性,使得程序能够在运行时根据实际情况动态调整行为,这在诸如框架设计、插件化开发等场景中有着重要应用。
二、消息传递的基本流程
- 编译期处理
在编译阶段,编译器会将
[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
方法。
- 运行期查找
运行时系统接收到
objc_msgSend
调用后,会开始查找方法的实现。它首先会在接收者对象的类的方法缓存(method cache)中查找。方法缓存是为了提高查找效率而存在的,它存储了最近使用过的方法。如果在缓存中找到了对应的方法,就直接调用该方法实现。
如果在缓存中未找到,运行时系统会在类的方法列表(method list)中查找。类的方法列表包含了该类及其所有父类(从最近的父类开始)定义的方法。如果在类的方法列表中找到了方法,就将其加入到方法缓存中,然后调用该方法实现。
如果在类的方法列表中也未找到,运行时系统会进入动态方法解析阶段。
三、动态方法解析
- 动态方法解析机制
当运行时系统在方法缓存和方法列表中都未找到方法实现时,会触发动态方法解析。它首先会调用类的
+ (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
。
- 动态方法解析的应用场景
动态方法解析在很多框架中都有应用,例如在
Core Data
框架中,它可以根据模型动态生成访问属性的方法。在一些插件化开发中,也可以利用动态方法解析在运行时加载插件的方法,实现功能的动态扩展。
四、备用接收者(Fast Forwarding)
- 备用接收者机制
如果动态方法解析阶段没有成功添加方法实现,运行时系统会进入备用接收者阶段。它会调用
-(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
对象来处理。
- 备用接收者的使用场景 备用接收者机制在代理模式的实现中有很好的应用。例如,在一些视图控制器之间的通信中,一个视图控制器可以作为另一个视图控制器的备用接收者,处理其未实现的消息,实现功能的解耦和复用。
五、完整转发(Full Forwarding)
- 完整转发机制
如果备用接收者阶段没有找到合适的接收者,运行时系统会进入完整转发阶段。它首先会调用
-(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
对象来处理。
- 完整转发的应用场景 完整转发在一些框架的适配和扩展中有重要应用。例如,当我们需要兼容旧版本的 API 或者对系统类进行功能扩展时,可以利用完整转发机制,将消息转发给自定义的实现类,实现功能的定制化。
六、消息传递中的 SEL 和 IMP
- SEL(选择子)
SEL
是一种数据类型,它用于唯一标识一个方法。在 Objective-C 中,@selector
表达式会返回一个SEL
对象。例如@selector(sayHello)
就是一个SEL
。SEL
对象在程序启动时就被注册到运行时系统中,不同类中相同名字的方法会对应同一个SEL
。
SEL
的优点是占用内存小,因为它本质上是一个指针,而且在比较两个 SEL
是否相等时,速度非常快,因为只需要比较指针的值。例如,我们可以这样比较两个 SEL
:
SEL sel1 = @selector(sayHello);
SEL sel2 = @selector(sayHello);
BOOL isEqual = sel1 == sel2;
- IMP(实现)
IMP
也是一种数据类型,它是一个指向方法实现的函数指针。当运行时系统找到方法的实现后,IMP
就指向这个具体的实现函数。例如,对于Person
类的sayHello
方法,其实现函数可能被IMP
指向如下:
void sayHelloIMP(id self, SEL _cmd) {
NSLog(@"Hello!");
}
在运行时,objc_msgSend
最终会通过 IMP
来调用实际的方法实现。IMP
与具体的类和方法实现紧密相关,不同类中相同名字的方法,其 IMP
可能不同。
七、消息传递机制的性能优化
-
缓存的利用 由于消息传递首先会在方法缓存中查找,我们应该尽量让常用的方法能够被缓存。在设计类的方法调用逻辑时,尽量让频繁调用的方法集中在少数几个选择子上。例如,在一个视图控制器中,如果某个视图的更新方法被频繁调用,可以将相关的逻辑封装在一个方法中,这样可以提高缓存命中率。
-
减少动态方法解析和转发 动态方法解析和转发过程相对复杂,会带来一定的性能开销。尽量在设计阶段就确定好类的方法,避免在运行时大量使用动态方法解析和转发。如果确实需要动态功能,可以在程序启动时进行预加载和初始化,将可能用到的动态方法提前添加好,减少运行时的动态操作。
八、与其他语言机制的对比
- 与 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
)来确定的。
- 与 Java 方法调用对比
Java 的方法调用在大部分情况下是静态绑定的,但对于被声明为
virtual
(Java 中默认所有非final
方法都是virtual
)的方法,会采用动态绑定。然而,Java 的动态绑定是基于类继承体系和虚函数表的,相对 Objective-C 的消息传递机制,灵活性稍逊一筹。例如,Java 中不能在运行时动态为类添加方法,而 Objective-C 可以通过动态方法解析来实现这一点。
九、消息传递机制在框架中的应用
- 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:
方法就是接收按钮点击消息的方法。
- 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 消息传递机制的独特之处和优势所在。在实际项目开发中,充分利用消息传递机制的特性,可以编写出更具扩展性和灵活性的高质量代码。