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

Objective-C 运行时(Runtime)机制探秘

2021-06-127.1k 阅读

Objective-C 运行时(Runtime)机制探秘

Objective-C 作为一门面向对象的编程语言,其运行时(Runtime)机制是其强大功能和灵活性的核心所在。Runtime 提供了一种动态的消息传递机制,允许在运行时决定对象如何响应消息,这种动态性为开发者带来了巨大的便利,同时也使得代码具有更高的灵活性和扩展性。下面我们将深入探讨 Objective-C 的运行时机制。

一、Runtime 的基础概念

  1. 消息传递(Messaging) 在 Objective-C 中,向对象发送消息使用 [对象 方法] 的语法。例如:
NSString *str = @"Hello, Runtime!";
NSLog(@"%@", [str length]);

这里 [str length] 就是向 str 对象发送 length 消息。在编译阶段,编译器并不会直接调用 length 方法,而是将这个消息发送过程转化为一个 objc_msgSend 函数调用。objc_msgSend 函数是运行时系统的核心函数之一,它接收两个主要参数:接收消息的对象和选择器(Selector)。选择器是一个表示方法的唯一标识符,它在编译时就已经确定。

  1. 类和对象 在运行时,类(Class)和对象(Object)有着明确的结构。一个对象本质上是一个结构体,其第一个成员是一个指向其类的指针,这个指针被称为 isa 指针。通过 isa 指针,对象可以找到它所属的类,从而获取类的元数据,包括类的属性、方法列表等。

类也是一个结构体,它包含了类的名称、父类指针、实例变量列表、方法列表、协议列表等信息。例如,我们可以通过以下代码获取一个类的元数据:

#import <objc/runtime.h>

@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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class personClass = [Person class];
        // 获取类的名称
        const char *className = class_getName(personClass);
        NSLog(@"Class name: %s", className);
        
        // 获取实例变量列表
        Ivar *ivars;
        unsigned int ivarCount;
        ivars = class_copyIvarList(personClass, &ivarCount);
        for (unsigned int i = 0; i < ivarCount; i++) {
            Ivar ivar = ivars[i];
            const char *ivarName = ivar_getName(ivar);
            NSLog(@"Instance variable: %s", ivarName);
        }
        free(ivars);
        
        // 获取方法列表
        Method *methods;
        unsigned int methodCount;
        methods = class_copyMethodList(personClass, &methodCount);
        for (unsigned int i = 0; i < methodCount; i++) {
            Method method = methods[i];
            SEL methodSel = method_getName(method);
            NSLog(@"Method: %@", NSStringFromSelector(methodSel));
        }
        free(methods);
    }
    return 0;
}

在上述代码中,我们通过 class_getName 获取类名,class_copyIvarList 获取实例变量列表,class_copyMethodList 获取方法列表。

二、Runtime 中的动态特性

  1. 动态方法解析(Dynamic Method Resolution) 当一个对象接收到一个它无法识别的消息时,运行时系统会启动动态方法解析机制。首先,运行时会调用类的 + (BOOL)resolveInstanceMethod:(SEL)sel 类方法(对于实例方法)或 + (BOOL)resolveClassMethod:(SEL)sel 类方法(对于类方法)。如果在这个方法中我们为该选择器动态添加了方法实现,那么消息传递就可以继续进行。

例如,我们定义一个类 DynamicClass

#import <objc/runtime.h>

@interface DynamicClass : NSObject
@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 called.");
}
@end

然后在 main 函数中调用这个动态方法:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DynamicClass *obj = [[DynamicClass alloc] init];
        [obj performSelector:@selector(dynamicMethod)];
    }
    return 0;
}

在上述代码中,当 obj 接收到 dynamicMethod 消息时,运行时会调用 + (BOOL)resolveInstanceMethod:(SEL)sel,我们在这个方法中通过 class_addMethoddynamicMethod 动态添加了实现。

  1. 备用接收者(Forwarding Target For Selector) 如果动态方法解析没有成功,运行时会尝试寻找备用接收者。它会调用对象的 - (id)forwardingTargetForSelector:(SEL)aSelector 方法。如果这个方法返回一个非 nil 的对象,那么消息就会被转发给这个备用对象。

例如:

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

@implementation ForwardingSource
- (void)forwardedMethod {
    NSLog(@"Forwarded method called in ForwardingSource.");
}
@end

@interface ForwardingTarget : NSObject
@property (nonatomic, strong) ForwardingSource *source;
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(forwardedMethod)) {
        return self.source;
    }
    return nil;
}
@end

main 函数中:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ForwardingTarget *target = [[ForwardingTarget alloc] init];
        target.source = [[ForwardingSource alloc] init];
        [target forwardedMethod];
    }
    return 0;
}

