Objective-C 的消息机制

Objective-C 的消息机制是一种动态运行时特性,它允许开发者在运行时动态地添加、修改或删除方法,并且可以通过消息转发机制来处理一些未实现的方法。Objective-C 的消息机制提供了很多便利,使得开发者可以更加灵活地编写代码。它的调用过程如下:

  1. 在编译时,编译器会将方法调用转换成对一个函数或消息的调用。

  2. 在运行时,Objective-C runtime 系统会查找该消息对应的方法实现。

  3. 如果在当前对象的方法列表中找到该方法,则直接跳到方法实现的地址开始执行。

  4. 如果在当前对象的方法列表中没有找到该方法,则在该对象所属的类的方法列表中查找。

  5. 如果在当前对象所属的类中找到了该方法,则直接跳到方法实现的地址开始执行。

  6. 如果在当前对象所属的类中没有找到该方法,则在该类的父类中查找。

  7. 如果在某个父类中找到了该方法,则直接跳到方法实现的地址开始执行。

  8. 如果一直找到了根类仍然没有找到该方法,则会触发 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 即可实现。具体步骤如下:

  1. 定义要添加的方法的实现。该方法必须遵循一定的格式,包括方法名、返回值类型、参数类型和具体实现代码。

  2. 获取要添加方法的类的对象。

  3. 使用 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 提供的一系列函数,来获取类的信息、调用方法、获取变量等等操作。通过反射,我们可以在运行时动态地获取类的信息,并且根据获取的信息来执行相应的操作。

下面是一些常用的反射操作:

  1. 获取类的信息:可以使用 objc_getClass 函数获取类对象,然后使用 class_copyMethodList 函数获取类的方法列表,使用 class_copyIvarList 函数获取类的实例变量列表,使用 class_copyPropertyList 函数获取类的属性列表,使用 class_copyProtocolList 函数获取类实现的协议列表等等。

  2. 动态调用方法:可以使用 objc_msgSend 函数调用方法。需要注意的是,objc_msgSend 函数的第一个参数是消息接收者,第二个参数是方法选择器,之后的参数是方法的参数列表。如果方法的返回值为对象类型,则需要进行类型转换。

  3. 动态添加方法:可以使用 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 等函数 函数来实现。

方法交换的步骤如下:

  1. 获取要交换的方法和新方法的实现。

  2. 使用 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 的消息转发机制分为三个阶段:

  1. 动态方法解析阶段:当对象接收到一个未实现的消息时,Objective-C 运行时会首先调用 resolveInstanceMethod: 或 resolveClassMethod: 方法,让开发者有机会动态添加方法实现。如果这个方法被实现了,则消息将被传递到这个新添加的方法实现。

  2. 快速消息转发阶段:如果第一阶段无法处理这个未实现的消息,则会进入快速消息转发阶段。在这个阶段中,Objective-C 运行时会调用 forwardingTargetForSelector: 方法,让开发者有机会将这个消息转发给另一个对象来处理。如果该方法返回一个非 nil 对象,则消息将被传递给这个对象,如果该方法返回 nil,则进入下一阶段。

  3. 完整消息转发阶段:如果前两个阶段都无法处理这个未实现的消息,则会进入完整消息转发阶段。在这个阶段中,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: 方法,并在其中发送一个消息给观察者,通知它们属性的变化。

KVO和KVC

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容