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

Objective-C关联对象(Associated Object)关联策略语法

2022-05-193.9k 阅读

一、Objective-C关联对象概述

在Objective-C编程中,关联对象(Associated Object)是一种强大的特性,它允许我们为已有的类动态添加额外的属性,即使这些类并非由我们自己创建,或者没有在类的定义中显式声明这些属性。这一特性在运行时通过运行时系统(Runtime System)来实现。

传统上,我们在定义一个类时,需要在类的接口部分声明属性,并在实现部分进行相关的内存管理等操作。然而,对于一些第三方类或者不方便修改其源码的类,这种方式就不再适用。关联对象则提供了一种灵活的解决方案,让我们可以在运行时为对象关联额外的数据。

二、关联策略(Associative Policy)

关联策略决定了关联对象的内存管理方式,它定义在objc/objc - runtime.h头文件中,以枚举的形式存在。以下是几种常见的关联策略及其含义:

  1. OBJC_ASSOCIATION_ASSIGN:弱引用关联,类似于声明属性时使用assign关键字。这种策略不会增加关联对象的引用计数,当关联对象被释放时,指向它的指针会被设置为nil。适用于避免循环引用的场景,例如关联一个可能随时被释放的对象,且我们不希望阻止其释放。
// 示例代码
#import <objc/runtime.h>
@interface MyClass : NSObject
@end
@implementation MyClass
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        NSString *associatedObj = @"Weak Associated String";
        objc_setAssociatedObject(obj, @selector(weakAssociatedProperty), associatedObj, OBJC_ASSOCIATION_ASSIGN);
        NSString *retrievedObj = objc_getAssociatedObject(obj, @selector(weakAssociatedProperty));
        NSLog(@"Retrieved Object: %@", retrievedObj);
    }
    return 0;
}
  1. OBJC_ASSOCIATION_RETAIN_NONATOMIC:非原子性强引用关联,类似于声明属性时使用retain关键字(在ARC环境下等效于strong)。关联对象的引用计数会增加,并且这种关联是非原子性的,即多线程访问时可能存在竞争条件。适用于对性能要求较高且在单线程环境或者对线程安全要求不高的场景。
#import <objc/runtime.h>
@interface MyClass : NSObject
@end
@implementation MyClass
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        NSString *associatedObj = [[NSString alloc] initWithFormat:@"Retain Non - atomic String"];
        objc_setAssociatedObject(obj, @selector(retainNonAtomicProperty), associatedObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        NSString *retrievedObj = objc_getAssociatedObject(obj, @selector(retainNonAtomicProperty));
        NSLog(@"Retrieved Object: %@", retrievedObj);
    }
    return 0;
}
  1. OBJC_ASSOCIATION_COPY_NONATOMIC:非原子性拷贝关联。关联对象会被拷贝一份,新的拷贝对象与原对象具有相同的值但不同的内存地址,并且这种关联是非原子性的。适用于关联对象需要保持独立副本且对性能有一定要求的场景,比如关联一个字符串对象,我们希望有自己独立的副本,而不是共享同一个对象。
#import <objc/runtime.h>
@interface MyClass : NSObject
@end
@implementation MyClass
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        NSMutableString *mutableStr = [NSMutableString stringWithString:@"Mutable String"];
        objc_setAssociatedObject(obj, @selector(copyNonAtomicProperty), mutableStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
        NSString *retrievedObj = objc_getAssociatedObject(obj, @selector(copyNonAtomicProperty));
        NSLog(@"Retrieved Object: %@", retrievedObj);
        [mutableStr appendString:@" Modified"];
        NSLog(@"Original Mutable String: %@", mutableStr);
        NSLog(@"Retrieved Copied String: %@", retrievedObj);
    }
    return 0;
}
  1. OBJC_ASSOCIATION_RETAIN:原子性强引用关联,类似于声明属性时使用retain关键字(在ARC环境下等效于strong)且是原子性的。关联对象的引用计数会增加,并且在多线程环境下,对关联对象的访问是线程安全的。但由于原子性操作带来的额外开销,性能上会比非原子性关联略低。适用于多线程环境下需要确保关联对象访问安全的场景。
