【iOS】关联对象详解

前言

associatedObject又称关联对象。顾名思义,就是把一个对象关联到另外一个对象身上。使两者能够产生联系。目前我能想到的关联对象的使用场景有如下几点:

  • 运行时给cagetory添加getter和setter。因为category中添加的property不会生成带下划线"_"的成员变量以及getter和setter的实现。所以可以通过关联对象实现getter和setter。
  • 有时需要在对象中存储一些额外的信息,我们通常会从对象所属的类中继承一个子类。然后给这个子类添加额外的属性,改用这个子类。然而并非所有的情况都能这么做,有时候类的实例可能是由某种机制创建的,而开发者无法另这种机制创建出自己所写的子类实例。此时可以使用“关联对象”。
  • 有时只是给某个类添加一个额外的属性,完全没有必要继承出来一个子类。此时可以使用“关联对象”。
  • delegate回调的方法中使用关联对象。有时候在一些delegate回调的方法中需要处理一些回调任务。比如发起网络请求和在delegate回调的方法中做UI的更新。这样一来,发起网络请求和在回调中更新UI的代码被分散到了两个地方,不利于管理和阅读。此时可以使用“关联对象”。

关联对象可以给某个object关联一个或者多个其他对象,这些对象通过“键”来区分,我们可以通过键给这个object绑定一个对象,也可以通过键获取object绑定的对象。object身上的一个键就对应一个关联对象。所以我们可以给object关联多个关联对象。我们可以把这个object想象成一个字典。把关联到该object的对象理解为字典中的value,这些value通过key来存或取。存取关联对象的值就相当于在字典对象上调用[object setObject:value forKey:key]与[object objectForKey:key]方法。然而两者之间有个重要差别:如果两个键指针不同,但是值相同,在这两个键上调用“isEqual:”方法的返回值是YES,那么字典也认为二者相等。(详情可参考笔者的《浅析对象等同性判断》)。设置关联对象时用的键是个“不透明指针(opaque pointer)”。在设置关联对象时,若想另两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,存取关联对象用的key通常是一个静态全局变量。

注意
存取关联对象用的key通常是一个静态全局变量。
使用关联对象必须导入#import <objc/runtime.h>框架。

关联对象的存储需要指明存储策略。和property类似,objc以枚举的方式提供了存储策略。本质上就是关联对象的内存管理语义。如下:

/* Associative References */

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

  • OBJC_ASSOCIATION_ASSIGN 相当于@property的assign
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC相当于@property的nonatomic + retain
  • OBJC_ASSOCIATION_COPY_NONATOMIC相当于@property的nonatomic + copy
  • OBJC_ASSOCIATION_RETAIN相当于@property的retain
  • OBJC_ASSOCIATION_COPY相当于@property的copy

关联对象的管理函数

使用下面3个函数管理关联对象(存储、获取、移除):

/** 
 * Sets an associated value for a given object using a given key and association policy.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * @param value The value to associate with the key key for object. Pass nil to clear an existing association.
 * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

/** 
 * Removes all associations for a given object.
 * 
 * @param object An object that maintains associated objects.
 * 
 * @note The main purpose of this function is to make it easy to return an object 
 *  to a "pristine state”. You should not use this function for general removal of
 *  associations from objects, since it also removes associations that other clients
 *  may have added to the object. Typically you should use \c objc_setAssociatedObject 
 *  with a nil value to clear an association.
 * 
 * @see objc_setAssociatedObject
 * @see objc_getAssociatedObject
 */
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

// 关联对象
// 使用objc_setAssociatedObject函数可以给某个对象关联其他的对象。
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

// 获取关联的对象
// 使用objc_getAssociatedObject函数可以通过键来取出某个对象的关联对象。
id objc_getAssociatedObject(id object, const void *key)

// 移除关联的对象
// 使用objc_removeAssociatedObjects函数可以移除某个对象身上的所有关联的对象。
void objc_removeAssociatedObjects(id object)

注意:void objc_removeAssociatedObjects(id object)函数移除的是某个对象身上的所有关联的对象。objc没有给我们提供移除object身上单个关联对象的函数,所以,我们一般通过objc_setAssociatedObject函数传入nil来达到移除某个关联对象的目的。如下:

void objc_setAssociatedObject(object, key, nil, policy);

用法举例

给category的property添加getter和setter

