Objective-C Associated Objects 的實(shí)現(xiàn)原理

當(dāng)我們寫個(gè)分類的時(shí)候,有時(shí)候想給這個(gè)分類添加屬性,但是卻不能添加實(shí)例變量痊硕,然而我們可以通過 Associated Objects來完成這件事,下面我將介紹Objective-C 中 Associated Objects 的實(shí)現(xiàn)原理押框。

使用場景

按照 Mattt Thompson 大神的文章 Associated Objects(http://nshipster.com/associated-objects/) 中的說法岔绸,Associated Objects 主要有以下三個(gè)使用場景:

1.為現(xiàn)有的類添加私有變量以幫助實(shí)現(xiàn)細(xì)節(jié);
2.為現(xiàn)有的類添加公有屬性;
3.為 KVO 創(chuàng)建一個(gè)關(guān)聯(lián)的觀察者盒揉。
從本質(zhì)上看晋被,第 1 、2 個(gè)場景其實(shí)是一個(gè)意思刚盈,唯一的區(qū)別就在于新添加的這個(gè)屬性是公有的還是私有的而已羡洛。就目前來說,我在實(shí)際工作中使用得最多的是第 2 個(gè)場景藕漱,而第 3 個(gè)場景我還沒有使用過欲侮。

相關(guān)函數(shù)

與 Associated Objects 相關(guān)的函數(shù)主要有三個(gè),我們可以在 runtime 源碼的 runtime.h 文件中找到它們的聲明:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);

objc_setAssociatedObject 用于給對象添加關(guān)聯(lián)對象肋联,傳入 nil 則可以移除已有的關(guān)聯(lián)對象威蕉;
objc_getAssociatedObject 用于獲取關(guān)聯(lián)對象;
objc_removeAssociatedObjects 用于移除一個(gè)對象的所有關(guān)聯(lián)對象橄仍。

注:objc_removeAssociatedObjects 函數(shù)我們一般是用不上的韧涨,因?yàn)檫@個(gè)函數(shù)會移除一個(gè)對象的所有關(guān)聯(lián)對象,將該對象恢復(fù)成“原始”狀態(tài)沙兰。這樣做就很有可能把別人添加的關(guān)聯(lián)對象也一并移除氓奈,這并不是我們所希望的。所以一般的做法是通過給 objc_setAssociatedObject 函數(shù)傳入 nil 來移除某個(gè)已有的關(guān)聯(lián)對象鼎天。

key 值

關(guān)于前兩個(gè)函數(shù)中的 key 值是我們需要重點(diǎn)關(guān)注的一個(gè)點(diǎn)舀奶,這個(gè) key 值必須保證是一個(gè)對象級別(為什么是對象級別?看完下面的章節(jié)你就會明白了)的唯一常量斋射。一般來說育勺,有以下三種推薦的 key 值:

聲明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作為 key 值;
聲明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; 罗岖,使用 kAssociatedObjectKey 作為 key 值涧至;
用 selector ,使用 getter 方法的名稱作為 key 值桑包。

關(guān)聯(lián)策略

在給一個(gè)對象添加關(guān)聯(lián)對象時(shí)有五種關(guān)聯(lián)策略可供選擇:

關(guān)聯(lián)策略 等價(jià)屬性 說明
OBJC_ASSOCIATION_ASSIGN @property (assign) or @property (unsafe_unretained) 弱引用關(guān)聯(lián)對象
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (strong, nonatomic) 強(qiáng)引用關(guān)聯(lián)對象南蓬,且為非原子操作
OBJC_ASSOCIATION_COPY_NONATOMIC @property (copy, nonatomic) 復(fù)制關(guān)聯(lián)對象,且為非原子操作
OBJC_ASSOCIATION_RETAIN @property (strong, atomic) 強(qiáng)引用關(guān)聯(lián)對象哑了,且為原子操作
OBJC_ASSOCIATION_COPY @property (copy, atomic) 復(fù)制關(guān)聯(lián)對象赘方,且為原子操作
其中,第 2 種與第 4 種弱左、第 3 種與第 5 種關(guān)聯(lián)策略的唯一差別就在于操作是否具有原子性窄陡。由于操作的原子性不在本文的討論范圍內(nèi),所以下面的實(shí)驗(yàn)和討論就以前三種以例進(jìn)行展開拆火。