#import <objc/runtime.h>
@interface MyClass : NSObject
@end
@implementation MyClass
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        NSString *associatedObj = [[NSString alloc] initWithFormat:@"Retain Atomic String"];
        objc_setAssociatedObject(obj, @selector(retainAtomicProperty), associatedObj, OBJC_ASSOCIATION_RETAIN);
        NSString *retrievedObj = objc_getAssociatedObject(obj, @selector(retainAtomicProperty));
        NSLog(@"Retrieved Object: %@", retrievedObj);
    }
    return 0;
}
  1. OBJC_ASSOCIATION_COPY:原子性拷贝关联。关联对象会被拷贝一份,新的拷贝对象与原对象具有相同的值但不同的内存地址,并且这种关联是原子性的。适用于需要在多线程环境下保持关联对象独立副本且访问安全的场景。
#import <objc/runtime.h>
@interface MyClass : NSObject
@end
@implementation MyClass
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        NSMutableString *mutableStr = [NSMutableString stringWithString:@"Mutable String"];
        objc_setAssociatedObject(obj, @selector(copyAtomicProperty), mutableStr, OBJC_ASSOCIATION_COPY);
        NSString *retrievedObj = objc_getAssociatedObject(obj, @selector(copyAtomicProperty));
        NSLog(@"Retrieved Object: %@", retrievedObj);
        [mutableStr appendString:@" Modified"];
        NSLog(@"Original Mutable String: %@", mutableStr);
        NSLog(@"Retrieved Copied String: %@", retrievedObj);
    }
    return 0;
}

三、关联对象的实现原理

关联对象的实现依赖于Objective-C的运行时系统。在运行时,每个对象都有一个关联对象表(Associations HashMap),这个表存储了对象与其关联对象的映射关系。当我们使用objc_setAssociatedObject函数来设置关联对象时,实际上是在这个关联对象表中添加一条记录,记录包含了关联的键(通常是一个SEL,即方法选择器)、关联对象以及关联策略。

当对象被释放时,运行时系统会遍历其关联对象表,根据关联策略来处理关联对象。例如,如果关联策略是OBJC_ASSOCIATION_RETAINOBJC_ASSOCIATION_RETAIN_NONATOMIC,则会减少关联对象的引用计数;如果是OBJC_ASSOCIATION_ASSIGN,则不会对关联对象的引用计数做任何操作。

