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

Objective-C运行时(Runtime)基础与应用场景

2023-10-254.7k 阅读

一、Objective-C 运行时简介

Objective-C 运行时(Runtime)是 Objective-C 语言的核心机制,它是一个基于 C 语言实现的动态运行时系统。这个运行时系统在程序运行期间动态地处理对象消息发送、动态方法解析、消息转发等操作,使得 Objective-C 语言具备了动态特性。

在编译阶段,Objective-C 代码会被编译成 C 语言代码,然后再由 C 编译器编译成机器码。而运行时系统则在程序运行时发挥作用,负责管理对象的生命周期、方法调用等关键操作。

二、Objective-C 运行时的基础结构

2.1 类(Class)结构

在运行时,类是一个非常重要的概念。类结构在 runtime.h 头文件中定义如下:

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 指针是每个对象和类的必备成员,它指向对象所属的类或者类的元类(meta - class)。在非 ARC 环境下,super_class 指向父类,name 是类的名称,instance_size 表示实例对象的大小,ivars 是实例变量列表,methodLists 是方法列表,cache 用于缓存方法调用以提高效率,protocols 是类所遵循的协议列表。

2.2 对象(Object)结构

对象本质上就是一个结构体,它的第一个成员就是 isa 指针,通过 isa 指针可以找到对象所属的类,进而获取类的相关信息。

typedef struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
} *id;

例如,我们定义一个简单的 Person 类:

@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 = [[Person alloc] init];
person.name = @"John";
person.age = 30;
[person sayHello];

在底层,person 就是一个包含 isa 指针的结构体,isa 指针指向 Person 类。

2.3 方法(Method)结构

方法在运行时也有对应的结构,定义如下:

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                               OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                    OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

method_name 是方法的选择器(SEL),它是一个唯一标识方法的字符串哈希值。method_types 描述了方法的参数和返回值类型。method_imp 是方法的实现,它是一个函数指针,指向具体实现方法功能的代码。

例如,对于 Person 类中的 sayHello 方法,method_name@selector(sayHello)method_types 描述了无参数和无返回值的类型,method_imp 指向 Person 类实现中 sayHello 方法的具体代码。

三、消息发送机制

3.1 动态绑定

Objective-C 的方法调用本质上是消息发送。当我们使用 [object message] 这样的语法调用方法时,在编译阶段,编译器并不会直接生成调用函数的代码,而是将其转化为 objc_msgSend 函数调用。

id objc_msgSend(id self, SEL op, ...);

objc_msgSend 函数的第一个参数是接收消息的对象,第二个参数是方法选择器,后面可变参数是方法的参数。在运行时,objc_msgSend 函数会根据对象的 isa 指针找到对应的类,然后在类的方法列表中查找与选择器匹配的方法。如果在当前类中没有找到,会沿着继承链向上查找,直到找到匹配的方法或者到达根类 NSObject

例如,对于上面的 Person 类调用 sayHello 方法:

[person sayHello];

实际上在运行时会被转化为:

objc_msgSend(person, @selector(sayHello));

这种动态绑定机制使得 Objective-C 可以在运行时根据对象的实际类型来决定调用哪个方法,实现了多态性。

3.2 缓存机制

为了提高消息发送的效率,运行时引入了缓存机制。每个类都有一个 cache,当一个方法被调用时,运行时首先会在缓存中查找是否有匹配的方法。如果缓存命中,直接调用缓存中的方法实现,避免了在方法列表中逐个查找的开销。

缓存的结构是一个哈希表,SEL 作为键,IMP 作为值。当一个方法被调用且在缓存中未找到时,运行时会将该方法的 SELIMP 插入到缓存中,以便下次快速查找。

四、动态方法解析

4.1 实例方法动态解析

在消息发送过程中,如果在类的方法列表和缓存中都没有找到匹配的方法,运行时会启动动态方法解析机制。对于实例方法,运行时会调用类的 + (BOOL)resolveInstanceMethod:(SEL)sel 方法。

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

void runFunction(id self, SEL _cmd) {
    NSLog(@"%@ is running", [(Person *)self name]);
}
@end

在上述代码中,如果调用 [person run] 方法,而 Person 类中原本没有 run 方法,运行时会调用 resolveInstanceMethod: 方法。我们在这个方法中通过 class_addMethod 动态添加了 run 方法的实现,这样就可以成功调用 run 方法了。

4.2 类方法动态解析

对于类方法,运行时会调用类的 + (BOOL)resolveClassMethod:(SEL)sel 方法。

