Objective-C 的消息机制是一种动态运行时特性,它允许开发者在运行时动态地添加、修改或删除方法,并且可以通过消息转发机制来处理一些未实现的方法。Objective-C 的消息机制提供了很多便利,使得开发者可以更加灵活地编写代码。它的调用过程如下:
在编译时,编译器会将方法调用转换成对一个函数或消息的调用。
在运行时,Objective-C runtime 系统会查找该消息对应的方法实现。
如果在当前对象的方法列表中找到该方法,则直接跳到方法实现的地址开始执行。
如果在当前对象的方法列表中没有找到该方法,则在该对象所属的类的方法列表中查找。
如果在当前对象所属的类中找到了该方法,则直接跳到方法实现的地址开始执行。
如果在当前对象所属的类中没有找到该方法,则在该类的父类中查找。
如果在某个父类中找到了该方法,则直接跳到方法实现的地址开始执行。
如果一直找到了根类仍然没有找到该方法,则会触发 doesNotRecognizeSelector: 方法。
Objective-C 消息机制的优点是可以在运行时动态地改变对象的行为,使得程序更加灵活,也方便了一些运行时的技巧,比如 Method Swizzling、KVO 等。但是,使用消息机制也可能会导致一些性能问题,比如动态绑定需要更多的时间和空间,同时也会使得编译器优化失效。
消息机制的具体应用
Objective-C 的消息机制是一种动态运行时特性,它允许开发者在运行时动态地添加、修改或删除方法,并且可以通过消息转发机制来处理一些未实现的方法。Objective-C 的消息机制提供了很多便利,使得开发者可以更加灵活地编写代码。
以下是 Objective-C 消息机制的一些用途:
动态类型:Objective-C 是一门动态类型语言,它允许对象在运行时改变类型,开发者可以利用消息机制来实现这一特性。
动态添加方法:在运行时,开发者可以动态地添加方法,以便满足一些动态需求,如动态生成方法、动态替换方法等。
反射:开发者可以使用 Objective-C 消息机制来实现反射机制,例如获取某个类的所有方法列表、获取某个方法的参数和返回值等。
方法交换:开发者可以通过 Method Swizzling 来交换两个方法的实现,以达到一些特殊的需求,如记录方法执行时间、跟踪方法调用等。
消息转发:当一个对象接收到一个未实现的消息时,Objective-C 运行时提供了一套完整的消息转发机制,开发者可以在这个过程中进行一些自定义的处理。
KVO实现: KVO(Key-Value Observing)是一种用于监听对象属性变化的机制。在 iOS/macOS 中,KVO 是通过 Objective-C 消息机制来实现的
动态类型
Objective-C 中有一个特殊的数据类型 id,它被称为动态类型。id 可以存储任何类型的对象,类似于 C++ 中的 void* 或 Java 中的 Object。在编译时,编译器不会对 id 进行类型检查,只有在运行时才会根据对象的实际类型进行方法调用和数据操作。
使用 id 可以使代码更加灵活和动态。比如,可以使用 id 来实现动态绑定,也就是在运行时根据对象的实际类型动态调用方法。另外,Objective-C 中的一些动态特性,如消息机制、运行时类型识别(Runtime Type Identification,RTTI)等,都是基于 id 实现的。
需要注意的是,由于 id 不进行类型检查,因此在使用时需要格外小心,避免出现类型不匹配的错误。
动态添加方法
在 Objective-C 中,可以在运行时动态添加方法。使用 Runtime 提供的函数 class_addMethod 即可实现。具体步骤如下:
定义要添加的方法的实现。该方法必须遵循一定的格式,包括方法名、返回值类型、参数类型和具体实现代码。
获取要添加方法的类的对象。
使用 class_addMethod 函数为该类添加方法。
4.该函数接受四个参数:要添加方法的类的对象、方法名、方法实现、方法类型。测试刚刚添加的方法是否成功,可以通过 performSelector: 方法或者消息转发机制来调用该方法。
下面是一个示例代码,实现了在 Person 类中动态添加一个名为 run 的方法:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface Person : NSObject
@end
@implementation Person
@end
void run(id self, SEL _cmd) {
NSLog(@"%@ is running", self);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 获取 Person 类
Class personClass = [Person class];
// 添加 run 方法
SEL runSelector = @selector(run);
Method runMethod = class_getInstanceMethod(personClass, runSelector);
if (!runMethod) {
class_addMethod(personClass, runSelector, (IMP)run, "v@:");
}
// 调用 run 方法
id person = [[Person alloc] init];
[person performSelector:runSelector];
}
return 0;
}
在上面的示例代码中,我们先定义了一个名为 run 的方法,然后通过 class_addMethod 函数将该方法添加到了 Person 类中。最后,我们创建了一个 Person 类的实例对象,然后通过 performSelector: 方法调用了刚刚添加的 run 方法。输出结果如下:
<Person> is running
反射
在 Objective-C 中,反射指的是使用 Runtime 提供的一系列函数,来获取类的信息、调用方法、获取变量等等操作。通过反射,我们可以在运行时动态地获取类的信息,并且根据获取的信息来执行相应的操作。
下面是一些常用的反射操作:
获取类的信息:可以使用 objc_getClass 函数获取类对象,然后使用 class_copyMethodList 函数获取类的方法列表,使用 class_copyIvarList 函数获取类的实例变量列表,使用 class_copyPropertyList 函数获取类的属性列表,使用 class_copyProtocolList 函数获取类实现的协议列表等等。
动态调用方法:可以使用 objc_msgSend 函数调用方法。需要注意的是,objc_msgSend 函数的第一个参数是消息接收者,第二个参数是方法选择器,之后的参数是方法的参数列表。如果方法的返回值为对象类型,则需要进行类型转换。
动态添加方法:可以使用 class_addMethod 函数在运行时动态地为类添加方法。需要注意的是,新添加的方法的实现必须是一个 C 函数或者一个 Objective-C 方法,不能是一个 block。
下面是一个示例代码,展示了如何使用反射获取类的信息、动态调用方法和动态添加方法:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)eat:(NSString *)food;
@end
@implementation Person
- (void)eat:(NSString *)food {
NSLog(@"%@ is eating %@", self.name, food);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 获取 Person 类的信息
Class personClass = objc_getClass("Person");
Method eatMethod = class_getInstanceMethod(personClass, @selector(eat:));
Ivar nameIvar = class_getInstanceVariable(personClass, "_name");
objc_property_t nameProperty = class_getProperty(personClass, "name");
// 动态调用 eat 方法
Person *person = [[Person alloc] init];
[person setValue:@"Tom" forKey:@"name"];
((void (*)(id, SEL, NSString *))objc_msgSend)(person, @selector(eat:), @"apple");
// 动态添加 run 方法
IMP runImp = imp_implementationWithBlock(^{
NSLog(@"%@ is running", person.name);
});
class_addMethod(personClass, @selector(run), runImp, "v@:");
// 调用 run 方法
[person performSelector:@selector(run)];
}
return 0;
}
在上面的示例代码中,我们先使用 objc_getClass 函数获取了 Person 类的信息,然后通过 class_getInstanceMethod 函数获取了 eat 方法的信息,通过 class_getInstanceVariable 函数获取了 _name 实例变量的信息,通过 class_getProperty 函数获取了 name 属性的信息。接下来,我们使用 objc_msgSend 函数动态调用了 eat 方法,并使用 class_addMethod 函数动态添加了 run 方法的
方法交换
在 Objective-C 中,可以通过方法交换来修改已有的方法实现。方法交换可以让我们在不改变已有代码的前提下,修改方法的行为。方法交换使用 Runtime 提供的 method_exchangeImplementations 等函数 函数来实现。
方法交换的步骤如下:
获取要交换的方法和新方法的实现。
使用 method_exchangeImplementations 函数交换方法的实现。
下面是一个示例代码,实现了交换 Person 类中的 eat 方法和 sleep 方法:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface Person : NSObject
- (void)eat;
- (void)sleep;
@end
@implementation Person
- (void)eat {
NSLog(@"I'm eating...");
}
- (void)sleep {
NSLog(@"I'm sleeping...");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 获取 eat 和 sleep 方法
Method eatMethod = class_getInstanceMethod([Person class], @selector(eat));
Method sleepMethod = class_getInstanceMethod([Person class], @selector(sleep));
// 交换 eat 和 sleep 方法
method_exchangeImplementations(eatMethod, sleepMethod);
// 调用 eat 和 sleep 方法
Person *person = [[Person alloc] init];
[person eat];
[person sleep];
}
return 0;
}
在上面的示例代码中,我们先获取了 Person 类中的 eat 方法和 sleep 方法,然后通过 method_exchangeImplementations 函数交换了这两个方法的实现。最后,我们创建了一个 Person 类的实例对象,并调用了 eat 方法和 sleep 方法。输出结果如下:
I'm sleeping...
I'm eating...
从输出结果可以看出,方法交换成功地修改了 eat 方法和 sleep 方法的行为。在实际开发中,方法交换常常用于修改已有框架或库的行为,以满足特定的需求。但是需要谨慎使用,因为方法交换可能会对程序的稳定性和可维护性造成一定的影响。
消息转发
Objective-C 中的消息转发机制是指当对象接收到一个未实现的消息时,它有机会在运行时动态处理这个消息,而不是直接抛出一个异常或崩溃。
Objective-C 的消息转发机制分为三个阶段:
动态方法解析阶段:当对象接收到一个未实现的消息时,Objective-C 运行时会首先调用 resolveInstanceMethod: 或 resolveClassMethod: 方法,让开发者有机会动态添加方法实现。如果这个方法被实现了,则消息将被传递到这个新添加的方法实现。
快速消息转发阶段:如果第一阶段无法处理这个未实现的消息,则会进入快速消息转发阶段。在这个阶段中,Objective-C 运行时会调用 forwardingTargetForSelector: 方法,让开发者有机会将这个消息转发给另一个对象来处理。如果该方法返回一个非 nil 对象,则消息将被传递给这个对象,如果该方法返回 nil,则进入下一阶段。
完整消息转发阶段:如果前两个阶段都无法处理这个未实现的消息,则会进入完整消息转发阶段。在这个阶段中,Objective-C 运行时会调用 methodSignatureForSelector: 方法和 forwardInvocation: 方法。在完整消息转发阶段中,开发者可以在 forwardInvocation: 方法中自己实现消息转发的逻辑,如将消息转发到另一个对象或者动态生成方法实现。
- methodSignatureForSelector: 方法会返回一个方法签名,描述这个未实现的方法的参数和返回值类型。如果该方法返回 nil,则消息转发失败。
- forwardInvocation: 方法会创建一个 NSInvocation 对象,包含了原始消息的方法选择器和参数,开发者可以使用这个对象来调用另一个方法实现或者生成一个异常。
通过消息转发机制,Objective-C 提供了一种灵活的机制来动态处理未实现的消息,方便开发者实现一些高级的运行时特性。
下面是一个简单的示例代码,演示了如何自定义消息转发:
@interface MyClass : NSObject
@end
@implementation MyClass
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
if (sel == @selector(undefinedMethod)) {
// 在这里可以自定义消息转发的处理逻辑
NSLog(@"Undefined method is called!");
} else {
[super forwardInvocation:anInvocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(undefinedMethod)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
} else {
return [super methodSignatureForSelector:aSelector];
}
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass *obj = [[MyClass alloc] init];
[obj undefinedMethod]; // 会被转发到 forwardInvocation: 方法中
}
return 0;
}
在这个示例中,MyClass 对象定义了一个未实现的方法 undefinedMethod,当这个方法被调用时,Objective-C 运行时会先尝试动态解析这个方法,如果失败,则进入 Fast Forwarding 和 Normal Forwarding 过程,最终将消息转发到 forwardInvocation: 方法中。在这个方法中,我们可以自定义消息转发的处理逻辑,比如输出一些信息。
KVO
当一个对象的属性发生变化时,KVO 机制会自动触发对象的 willChangeValueForKey: 和 didChangeValueForKey: 方法,并在其中发送一个消息给观察者,通知它们属性的变化。