四、关联对象的使用场景

  1. 为第三方类添加属性:在开发中,我们经常会使用一些第三方库提供的类,而这些类可能没有我们需要的某些属性。通过关联对象,我们可以在运行时为这些类添加我们自己的属性,而无需修改第三方库的源码。例如,在使用UIButton类时,如果我们希望为按钮添加一个额外的标识属性,用于在某些业务逻辑中区分不同的按钮,可以通过关联对象来实现。
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@interface UIButton (CustomProperty)
@property (nonatomic, copy) NSString *customIdentifier;
@end
@implementation UIButton (CustomProperty)
- (void)setCustomIdentifier:(NSString *)customIdentifier {
    objc_setAssociatedObject(self, @selector(customIdentifier), customIdentifier, OBJC_ASSOCIATION_COPY);
}
- (NSString *)customIdentifier {
    return objc_getAssociatedObject(self, @selector(customIdentifier));
}
@end
// 使用示例
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.customIdentifier = @"Button1";
NSString *identifier = button.customIdentifier;
NSLog(@"Button Identifier: %@", identifier);
  1. 实现代理模式的简化版本:在某些情况下,我们可能不需要完整的代理模式来处理对象之间的通信,而是希望通过一种更轻量级的方式来实现类似的功能。关联对象可以用来实现这种简化的代理模式。例如,我们有一个视图控制器ViewController,它有一个子视图SubView,我们希望在SubView发生某些事件时通知ViewController。我们可以通过在SubView中关联一个指向ViewController的弱引用对象,当事件发生时,直接调用ViewController的相关方法。
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@interface SubView : UIView
@end
@implementation SubView
- (void)setDelegate:(id)delegate {
    objc_setAssociatedObject(self, @selector(delegate), delegate, OBJC_ASSOCIATION_ASSIGN);
}
- (id)delegate {
    return objc_getAssociatedObject(self, @selector(delegate));
}
- (void)someEventHappened {
    if ([self.delegate respondsToSelector:@selector(subViewDidTriggerEvent:)]) {
        [self.delegate performSelector:@selector(subViewDidTriggerEvent:) withObject:self];
    }
}
@end
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    SubView *subView = [[SubView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    subView.delegate = self;
    [self.view addSubView:subView];
}
- (void)subViewDidTriggerEvent:(SubView *)subView {
    NSLog(@"Sub - view event triggered in ViewController");
}
@end
  1. 在分类中添加属性:当我们为一个类创建分类时,无法直接在分类中声明属性。关联对象提供了一种解决方案,让我们可以在分类中添加属性的功能。例如,为NSString类创建一个分类,添加一个属性用于存储字符串的哈希值,这样可以在需要多次使用哈希值时避免重复计算。
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface NSString (HashValue)
@property (nonatomic, assign) NSUInteger cachedHash;
@end
@implementation NSString (HashValue)
- (void)setCachedHash:(NSUInteger)cachedHash {
    objc_setAssociatedObject(self, @selector(cachedHash), @(cachedHash), OBJC_ASSOCIATION_ASSIGN);
}
- (NSUInteger)cachedHash {
    NSNumber *cachedHashNumber = objc_getAssociatedObject(self, @selector(cachedHash));
    return cachedHashNumber ? cachedHashNumber.unsignedIntegerValue : 0;
}
@end
// 使用示例
NSString *str = @"Hello, World!";
NSUInteger hashValue = str.hash;
str.cachedHash = hashValue;
NSUInteger retrievedHash = str.cachedHash;
NSLog(@"Retrieved Hash: %lu", (unsigned long)retrievedHash);

五、使用关联对象的注意事项

  1. 内存管理:选择合适的关联策略至关重要,不当的关联策略可能导致内存泄漏或者悬空指针的问题。例如,如果使用OBJC_ASSOCIATION_ASSIGN关联一个对象,而该对象在其他地方被释放,那么在访问关联对象时可能会导致程序崩溃。因此,在使用关联对象时,需要仔细考虑关联对象的生命周期以及内存管理方式。
  2. 多线程问题:如果在多线程环境下使用关联对象,需要注意关联策略的原子性。非原子性的关联策略(如OBJC_ASSOCIATION_RETAIN_NONATOMICOBJC_ASSOCIATION_COPY_NONATOMIC)在多线程访问时可能会出现竞争条件,导致数据不一致。如果需要在多线程环境下安全地访问关联对象,应选择原子性的关联策略(如OBJC_ASSOCIATION_RETAINOBJC_ASSOCIATION_COPY)。
  3. 关联键的唯一性:在设置关联对象时,关联键(通常是SEL)需要保证唯一性。如果不同的地方使用相同的关联键来关联不同的对象,可能会导致数据覆盖等问题。通常建议使用一个唯一的SEL,例如通过NSStringFromSelector等方法生成一个唯一的字符串,再转换为SEL来作为关联键。
  4. 性能影响:虽然关联对象提供了很大的灵活性,但频繁地设置和获取关联对象会带来一定的性能开销。因为每次操作都需要访问关联对象表,进行查找、添加或删除操作。在性能敏感的代码中,需要谨慎使用关联对象,或者对关联对象的使用进行优化,例如缓存关联对象的获取结果等。

六、关联对象与其他类似特性的比较

  1. 与继承的比较:继承是一种在编译时确定的关系,通过继承,子类可以获得父类的属性和方法,并可以进行扩展。而关联对象是在运行时动态添加属性,不需要继承关系。继承适用于对象之间有明确的“是一种”关系的场景,例如UIButtonUIControl的一种,UIButton可以继承UIControl的属性和方法。而关联对象更适用于在不改变类的继承结构的前提下,为对象添加额外的行为或数据,例如为UIButton添加一个自定义的标识属性,而不需要通过继承来实现。
  2. 与组合的比较:组合是将一个对象作为另一个对象的属性,以实现对象之间的功能组合。与关联对象相比,组合是在类的定义阶段就确定了对象之间的关系,而关联对象是在运行时动态建立关系。例如,我们有一个Car类和一个Engine类,通过组合,Car类可以包含一个Engine对象作为属性。而关联对象则可以在运行时为Car对象关联一个额外的GPS导航对象,即使Car类在定义时并没有包含GPS相关的属性。
  3. 与属性的比较:常规的属性是在类的定义中声明,编译器会为属性生成相应的实例变量、存取方法等。而关联对象是在运行时动态添加,不需要在类的定义中预先声明。属性的优点是在编译时进行类型检查和自动生成存取方法,代码结构更清晰。关联对象则更灵活,适用于无法在编译时确定属性需求的场景,或者为已有的类添加属性而不修改其源码。

七、关联对象在实际项目中的案例分析

  1. 电商项目中的商品展示:在一个电商应用中,有一个展示商品的ProductCell类,它继承自UITableViewCell。假设我们使用的是一个第三方的商品数据模型类ThirdPartyProductModel,这个类没有提供一个属性来存储商品是否在当前用户的收藏列表中。为了实现商品收藏功能,我们可以通过关联对象为ProductCell添加一个属性来标记商品是否被收藏。
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@interface ProductCell : UITableViewCell
@property (nonatomic, strong) ThirdPartyProductModel *productModel;
@end
@implementation ProductCell
- (void)setIsFavorite:(BOOL)isFavorite {
    objc_setAssociatedObject(self, @selector(isFavorite), @(isFavorite), OBJC_ASSOCIATION_ASSIGN);
}
- (BOOL)isFavorite {
    NSNumber *isFavoriteNumber = objc_getAssociatedObject(self, @selector(isFavorite));
    return isFavoriteNumber ? isFavoriteNumber.boolValue : NO;
}
@end
// 在视图控制器中使用
@interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSMutableArray<ThirdPartyProductModel *> *productModels;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 初始化tableView和productModels
    [self.tableView registerClass:[ProductCell class] forCellReuseIdentifier:@"ProductCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ProductCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ProductCell" forIndexPath:indexPath];
    ThirdPartyProductModel *model = self.productModels[indexPath.row];
    cell.productModel = model;
    // 假设通过网络请求或本地存储判断商品是否被收藏
    BOOL isFavorite = [self isProductFavorite:model];
    cell.isFavorite = isFavorite;
    return cell;
}
@end
  1. 社交应用中的用户资料扩展:在一个社交应用中,用户资料页面使用UserProfileViewController来展示用户信息。假设用户信息使用的是一个基础的UserModel类,这个类由后端团队提供,并且在应用的很多地方都在使用,不方便直接修改其源码。现在我们希望为用户添加一个自定义的“个性签名”属性,并且在用户资料页面展示。我们可以通过关联对象来实现。
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@interface UserModel (CustomSignature)
@property (nonatomic, copy) NSString *customSignature;
@end
@implementation UserModel (CustomSignature)
- (void)setCustomSignature:(NSString *)customSignature {
    objc_setAssociatedObject(self, @selector(customSignature), customSignature, OBJC_ASSOCIATION_COPY);
}
- (NSString *)customSignature {
    return objc_getAssociatedObject(self, @selector(customSignature));
}
@end
@interface UserProfileViewController : UIViewController
@property (nonatomic, strong) UserModel *userModel;
@end
@implementation UserProfileViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 假设从服务器获取用户模型
    self.userModel = [self fetchUserModelFromServer];
    // 设置或获取用户个性签名
    NSString *signature = self.userModel.customSignature;
    if (!signature) {
        signature = @"默认个性签名";
        self.userModel.customSignature = signature;
    }
    // 在界面上展示个性签名
    UILabel *signatureLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 100, 200, 30)];
    signatureLabel.text = signature;
    [self.view addSubview:signatureLabel];
}
@end

