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

Objective-C运行时中的关联对象与属性注入技术

2021-11-254.8k 阅读

关联对象基础

在Objective-C中,对象的属性通常在类的定义中声明,并且存储在对象的内存布局中。然而,运行时提供了一种机制,可以在运行时为对象动态添加额外的属性,这就是关联对象(Associated Objects)技术。

关联对象允许你在运行时将一个对象与另一个对象相关联,而无需在类的定义中提前声明属性。这种机制基于运行时的objc_setAssociatedObjectobjc_getAssociatedObject函数。

下面我们来看一个简单的代码示例,展示如何使用关联对象:

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface NSObject (AssociatedObject)
@property (nonatomic, strong) NSString *associatedString;
@end

@implementation NSObject (AssociatedObject)
static char AssociatedStringKey;

- (void)setAssociatedString:(NSString *)associatedString {
    objc_setAssociatedObject(self, &AssociatedStringKey, associatedString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)associatedString {
    return objc_getAssociatedObject(self, &AssociatedStringKey);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        obj.associatedString = @"Hello, Associated Object!";
        NSString *string = obj.associatedString;
        NSLog(@"%@", string);
    }
    return 0;
}

在这个例子中,我们通过分类(Category)为NSObject添加了一个新的属性associatedString。虽然NSObject类本身并没有这个属性,但通过关联对象技术,我们可以像访问普通属性一样访问它。

关联对象的原理

关联对象的实现依赖于Objective-C运行时的底层数据结构。当你调用objc_setAssociatedObject时,运行时会在一个全局的关联对象表中为目标对象创建一个记录。这个表是一个哈希表,以对象的内存地址作为键,关联对象的信息作为值。

关联对象的信息包括关联的对象、关联策略(决定对象的内存管理方式)以及一个唯一的标识(通常是一个静态变量的地址)。关联策略有以下几种:

  • OBJC_ASSOCIATION_ASSIGN:弱引用,不持有对象。
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC:强引用,非原子操作。
  • OBJC_ASSOCIATION_COPY_NONATOMIC:拷贝对象,非原子操作。
  • OBJC_ASSOCIATION_RETAIN:强引用,原子操作。
  • OBJC_ASSOCIATION_COPY:拷贝对象,原子操作。

例如,在我们之前的代码中,使用的是OBJC_ASSOCIATION_RETAIN_NONATOMIC策略,这意味着关联对象会被强引用,并且访问操作不是线程安全的。

属性注入技术

属性注入(Property Injection)是基于关联对象的一种高级应用,它允许在运行时动态地为类添加属性,并且可以像访问普通属性一样访问这些动态添加的属性。

属性注入通常用于一些特殊的场景,比如在运行时根据不同的条件为对象添加不同的属性,或者为已有的类添加一些扩展属性而不需要修改类的定义。

下面我们通过一个更复杂的例子来展示属性注入技术:

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
@end

@interface Person (DynamicProperties)
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person (DynamicProperties)
static char AgeKey;

- (void)setAge:(NSInteger)age {
    objc_setAssociatedObject(self, &AgeKey, @(age), OBJC_ASSOCIATION_ASSIGN);
}

- (NSInteger)age {
    NSNumber *number = objc_getAssociatedObject(self, &AgeKey);
    return number.integerValue;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.name = @"John";
        person.age = 30;
        
        NSLog(@"Name: %@, Age: %ld", person.name, (long)person.age);
    }
    return 0;
}

在这个例子中,我们为Person类添加了一个动态属性age。通过分类和关联对象技术,我们实现了属性的注入,使得Person类在运行时拥有了一个原本不存在的属性。

关联对象与属性注入的应用场景

  1. 扩展已有类的功能:当你无法修改某个类的源代码,但又需要为其添加一些额外的属性或功能时,关联对象和属性注入是非常有用的。例如,你可以为UIView添加一个自定义的属性来存储一些与视图相关的额外数据,而不需要继承UIView

  2. 运行时配置:在某些情况下,你可能需要根据运行时的条件动态地为对象添加属性。比如,根据用户的设置或网络请求的结果,为对象添加不同的属性。

  3. AOP(面向切面编程):关联对象可以用于实现AOP的一些功能,比如在方法调用前后添加一些额外的逻辑。通过为对象添加关联对象来存储切面逻辑的相关信息,然后在方法调用时进行相应的处理。

