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

掌握Objective-C运行时(Runtime)的基础语法与应用

2022-08-284.2k 阅读

1. 什么是Objective-C运行时(Runtime)

Objective-C运行时是Objective-C语言的核心特性之一,它提供了一种动态的运行时环境,允许在运行时进行对象的创建、方法的调用以及类的结构和行为的修改。与许多其他静态语言不同,Objective-C的很多决策不是在编译时确定,而是在运行时动态处理。这使得Objective-C具有高度的灵活性和扩展性。

在底层,Objective-C运行时是基于C语言实现的。它定义了一系列的数据结构和函数,用于管理对象、类、方法等。例如,objc_class 结构体表示一个类,包含了类的元数据,如类名、父类、方法列表等信息。

2. 基础语法

2.1 类与对象的基本操作

在Objective-C运行时中,创建对象和获取类对象是最基本的操作。我们可以使用 objc_getClass 函数来获取一个类对象。例如,获取 NSString 类对象:

Class stringClass = objc_getClass("NSString");

创建对象可以使用 objc_msgSend 函数,它是Objective-C方法调用的底层实现。以创建一个 NSString 对象为例:

NSString *str = objc_msgSend(stringClass, @selector(stringWithUTF8String:), "Hello, Runtime");

这里 objc_msgSend 第一个参数是类对象,第二个参数是方法选择器(@selector),后面的参数是方法的实际参数。

2.2 方法相关操作

2.2.1 获取方法列表 我们可以通过运行时获取一个类的所有实例方法列表。下面是一个示例,获取 UIViewController 类的实例方法列表:

unsigned int methodCount;
Method *methods = class_copyMethodList([UIViewController class], &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
    Method method = methods[i];
    SEL methodSEL = method_getName(method);
    const char *methodName = sel_getName(methodSEL);
    NSLog(@"Method name: %s", methodName);
}
free(methods);

这里 class_copyMethodList 函数返回一个指向方法列表的指针,methodCount 表示方法的数量。我们遍历这个列表,获取每个方法的选择器,并转换为方法名进行打印。

2.2.2 添加方法 在运行时,我们甚至可以向一个类动态添加方法。假设有一个简单的类 MyClass

@interface MyClass : NSObject
@end

@implementation MyClass
@end

现在我们要在运行时为 MyClass 添加一个方法:

void newMethodIMP(id self, SEL _cmd) {
    NSLog(@"This is a newly added method.");
}

Class myClass = [MyClass class];
SEL newMethodSEL = @selector(newMethod);
if (!class_getInstanceMethod(myClass, newMethodSEL)) {
    class_addMethod(myClass, newMethodSEL, (IMP)newMethodIMP, "v@:");
}

这里 class_addMethod 函数的参数依次为类对象、方法选择器、方法实现指针和方法的类型编码。"v@:" 表示该方法返回值为 void,第一个参数是 id 类型的 self,第二个参数是 SEL 类型的 _cmd

2.2.3 替换方法实现 有时候我们需要在运行时替换一个已存在方法的实现。例如,我们想替换 NSStringdescription 方法:

@interface NSString (RuntimeAdditions)
@end

@implementation NSString (RuntimeAdditions)
+ (void)load {
    Method originalMethod = class_getInstanceMethod([NSString class], @selector(description));
    Method newMethod = class_getInstanceMethod([self class], @selector(newDescription));
    method_exchangeImplementations(originalMethod, newMethod);
}

- (NSString *)newDescription {
    return [NSString stringWithFormat:@"Custom description: %@", [self newDescription]];
}
@end

+load 方法中,我们使用 method_exchangeImplementations 函数交换了 description 方法和我们自定义的 newDescription 方法的实现。这样,当调用 description 方法时,实际上执行的是 newDescription 方法的逻辑。

3. 应用场景

3.1 动态方法解析

动态方法解析是运行时的一个强大特性,当向一个对象发送一个它无法识别的消息时,运行时会给类一次机会来动态解析这个方法。例如:

@interface DynamicClass : NSObject
@end

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

void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"Dynamic method implemented at runtime.");
}
@end

当向 DynamicClass 的实例发送 missingMethod 消息时,运行时会调用 +resolveInstanceMethod: 方法。如果我们在这个方法中添加了对应的方法实现,就可以成功处理这个消息。

3.2 消息转发

如果动态方法解析没有处理该消息,运行时会进入消息转发阶段。消息转发分为快速转发和标准转发。