众所周知,category中虽然可以添加属性,但是并不会生成带下划线的成员变量,也不会生成getter和setter的实现(详情参考笔者的《【iOS】Category VS Extension 原理详解》)。我们可以通过关联对象这个技术为category中的属性添加getter和setter,代码如下:

// EOCPerson 的BaseInfo category
#import "EOCPerson.h"

@interface EOCPerson (BaseInfo)
// 给category添加一个name属性
@property(nonatomic,copy) NSString *name;
@end

设置关联对象后可以成功访问getter和setter,代码以及输出结果如下图:

image

delegate回调的方法中使用关联对象

《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》的item10中介绍了使用关联对象的一种场景。
以下摘抄自《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》。

开发iOS时经常用到UIAlertView类,该类提供了一种标准视图,可向用户展示警告信息。当用户按下按钮关闭该视图时,需要用委托协议(delegate protocol)来处理此动作,但是,要想设置好这个委托机制,就得把创建警告视图和处理按钮动作的代码分开。由于代码分作两块,所以读起来有点乱。比方说,我们在使用UIAlertView时,一般都会这么写:

    - (void)askUserAQuestion {  
        UIAlertView *alert = [[UIAlertView alloc]  
                                 initWithTitle:@"Question"  
                                   message:@"What do you want to do?"  
                                     delegate:self  
                            cancelButtonTitle:@"Cancel"  
                            otherButtonTitles:@"Continue", nil];  
            [alert show];  
    }  

    // UIAlertViewDelegate protocol method  
    - (void)alertView:(UIAlertView *)alertView  
            clickedButtonAtIndex:(NSInteger)buttonIndex  
    {  
        if (buttonIndex == 0) {  
            [self doCancel];  
        } else {  
            [self doContinue];  
        }  
    } 

如果想在同一个类里处理多个警告信息视图,那么代码就会变得更为复杂,我们必须在delegate方法中检查传入的alertView参数,并据此选用相应的逻辑。要是能在创建警告视图的时候直接把处理每个按钮的逻辑都写好,那就简单多了。这可以通过关联对象来做。创建完警告视图之后,设定一个与之关联的“块”(block),等到执行delegate方法时再将其读出来。此方案的实现代码如下:

    #import <objc/runtime.h> 

    static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";  

    - (void)askUserAQuestion {  
        UIAlertView *alert = [[UIAlertViewalloc]  
                                 initWithTitle:@"Question"  
                                   message:@"What do you want to do?"  
                                      delegate:self  
                            cancelButtonTitle:@"Cancel"  
                            otherButtonTitles:@"Continue", nil];  

            void (^block)(NSInteger) = ^(NSInteger buttonIndex){  
              if (buttonIndex == 0) {  
                  [self doCancel];  
            } else {  
                [self doContinue];  
            }  
        };  

          objc_setAssociatedObject(alert,  
                                   EOCMyAlertViewKey,  
                                   block,  
    O                              BJC_ASSOCIATION_COPY);  

          [alert show];  
    }  

    // UIAlertViewDelegate protocol method  
    - (void)alertView:(UIAlertView*)alertView  
            clickedButtonAtIndex:(NSInteger)buttonIndex  
    {  
        void (^block)(NSInteger) =  
            objc_getAssociatedObject(alertView, EOCMyAlertViewKey);  
        block(buttonIndex);  
    } 

结尾

关联对象虽然好用,但不要滥用,开发者经常会陷入一种困境:正在学习或者刚刚学完某个技术,就急于在项目中使用,却忽略了场景。过多的使用关联对象将会降低代码的可读性和维护性,同时也会增大调试的难度。我们要谨慎的使用关联对象的内存管理策略,知道什么时候使用OBJC_ASSOCIATION_RETAIN_NONATOMIC什么时候使用OBJC_ASSOCIATION_ASSIGN,避免出现循环引用和一些奇怪的现象。

文/VV木公子(简书作者)
PS:如非特别说明,所有文章均为原创作品,著作权归作者所有,转载请联系作者获得授权,并注明出处!

如果您是iOS开发者,或者对本篇文章感兴趣,请关注本人,后续会更新更多相关文章!敬请期待!

参考文章

Objective-C的Category与关联对象实现原理
iOS Runtime之四:关联对象
《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》

作者:VV木公子
链接:https://www.jianshu.com/p/bf51e9d52188
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容