Objective-C运行时机制中的属性与方法管理
一、Objective-C 运行时简介
Objective-C 运行时(Runtime)是一个用 C 和汇编语言编写的库,它是 Objective-C 面向对象特性实现的基础。运行时系统在运行时(而非编译时)处理消息发送、动态方法解析、消息转发等操作,这赋予了 Objective-C 语言动态性的特点。这种动态性在属性与方法管理方面有着独特的体现。
二、属性(Properties)在运行时的管理
2.1 属性的本质
在 Objective-C 中,我们通过 @property
关键字来声明属性。属性不仅仅是简单的成员变量,它实际上包含了实例变量(Ivar)、存取方法(accessor methods,即 getter 和 setter 方法)以及一些其他的特性。
从运行时的角度来看,属性在类的元数据中有相应的描述。在类的结构体 objc_class
中,有一个指向属性列表的指针 properties
。属性列表是一个 objc_property_list
结构体,它包含了属性的数量以及一个指向 objc_property
结构体数组的指针。
typedef struct objc_property *Property;
struct objc_property_list {
uint count;
struct objc_property *properties[1];
};
struct objc_property {
const char *name;
const char *attributes;
};
属性的 attributes
字符串包含了属性的各种信息,比如类型编码、所有权修饰符(如 retain
、strong
、weak
等)、是否是原子性的(nonatomic
或 atomic
)等。
2.2 属性的存取方法生成
当我们声明一个属性时,编译器默认会为我们生成存取方法。如果我们没有手动实现 getter
和 setter
方法,运行时会在需要的时候动态生成这些方法。
例如,我们定义一个简单的类 Person
有一个 name
属性:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation Person
@end
在运行时,当我们访问 name
属性(比如 person.name
)时,如果没有手动实现 getter
方法,运行时会动态生成一个类似这样的 getter
方法:
- (NSString *)name {
return objc_getProperty(self, _cmd, __OFFSETOFIVAR__(self, _name), YES);
}
而 setter
方法如果没有手动实现,运行时生成的代码类似如下:
- (void)setName:(NSString *)name {
objc_setProperty(self, _cmd, __OFFSETOFIVAR__(self, _name), name, YES, __HOT__);
}
这里 objc_getProperty
和 objc_setProperty
是运行时提供的函数,用于获取和设置属性的值。__OFFSETOFIVAR__
宏用于获取实例变量在对象中的偏移量。
2.3 动态添加属性
在运行时,我们还可以动态地为类添加属性。这需要使用 objc_setAssociatedObject
和 objc_getAssociatedObject
函数。
#import <objc/runtime.h>
@interface UIView (DynamicProperty)
@property (nonatomic, strong) NSString *dynamicText;
@end
@implementation UIView (DynamicProperty)
static const char dynamicTextKey;
- (void)setDynamicText:(NSString *)dynamicText {
objc_setAssociatedObject(self, &dynamicTextKey, dynamicText, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)dynamicText {
return objc_getAssociatedObject(self, &dynamicTextKey);
}
@end
在上述代码中,我们为 UIView
类别添加了一个 dynamicText
属性。通过 objc_setAssociatedObject
函数,我们将一个对象(这里是 NSString
)与指定的键(dynamicTextKey
)和关联策略(OBJC_ASSOCIATION_RETAIN_NONATOMIC
)关联到 UIView
对象上。objc_getAssociatedObject
函数则用于获取这个关联的对象。
三、方法(Methods)在运行时的管理
3.1 方法的结构
在运行时,方法被表示为 objc_method
结构体。在类的 objc_class
结构体中有一个指向方法列表的指针 methods
。
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
};
其中,SEL
(Selector)是方法的唯一标识,本质是一个指向方法名的字符串的指针。method_types
是一个字符串,描述了方法的参数和返回值类型。IMP
(Implementation)是方法实现的函数指针,指向实际执行的代码。
3.2 消息发送机制
当我们向一个对象发送消息时,比如 [object message]
,运行时会进行如下操作:
- 定位方法:运行时首先会在对象的类的方法列表中查找与
SEL
对应的IMP
。如果在本类中没有找到,会沿着继承链在父类的方法列表中查找。 - 缓存优化:为了提高查找效率,运行时会使用方法缓存。当一个方法被调用时,它的
SEL
和IMP
会被缓存到类的缓存列表中。下次再调用相同的方法时,首先会在缓存中查找,大大提高了查找速度。
例如,我们有如下类和方法调用:
@interface Animal : NSObject
- (void)eat;
@end
@implementation Animal
- (void)eat {
NSLog(@"Animal is eating.");
}
@end
@interface Dog : Animal
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc] init];
[dog eat];
}
return 0;
}
在上述代码中,当 [dog eat]
被调用时,运行时首先在 Dog
类的方法列表中查找 eat
方法对应的 IMP
,由于 Dog
类没有实现 eat
方法,会在其父类 Animal
的方法列表中查找,找到后执行对应的 IMP
,即输出 Animal is eating.
。
3.3 动态方法解析
在运行时,如果在方法列表和缓存中都没有找到对应的方法,运行时会启动动态方法解析机制。
动态方法解析分为两个阶段:
- 类方法解析:运行时会调用
+ (BOOL)resolveClassMethod:(SEL)sel
类方法,允许类动态地添加类方法。 - 实例方法解析:如果类方法解析没有处理该方法,运行时会调用
+ (BOOL)resolveInstanceMethod:(SEL)sel
类方法,允许类动态地添加实例方法。
例如,我们可以在运行时动态添加一个实例方法:
#import <objc/runtime.h>
@interface MyClass : NSObject
- (void)dynamicMethod;
@end
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"Dynamic method called.");
}
@end
在上述代码中,当向 MyClass
对象发送 dynamicMethod
消息且在方法列表和缓存中未找到时,运行时会调用 resolveInstanceMethod:
方法。我们在这个方法中通过 class_addMethod
动态添加了 dynamicMethod
方法的实现 dynamicMethodIMP
。
3.4 消息转发
如果动态方法解析也没有处理该方法,运行时会进入消息转发阶段。消息转发分为快速转发和完整转发。
快速转发:运行时会调用 - (id)forwardingTargetForSelector:(SEL)aSelector
实例方法。如果这个方法返回一个非 nil
的对象,运行时会将消息转发给这个对象处理。
例如:
@interface Helper : NSObject
- (void)helpMethod;
@end
@implementation Helper
- (void)helpMethod {
NSLog(@"Helper method called.");
}
@end
@interface MyClass : NSObject
- (void)forwardedMethod;
@end
@implementation MyClass
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(forwardedMethod)) {
return [[Helper alloc] init];
}
return nil;
}
@end
在上述代码中,当向 MyClass
对象发送 forwardedMethod
消息且未找到方法实现时,运行时会调用 forwardingTargetForSelector:
方法,我们返回一个 Helper
对象,这样消息就会转发给 Helper
对象的 helpMethod
方法处理。
完整转发:如果快速转发没有处理该消息,运行时会进入完整转发阶段。首先会调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
实例方法,该方法需要返回一个 NSMethodSignature
对象,描述消息的参数和返回值类型。如果这个方法返回 nil
,运行时会抛出 unrecognized selector
异常。如果返回了有效的 NSMethodSignature
,运行时会调用 - (void)forwardInvocation:(NSInvocation *)anInvocation
实例方法,在这个方法中我们可以手动处理消息的转发。
例如:
@interface Helper : NSObject
- (void)helpMethodWithParam:(NSString *)param;
@end
@implementation Helper
- (void)helpMethodWithParam:(NSString *)param {
NSLog(@"Helper method with param: %@", param);
}
@end
@interface MyClass : NSObject
- (void)forwardedMethodWithParam:(NSString *)param;
@end
@implementation MyClass
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(forwardedMethodWithParam:)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
Helper *helper = [[Helper alloc] init];
if ([helper respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:helper];
}
}
@end
在上述代码中,当向 MyClass
对象发送 forwardedMethodWithParam:
消息且未找到方法实现时,运行时会调用 methodSignatureForSelector:
方法获取方法签名,然后调用 forwardInvocation:
方法,我们在这个方法中将消息转发给 Helper
对象处理。
四、属性与方法管理的应用场景
4.1 实现 KVO(Key - Value Observing)
KVO 是一种基于观察者模式的机制,允许我们监听对象属性值的变化。运行时通过动态生成一个子类,并重写属性的 setter
方法来实现 KVO。在 setter
方法中,会通知观察者属性值的变化。
例如,我们有一个 Person
类,其 age
属性可以被观察:
@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;
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {
NSLog(@"Age changed to: %@", change[NSKeyValueChangeNewKey]);
}
}
运行时会为 Person
类动态生成一个子类,比如 NSKVONotifying_Person
,重写 age
属性的 setter
方法,在 setter
方法中调用 willChangeValueForKey:
和 didChangeValueForKey:
方法来通知观察者。
4.2 实现 AOP(Aspect - Oriented Programming)
AOP 可以在不修改原有代码的情况下,为方法添加额外的功能,如日志记录、性能统计等。我们可以利用运行时的方法交换(Method Swizzling)技术来实现 AOP。
例如,我们为 UIViewController
的 viewDidLoad
方法添加日志记录功能:
#import <objc/runtime.h>
@implementation UIViewController (AOP)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(swizzled_viewDidLoad);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
BOOL didAddMethod =
class_addMethod(self,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(self,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)swizzled_viewDidLoad {
NSLog(@"Before viewDidLoad");
[self swizzled_viewDidLoad];
NSLog(@"After viewDidLoad");
}
@end
在上述代码中,我们在 UIViewController
的 load
方法中通过 method_exchangeImplementations
函数交换了 viewDidLoad
方法和 swizzled_viewDidLoad
方法的实现,从而在 viewDidLoad
方法前后添加了日志记录功能。
4.3 动态加载与插件化
利用运行时的动态方法解析和消息转发机制,可以实现动态加载代码和插件化。例如,我们可以在运行时加载一个动态库(.dylib),并通过运行时函数获取库中的类和方法,实现插件化的功能。
假设我们有一个动态库 Plugin.dylib
,其中定义了一个 PluginClass
类和 pluginMethod
方法:
// PluginClass.h
@interface PluginClass : NSObject
- (void)pluginMethod;
@end
// PluginClass.m
@implementation PluginClass
- (void)pluginMethod {
NSLog(@"Plugin method called.");
}
@end
在主程序中,我们可以动态加载这个库并调用方法:
#import <dlfcn.h>
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
void *handle = dlopen("/path/to/Plugin.dylib", RTLD_LAZY);
if (handle) {
Class pluginClass = objc_getClass("PluginClass");
if (pluginClass) {
id pluginObject = [[pluginClass alloc] init];
SEL selector = @selector(pluginMethod);
if ([pluginObject respondsToSelector:selector]) {
((void (*)(id, SEL))objc_msgSend)(pluginObject, selector);
}
}
dlclose(handle);
}
}
return 0;
}
在上述代码中,我们通过 dlopen
函数动态加载动态库,然后通过 objc_getClass
获取库中的类,进而创建对象并调用方法,实现了动态加载和插件化的功能。
通过对 Objective - C 运行时机制中属性与方法管理的深入理解,我们可以更好地利用 Objective - C 的动态特性,编写出更加灵活、强大的代码。无论是实现高级的设计模式,还是进行底层的性能优化,运行时机制都为我们提供了丰富的工具和手段。在实际开发中,合理运用这些特性可以提高代码的可维护性、可扩展性以及运行效率。