3.2.1 快速转发 在快速转发中,运行时会询问对象是否能将消息转发给其他对象处理。例如:

@interface ForwardingClass : NSObject
@end

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

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

@implementation ForwardingClass
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(forwardedMethod)) {
        return [[HelperClass alloc] init];
    }
    return nil;
}
@end

ForwardingClass 的实例收到 forwardedMethod 消息且自身无法处理时,运行时会调用 forwardingTargetForSelector: 方法。如果该方法返回一个能处理该消息的对象(这里是 HelperClass 的实例),则消息会被转发给这个对象处理。

3.2.2 标准转发 如果快速转发没有成功处理消息,运行时会进入标准转发阶段。在标准转发中,运行时会创建一个 NSInvocation 对象,封装消息的所有信息,包括选择器、参数等。对象可以通过 forwardInvocation: 方法来处理这个 NSInvocation 对象。例如:

@interface StandardForwardingClass : NSObject
@end

@interface AnotherHelperClass : NSObject
- (void)standardForwardedMethodWithParam:(NSString *)param;
@end

@implementation AnotherHelperClass
- (void)standardForwardedMethodWithParam:(NSString *)param {
    NSLog(@"Standard forwarding to AnotherHelperClass with param: %@", param);
}
@end

@implementation StandardForwardingClass
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    if ([AnotherHelperClass instancesRespondToSelector:sel]) {
        AnotherHelperClass *helper = [[AnotherHelperClass alloc] init];
        [anInvocation invokeWithTarget:helper];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [AnotherHelperClass instanceMethodSignatureForSelector:aSelector];
    return signature;
}
@end

forwardInvocation: 方法中,我们检查 AnotherHelperClass 是否能响应这个消息,如果可以,则通过 NSInvocation 调用 AnotherHelperClass 的方法。methodSignatureForSelector: 方法则为 NSInvocation 提供正确的方法签名。

3.3 关联对象(Associated Objects)

运行时允许我们为对象动态添加额外的属性,这就是关联对象的作用。例如,我们想为 UIButton 添加一个自定义的属性 customData

#import <objc/runtime.h>

@interface UIButton (CustomData)
@property (nonatomic, strong) id customData;
@end

@implementation UIButton (CustomData)
static const char *customDataKey = "customDataKey";

