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

深入解析Objective-C运行时机制:基础概念与核心原理

2021-10-294.6k 阅读

一、Objective-C 运行时简介

Objective-C 运行时(Objective-C Runtime)是一个基于 C 语言的库,它提供了一种在运行时动态加载和链接代码的机制。这意味着在编译阶段,编译器并不会完全确定对象的类型和方法调用,而是在运行时根据对象的实际类型来确定如何执行方法。这种动态特性是 Objective-C 区别于其他静态语言(如 C++)的重要特征之一。

1.1 动态性基础

Objective-C 的动态性体现在多个方面。例如,在编译时,对象的类型声明并不决定其实际运行时的类型。我们可以声明一个 id 类型的变量,它可以指向任何 Objective-C 对象。

id obj;
obj = [[NSObject alloc] init];

在这段代码中,obj 变量的类型为 id,在编译时,编译器只知道它可以指向任何对象,但在运行时,obj 实际指向了 NSObject 类的实例。

1.2 运行时库的作用

Objective-C 运行时库负责管理对象的内存布局、方法调用、动态类型识别等核心功能。它就像是 Objective-C 程序的幕后大脑,在程序运行过程中默默地处理各种复杂的任务。例如,当我们向一个对象发送消息时,运行时会查找对象所属类的方法列表,找到对应的方法实现并执行。

二、Objective-C 运行时的基础概念

2.1 对象与类

在 Objective-C 中,对象是类的实例。类定义了对象的属性和行为。从运行时的角度来看,每个对象在内存中都有一个 isa 指针,这个指针指向该对象所属的类。

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)sayHello;
@end

@implementation Person
- (void)sayHello {
    NSLog(@"Hello, my name is %@, and I'm %ld years old.", self.name, (long)self.age);
}
@end

Person *person = [[Person alloc] init];
person.name = @"John";
person.age = 30;
[person sayHello];

在上述代码中,personPerson 类的一个对象。通过 isa 指针,运行时可以确定 personPerson 类的实例,从而能够找到 Person 类中定义的 sayHello 方法并执行。

2.2 类的结构

在运行时,类被表示为一个 objc_class 结构体。这个结构体包含了类的元数据,如类名、超类、属性列表、方法列表等。

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    #if!__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
    #endif
} OBJC2_UNAVAILABLE;

isa 指针指向类自身,super_class 指向该类的超类。methodLists 包含了类中定义的方法列表。通过这些信息,运行时可以对类进行各种操作,如方法查找。

2.3 元类(Meta Class)

元类是 Objective-C 运行时中的一个重要概念。每个类都有一个与之对应的元类。元类存储了类方法(以 + 开头的方法)的列表。类对象的 isa 指针指向类本身,而类的 isa 指针则指向它的元类。元类的 isa 指针指向根元类,根元类的 isa 指针指向自身。这种结构形成了一个闭环,确保了运行时能够正确地处理类方法的调用。

@interface Animal : NSObject
+ (void)showInfo;
@end

@implementation Animal
+ (void)showInfo {
    NSLog(@"This is an animal.");
}
@end

[Animal showInfo];

在上述代码中,showInfo 是一个类方法。运行时通过 Animal 类的元类找到 showInfo 方法的实现并执行。

三、消息发送机制

3.1 消息发送的过程

当我们在 Objective-C 中向一个对象发送消息时,例如 [object message],实际上发生了一系列复杂的步骤。首先,运行时会根据对象的 isa 指针找到对象所属的类。然后,在类的方法列表中查找与消息对应的方法实现。如果在当前类中没有找到,会沿着继承链向超类查找,直到找到方法实现或者到达根类。

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

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

@interface Circle : Shape
@end

@implementation Circle
@end

Circle *circle = [[Circle alloc] init];
[circle draw];

在上述代码中,当向 circle 对象发送 draw 消息时,运行时首先根据 circleisa 指针找到 Circle 类。由于 Circle 类中没有 draw 方法,运行时会沿着继承链找到 Shape 类,并在 Shape 类中找到 draw 方法的实现并执行。

3.2 动态方法解析

如果在方法列表中没有找到对应的方法实现,运行时会进入动态方法解析阶段。在这个阶段,运行时会尝试动态地添加方法实现。有两种类型的动态方法解析:实例方法解析和类方法解析。

@interface DynamicClass : NSObject
- (void)dynamicMethod;
@end