實(shí)現(xiàn)

接下來跳夭,我們就一起看看 runtime 中的源碼涂圆。

objc_setAssociatedObject

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);
}

在看這段代碼前,我們需要先了解一下幾個(gè)數(shù)據(jù)結(jié)構(gòu)以及它們之間的關(guān)系:

AssociationsManager 是頂級的對象币叹,維護(hù)了一個(gè)從 spinlock_t 鎖到 AssociationsHashMap 哈希表的單例鍵值對映射润歉;
AssociationsHashMap 是一個(gè)無序的哈希表,維護(hù)了從對象地址到 ObjectAssociationMap 的映射套硼;
ObjectAssociationMap 是一個(gè) C++ 中的 map 卡辰,維護(hù)了從 key 到 ObjcAssociation 的映射,即關(guān)聯(lián)記錄邪意;
ObjcAssociation 是一個(gè) C++ 的類,表示一個(gè)具體的關(guān)聯(lián)結(jié)構(gòu)反砌,主要包括兩個(gè)實(shí)例變量雾鬼,_policy 表示關(guān)聯(lián)策略,_value 表示關(guān)聯(lián)對象宴树。
每一個(gè)對象地址對應(yīng)一個(gè) ObjectAssociationMap 對象策菜,而一個(gè) ObjectAssociationMap 對象保存著這個(gè)對象的若干個(gè)關(guān)聯(lián)記錄。

弄清楚這些數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系后酒贬,再回過頭來看上面的代碼就不難了又憨。我們發(fā)現(xiàn),在蘋果的底層代碼中一般都會充斥著各種 if else 锭吨,可見寫好 if else 后我們就距離成為高手不遠(yuǎn)了蠢莺。開個(gè)玩笑,我們來看下面的流程圖零如,一圖勝千言:

objc_setAssociatedObject.png

objc_getAssociatedObject

同樣的躏将,我們也可以在 objc-references.mm 文件中找到 objc_getAssociatedObject 函數(shù)最終調(diào)用的函數(shù):

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        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()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;
}

看懂了 objc_setAssociatedObject 函數(shù)后,objc_getAssociatedObject 函數(shù)對我們來說就是小菜一碟了考蕾。這個(gè)函數(shù)先根據(jù)對象地址在 AssociationsHashMap 中查找其對應(yīng)的 ObjectAssociationMap 對象祸憋,如果能找到則進(jìn)一步根據(jù) key 在 ObjectAssociationMap 對象中查找這個(gè) key 所對應(yīng)的關(guān)聯(lián)結(jié)構(gòu) ObjcAssociation ,如果能找到則返回 ObjcAssociation 對象的 value 值肖卧,否則返回 nil 蚯窥。

objc_removeAssociatedObjects

同理,我們也可以在 objc-references.mm 文件中找到 objc_removeAssociatedObjects 函數(shù)最終調(diào)用的函數(shù):

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());
}

這個(gè)函數(shù)負(fù)責(zé)移除一個(gè)對象的所有關(guān)聯(lián)對象塞帐,具體實(shí)現(xiàn)也是先根據(jù)對象的地址獲取其對應(yīng)的 ObjectAssociationMap 對象拦赠,然后將所有的關(guān)聯(lián)結(jié)構(gòu)保存到一個(gè) vector 中,最終釋放 vector 中保存的所有關(guān)聯(lián)對象壁榕。根據(jù)前面的實(shí)驗(yàn)觀察到的情況矛紫,在一個(gè)對象被釋放時(shí),也正是調(diào)用的這個(gè)函數(shù)來移除其所有的關(guān)聯(lián)對象牌里。

