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

Objective-C中的@dynamic属性实现原理

2021-06-152.3k 阅读

@dynamic属性基础概念

在Objective - C编程中,属性(property)是一种封装实例变量的机制,它提供了简洁的语法来访问和修改对象的实例变量。通常情况下,当我们在类中声明一个属性时,编译器会自动为我们生成访问器方法(getter和setter),除非我们明确指定 @synthesize 或者自己手动实现这些方法。

@dynamic 关键字则是用于告诉编译器,不要为该属性自动生成访问器方法。这意味着,当我们使用 @dynamic 声明属性时,我们需要自己在运行时提供这些访问器方法的实现,否则在访问该属性时会引发运行时错误。

例如,假设我们有一个简单的类 Person

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation Person

// 编译器会自动为name属性生成访问器方法
@end

在上述代码中,编译器会自动为 name 属性生成 name(getter)和 setName:(setter)方法。

但如果我们使用 @dynamic

#import <Foundation/Foundation.h>

@interface Person : NSObject

@dynamic name;

@end

@implementation Person
// 这里没有为name属性提供访问器方法的实现
@end

在这种情况下,如果在其他地方尝试访问 Person 对象的 name 属性,如 Person *person = [[Person alloc] init]; NSString *personName = person.name;,运行时会因为找不到对应的访问器方法而报错。

@dynamic属性的应用场景

  1. 延迟实现访问器方法 有时候,我们可能希望在运行时根据具体情况来决定如何实现属性的访问器方法。例如,在一个数据库相关的类中,属性可能对应数据库中的字段,但我们不想在编译时就确定如何从数据库中读取或写入这些值,而是希望在运行时根据数据库连接状态、事务情况等动态生成访问器方法。

  2. 实现动态行为 在一些动态框架或者需要实现特定动态特性的场景中,@dynamic 可以派上用场。比如,一个用于创建动态UI的框架,其中的视图属性可能需要根据运行时的用户交互、设备特性等动态改变其获取和设置逻辑。

  3. 避免编译器生成不必要的代码 在某些情况下,编译器生成的默认访问器方法可能不符合我们的需求,而且还会增加不必要的代码体积。使用 @dynamic 可以避免编译器生成这些默认方法,从而让我们可以完全按照自己的方式来实现属性访问逻辑。

@dynamic属性实现原理深入剖析

  1. 消息传递机制 Objective - C是一门基于运行时的语言,其核心机制是消息传递(message passing)。当我们向一个对象发送消息,比如访问属性的getter或setter方法时,运行时系统会在该对象的方法列表中查找对应的方法实现。

当使用 @dynamic 声明属性时,编译器不会生成访问器方法,所以当发送访问属性的消息时,运行时系统无法在对象的方法列表中找到默认的访问器方法。此时,运行时会启动动态方法解析(dynamic method resolution)流程。

  1. 动态方法解析 动态方法解析分为两个阶段:
    • 类方法解析:运行时首先会调用 + (BOOL)resolveInstanceMethod:(SEL)sel (对于实例方法,类方法为 + (BOOL)resolveClassMethod:(SEL)sel)。如果我们在类的实现中重写了这个方法,并在其中为对应的选择器(SEL,即方法的唯一标识符)动态添加方法实现,那么运行时就会使用我们添加的方法实现来处理消息。 例如:
#import <Foundation/Foundation.h>

@interface DynamicPropertyClass : NSObject

@dynamic dynamicProperty;

@end

@implementation DynamicPropertyClass

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicProperty)) {
        class_addMethod(self, sel, (IMP)customGetter, "@@:");
        return YES;
    } else if (sel == @selector(setDynamicProperty:)) {
        class_addMethod(self, sel, (IMP)customSetter, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

id customGetter(id self, SEL _cmd) {
    // 这里可以实现自定义的获取属性值逻辑
    return @"Custom value";
}

void customSetter(id self, SEL _cmd, id value) {
    // 这里可以实现自定义的设置属性值逻辑
    NSLog(@"Setting value: %@", value);
}

@end

在上述代码中,当访问 dynamicProperty 的getter或setter方法时,运行时会调用 resolveInstanceMethod: 方法。我们在该方法中判断如果是 dynamicProperty 的访问器方法对应的 SEL,就使用 class_addMethod 动态添加方法实现。class_addMethod 的参数中,self 表示当前类,sel 是方法选择器,(IMP)customGetter(IMP)customSetter 是方法实现的函数指针,"@@:""v@:@" 是方法的类型编码。

  • 备用接收者(备用接受者):如果类方法解析阶段没有找到对应的方法实现,运行时会进入备用接收者阶段。它会调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法。如果我们在类中重写这个方法,并返回一个可以处理该消息的对象,运行时就会将消息转发给这个备用接收者对象。 例如:
#import <Foundation/Foundation.h>

@interface HelperClass : NSObject

@property (nonatomic, copy) NSString *helperProperty;

@end

@implementation HelperClass

@end

@interface DynamicPropertyClass : NSObject

@dynamic dynamicProperty;

@end

@implementation DynamicPropertyClass

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(dynamicProperty) || aSelector == @selector(setDynamicProperty:)) {
        HelperClass *helper = [[HelperClass alloc] init];
        return helper;
    }
    return nil;
}