@implementation DynamicClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod(self, sel, (IMP)dynamicMethodImplementation, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void dynamicMethodImplementation(id self, SEL _cmd) {
    NSLog(@"Dynamic method implemented.");
}
@end

DynamicClass *dynamicObj = [[DynamicClass alloc] init];
[dynamicObj dynamicMethod];

在上述代码中,DynamicClass 类没有 dynamicMethod 方法的实现。当向 dynamicObj 发送 dynamicMethod 消息时,运行时会调用 resolveInstanceMethod: 方法。在这个方法中,我们动态地添加了 dynamicMethod 的实现,从而使消息能够正确处理。

3.3 备用接收者

如果动态方法解析没有成功,运行时会进入备用接收者阶段。在这个阶段,运行时会询问对象是否有其他对象可以处理该消息。

@interface Receiver1 : NSObject
- (void)handleMessage;
@end

@implementation Receiver1
- (void)handleMessage {
    NSLog(@"Receiver1 handles the message.");
}
@end

@interface Receiver2 : NSObject
@property (nonatomic, strong) Receiver1 *receiver;
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(handleMessage)) {
        return self.receiver;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

@implementation Receiver2
@end

Receiver2 *receiver2 = [[Receiver2 alloc] init];
receiver2.receiver = [[Receiver1 alloc] init];
[receiver2 handleMessage];

在上述代码中,Receiver2 类本身没有 handleMessage 方法。当向 receiver2 发送 handleMessage 消息时,运行时会调用 forwardingTargetForSelector: 方法。在这个方法中,我们返回了 Receiver1 的实例,使得 Receiver1 能够处理该消息。

3.4 完整的消息转发

如果备用接收者也没有找到,运行时会进入完整的消息转发阶段。在这个阶段,运行时会创建一个 NSInvocation 对象,该对象包含了消息的所有信息,如选择器、参数等。然后,运行时会调用 forwardInvocation: 方法,在这个方法中,我们可以手动处理消息转发。

@interface ForwardingClass : NSObject
@end

@implementation ForwardingClass
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    if ([OtherClass instancesRespondToSelector:sel]) {
        OtherClass *otherObj = [[OtherClass alloc] init];
        [anInvocation invokeWithTarget:otherObj];
    }
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [OtherClass instanceMethodSignatureForSelector:aSelector];
    return signature;
}
@end

@interface OtherClass : NSObject
- (void)forwardedMethod;
@end

@implementation OtherClass
- (void)forwardedMethod {
    NSLog(@"Method forwarded to OtherClass.");
}
@end

ForwardingClass *forwardingObj = [[ForwardingClass alloc] init];
[forwardingObj forwardedMethod];

在上述代码中,ForwardingClass 类没有 forwardedMethod 方法。当向 forwardingObj 发送 forwardedMethod 消息时,运行时会调用 methodSignatureForSelector: 方法获取方法签名,然后调用 forwardInvocation: 方法。在 forwardInvocation: 方法中,我们将消息转发给了 OtherClass 的实例。

四、属性与实例变量

4.1 实例变量

实例变量是类中定义的成员变量,它们存储了对象的状态。在运行时,实例变量按照声明的顺序依次存储在对象的内存空间中。

@interface Book : NSObject {
    NSString *title;
    NSInteger pageCount;
}
@end

@implementation Book
@end

在上述代码中,Book 类定义了两个实例变量 titlepageCount。当创建 Book 类的实例时,这两个实例变量会按照顺序存储在对象的内存空间中。

4.2 属性

属性是 Objective-C 中用于封装实例变量的一种机制。属性提供了访问器方法(getter 和 setter),使得我们可以方便地访问和修改实例变量。

@interface Car : NSObject
@property (nonatomic, copy) NSString *model;
@property (nonatomic, assign) NSInteger year;
@end

@implementation Car
@end

Car *car = [[Car alloc] init];
car.model = @"Tesla Model S";
car.year = 2023;
NSLog(@"Car model: %@, year: %ld", car.model, (long)car.year);

在上述代码中,Car 类定义了两个属性 modelyear。通过属性,我们可以方便地设置和获取实例变量的值。在运行时,属性的访问器方法是通过运行时库动态生成的,具体的实现依赖于属性的修饰符,如 nonatomiccopy 等。

4.3 属性与实例变量的关系

属性通常会与实例变量关联。默认情况下,编译器会自动为属性生成对应的实例变量,实例变量的名称通常是下划线加上属性名。例如,对于属性 name,编译器会生成一个名为 _name 的实例变量。我们也可以手动指定与属性关联的实例变量。

@interface Student : NSObject {
    NSString *studentName;
}
@property (nonatomic, copy) NSString *name;
@end

@implementation Student
- (NSString *)name {
    return studentName;
}
- (void)setName:(NSString *)newName {
    studentName = [newName copy];
}
@end

在上述代码中,我们手动定义了一个实例变量 studentName,并在属性的访问器方法中使用它。通过这种方式,我们可以更灵活地控制属性与实例变量之间的关系。

