iOS的RunTime:關(guān)聯(lián)變量是如何實(shí)現(xiàn)的?objc_setAssociatedObject和objc_getAssociatedObject

runtime中一個(gè)重要功能就是關(guān)聯(lián)對(duì)象。作用就是運(yùn)行時(shí)動(dòng)態(tài)的給對(duì)象添加變量扔茅。

本文分三個(gè)部分介紹關(guān)聯(lián)變量:

1. 關(guān)聯(lián)變量是如何實(shí)現(xiàn)。

???那么runtime是如何實(shí)現(xiàn)動(dòng)態(tài)添加的呢秸苗?直接上結(jié)論
關(guān)聯(lián)對(duì)象并沒(méi)有存放在對(duì)象的實(shí)體中召娜,而是runtime維護(hù)了一個(gè)全局二維map來(lái)管理所有關(guān)聯(lián)對(duì)象。
如果你對(duì)iOS實(shí)現(xiàn)過(guò)程不敢興趣惊楼,那么你可以關(guān)閉這一篇文章了玖瘸。但是如果你想了解可以繼續(xù)看下去
來(lái)看看runtime源碼,objc_setAssociatedObject的具體實(shí)現(xiàn)

id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}

//關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
//移除關(guān)聯(lián)對(duì)象
void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

通過(guò)這兩個(gè)方法就可以輕易的找到關(guān)聯(lián)對(duì)象的實(shí)際調(diào)用方法是_object_get_associative_reference和_object_set_associative_reference

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
函數(shù)中涉及幾個(gè)4個(gè)重要的數(shù)據(jù)結(jié)構(gòu):

??1.AssociationsManager //管理全局AssociationsHashMap
??2.AssociationsHashMap //存放對(duì)象的關(guān)聯(lián)對(duì)象map的map(key為傳入的object檀咙,value為map雅倒,也就是ObjectAssociationMap)
??3.ObjectAssociationMap //存放關(guān)聯(lián)對(duì)象的map(key為傳入的key,value為關(guān)聯(lián)對(duì)象)
??4.ObjcAssociation //關(guān)聯(lián)對(duì)象實(shí)體包含了value和policy兩個(gè)重要信息(policy決定了value的內(nèi)存管理方式)
文字介紹可能比較繞弧可,引用一下另一位間書(shū)大神的文章的圖
http://www.reibang.com/p/4b463169a84a

5796542-048198c289ac366b.png

四個(gè)結(jié)構(gòu)圖關(guān)系一目了然蔑匣。所以:
runtime中維護(hù)了一個(gè)全局二維map來(lái)管理關(guān)聯(lián)對(duì)象。

從源碼可以發(fā)現(xiàn)以下幾點(diǎn)信息:
1.從源碼中可以看到如果傳入value是nil,會(huì)把對(duì)應(yīng)的關(guān)聯(lián)變量為key的相關(guān)數(shù)據(jù)也就是ObjcAssociation從這個(gè)變量的字典中移除
2.一個(gè)對(duì)象可以關(guān)聯(lián)多個(gè)對(duì)象裁良。
3.所有對(duì)象的關(guān)聯(lián)變量信息都在要一個(gè)AssociationsHashMap 里凿将。

來(lái)看看_object_remove_assocations的源碼:

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}

從源碼可以看出也就是移除了對(duì)象的ObjectAssociationMap這個(gè)二級(jí)map。


image.png

查看這個(gè)方法的調(diào)用价脾,可以看到在NSObject的dealloc的時(shí)候會(huì)自動(dòng)調(diào)用這個(gè)方法牧抵,移除該變量所有關(guān)聯(lián)信息。

2. 關(guān)聯(lián)變量的內(nèi)存管理方式彼棍。

???iOS關(guān)于關(guān)聯(lián)變量的內(nèi)存管理有以下幾種方式:

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. */
};

這里需要謹(jǐn)慎使用OBJC_ASSOCIATION_ASSIGN灭忠,因?yàn)樵陉P(guān)聯(lián)變量被釋放之后,再次取這個(gè)變量會(huì)引起crash座硕。OBJC_ASSOCIATION_ASSIGN并不會(huì)持有變量類似于_unsafe_unreatin的內(nèi)存管理方式弛作,所以這個(gè)變量釋放之后對(duì)應(yīng)的關(guān)聯(lián)對(duì)象體并沒(méi)有沒(méi)移除,而變量指針會(huì)成為野指針华匾。