这里 ForwardingTarget 接收到 forwardedMethod 消息,由于自身没有实现,通过 - (id)forwardingTargetForSelector:(SEL)aSelector 将消息转发给了 ForwardingSource 对象。

  1. 完整的消息转发(Full Forwarding) 如果备用接收者也没有找到,运行时会进入完整的消息转发阶段。首先,运行时会调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法,该方法需要返回一个 NSMethodSignature 对象,用于描述方法的参数和返回值类型。如果这个方法返回 nil,运行时会抛出 unrecognized selector 异常。如果返回了有效的 NSMethodSignature,运行时会接着调用 - (void)forwardInvocation:(NSInvocation *)anInvocation 方法,在这个方法中我们可以手动处理消息的转发,例如将消息转发给其他对象。

例如:

@interface FullForwardingTarget : NSObject
- (void)fullForwardedMethod;
@end

@implementation FullForwardingTarget
- (void)fullForwardedMethod {
    NSLog(@"Full forwarded method called in FullForwardingTarget.");
}
@end

@interface FullForwardingSource : NSObject
@property (nonatomic, strong) FullForwardingTarget *target;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(fullForwardedMethod)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    if ([self.target respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self.target];
    }
}
@end

main 函数中:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FullForwardingSource *source = [[FullForwardingSource alloc] init];
        source.target = [[FullForwardingTarget alloc] init];
        [source fullForwardedMethod];
    }
    return 0;
}

这里 FullForwardingSource 接收到 fullForwardedMethod 消息,通过完整的消息转发流程将消息转发给了 FullForwardingTarget

三、Runtime 与属性(Properties)

  1. 属性的本质 在 Objective-C 中,属性(Properties)是一种语法糖。当我们声明一个属性时,编译器会自动为我们生成实例变量、访问器方法(getter 和 setter)。例如:
@interface PropertyClass : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation PropertyClass
@end

这里声明了一个 name 属性,编译器会自动生成一个名为 _name 的实例变量(默认情况下),以及 name 的 getter 方法 -(NSString *)name 和 setter 方法 -(void)setName:(NSString *)name

我们可以通过运行时获取属性的相关信息,例如:

#import <objc/runtime.h>

@interface PropertyClass : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation PropertyClass
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class propertyClass = [PropertyClass class];
        objc_property_t *properties;
        unsigned int propertyCount;
        properties = class_copyPropertyList(propertyClass, &propertyCount);
        for (unsigned int i = 0; i < propertyCount; i++) {
            objc_property_t property = properties[i];
            const char *propertyName = property_getName(property);
            NSLog(@"Property: %s", propertyName);
        }
        free(properties);
    }
    return 0;
}

上述代码通过 class_copyPropertyList 获取类的属性列表,并打印出属性名称。

  1. 关联对象(Associated Objects) 运行时提供了关联对象的功能,允许我们在运行时为对象动态添加属性。这在一些情况下非常有用,例如为系统类添加自定义属性。关联对象使用 objc_setAssociatedObjectobjc_getAssociatedObject 函数。

例如:

#import <objc/runtime.h>

@interface AssociatedObjectClass : NSObject
@end

@implementation AssociatedObjectClass
@end

void addAssociatedProperty(id object, NSString *key, id value) {
    objc_setAssociatedObject(object, (__bridge const void *)(key), value, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

id getAssociatedProperty(id object, NSString *key) {
    return objc_getAssociatedObject(object, (__bridge const void *)(key));
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        AssociatedObjectClass *obj = [[AssociatedObjectClass alloc] init];
        addAssociatedProperty(obj, @"customProperty", @"Hello, Associated Object!");
        id value = getAssociatedProperty(obj, @"customProperty");
        NSLog(@"Associated property value: %@", value);
    }
    return 0;
}

在上述代码中,我们通过 objc_setAssociatedObjectAssociatedObjectClass 对象添加了一个关联属性 customProperty,并通过 objc_getAssociatedObject 获取这个属性的值。

四、Runtime 与协议(Protocols)

  1. 协议的运行时表示 协议(Protocols)在运行时也有相应的表示。一个类遵守某个协议,在运行时会将协议的方法列表添加到类的相关数据结构中。我们可以通过运行时获取一个类遵守的协议列表。

例如:

#import <objc/runtime.h>

@protocol MyProtocol <NSObject>
- (void)protocolMethod;
@end

@interface ProtocolClass : NSObject <MyProtocol>
@end

@implementation ProtocolClass
- (void)protocolMethod {
    NSLog(@"Protocol method called.");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class protocolClass = [ProtocolClass class];
        __unsafe_unretained Protocol **protocols;
        unsigned int protocolCount;
        protocols = class_copyProtocolList(protocolClass, &protocolCount);
        for (unsigned int i = 0; i < protocolCount; i++) {
            Protocol *protocol = protocols[i];
            const char *protocolName = protocol_getName(protocol);
            NSLog(@"Protocol: %s", protocolName);
        }
        free(protocols);
    }
    return 0;
}

