避免为关联对象键使用额外的静态变量

23

在使用关联对象时,这是一种从iOS 4和OSX 10.6开始提供的Objective-C运行时特性,需要定义一个关键字来在运行时存储和检索对象。

典型的用法是像下面这样定义关键字

static char const * const ObjectTagKey = "ObjectTag";

然后使用它来存储对象

objc_setAssociatedObject(self, ObjectTagKey, newObjectTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

并检索它

objc_getAssociatedObject(self, ObjectTagKey);

(示例来自http://oleb.net/blog/2011/05/faking-ivars-in-objc-categories-with-associative-references/)

有没有更简洁的方法来定义关联对象键,而不涉及声明额外变量?

3个回答

50
根据Erica Sadun在这篇博客文章中(其来源是Gwynne Raskind),我们需要在使用objc_getAssociatedObjectobjc_setAssociatedObject 时提供一个用于存储对象的键,该键必须是常量void指针。所以我们只需要一个固定的地址,在时间上保持不变即可。
事实证明,@selector实现提供了我们所需的几乎一切,因为它使用固定的地址。
因此,我们可以摆脱键声明,直接使用属性的选择器地址。
因此,如果您要在运行时关联一个属性,例如:
@property (nonatomic, retain) id anAssociatedObject;

我们可以为其getter/setter提供动态实现,看起来像这样:
- (void)setAnAssociatedObject:(id)newAssociatedObject {
     objc_setAssociatedObject(self, @selector(anAssociatedObject), newAssociatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)anAssociatedObject {
    return objc_getAssociatedObject(self, @selector(anAssociatedObject));
}

非常整洁,肯定比为每个关联对象定义额外的静态变量键更干净。
这样做是否安全?
由于这取决于实现,一个合理的问题是:它会很容易出错吗? 引用博客文章
“要发生这种情况,苹果可能需要实现完全新的ABI。”
如果我们认为这些话是真实的,那么就相当安全。

1
有时候代码很整洁,有时候却很混乱。你可能会搞砸它,但我见过许多运行良好的优秀代码的例子。 - Gabriele Petronella
8
objc_{set, get}AssociatedObject() 是使用 Objective-C 运行时非常安全的部分。 - zadr
这有点离题。也许这个问题更适合这样的离题 :) - Gabriele Petronella
示例代码中getter方法缺少返回语句 - 应该是return objc_getAssociatedObject(self, @selector(anAssociatedObject)); - Flexicoder
只想说,正如上面提到的那样,没有100%保证这个方法能够工作。例如,截至2014年4月4日,使用该技术在NSURLSessionTask上将不起作用:https://dev59.com/l2Ei5IYBdhLWcg3wHJAR,更多信息也在这里:https://devforums.apple.com/message/838955#838955(需要苹果开发者登录)。 - Victor
显示剩余3条评论

6

如果你需要在单个方法的作用域之外访问密钥,一种很好的模式是创建一个指向堆栈中自己地址的指针,这将导致更可读的代码。例如:

static void const *MyAssocKey = &MyAssocKey;

如果您只需要在单个方法的范围内访问,实际上可以直接使用_cmd,这是保证唯一的。例如:

objc_setAssociatedObject(obj, _cmd, associatedObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

5
一个稍微变化的想法是,为每个对象关联一个字典,与Gabriele Petronella讨论的想法相似。
//NSObject+ADDLAssociatedDictionary.h

#import <Foundation/Foundation.h>

@interface NSObject (ADDLAssociatedDictionary)
- (void)addl_setAssociatedObject:(id)object forKey:(id<NSCopying>)key;
- (id)addl_associatedObjectForKey:(id<NSCopying>)key;
@end

//NSObject+ADDLAssociatedDictionary.m

#import <objc/runtime.h>

@interface NSObject (ADDLAssociatedDictionaryInternal)
- (NSMutableDictionary *)addl_associatedDictionary;
@end

@implementation NSObject (ADDLAssociatedDictionary)

- (void)addl_setAssociatedObject:(id)object forKey:(id<NSCopying>)key
{
    if (object) {
        self.addl_associatedDictionary[key] = object;
    } else {
        [self.addl_associatedDictionary removeObjectForKey:key];
    }
}
- (id)addl_associatedObjectForKey:(id<NSCopying>)key
{
    return self.addl_associatedDictionary[key];
}

@end

@implementation NSObject (ADDLAssociatedDictionaryInternal)
const char addl_associatedDictionaryAssociatedObjectKey;
- (NSMutableDictionary *)addl_associatedDictionaryPrimitive
{
    return objc_getAssociatedObject(self, &addl_associatedDictionaryAssociatedObjectKey);
}
- (void)addl_setAssociatedDictionaryPrimitive:(NSMutableDictionary *)associatedDictionary
{
    objc_setAssociatedObject(self, &addl_associatedDictionaryAssociatedObjectKey, associatedDictionary, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSMutableDictionary *)addl_generateAssociatedDictionary
{
    NSMutableDictionary *associatedDictionary = [[NSMutableDictionary alloc] init];
    [self addl_setAssociatedDictionaryPrimitive:associatedDictionary];
    return associatedDictionary;
}

- (NSMutableDictionary *)addl_associatedDictionary
{
    NSMutableDictionary *res = nil;

    @synchronized(self) {
        if (!(res = [self addl_associatedDictionaryPrimitive])) {
            res = [self addl_generateAssociatedDictionary];
        }
    }

    return res;
}
@end

然后,在我们的某个NSObject的子类Derived中:

//Derived+Additions.h

#import "Derived.h"

@interface Derived (Additions)
@property (nonatomic) id anAssociatedObject;
@end

//Derived+Additions.m

#import "NSObject+ADDLAssociatedDictionary.h"

@implementation Derived (Additions)
- (void)setAnAssociatedObject:(id)anAssociatedObject
{
    [self addl_setAssociatedObject:anAssociatedObject forKey:NSStringFromSelector(@selector(anAssociatedObject))];
}
- (id)anAssociatedObject
{
    return [self addl_associatedObjectForKey:NSStringFromSelector(@selector(anAssociatedObject))];
}
@end

使用关联字典的好处之一在于它增加了灵活性,可以设置在运行时生成的键的对象,而且语法更加简洁易懂。

特别是使用时的一项好处是:

NSStringFromSelector(@selector(anAssociatedObject))

这意味着 NSStringFromSelector 保证提供的 NSString 表示选择器,将始终是可接受的字典键。 因此,我们不必担心ABI更改(尽管我认为这不是一个合理的担忧)。


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