- (void)associatiedTest
{
    {
        NSObject* associatedObject = NSObject.new;
        objc_setAssociatedObject(self, @selector(associatiedTest), associatedObject, OBJC_ASSOCIATION_ASSIGN);
        id obj = objc_getAssociatedObject(self, @selector(associatiedTest));
    }
    id obj = objc_getAssociatedObject(self, @selector(associatiedTest));
}

這里最后一行取關(guān)聯(lián)變量的時(shí)候就會(huì)崩潰映琳,因?yàn)閍ssociatedObject已經(jīng)超出作用區(qū)被釋放掉了。
關(guān)于關(guān)聯(lián)變量的內(nèi)存管理在代碼中只能看到如下代碼:

//對(duì)關(guān)聯(lián)的變量做內(nèi)存管理
static id acquireValue(id value, uintptr_t policy) {
    switch (policy & 0xFF) {
    case OBJC_ASSOCIATION_SETTER_RETAIN:
        return ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
    case OBJC_ASSOCIATION_SETTER_COPY:
        return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
    }
    return value;
}

也就是對(duì)變量發(fā)送SEL_retain或者SEL_copy消息蜘拉。但是具體怎么實(shí)現(xiàn)的原子操作我并沒(méi)有找到萨西,如果有知道這里具體怎么實(shí)現(xiàn)的小伙伴麻煩留言告訴我。

3. 如何實(shí)現(xiàn)weak特性的關(guān)聯(lián)變量

1.最佳方案:利用block捕獲特性和__weak變量特性

#import "NSObject+Weak.h"
#import <objc/runtime.h>

typedef id(^WeakWrapperBlock)(void);
@implementation NSObject (Weak)

- (id)weakAssObject {
    WeakWrapperBlock wrapper = objc_getAssociatedObject(self, @selector(weakAssObject));
    return wrapper ? wrapper() : nil;
}

- (void)setWeakAssObject:(id)obj {
    id __weak weakObject = obj;
    WeakWrapperBlock wrap = ^{
        return weakObject;
    };
    objc_setAssociatedObject(self, @selector(weakAssObject), wrap, OBJC_ASSOCIATION_COPY);
}
@end

利用__weak變量會(huì)在對(duì)象dealloc銷(xiāo)毀之后置空的特性旭旭。用Block去捕獲這個(gè)__weak變量谎脯,然后把block設(shè)置成關(guān)聯(lián)變量,注意這里要用Copy持寄,因?yàn)閏opy會(huì)把block拷貝到堆上源梭。
這樣實(shí)現(xiàn)只是模擬了weak特性,在對(duì)象釋放后通過(guò)getter方法獲取到的值是空稍味。但是废麻,實(shí)際上這個(gè)關(guān)聯(lián)變量并沒(méi)有被置為空。
類似于用block的方式模庐,還可以用于一個(gè)中間容器實(shí)現(xiàn)烛愧,實(shí)現(xiàn)原理類似:
2.用中間容器實(shí)現(xiàn)

///WeakAssociatedObjectWrapper.h
@interface WeakAssociatedObjectWrapper : NSObject
@property (nonatomic, weak) id weakObject;
@end
@implementation WeakAssociatedObjectWrapper

@end


#import "WeakAssociatedObjectWrapper.h"
#import <objc/runtime.h>