給類對象添加關(guān)聯(lián)對象

看完源代碼后颊咬,我們知道對象地址與 AssociationsHashMap 哈希表是一一對應(yīng)的务甥。那么我們可能就會思考這樣一個(gè)問題,是否可以給類對象添加關(guān)聯(lián)對象呢喳篇?答案是肯定的敞临。我們完全可以用同樣的方式給類對象添加關(guān)聯(lián)對象,只不過我們一般情況下不會這樣做麸澜,因?yàn)楦鄷r(shí)候我們可以通過 static 變量來實(shí)現(xiàn)類級別的變量挺尿。我在分類 ViewController+AssociatedObjects 中給 ViewController 類對象添加了一個(gè)關(guān)聯(lián)對象 associatedObject ,讀者可以親自在 viewDidLoad 方法中調(diào)用一下以下兩個(gè)方法驗(yàn)證一下:

+ (NSString *)associatedObject;
+ (void)setAssociatedObject:(NSString *)associatedObject;

總結(jié)

讀到這里炊邦,相信你對開篇的那三個(gè)問題已經(jīng)有了一定的認(rèn)識编矾,下面我們再梳理一下:

關(guān)聯(lián)對象與被關(guān)聯(lián)對象本身的存儲并沒有直接的關(guān)系,它是存儲在單獨(dú)的哈希表中的馁害;
關(guān)聯(lián)對象的五種關(guān)聯(lián)策略與屬性的限定符非常類似窄俏,在絕大多數(shù)情況下,我們都會使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的關(guān)聯(lián)策略碘菜,這可以保證我們持有關(guān)聯(lián)對象凹蜈;
關(guān)聯(lián)對象的釋放時(shí)機(jī)與移除時(shí)機(jī)并不總是一致,比如實(shí)驗(yàn)中用關(guān)聯(lián)策略 OBJC_ASSOCIATION_ASSIGN 進(jìn)行關(guān)聯(lián)的對象忍啸,很早就已經(jīng)被釋放了仰坦,但是并沒有被移除,而再使用這個(gè)關(guān)聯(lián)對象時(shí)就會造成 Crash 计雌。
在弄懂 Associated Objects 的實(shí)現(xiàn)原理后悄晃,可以幫助我們更好地使用它,在出現(xiàn)問題時(shí)也能盡快地定位問題白粉,最后希望本文能夠?qū)δ阌兴鶐椭?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末传泊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鸭巴,更是在濱河造成了極大的恐慌眷细,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹃祖,死亡現(xiàn)場離奇詭異溪椎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)恬口,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門校读,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人祖能,你說我怎么就攤上這事歉秫。” “怎么了养铸?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵雁芙,是天一觀的道長轧膘。 經(jīng)常有香客問我,道長兔甘,這世上最難降的妖魔是什么谎碍? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮洞焙,結(jié)果婚禮上蟆淀,老公的妹妹穿的比我還像新娘。我一直安慰自己澡匪,他們只是感情好熔任,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著仙蛉,像睡著了一般笋敞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荠瘪,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機(jī)與錄音赛惩,去河邊找鬼哀墓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛喷兼,可吹牛的內(nèi)容都是我干的篮绰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼季惯,長吁一口氣:“原來是場噩夢啊……” “哼吠各!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起勉抓,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤贾漏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后藕筋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纵散,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年隐圾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伍掀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡暇藏,死狀恐怖蜜笤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盐碱,我是刑警寧澤把兔,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布沪伙,位于F島的核電站,受9級特大地震影響垛贤,放射性物質(zhì)發(fā)生泄漏焰坪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一聘惦、第九天 我趴在偏房一處隱蔽的房頂上張望某饰。 院中可真熱鬧,春花似錦善绎、人聲如沸黔漂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽炬守。三九已至,卻和暖如春剂跟,著一層夾襖步出監(jiān)牢的瞬間减途,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工曹洽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鳍置,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓送淆,卻偏偏與公主長得像税产,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子偷崩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

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