关联对象的内存管理

关联对象的内存管理依赖于所使用的关联策略。例如,当使用OBJC_ASSOCIATION_RETAINOBJC_ASSOCIATION_RETAIN_NONATOMIC策略时,关联对象会被强引用,当目标对象被释放时,关联对象也会被释放。

然而,当使用OBJC_ASSOCIATION_ASSIGN策略时,关联对象不会被强引用,这可能会导致悬空指针的问题。如果关联对象在目标对象之前被释放,访问关联对象时可能会导致程序崩溃。

为了避免这种情况,你需要确保关联对象的生命周期与目标对象相匹配,或者在目标对象的dealloc方法中手动清理关联对象。例如:

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface NSObject (AssociatedObject)
@property (nonatomic, assign) NSObject *associatedObject;
@end

@implementation NSObject (AssociatedObject)
static char AssociatedObjectKey;

- (void)setAssociatedObject:(NSObject *)associatedObject {
    objc_setAssociatedObject(self, &AssociatedObjectKey, associatedObject, OBJC_ASSOCIATION_ASSIGN);
}

- (NSObject *)associatedObject {
    return objc_getAssociatedObject(self, &AssociatedObjectKey);
}

- (void)dealloc {
    objc_setAssociatedObject(self, &AssociatedObjectKey, nil, OBJC_ASSOCIATION_ASSIGN);
}
@end

在这个例子中,我们在dealloc方法中手动将关联对象设置为nil,以避免悬空指针的问题。

关联对象与KVO(Key - Value Observing)

KVO是Objective-C中一种基于观察者模式的机制,用于监听对象属性的变化。关联对象本身并不直接支持KVO,因为它们不是对象内存布局的一部分,KVO无法自动检测到关联对象的变化。

然而,你可以通过手动实现KVO来监听关联对象的变化。例如,你可以在设置关联对象的方法中手动发送KVO通知:

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface NSObject (AssociatedObject)
@property (nonatomic, strong) NSString *associatedString;
@end

@implementation NSObject (AssociatedObject)
static char AssociatedStringKey;

- (void)setAssociatedString:(NSString *)associatedString {
    NSString *oldValue = self.associatedString;
    objc_setAssociatedObject(self, &AssociatedStringKey, associatedString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self willChangeValueForKey:@"associatedString"];
    [self didChangeValueForKey:@"associatedString"];
}

- (NSString *)associatedString {
    return objc_getAssociatedObject(self, &AssociatedStringKey);
}
@end

在这个例子中,我们在设置关联对象时手动发送了KVO通知,使得其他对象可以监听associatedString属性的变化。

关联对象与性能

使用关联对象和属性注入技术时,需要注意性能问题。由于关联对象的存储和访问依赖于全局的哈希表,每次设置或获取关联对象都需要进行哈希查找操作,这比访问普通属性的开销要大。

此外,如果在一个循环中频繁地设置或获取关联对象,性能问题可能会更加明显。因此,在性能敏感的代码中,应该谨慎使用关联对象和属性注入技术,或者尽量减少对关联对象的频繁操作。

关联对象与线程安全

关联对象本身并不是线程安全的。如果多个线程同时访问或修改关联对象,可能会导致数据竞争和未定义行为。

为了确保线程安全,你可以使用锁机制来保护对关联对象的访问。例如,使用@synchronized关键字:

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface NSObject (AssociatedObject)
@property (nonatomic, strong) NSString *associatedString;
@end

@implementation NSObject (AssociatedObject)
static char AssociatedStringKey;