@end

在上述代码中,当 DynamicPropertyClass 对象接收到 dynamicProperty 相关的访问器消息且在动态方法解析阶段未处理时,会调用 forwardingTargetForSelector: 方法。如果是 dynamicProperty 相关的选择器,就返回一个 HelperClass 对象,运行时会将消息转发给 HelperClass 对象处理。

  • 完整的消息转发:如果备用接收者阶段也没有处理消息,运行时会进入完整的消息转发阶段。它会首先调用 - (void)forwardInvocation:(NSInvocation *)anInvocation 方法。在这个方法中,我们可以手动构建一个新的调用,将消息转发到其他对象或者自己处理这个消息。同时,运行时还会调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法来获取方法签名,以便构建 NSInvocation 对象。 例如:
#import <Foundation/Foundation.h>

@interface HelperClass : NSObject

@property (nonatomic, copy) NSString *helperProperty;

@end

@implementation HelperClass

@end

@interface DynamicPropertyClass : NSObject

@dynamic dynamicProperty;

@end

@implementation DynamicPropertyClass

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(dynamicProperty)) {
        return [NSMethodSignature signatureWithObjCTypes:"@@:"];
    } else if (aSelector == @selector(setDynamicProperty:)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    HelperClass *helper = [[HelperClass alloc] init];
    SEL selector = anInvocation.selector;
    if ([helper respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:helper];
    }
}

@end

在上述代码中,methodSignatureForSelector: 方法为 dynamicProperty 的访问器方法提供方法签名,forwardInvocation: 方法将消息转发给 HelperClass 对象处理。

@dynamic属性与KVO(Key - Value Observing)

  1. KVO基础 KVO是一种基于观察者模式的机制,它允许我们监听对象属性值的变化。当一个对象的属性值发生改变时,注册了监听的观察者对象会收到通知。

  2. @dynamic属性与KVO的兼容性 对于使用 @dynamic 声明的属性,如果我们想要对其使用KVO,需要特别注意。因为编译器没有为 @dynamic 属性生成默认的访问器方法,所以我们需要自己确保在属性值变化时手动触发KVO通知。

例如,假设我们有一个使用 @dynamic 属性的类 MyClass

#import <Foundation/Foundation.h>

@interface MyClass : NSObject

@dynamic myDynamicProperty;

@end

@implementation MyClass

// 假设在某个方法中设置属性值
- (void)setMyDynamicPropertyValue:(id)value {
    // 手动触发KVO通知
    [self willChangeValueForKey:@"myDynamicProperty"];
    // 这里假设存在一个实际存储属性值的实例变量_actualValue
    _actualValue = value;
    [self didChangeValueForKey:@"myDynamicProperty"];
}

@end

在上述代码中,当我们通过 setMyDynamicPropertyValue: 方法设置属性值时,手动调用 willChangeValueForKey:didChangeValueForKey: 方法来触发KVO通知,这样注册了对 myDynamicProperty 监听的观察者对象就能收到属性值变化的通知。

@dynamic属性与内存管理

  1. ARC(自动引用计数)下的情况 在ARC环境下,虽然编译器不会为 @dynamic 属性生成默认的内存管理相关的访问器方法,但我们自己实现的访问器方法需要遵循ARC的内存管理规则。

例如,对于一个 copy 类型的 @dynamic 属性:

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@dynamic myCopyProperty;

@end

@implementation MyObject

- (NSString *)myCopyProperty {
    return _myCopyProperty;
}

- (void)setMyCopyProperty:(NSString *)newValue {
    if (_myCopyProperty != newValue) {
        _myCopyProperty = [newValue copy];
    }
}

@end

在上述代码中,setMyCopyProperty: 方法中对新值进行了 copy 操作,符合 copy 类型属性的内存管理要求,同时也遵循了ARC的规则,不需要手动进行 retainrelease 等操作。

  1. MRC(手动引用计数)下的情况 在MRC环境下,我们自己实现的 @dynamic 属性访问器方法需要手动进行内存管理。

例如,对于一个 retain 类型的 @dynamic 属性:

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@dynamic myRetainProperty;

@end

@implementation MyObject

- (NSString *)myRetainProperty {
    return _myRetainProperty;
}

- (void)setMyRetainProperty:(NSString *)newValue {
    if (_myRetainProperty != newValue) {
        [_myRetainProperty release];
        _myRetainProperty = [newValue retain];
    }
}

@end

在上述代码中,setMyRetainProperty: 方法中对旧值进行了 release 操作,对新值进行了 retain 操作,以确保内存管理的正确性。

总结

@dynamic 属性在Objective - C中提供了一种强大的机制,允许我们在运行时动态控制属性的访问器方法实现。通过深入理解其实现原理,包括消息传递机制、动态方法解析、与KVO和内存管理的关系等,我们可以在实际编程中更加灵活地使用 @dynamic 属性,实现一些复杂的动态特性和优化代码结构。同时,在使用 @dynamic 属性时,要特别注意确保访问器方法的正确实现以及与其他特性(如KVO、内存管理)的兼容性,以避免运行时错误。