上述代码通过 class_copyProtocolList 获取 ProtocolClass 遵守的协议列表,并打印出协议名称。

  1. 动态遵守协议 通过运行时,我们甚至可以在运行时动态地让一个类遵守某个协议。例如:
#import <objc/runtime.h>

@protocol DynamicProtocol <NSObject>
- (void)dynamicProtocolMethod;
@end

@interface DynamicClass : NSObject
@end

@implementation DynamicClass
@end

void addProtocolToClass(Class class, Protocol *protocol) {
    unsigned int protocolCount;
    __unsafe_unretained Protocol **protocols = class_copyProtocolList(class, &protocolCount);
    __unsafe_unretained Protocol **newProtocols = (Protocol **)malloc((protocolCount + 1) * sizeof(Protocol *));
    for (unsigned int i = 0; i < protocolCount; i++) {
        newProtocols[i] = protocols[i];
    }
    newProtocols[protocolCount] = protocol;
    free(protocols);
    class_setProtolist(class, newProtocols, protocolCount + 1);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class dynamicClass = [DynamicClass class];
        Protocol *dynamicProtocol = objc_getProtocol("DynamicProtocol");
        addProtocolToClass(dynamicClass, dynamicProtocol);
        // 检查是否遵守协议
        if (class_conformsToProtocol(dynamicClass, dynamicProtocol)) {
            NSLog(@"DynamicClass now conforms to DynamicProtocol.");
        }
    }
    return 0;
}

在上述代码中,我们通过 addProtocolToClass 函数在运行时为 DynamicClass 添加了 DynamicProtocol 协议,并通过 class_conformsToProtocol 检查是否成功遵守协议。

五、Runtime 的应用场景

  1. KVO(Key - Value Observing) KVO 是基于运行时实现的。当一个对象的属性值发生变化时,KVO 机制会通知观察者。运行时通过动态生成一个子类,并在子类中重写被观察属性的 setter 方法,在 setter 方法中发送属性变化通知。

例如,我们有一个 Person 类:

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

@implementation Person
@end

在使用 KVO 时:

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

这里当 personage 属性值改变时,运行时会触发相关的通知机制,通知观察者。

  1. AOP(Aspect - Oriented Programming) 通过运行时的方法交换(Method Swizzling)技术,可以实现 AOP。方法交换是指在运行时将两个方法的实现进行交换。例如,我们可以在一个类的某个方法执行前后添加一些通用的逻辑,如日志记录。
#import <objc/runtime.h>

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

@implementation AOPClass
- (void)originalMethod {
    NSLog(@"Original method called.");
}
@end

void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

- (void)swizzledMethod {
    NSLog(@"Before original method.");
    [self swizzledMethod];
    NSLog(@"After original method.");
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class aopClass = [AOPClass class];
        swizzleMethod(aopClass, @selector(originalMethod), @selector(swizzledMethod));
        AOPClass *obj = [[AOPClass alloc] init];
        [obj originalMethod];
    }
    return 0;
}

在上述代码中,通过 swizzleMethod 函数将 originalMethodswizzledMethod 的实现进行了交换,从而在 originalMethod 执行前后添加了日志记录。

  1. 自动化测试 在自动化测试中,运行时可以用于模拟对象的行为。例如,我们可以通过动态方法解析或消息转发,为测试对象添加一些临时的方法实现,以满足测试需求。比如在测试一个网络请求类时,我们可以通过运行时动态替换网络请求方法,返回预先定义好的测试数据,而不需要真正发起网络请求。

六、Runtime 的性能考量

  1. 动态特性带来的性能开销 虽然运行时的动态特性非常强大,但也带来了一定的性能开销。例如,消息传递过程中的动态查找方法实现,以及动态方法解析、消息转发等机制,都需要额外的时间和资源。相比静态语言直接调用函数,Objective - C 的动态消息传递在性能上会稍逊一筹。

  2. 优化建议 为了减少运行时动态特性带来的性能开销,可以尽量避免在性能敏感的代码路径中使用过多的动态特性。例如,对于一些频繁调用的方法,确保它们在编译时就有明确的实现,而不是依赖动态方法解析。另外,在使用关联对象时,要注意关联对象的生命周期管理,避免过多的内存开销。同时,在进行方法交换时,要权衡交换带来的功能增强与性能影响,确保整体性能在可接受范围内。

总之,Objective - C 的运行时机制是其强大功能和灵活性的基石。深入理解运行时机制,不仅能让我们编写出更高效、灵活的代码,还能帮助我们解决一些复杂的编程问题。无论是在日常开发、框架设计还是自动化测试等方面,运行时机制都有着广泛的应用。通过合理利用运行时的各种特性,并注意性能优化,我们可以充分发挥 Objective - C 语言的优势。