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

Objective-C运行时机制中的属性与方法管理

2023-03-065.3k 阅读

一、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 字符串包含了属性的各种信息,比如类型编码、所有权修饰符(如 retainstrongweak 等)、是否是原子性的(nonatomicatomic)等。

2.2 属性的存取方法生成

当我们声明一个属性时,编译器默认会为我们生成存取方法。如果我们没有手动实现 gettersetter 方法,运行时会在需要的时候动态生成这些方法。

例如,我们定义一个简单的类 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_getPropertyobjc_setProperty 是运行时提供的函数,用于获取和设置属性的值。__OFFSETOFIVAR__ 宏用于获取实例变量在对象中的偏移量。

2.3 动态添加属性

在运行时,我们还可以动态地为类添加属性。这需要使用 objc_setAssociatedObjectobjc_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],运行时会进行如下操作:

  1. 定位方法:运行时首先会在对象的类的方法列表中查找与 SEL 对应的 IMP。如果在本类中没有找到,会沿着继承链在父类的方法列表中查找。
  2. 缓存优化:为了提高查找效率,运行时会使用方法缓存。当一个方法被调用时,它的 SELIMP 会被缓存到类的缓存列表中。下次再调用相同的方法时,首先会在缓存中查找,大大提高了查找速度。

例如,我们有如下类和方法调用:

@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 动态方法解析

在运行时,如果在方法列表和缓存中都没有找到对应的方法,运行时会启动动态方法解析机制。

动态方法解析分为两个阶段:

  1. 类方法解析:运行时会调用 + (BOOL)resolveClassMethod:(SEL)sel 类方法,允许类动态地添加类方法。
  2. 实例方法解析:如果类方法解析没有处理该方法,运行时会调用 + (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。

例如,我们为 UIViewControllerviewDidLoad 方法添加日志记录功能:

#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

在上述代码中,我们在 UIViewControllerload 方法中通过 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 的动态特性,编写出更加灵活、强大的代码。无论是实现高级的设计模式,还是进行底层的性能优化,运行时机制都为我们提供了丰富的工具和手段。在实际开发中,合理运用这些特性可以提高代码的可维护性、可扩展性以及运行效率。