八、关联对象的未来发展与潜在优化

随着Objective-C语言的发展以及运行时系统的不断完善,关联对象的性能和使用方式可能会得到进一步的优化。例如,未来可能会出现更高效的关联对象表数据结构,以减少设置和获取关联对象时的时间复杂度。同时,在内存管理方面,可能会有更智能的机制来根据对象的实际使用情况自动调整关联策略,从而进一步减少内存泄漏和悬空指针等问题的发生。

在使用方式上,可能会提供更简洁的语法糖,让开发者可以更方便地使用关联对象,而不需要手动调用objc_setAssociatedObjectobjc_getAssociatedObject等函数。例如,类似于属性的声明方式,通过某种修饰符来声明一个关联属性,编译器在后台自动处理关联对象的设置和获取逻辑。

另外,随着多线程编程和并发编程在移动应用开发中的应用越来越广泛,关联对象在多线程环境下的性能和安全性也将成为优化的重点方向。可能会出现更细粒度的线程安全控制机制,让开发者可以根据具体的需求选择最合适的线程安全策略,而不是仅仅依赖于原子性和非原子性的关联策略。

总之,关联对象作为Objective-C语言中一个强大而灵活的特性,在未来的开发中有望发挥更大的作用,同时也需要开发者不断关注其发展和优化,以更好地应用于实际项目中。