@implementation Person
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(printClassName)) {
        class_addMethod(object_getClass(self), sel, (IMP)printClassNameFunction, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

void printClassNameFunction(id self, SEL _cmd) {
    NSLog(@"The class name is %@", NSStringFromClass(object_getClass(self)));
}
@end

当调用 [Person printClassName] 这样的类方法且类中没有定义该方法时,运行时会触发 resolveClassMethod: 方法,我们可以在这个方法中动态添加类方法的实现。

五、消息转发

5.1 备用接收者(Fast Forwarding)

如果动态方法解析没有找到合适的方法,运行时会进入消息转发阶段。首先是备用接收者阶段,运行时会调用 -(id)forwardingTargetForSelector:(SEL)aSelector 方法,尝试寻找一个备用的对象来处理该消息。

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

@interface Food : NSObject
- (void)eat;
@end

@implementation Food
- (void)eat {
    NSLog(@"Eating delicious food.");
}
@end

在上述代码中,如果 Person 类收到 eat 方法的消息且自身没有实现该方法,会调用 forwardingTargetForSelector: 方法,返回一个 Food 对象来处理 eat 消息。

5.2 完整转发(Normal Forwarding)

如果备用接收者阶段没有找到合适的对象,运行时会进入完整转发阶段。首先会调用 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法,要求返回一个方法签名,描述方法的参数和返回值类型。然后会调用 -(void)forwardInvocation:(NSInvocation *)anInvocation 方法,在这里可以对消息进行处理,比如转发给其他对象。

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

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

@interface Dog : NSObject
- (void)jump;
@end

@implementation Dog
- (void)jump {
    NSLog(@"Dog is jumping.");
}
@end

在上述代码中,Person 类收到 jump 方法的消息且自身未实现,会先调用 methodSignatureForSelector: 方法返回方法签名,然后在 forwardInvocation: 方法中将消息转发给 Dog 对象来处理。

六、运行时的应用场景

6.1 关联对象(Associated Objects)

运行时提供了关联对象的功能,允许我们为对象动态添加属性。这在一些框架扩展或者需要为已有类添加临时属性的场景中非常有用。

#import <objc/runtime.h>

@interface UIButton (Custom)
@property (nonatomic, copy) NSString *customTitle;
@end

@implementation UIButton (Custom)
static char kCustomTitleKey;

- (void)setCustomTitle:(NSString *)customTitle {
    objc_setAssociatedObject(self, &kCustomTitleKey, customTitle, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)customTitle {
    return objc_getAssociatedObject(self, &kCustomTitleKey);
}
@end

在上述代码中,我们为 UIButton 类通过运行时关联对象的方式添加了一个 customTitle 属性,在运行时可以像使用普通属性一样使用它。

6.2 实现 KVO(Key - Value Observing)

KVO 是一种基于观察者模式的机制,用于监听对象属性的变化。运行时在 KVO 的实现中起到了关键作用。当一个对象的属性被观察时,运行时会动态生成一个子类,并重写被观察属性的 setter 方法,在 setter 方法中通知观察者属性的变化。

@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person
@end

Person *person = [[Person alloc] init];
[person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
person.age = 25;

在上述代码中,addObserver:forKeyPath:options:context: 方法利用运行时机制为 Person 类动态生成子类并设置相关监听逻辑,当 age 属性值改变时,会通知观察者。

6.3 实现 KVC(Key - Value Coding)

KVC 是一种通过键值对来访问对象属性的机制。运行时在 KVC 的实现中负责查找对象的属性,无论是直接属性还是通过访问器方法访问的属性。当使用 valueForKey: 方法获取属性值时,运行时会按照一定的顺序查找属性,包括实例变量、访问器方法等。

Person *person = [[Person alloc] init];
person.name = @"Alice";
NSString *name = [person valueForKey:@"name"];

在上述代码中,valueForKey:@"name" 方法利用运行时机制查找 Person 类中 name 属性对应的实例变量或者访问器方法来获取属性值。

6.4 方法交换(Method Swizzling)

方法交换是运行时非常强大的一个应用场景。它允许我们在运行时交换两个方法的实现。这在很多场景下都非常有用,比如在不修改原有类代码的情况下为其添加功能。

#import <objc/runtime.h>

@interface UIViewController (LogLifeCycle)
@end

@implementation UIViewController (LogLifeCycle)
+ (void)load {
    Method originalMethod = class_getInstanceMethod(self, @selector(viewDidLoad));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(xx_viewDidLoad));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)xx_viewDidLoad {
    [self xx_viewDidLoad];
    NSLog(@"%@ viewDidLoad", self.class);
}
@end

在上述代码中,我们在 UIViewController 的分类中通过 method_exchangeImplementations 方法交换了 viewDidLoad 方法和我们自定义的 xx_viewDidLoad 方法的实现,这样每次 UIViewController 及其子类调用 viewDidLoad 方法时,都会先执行原 viewDidLoad 方法的功能,然后打印日志。

6.5 实现 AOP(Aspect - Oriented Programming)

AOP 是一种编程范式,旨在将横切关注点(如日志记录、性能监控等)与业务逻辑分离。通过运行时的方法交换、消息转发等机制,可以方便地实现 AOP。例如,我们可以在不修改业务类代码的情况下,为业务类的方法添加日志记录功能。

@interface BusinessClass : NSObject
- (void)businessMethod;
@end

@implementation BusinessClass
- (void)businessMethod {
    NSLog(@"Doing business logic.");
}
@end

// 利用运行时为 BusinessClass 的 businessMethod 方法添加日志记录
@implementation BusinessClass (LogAspect)
+ (void)load {
    Method originalMethod = class_getInstanceMethod(self, @selector(businessMethod));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(xx_businessMethod));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)xx_businessMethod {
    NSLog(@"Before businessMethod");
    [self xx_businessMethod];
    NSLog(@"After businessMethod");
}
@end

在上述代码中,通过方法交换为 BusinessClassbusinessMethod 方法添加了日志记录的横切逻辑,实现了 AOP 的效果。

七、总结运行时操作的注意事项

在使用运行时进行各种操作时,需要注意一些事项。首先,运行时操作通常是对底层结构的直接操作,错误的使用可能会导致程序崩溃或者未定义行为。例如,在动态添加方法时,确保方法签名的正确性,否则可能在调用时出现参数传递错误。

其次,在进行方法交换时,要注意交换的顺序和范围。避免在多个分类中同时对同一个方法进行交换,以免造成混乱。同时,要确保在合适的时机进行方法交换,通常在 +load 方法中进行是比较安全的选择,因为 +load 方法在类加载时就会被调用,且只调用一次。

另外,关联对象的使用要注意内存管理。根据关联策略的不同,要确保对象的生命周期管理正确,避免内存泄漏或者悬空指针的问题。

总之,Objective - C 运行时是一个强大但也较为复杂的机制,深入理解并谨慎使用它,可以为我们的程序开发带来很多便利和灵活性。