@implementation NSObject (WeakAssociatedObject)
- (NSObject *)assObject {
    id object = objc_getAssociatedObject(self, @selector(assObject));
    if ([object isKindOfClass:WeakAssociatedObjectWrapper.class]) {
        if ([(WeakAssociatedObjectWrapper*)object weakObject] == NULL) {
            objc_setAssociatedObject(self, @selector(assObject), NULL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        return [(WeakAssociatedObjectWrapper*)object weakObject];
    }
    return object;
}

- (void)setAssObject:(NSObject *)assObject {
    
    if (assObject) {
        WeakAssociatedObjectWrapper* wrapper = objc_getAssociatedObject(self, @selector(assObject));
        if (!wrapper) {
            WeakAssociatedObjectWrapper* wrapper = WeakAssociatedObjectWrapper.new;
            objc_setAssociatedObject(self, @selector(assObject), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        wrapper.assObject = assObject;
    }
}
@end

這種方式其實(shí)和block原理類似,只是實(shí)現(xiàn)了一個(gè)wrapper替代block掂碱,wrapper內(nèi)有一個(gè)weak屬性怜姿。用把關(guān)聯(lián)變量賦值給這個(gè)weak屬性。
這么實(shí)現(xiàn)的問(wèn)題疼燥,同樣是在關(guān)聯(lián)變量釋放時(shí)社牲,wrapper并沒(méi)有為空,只有在通過(guò)getter取值的時(shí)候悴了,會(huì)返回空搏恤,同時(shí)發(fā)現(xiàn)變量已經(jīng)為空了违寿,再去通過(guò)objc_setAssociatedObject把wrapper置為空。

4. 利用關(guān)聯(lián)變量監(jiān)控一個(gè)對(duì)象的釋放

實(shí)現(xiàn)一個(gè)方法熟空,對(duì)傳入的對(duì)象進(jìn)行監(jiān)控藤巢,在對(duì)象釋放時(shí),調(diào)用一個(gè)block息罗。

@interface ObjectDeallocMonitor : NSObject
- (instancetype)initWithBlock:(dispatch_block_t)block;
@end

@interface ObjectDeallocMonitor()
@property (nonatomic, copy) dispatch_block_t block;
@end

@implementation ObjectDeallocMonitor

- (instancetype)initWithBlock:(dispatch_block_t)block {
    self = [super init];
    if (self) {
        self.block = block;
    }
    return self;
}

- (void)dealloc {
    if (self.block) {
        self.block();
    }
}
@end

把ObjectDeallocMonitor的實(shí)例作為一個(gè)關(guān)聯(lián)對(duì)象設(shè)置給對(duì)應(yīng)的對(duì)象掂咒,在對(duì)象被釋放后,會(huì)同時(shí)釋放對(duì)應(yīng)的關(guān)聯(lián)變量迈喉,調(diào)用ObjectDeallocMonitor的dealloc方法绍刮。

@interface NSObject (DeallocMonitor)
- (void)addDeallocMonitorFor:(id)obj withBlock:(dispatch_block_t)block;
@end

#import "ObjectDeallocMonitor.h"
#import <objc/runtime.h>

@implementation NSObject (DeallocMonitor)
- (void)addDeallocMonitorFor:(id)obj withBlock:(dispatch_block_t)block {
    if (!obj || !block) return;
    ObjectDeallocMonitor* monitor = [[ObjectDeallocMonitor alloc] initWithBlock:block];
    objc_setAssociatedObject(self, @"DeallocMonitor", monitor, OBJC_ASSOCIATION_RETAIN);
}

相應(yīng)第三方庫(kù):http://tutuge.me/2017/03/11/TTGDeallocTaskHelper/

5. 關(guān)聯(lián)變量的應(yīng)用場(chǎng)景和坑

??關(guān)于關(guān)聯(lián)變量的應(yīng)用場(chǎng)景,Mattt大神給出過(guò)建議:

引用:
http://www.reibang.com/p/4b463169a84a
https://nshipster.com/associated-objects/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末挨摸,一起剝皮案震驚了整個(gè)濱河市孩革,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌得运,老刑警劉巖膝蜈,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異熔掺,居然都是意外死亡饱搏,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)置逻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)推沸,“玉大人,你說(shuō)我怎么就攤上這事券坞±ぱВ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵报慕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我压怠,道長(zhǎng)眠冈,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任菌瘫,我火速辦了婚禮蜗顽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雨让。我一直安慰自己雇盖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布栖忠。 她就那樣靜靜地躺著崔挖,像睡著了一般贸街。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狸相,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天薛匪,我揣著相機(jī)與錄音,去河邊找鬼脓鹃。 笑死逸尖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瘸右。 我是一名探鬼主播娇跟,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼太颤!你這毒婦竟也來(lái)了苞俘?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤栋齿,失蹤者是張志新(化名)和其女友劉穎苗胀,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體瓦堵,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡基协,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了菇用。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澜驮。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖惋鸥,靈堂內(nèi)的尸體忽然破棺而出杂穷,到底是詐尸還是另有隱情,我是刑警寧澤卦绣,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布耐量,位于F島的核電站,受9級(jí)特大地震影響滤港,放射性物質(zhì)發(fā)生泄漏廊蜒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一溅漾、第九天 我趴在偏房一處隱蔽的房頂上張望山叮。 院中可真熱鬧,春花似錦添履、人聲如沸屁倔。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)锐借。三九已至问麸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瞎饲,已是汗流浹背口叙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嗅战,地道東北人妄田。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像驮捍,于是被迫代替她去往敵國(guó)和親疟呐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容