Objective-C运行时(Runtime)基础与应用场景
一、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
作为值。当一个方法被调用且在缓存中未找到时,运行时会将该方法的 SEL
和 IMP
插入到缓存中,以便下次快速查找。
四、动态方法解析
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
在上述代码中,通过方法交换为 BusinessClass
的 businessMethod
方法添加了日志记录的横切逻辑,实现了 AOP 的效果。
七、总结运行时操作的注意事项
在使用运行时进行各种操作时,需要注意一些事项。首先,运行时操作通常是对底层结构的直接操作,错误的使用可能会导致程序崩溃或者未定义行为。例如,在动态添加方法时,确保方法签名的正确性,否则可能在调用时出现参数传递错误。
其次,在进行方法交换时,要注意交换的顺序和范围。避免在多个分类中同时对同一个方法进行交换,以免造成混乱。同时,要确保在合适的时机进行方法交换,通常在 +load
方法中进行是比较安全的选择,因为 +load
方法在类加载时就会被调用,且只调用一次。
另外,关联对象的使用要注意内存管理。根据关联策略的不同,要确保对象的生命周期管理正确,避免内存泄漏或者悬空指针的问题。
总之,Objective - C 运行时是一个强大但也较为复杂的机制,深入理解并谨慎使用它,可以为我们的程序开发带来很多便利和灵活性。