- (void)setCustomData:(id)customData {
    objc_setAssociatedObject(self, customDataKey, customData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)customData {
    return objc_getAssociatedObject(self, customDataKey);
}
@end

这里通过 objc_setAssociatedObjectobjc_getAssociatedObject 函数实现了为 UIButton 动态添加和获取自定义属性。

3.4 实现KVO(Key - Value Observing)

KVO是一种基于观察者模式的机制,运行时在其实现中起到了重要作用。当一个对象的属性值发生变化时,相关的观察者会收到通知。在底层,运行时通过动态生成一个子类,重写被观察属性的 setter 方法,在 setter 方法中发送属性变化通知。例如:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
@end

// 观察Person的name属性
Person *person = [[Person alloc] init];
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

// 改变name属性值
person.name = @"New Name";

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

运行时会动态生成 Person 的子类,并重写 name 属性的 setName: 方法,在方法中调用 willChangeValueForKey:didChangeValueForKey: 方法,从而触发观察者的回调。

4. 运行时性能考量

在使用运行时特性时,需要注意性能问题。例如,动态方法解析和消息转发虽然强大,但相比直接的方法调用,会带来额外的开销。这是因为动态方法解析需要在运行时查找方法实现,消息转发还涉及到更多的逻辑判断和对象间的传递。

在添加和替换方法时,也会对性能产生一定影响。因为这些操作涉及到对类结构的修改,运行时需要重新调整相关的数据结构。特别是在频繁进行这些操作时,可能会导致性能下降。

关联对象虽然方便,但如果使用不当,也可能造成内存管理问题。例如,如果关联对象设置为 OBJC_ASSOCIATION_RETAIN_NONATOMIC,在对象销毁时需要确保关联对象被正确释放,否则可能导致内存泄漏。

在使用KVO时,虽然运行时自动处理了很多底层逻辑,但过多的观察可能会增加系统开销。尤其是在属性变化频繁的情况下,频繁发送通知可能会影响应用的性能。

为了优化性能,在使用运行时特性时,应尽量避免不必要的动态操作。例如,如果某个功能可以通过常规的类继承和方法重写实现,就尽量不要使用动态方法解析和消息转发。对于关联对象,要合理选择关联策略,确保内存管理的正确性。在KVO方面,可以通过减少不必要的观察,或者批量处理属性变化来降低开销。

5. 与其他语言运行时的比较

与C++ 相比,C++ 是静态类型语言,其对象模型和方法调用在编译时就确定下来。而Objective - C运行时的动态特性使得对象的创建、方法调用更加灵活。例如,C++ 无法在运行时动态添加方法到一个类中,而Objective - C可以通过运行时轻松实现。

Java 同样是面向对象语言,Java 也有反射机制,类似于Objective - C运行时的部分功能,比如可以在运行时获取类的信息、调用方法等。但Java 的反射主要用于配置和框架开发,而Objective - C运行时更深入地融入到语言的日常使用中,例如消息转发、动态方法解析等功能是Java 所没有的。

Swift 作为苹果推出的新语言,与Objective - C运行时也有紧密联系。Swift 在兼容Objective - C的同时,也有自己的运行时。Swift 的运行时相对更轻量级和安全,例如在内存管理方面有更严格的规则。而Objective - C运行时的动态性在一些场景下仍具有优势,例如在需要高度灵活的运行时行为的框架开发中。

6. 实际项目中的应用案例

在很多iOS 开发框架中,Objective - C运行时都有广泛应用。例如,AFNetworking 框架在处理网络请求的过程中,使用运行时来动态注册和管理请求的序列化和反序列化策略。通过运行时获取类的属性信息,AFNetworking 可以自动将服务器返回的数据映射到相应的模型类对象中。

另一个例子是 Masonry 布局框架。Masonry 使用运行时来实现其链式调用的布局语法。通过运行时为视图类动态添加方法,使得开发者可以以一种简洁的链式方式来设置视图的约束。例如:

UIView *view = [[UIView alloc] init];
[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.view.mas_top).offset(10);
    make.left.equalTo(self.view.mas_left).offset(10);
    make.width.mas_equalTo(100);
    make.height.mas_equalTo(100);
}];

这里 mas_makeConstraints 等方法就是通过运行时动态添加到 UIView 类中的。

在一些大型项目中,运行时还用于实现插件化架构。通过运行时,主程序可以在运行时加载和管理插件模块,动态创建插件类的实例,并调用其方法,实现功能的动态扩展。

7. 运行时的局限性

尽管Objective - C运行时非常强大,但也存在一些局限性。首先,由于运行时的动态特性,在编译时编译器无法对一些动态操作进行检查,这可能导致运行时错误。例如,在动态添加方法时,如果方法签名错误,只有在运行时调用该方法时才会发现问题。

其次,运行时的使用增加了代码的复杂性。特别是对于不熟悉运行时机制的开发者,理解和调试使用运行时的代码会比较困难。例如,消息转发机制涉及到多个步骤和函数调用,追踪问题的根源可能需要花费更多时间。

另外,运行时操作可能会影响应用的性能,如前文所述,动态方法解析、消息转发等操作都有一定的性能开销。在对性能要求极高的场景下,过度使用运行时特性可能会导致应用响应变慢。

最后,运行时的一些功能依赖于特定的操作系统和平台。例如,iOS 和 macOS 平台上的Objective - C运行时虽然基本原理相同,但在一些细节上可能存在差异,这可能限制了代码的跨平台性。

8. 运行时的未来发展

随着iOS 和 macOS 系统的不断发展,Objective - C运行时也会持续演进。虽然Swift 语言越来越流行,但Objective - C在一些老项目和特定领域仍有广泛应用。运行时可能会在保持其核心动态特性的基础上,进一步优化性能,减少动态操作带来的开销。

同时,为了更好地与Swift 融合,运行时可能会提供更多的机制来支持两种语言之间的交互。例如,在运行时层面提供更高效的方式来处理Objective - C对象和Swift 对象之间的转换和通信。

在安全性方面,运行时可能会增加更多的编译期和运行时检查机制,以减少因动态操作导致的错误。例如,提供更严格的方法签名检查,或者在动态添加方法时进行更全面的验证。

此外,随着硬件性能的提升和应用场景的不断拓展,运行时可能会支持更多高级的动态特性,如更灵活的类结构修改、更强大的元编程能力等,为开发者提供更丰富的工具来构建复杂的应用和框架。