五、关联对象

5.1 关联对象的概念

关联对象是 Objective-C 运行时提供的一种机制,它允许我们在运行时为对象动态添加属性。这些属性并不会在类的定义中声明,而是在运行时通过关联的方式附加到对象上。

5.2 关联对象的使用

我们可以使用 objc_setAssociatedObjectobjc_getAssociatedObject 函数来设置和获取关联对象。

#import <objc/runtime.h>

@interface CustomView : UIView
@end

@implementation CustomView
@end

CustomView *view = [[CustomView alloc] init];
NSString *key = @"customKey";
NSString *value = @"Custom value";
objc_setAssociatedObject(view, (__bridge const void *)(key), value, OBJC_ASSOCIATION_COPY_NONATOMIC);
NSString *retrievedValue = (__bridge NSString *)objc_getAssociatedObject(view, (__bridge const void *)(key));
NSLog(@"Retrieved value: %@", retrievedValue);

在上述代码中,我们为 CustomView 对象添加了一个关联对象。通过指定一个唯一的键 customKey,我们可以设置和获取关联的值。OBJC_ASSOCIATION_COPY_NONATOMIC 表示关联对象的存储策略,这里采用了非原子性的拷贝策略。

5.3 关联对象的应用场景

关联对象在很多场景下都非常有用。例如,当我们需要为系统类(如 UIView)添加一些自定义的属性,但又不想继承该类时,就可以使用关联对象。另外,在一些框架开发中,关联对象可以用于实现一些临时性的属性附加,而不影响类的原有结构。

六、运行时的应用场景

6.1 实现 KVO(Key - Value Observing)

KVO 是一种基于观察者模式的机制,它允许我们监听对象属性的变化。运行时在 KVO 的实现中起到了关键作用。当一个对象的属性发生变化时,运行时会通过动态生成的方法来通知观察者。

@interface User : NSObject
@property (nonatomic, copy) NSString *username;
@end

@implementation User
@end

User *user = [[User alloc] init];
[user addObserver:self forKeyPath:@"username" options:NSKeyValueObservingOptionNew context:nil];
user.username = @"newUsername";

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"username"]) {
        NSLog(@"Username changed to: %@", change[NSKeyValueChangeNewKey]);
    }
}

在上述代码中,我们为 User 类的 username 属性添加了观察者。当 username 属性的值发生变化时,运行时会调用 observeValueForKeyPath:ofObject:change:context: 方法,从而实现了属性变化的监听。

6.2 实现 AOP(Aspect - Oriented Programming)

AOP 是一种编程范式,它允许我们在不修改原有代码的基础上,为程序添加一些横切关注点,如日志记录、性能监测等。通过运行时的方法交换机制,我们可以实现 AOP。

#import <objc/runtime.h>

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

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

void newMethod(id self, SEL _cmd) {
    NSLog(@"Before original method");
    ((void (*)(id, SEL))objc_msgSend)(self, @selector(originalMethod));
    NSLog(@"After original method");
}

@implementation MyClass
+ (void)load {
    Method originalMethod = class_getInstanceMethod(self, @selector(originalMethod));
    Method newMethodImp = class_getInstanceMethod(self, @selector(newMethod));
    method_exchangeImplementations(originalMethod, newMethodImp);
}
@end

MyClass *obj = [[MyClass alloc] init];
[obj originalMethod];

在上述代码中,我们通过 method_exchangeImplementations 方法交换了 originalMethodnewMethod 的实现。这样,当调用 originalMethod 时,实际上会先执行 newMethod 中的前置逻辑,然后执行原 originalMethod 的逻辑,最后执行后置逻辑,从而实现了 AOP。

6.3 动态加载与模块化

Objective-C 运行时的动态特性使得我们可以在运行时动态加载代码模块。这在应用开发中非常有用,例如,我们可以根据用户的需求动态加载一些功能模块,而不是在启动时就加载所有可能用不到的模块,从而提高应用的启动速度和性能。

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

void loadModule() {
    void *handle = dlopen("/path/to/module.dylib", RTLD_LAZY);
    if (handle) {
        Class moduleClass = objc_getClass("ModuleClass");
        id moduleInstance = [[moduleClass alloc] init];
        [moduleInstance performSelector:@selector(moduleMethod)];
        dlclose(handle);
    } else {
        NSLog(@"Failed to load module: %s", dlerror());
    }
}

在上述代码中,我们使用 dlopen 函数动态加载了一个动态链接库 module.dylib。然后通过运行时获取库中的类和方法,并执行相应的操作。这种动态加载机制为应用的模块化开发提供了强大的支持。