- (void)setAssociatedString:(NSString *)associatedString {
    @synchronized(self) {
        objc_setAssociatedObject(self, &AssociatedStringKey, associatedString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
}

- (NSString *)associatedString {
    @synchronized(self) {
        return objc_getAssociatedObject(self, &AssociatedStringKey);
    }
}
@end

在这个例子中,我们使用@synchronized来确保在同一时间只有一个线程可以访问或修改关联对象,从而保证了线程安全。

关联对象的局限性

  1. 不支持继承:关联对象是基于对象实例的,而不是基于类的。这意味着如果你为一个对象设置了关联对象,它的子类并不会自动继承这个关联对象。

  2. 性能开销:如前所述,关联对象的访问和存储涉及哈希查找等操作,相比于普通属性有更高的性能开销。

  3. 调试困难:由于关联对象不是对象内存布局的一部分,在调试时可能不太容易直接查看和分析关联对象的状态。

关联对象与runtime其他特性的结合

  1. 与Method Swizzling结合:Method Swizzling是一种在运行时替换方法实现的技术。可以结合关联对象,在方法替换时为对象添加一些额外的上下文信息。例如,在替换UIViewControllerviewDidLoad方法时,可以使用关联对象为其添加一些自定义的初始化数据。

  2. 与动态方法解析结合:动态方法解析允许在运行时为对象动态添加方法。可以利用关联对象来存储动态方法所需的数据,从而实现更灵活的动态方法调用。

示例:使用关联对象实现对象间的动态绑定

假设我们有两个类AB,在运行时我们希望A类的实例能够动态地与B类的实例进行绑定,并通过A类的实例访问B类实例的某些属性。

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface B : NSObject
@property (nonatomic, copy) NSString *bProperty;
@end

@implementation B
@end

@interface A : NSObject
@property (nonatomic, strong) B *associatedB;
@end

@implementation A
static char AssociatedBKey;

- (void)setAssociatedB:(B *)associatedB {
    objc_setAssociatedObject(self, &AssociatedBKey, associatedB, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (B *)associatedB {
    return objc_getAssociatedObject(self, &AssociatedBKey);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        A *a = [[A alloc] init];
        B *b = [[B alloc] init];
        b.bProperty = @"Hello from B";
        
        a.associatedB = b;
        
        NSLog(@"Accessed from A: %@", a.associatedB.bProperty);
    }
    return 0;
}

在这个示例中,通过关联对象实现了A类实例与B类实例的动态绑定,使得A类实例可以访问B类实例的属性。

示例:基于属性注入的插件化开发

假设我们正在开发一个插件化的应用,主程序中有一个PluginManager类,不同的插件可以通过属性注入为PluginManager添加自定义的属性和功能。

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

@interface PluginManager : NSObject
@end

@implementation PluginManager
@end

// 插件1
@interface PluginManager (Plugin1)
@property (nonatomic, assign) NSInteger plugin1Value;
@end

@implementation PluginManager (Plugin1)
static char Plugin1ValueKey;

- (void)setPlugin1Value:(NSInteger)plugin1Value {
    objc_setAssociatedObject(self, &Plugin1ValueKey, @(plugin1Value), OBJC_ASSOCIATION_ASSIGN);
}

- (NSInteger)plugin1Value {
    NSNumber *number = objc_getAssociatedObject(self, &Plugin1ValueKey);
    return number.integerValue;
}
@end

// 插件2
@interface PluginManager (Plugin2)
@property (nonatomic, copy) NSString *plugin2String;
@end

@implementation PluginManager (Plugin2)
static char Plugin2StringKey;

- (void)setPlugin2String:(NSString *)plugin2String {
    objc_setAssociatedObject(self, &Plugin2StringKey, plugin2String, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)plugin2String {
    return objc_getAssociatedObject(self, &Plugin2StringKey);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        PluginManager *manager = [[PluginManager alloc] init];
        manager.plugin1Value = 100;
        manager.plugin2String = @"Plugin 2 data";
        
        NSLog(@"Plugin 1 Value: %ld", (long)manager.plugin1Value);
        NSLog(@"Plugin 2 String: %@", manager.plugin2String);
    }
    return 0;
}

在这个示例中,不同的插件通过属性注入为PluginManager添加了不同的属性,实现了插件化开发的灵活性。

通过以上内容,我们详细探讨了Objective-C运行时中的关联对象与属性注入技术,包括它们的原理、应用场景、内存管理、性能以及与其他runtime特性的结合等方面,并通过丰富的代码示例进行了说明。希望这些内容能帮助你更好地理解和应用这两项强大的技术。