什么是objc_setAssociatedObject(),在哪些情况下应该使用它?

77
在我接手的一个项目中,原作者选择使用了objc_setAssociatedObject(),但我不完全清楚它是做什么,以及他们为什么决定使用它。我决定查找一下,不幸的是,文档对它的目的描述得不够清楚。 objc_setAssociatedObject
使用给定的键和关联策略为给定对象设置关联值。
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
参数
object
关联值源对象。
key
关联的键。
value
与对象的键关联的值。传递nil以清除现有关联。
policy
关联策略。可能的值,请参见“关联对象行为”。
那么这个函数到底是做什么的?在什么情况下应该使用它呢?

阅读答案后编辑

所以下面这段代码的意义是什么?
Device *device = [self.list objectAtIndex:[indexPath row]];
DeviceViewController *next = [[DeviceViewController alloc] initWithController:self.controller
                                                                            device:device
                                                                               item:self.rootVC.selectedItem];  
    objc_setAssociatedObject(device, &kDeviceControllerKey, next, OBJC_ASSOCIATION_RETAIN);

如果设备已经是实例变量,那么将其与视图控制器关联的意义是什么?

4个回答

73

objc_setAssociatedObject会为每个Objective-C对象添加一个键值存储,它允许您为对象存储附加状态,这些状态不反映在其实例变量中。

当您想要将某些属于对象的内容存储在主要实现之外时,这非常方便。其中一个主要用例是在无法添加实例变量的类别中。在这里,您可以使用objc_setAssociatedObject将附加变量附加到self对象。

如果使用正确的关联策略,则在主对象被释放时也会释放您的对象。


感谢您的解释。在阅读了这篇文章之后,我开始怀疑代码是否以有用的方式使用该函数。我编辑了问题并包含了代码。 - Jasarien
在这种情况下,我们不能使用字典来保留对象吗? - AppleBee
1
那就是 objc_setAssociatedObject 的语义含义。这种机制的优点在于它可以在不更改类(即不添加实例变量)的情况下执行。因此,您可以在框架类或其他您不拥有源代码的情况下使用它。 - Nikolai Ruhe
嗨@NikolaiRuhe - 你可能知道这个问题的完整答案/解释:https://dev59.com/h57ha4cB1Zd3GeqPkH57 有点棘手 :/ - Fattie

37
Objective-C Runtime Reference的参考文档中可知:
使用Objective-C运行时函数objc_setAssociatedObject可将一个对象与另一个对象进行关联。该函数有四个参数:源对象、键、值和关联策略常量。键是一个空指针。
  • 每个关联的键必须是唯一的,可采用静态变量的典型模式。
  • 策略指定关联对象是分配、保留还是复制,并且关联是原子性的还是非原子性的。这种模式类似于声明属性的属性(请参阅“属性声明属性”)。您可以使用常量(请参阅objc_AssociationPolicy和关联对象行为)为关系指定策略。
建立一个数组和字符串之间的关联。
static char overviewKey;



NSArray *array =

    [[NSArray alloc] initWithObjects:@"One", @"Two", @"Three", nil];

// For the purposes of illustration, use initWithFormat: to ensure

// the string can be deallocated

NSString *overview =

    [[NSString alloc] initWithFormat:@"%@", @"First three numbers"];



objc_setAssociatedObject (

    array,

    &overviewKey,

    overview,

    OBJC_ASSOCIATION_RETAIN

);



[overview release];

// (1) overview valid

[array release];

// (2) overview invalid

在第一点,字符串“overview”仍然是有效的,因为“OBJC_ASSOCIATION_RETAIN”策略指定数组保留相关对象。然而,当数组被释放时(在第二点),“overview”被释放,所以在这种情况下也被销毁。如果您尝试记录“overview”的值,您将生成运行时异常。


