Objective-C 运行时(Runtime)机制探秘
Objective-C 运行时(Runtime)机制探秘
Objective-C 作为一门面向对象的编程语言,其运行时(Runtime)机制是其强大功能和灵活性的核心所在。Runtime 提供了一种动态的消息传递机制,允许在运行时决定对象如何响应消息,这种动态性为开发者带来了巨大的便利,同时也使得代码具有更高的灵活性和扩展性。下面我们将深入探讨 Objective-C 的运行时机制。
一、Runtime 的基础概念
- 消息传递(Messaging)
在 Objective-C 中,向对象发送消息使用
[对象 方法]
的语法。例如:
NSString *str = @"Hello, Runtime!";
NSLog(@"%@", [str length]);
这里 [str length]
就是向 str
对象发送 length
消息。在编译阶段,编译器并不会直接调用 length
方法,而是将这个消息发送过程转化为一个 objc_msgSend
函数调用。objc_msgSend
函数是运行时系统的核心函数之一,它接收两个主要参数:接收消息的对象和选择器(Selector)。选择器是一个表示方法的唯一标识符,它在编译时就已经确定。
- 类和对象
在运行时,类(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 中的动态特性
- 动态方法解析(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_addMethod
为 dynamicMethod
动态添加了实现。
- 备用接收者(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
对象。
- 完整的消息转发(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)
- 属性的本质 在 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
获取类的属性列表,并打印出属性名称。
- 关联对象(Associated Objects)
运行时提供了关联对象的功能,允许我们在运行时为对象动态添加属性。这在一些情况下非常有用,例如为系统类添加自定义属性。关联对象使用
objc_setAssociatedObject
和objc_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_setAssociatedObject
为 AssociatedObjectClass
对象添加了一个关联属性 customProperty
,并通过 objc_getAssociatedObject
获取这个属性的值。
四、Runtime 与协议(Protocols)
- 协议的运行时表示 协议(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
遵守的协议列表,并打印出协议名称。
- 动态遵守协议 通过运行时,我们甚至可以在运行时动态地让一个类遵守某个协议。例如:
#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 的应用场景
- 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;
这里当 person
的 age
属性值改变时,运行时会触发相关的通知机制,通知观察者。
- 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
函数将 originalMethod
和 swizzledMethod
的实现进行了交换,从而在 originalMethod
执行前后添加了日志记录。
- 自动化测试 在自动化测试中,运行时可以用于模拟对象的行为。例如,我们可以通过动态方法解析或消息转发,为测试对象添加一些临时的方法实现,以满足测试需求。比如在测试一个网络请求类时,我们可以通过运行时动态替换网络请求方法,返回预先定义好的测试数据,而不需要真正发起网络请求。
六、Runtime 的性能考量
-
动态特性带来的性能开销 虽然运行时的动态特性非常强大,但也带来了一定的性能开销。例如,消息传递过程中的动态查找方法实现,以及动态方法解析、消息转发等机制,都需要额外的时间和资源。相比静态语言直接调用函数,Objective - C 的动态消息传递在性能上会稍逊一筹。
-
优化建议 为了减少运行时动态特性带来的性能开销,可以尽量避免在性能敏感的代码路径中使用过多的动态特性。例如,对于一些频繁调用的方法,确保它们在编译时就有明确的实现,而不是依赖动态方法解析。另外,在使用关联对象时,要注意关联对象的生命周期管理,避免过多的内存开销。同时,在进行方法交换时,要权衡交换带来的功能增强与性能影响,确保整体性能在可接受范围内。
总之,Objective - C 的运行时机制是其强大功能和灵活性的基石。深入理解运行时机制,不仅能让我们编写出更高效、灵活的代码,还能帮助我们解决一些复杂的编程问题。无论是在日常开发、框架设计还是自动化测试等方面,运行时机制都有着广泛的应用。通过合理利用运行时的各种特性,并注意性能优化,我们可以充分发挥 Objective - C 语言的优势。