谢谢你的解释。现在我有疑问,这个函数所在的代码是否健康可靠...我已经编辑了问题,包括了代码。 - Jasarien
如果y是一个ivar,那么它使用Device *device = [self.list objectAtIndex:[indexPath row]]; DeviceViewController *next = [[DeviceViewController alloc].... - visakh7
1
这就是我要问的...如果它已经是一个ivar,为什么你还要这样做呢?(老实说,我没有写过这段代码。) - Jasarien
3
目前仍不清楚在哪些情况下使用这种方法会更有用/必要,与已存在的retain/release/autorelease等方法相比较起来。 - Nicolas Miari
苹果文档可以在这里阅读。http://web.archive.org/web/20120818164935/http://developer.apple.com/library/ios/#/web/20120820002100/http://developer.apple.com/library/ios/documentation/cocoa/conceptual/objectivec/Chapters/ocAssociativeReferences.html - cnotethegr8
显示剩余5条评论

30

这里是关于对象关联的用例列表:

一:为类别添加实例变量。一般来说,这种技术是不建议使用的,但这里有一个合法的例子。假设您想模拟无法修改的对象的附加实例变量(我们谈论的是修改对象本身,即不使用子类化)。比如在UIImage上设置标题。

// UIImage-Title.h:
@interface UIImage(Title)
@property(nonatomic, copy) NSString *title;
@end 

// UIImage-Title.m:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

static char titleKey;

@implementation UIImage(Title)
- (NSString *)title
{
    return objc_getAssociatedObject(self, &titleKey);
}

- (void)setTitle:(NSString *)title
{
    objc_setAssociatedObject(self, &titleKey, title, OBJC_ASSOCIATION_COPY);
}
@end

此外,这里是一种相当复杂(但非常棒)的使用关联对象与类别的方式...它基本上允许您传递一个块而不是选择器到UIControl中。

第二步: 结合KVO动态地向对象添加状态信息,这些信息不被其实例变量所覆盖。

这个想法是你的对象只在运行时(即动态地)获得状态信息。因此,虽然你可以将这个状态信息存储在实例变量中,但你将这个信息附加到在运行时实例化的对象上,并动态地将它与其他对象关联起来,从而突出了这是对象的动态状态。

一个很好的例子是this库,其中使用关联对象和KVO通知。以下是代码摘录(注意:这个KVO通知并不是运行该库所必需的,作者只是为了方便而放置在那里,基本上任何注册到它的对象都会通过KVO被通知到它发生了变化):

static char BOOLRevealing;

- (BOOL)isRevealing
{
    return [(NSNumber*)objc_getAssociatedObject(self, &BOOLRevealing) boolValue];
} 

- (void)_setRevealing:(BOOL)revealing
{
    [self willChangeValueForKey:@"isRevealing"];
    objc_setAssociatedObject(self, &BOOLRevealing, 
       [NSNumber numberWithBool:revealing], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self didChangeValueForKey:@"isRevealing"];
}

额外福利:看看这篇由 AFNetworking 库的作者 Mattt Thompson 所撰写的discussion/explanation有关相关对象的讨论文章。


你的回答中的第一和第二个原因是相同的。它们都通过类别向对象添加实例变量。 - Jasarien
@Jasarien 对不起,我把它们合并了。 - abbood
这真的很糟糕,编译器应该允许在类别中合成属性。 - malhal
非常抱歉,我不太明白为什么在这里提到了KVO。因为它似乎与associatedObject没有任何关系... - Calios
@Calios 提到 KVO 是因为 UIImage 现在具有可以绑定的 isRevealing 属性。 - Elise van Looij

5
回答你修改后的问题:
如果已经是实例变量,为什么还要将设备与视图控制器关联?
有几个原因需要这样做。
- 设备类没有控制器实例变量或属性,并且您无法更改它或对其进行子类化(例如,您没有源代码)。 - 您希望将两个控制器与设备对象关联起来,但您无法更改设备类或对其进行子类化。
个人认为,需要使用低级Objective-C运行时函数的情况非常罕见。 在我看来,这似乎是一种代码异味。

这次你比我快了 :P。 - visakh7
2
我认为我同意你的看法。这确实有点不对劲。我完全打算好好做这件事。感谢澄清。 - Jasarien

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接