iOS 關(guān)聯(lián)對(duì)象詳解

在平時(shí)的工作中經(jīng)常碰到給類別添加屬性的操作,那么實(shí)現(xiàn)思路是怎么樣的呢?
代碼實(shí)現(xiàn):新建一個(gè)Person類和Person+Text的類別
//Person 代碼
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end


//類別代碼
#import "Person.h"
@interface Person (Test)
@property (copy, nonatomic) NSString *name;
@end

//調(diào)用代碼
Person *person = [[Person alloc] init];
person.age = 10;
person.name = @"jack";
NSLog(@"person - age is %d, name is %@", person.age, person.name);

輸出:age 10,name 沒(méi)有值
name賦值失敗原因:age是類里面的屬性戳表,系統(tǒng)會(huì)自動(dòng)生成成員變量_agegetAgesetAge方法的聲明和實(shí)現(xiàn)戈抄。所以賦值成功。name是利用類別添加的屬性凿歼,在類別里面添加屬性并不會(huì)生成_name成員變量,只會(huì)getNamesetName方法的聲明沒(méi)有實(shí)現(xiàn)毅往。所以賦值失敗牵咙。詳情請(qǐng)看:iOS Category的本質(zhì) iOS OC對(duì)象的本質(zhì)窺探(一)
本質(zhì)原因:Category 結(jié)構(gòu)體,并沒(méi)有存儲(chǔ)成員變量
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods; // 對(duì)象方法
    struct method_list_t *classMethods; // 類方法
    struct protocol_list_t *protocols; // 協(xié)議
    struct property_list_t *instanceProperties; // 屬性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
實(shí)現(xiàn)類別添加屬性思路1,設(shè)置一個(gè)全局字典自己保存成員變量的值攀唯,代碼實(shí)現(xiàn)如下洁桌。
#import "Person+Test.h"

#define Key [NSString stringWithFormat:@"%p", self]

@implementation Person (Test)

NSMutableDictionary *names_;
+ (void)load
{
    names_ = [NSMutableDictionary dictionary];
}

- (void)setName:(NSString *)name
{
    names_[Key] = name;
}

- (NSString *)name
{
    return names_[Key];
}
@end
通過(guò)這種思路確實(shí)可以實(shí)現(xiàn)給類別添加屬性的功能,但是也有明顯的弊端侯嘀。

1.每次給這個(gè)添加一個(gè)新的屬性時(shí)需要重新創(chuàng)建一個(gè)新的字典保存另凌。
2.給屬性賦值或者取值時(shí)會(huì)出現(xiàn)線程完全問(wèn)題,需要加鎖控制戒幔。
3.字典什么時(shí)候釋放吠谢,也存在內(nèi)存泄漏的隱患。

如果使用上述思路維護(hù)難度較大诗茎,使用runtime關(guān)聯(lián)對(duì)象方法工坊,代碼如下
- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name
{
    // 隱式參數(shù)
    // _cmd == @selector(name)
    return objc_getAssociatedObject(self, _cmd);
}

1.動(dòng)態(tài)添加屬性

//id  _Nonnull object  關(guān)聯(lián)的對(duì)象
//const void * _Nonnull key 存儲(chǔ)的key
//id  _Nullable value 存儲(chǔ)的value
//objc_AssociationPolicy policy 對(duì)應(yīng)的修飾符
objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)

參數(shù)一:id object : 給哪個(gè)對(duì)象添加屬性,這里要給自己添加屬性敢订,用self王污。
參數(shù)二:void * == id key : 屬性名,根據(jù)key獲取關(guān)聯(lián)對(duì)象的屬性的值楚午,在objc_getAssociatedObject中通過(guò)次key獲得屬性的值并返回昭齐。
參數(shù)三:id value : 關(guān)聯(lián)的值,也就是set方法傳入的值給屬性去保存矾柜。
參數(shù)四:objc_AssociationPolicy policy : 策略阱驾,屬性以什么形式保存。
策略有有以下幾種

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一個(gè)弱引用相關(guān)聯(lián)的對(duì)象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相關(guān)對(duì)象的強(qiáng)引用怪蔑,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相關(guān)的對(duì)象被復(fù)制里覆,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相關(guān)對(duì)象的強(qiáng)引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相關(guān)的對(duì)象被復(fù)制缆瓣,原子性   
};

策略對(duì)應(yīng)的屬性修飾符圖示


策略對(duì)應(yīng)的屬性修飾符圖示.png

key值只要是一個(gè)指針即可喧枷,我們可以傳入@selector(name)

2.獲取屬性

objc_getAssociatedObject(id object, const void *key);

參數(shù)一:id object : 獲取哪個(gè)對(duì)象里面的關(guān)聯(lián)的屬性。
參數(shù)二:void * == id key : 什么屬性捆愁,與objc_setAssociatedObject中的key相對(duì)應(yīng),即通過(guò)key值取出value窟却。

3.移除所有關(guān)聯(lián)對(duì)象

- (void)removeAssociatedObjects{
    // 移除所有關(guān)聯(lián)對(duì)象
    objc_removeAssociatedObjects(self);
}
運(yùn)行代碼
//調(diào)用代碼
Person *person = [[Person alloc] init];
person.age = 10;
person.name = @"jack";
NSLog(@"person - age is %d, name is %@", person.age, person.name);
賦值成功昼丑,那么關(guān)聯(lián)對(duì)象的原理是什么呢?
打開(kāi)源碼:找到objc_setAssociatedObject函數(shù)
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
來(lái)到_object_set_associative_reference里面
_object_set_associative_reference.png
我們發(fā)現(xiàn)

實(shí)現(xiàn)關(guān)聯(lián)對(duì)象技術(shù)的核心對(duì)象有
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
其中Map同我們平時(shí)使用的字典類似夸赫。通過(guò)key-value一一對(duì)應(yīng)存值菩帝。

分析這四個(gè)對(duì)象其內(nèi)部實(shí)現(xiàn)原理探尋他們之間的關(guān)系。
AssociationsManager

通過(guò)AssociationsManager內(nèi)部源碼發(fā)現(xiàn),AssociationsManager內(nèi)部有一個(gè)AssociationsHashMap對(duì)象呼奢。

AssociationsManager內(nèi)部方法.png
AssociationsHashMap
AssociationsHashMap.png

通過(guò)AssociationsHashMap內(nèi)部源碼我們發(fā)現(xiàn)AssociationsHashMap繼承自unordered_map首先來(lái)看一下unordered_map內(nèi)的源碼

unordered_map.png

unordered_map源碼中我們可以看出_Key_Tp也就是前兩個(gè)參數(shù)對(duì)應(yīng)著map中的KeyValue宜雀,那么對(duì)照上面AssociationsHashMap內(nèi)源碼發(fā)現(xiàn)_Key中傳入的是disguised_ptr_t_Tp中傳入的值則為ObjectAssociationMap*握础。

緊接著我們來(lái)到ObjectAssociationMap中辐董,上圖中ObjectAssociationMap已經(jīng)標(biāo)記出,我們發(fā)現(xiàn)ObjectAssociationMap中同樣以key禀综、Value的方式存儲(chǔ)著ObjcAssociation简烘。

接著我們來(lái)到ObjcAssociation中

ObjcAssociation.png

我們發(fā)現(xiàn)ObjcAssociation存儲(chǔ)著_policy_value定枷,而這兩個(gè)值我們可以發(fā)現(xiàn)正是我們調(diào)用objc_setAssociatedObject函數(shù)傳入的值孤澎,也就是說(shuō)我們?cè)谡{(diào)用objc_setAssociatedObject函數(shù)中傳入的valuepolicy這兩個(gè)值最終是存儲(chǔ)在ObjcAssociation中的。

現(xiàn)在我們已經(jīng)對(duì)AssociationsManager欠窒、 AssociationsHashMap覆旭、 ObjectAssociationMapObjcAssociation四個(gè)對(duì)象之間的關(guān)系有了簡(jiǎn)單的認(rèn)識(shí)岖妄,那么接下來(lái)我們來(lái)細(xì)讀源碼型将,看一下objc_setAssociatedObject函數(shù)中傳入的四個(gè)參數(shù)分別放在哪個(gè)對(duì)象中充當(dāng)什么作用。

重新回到_object_set_associative_reference函數(shù)實(shí)現(xiàn)中看看具體的實(shí)現(xiàn)
WeWork Helper20200114111018.png

細(xì)讀上述源碼我們可以發(fā)現(xiàn)衣吠,首先根據(jù)我們傳入的value經(jīng)過(guò)acquireValue函數(shù)處理獲取new_value茶敏。acquireValue函數(shù)內(nèi)部其實(shí)是通過(guò)對(duì)策略的判斷返回不同的值

acquireValue函數(shù)內(nèi)部實(shí)現(xiàn)
acquireValue函數(shù)內(nèi)部實(shí)現(xiàn).png

acquireValue函數(shù)通過(guò)對(duì)策略的判斷返回不同的值
之后創(chuàng)建AssociationsManager manager;以及拿到manager內(nèi)部的AssociationsHashMap即associations。
之后我們看到了我們傳入的第一個(gè)參數(shù)object
object經(jīng)過(guò)DISGUISE函數(shù)被轉(zhuǎn)化為了disguised_ptr_t類型的disguised_object缚俏。

disguised_ptr_t disguised_object = DISGUISE(object);
typedef uintptr_t disguised_ptr_t;
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }

DISGUISE函數(shù)其實(shí)僅僅對(duì)object做了位運(yùn)算

之后我們看到被處理成new_value的value惊搏,同policy被存入了ObjcAssociation中。 而ObjcAssociation對(duì)應(yīng)我們傳入的key被存入了ObjectAssociationMap中忧换。 disguised_object和ObjectAssociationMap則以key-value的形式對(duì)應(yīng)存儲(chǔ)在associations中也就是AssociationsHashMap中恬惯。

// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();

如果我們value設(shè)置為nil的話那么會(huì)執(zhí)行下面的代碼

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

從上述代碼中可以看出,如果我們?cè)O(shè)置value為nil時(shí)亚茬,就會(huì)將關(guān)聯(lián)對(duì)象從ObjectAssociationMap中移除酪耳。

原理解讀圖示
objc_setAssociatedObject源碼底層實(shí)現(xiàn).png
通過(guò)上圖我們可以總結(jié)為:一個(gè)實(shí)例對(duì)象就對(duì)應(yīng)一個(gè)ObjectAssociationMap,而ObjectAssociationMap中存儲(chǔ)著多個(gè)此實(shí)例對(duì)象的關(guān)聯(lián)對(duì)象的key以及ObjcAssociation刹缝,為ObjcAssociation中存儲(chǔ)著關(guān)聯(lián)對(duì)象的value和policy策略碗暗。

由此我們可以知道關(guān)聯(lián)對(duì)象并不是放在了原來(lái)的對(duì)象里面,而是自己維護(hù)了一個(gè)全局的map用來(lái)存放每一個(gè)對(duì)象及其對(duì)應(yīng)關(guān)聯(lián)屬性表格梢夯。

取值相關(guān)
objc_getAssociatedObject函數(shù)
id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}

objc_getAssociatedObject內(nèi)部調(diào)用的是_object_get_associative_reference
_object_get_associative_reference函數(shù)

_object_get_associative_reference函數(shù).png

從_object_get_associative_reference函數(shù)內(nèi)部可以看出言疗,向set方法中那樣,反向?qū)alue一層一層取出最后return出去颂砸。

移除函數(shù):objc_removeAssociatedObjects函數(shù)

objc_removeAssociatedObjects用來(lái)刪除所有的關(guān)聯(lián)對(duì)象噪奄,objc_removeAssociatedObjects函數(shù)內(nèi)部調(diào)用的是_object_remove_assocations函數(shù)

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}
_object_remove_assocations函數(shù)
WeWork Helper20200114113158.png

上述源碼可以看出_object_remove_assocations函數(shù)將object對(duì)象向?qū)?yīng)的所有關(guān)聯(lián)對(duì)象全部刪除死姚。

總結(jié):

關(guān)聯(lián)對(duì)象并不是存儲(chǔ)在被關(guān)聯(lián)對(duì)象本身內(nèi)存中,而是存儲(chǔ)在全局的統(tǒng)一的一個(gè)AssociationsManager中勤篮,如果設(shè)置關(guān)聯(lián)對(duì)象為nil都毒,就相當(dāng)于是移除關(guān)聯(lián)對(duì)象。

此時(shí)我們我們?cè)诨剡^(guò)頭來(lái)看objc_AssociationPolicy policy 參數(shù): 屬性以什么形式保存的策略碰缔。

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一個(gè)弱引用相關(guān)聯(lián)的對(duì)象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相關(guān)對(duì)象的強(qiáng)引用账劲,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相關(guān)的對(duì)象被復(fù)制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相關(guān)對(duì)象的強(qiáng)引用手负,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相關(guān)的對(duì)象被復(fù)制涤垫,原子性   
};

我們會(huì)發(fā)現(xiàn)其中只有RETAIN和COPY而為什么沒(méi)有weak呢?
總過(guò)上面對(duì)源碼的分析我們知道竟终,object經(jīng)過(guò)DISGUISE函數(shù)被轉(zhuǎn)化為了disguised_ptr_t類型的disguised_object蝠猬。

disguised_ptr_t disguised_object = DISGUISE(object);

而同時(shí)我們知道,weak修飾的屬性统捶,當(dāng)沒(méi)有擁有對(duì)象之后就會(huì)被銷毀榆芦,并且指針置位nil,那么在對(duì)象銷毀之后喘鸟,雖然在map中既然存在值object對(duì)應(yīng)的AssociationsHashMap匆绣,但是因?yàn)閛bject地址已經(jīng)被置位nil,會(huì)造成壞地址訪問(wèn)而無(wú)法根據(jù)object對(duì)象的地址轉(zhuǎn)化為disguised_object了什黑。

我的簡(jiǎn)書主頁(yè)
我的博客主頁(yè)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末崎淳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子愕把,更是在濱河造成了極大的恐慌拣凹,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恨豁,死亡現(xiàn)場(chǎng)離奇詭異嚣镜,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)橘蜜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門菊匿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人计福,你說(shuō)我怎么就攤上這事跌捆。” “怎么了象颖?”我有些...
    開(kāi)封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵佩厚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我力麸,道長(zhǎng)可款,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任克蚂,我火速辦了婚禮闺鲸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘埃叭。我一直安慰自己摸恍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布赤屋。 她就那樣靜靜地躺著立镶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪类早。 梳的紋絲不亂的頭發(fā)上媚媒,一...
    開(kāi)封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音涩僻,去河邊找鬼缭召。 笑死,一個(gè)胖子當(dāng)著我的面吹牛逆日,可吹牛的內(nèi)容都是我干的嵌巷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼室抽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼搪哪!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起坪圾,我...
    開(kāi)封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤晓折,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后神年,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體已维,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年已日,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了垛耳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡飘千,死狀恐怖堂鲜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情护奈,我是刑警寧澤缔莲,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站霉旗,受9級(jí)特大地震影響痴奏,放射性物質(zhì)發(fā)生泄漏蛀骇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一读拆、第九天 我趴在偏房一處隱蔽的房頂上張望擅憔。 院中可真熱鬧,春花似錦檐晕、人聲如沸暑诸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)个榕。三九已至,卻和暖如春芥喇,著一層夾襖步出監(jiān)牢的瞬間西采,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工继控, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苛让,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓湿诊,卻偏偏與公主長(zhǎng)得像狱杰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子厅须,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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

  • 前言 associatedObject又稱關(guān)聯(lián)對(duì)象仿畸。顧名思義,就是把一個(gè)對(duì)象關(guān)聯(lián)到另外一個(gè)對(duì)象身上朗和。使兩者能夠產(chǎn)生...
    luonaerduo閱讀 1,888評(píng)論 0 1
  • 前言 associatedObject又稱關(guān)聯(lián)對(duì)象错沽。顧名思義,就是把一個(gè)對(duì)象關(guān)聯(lián)到另外一個(gè)對(duì)象身上眶拉。使兩者能夠產(chǎn)生...
    VV木公子閱讀 6,837評(píng)論 4 13
  • 多線程千埃、特別是NSOperation 和 GCD 的內(nèi)部原理。運(yùn)行時(shí)機(jī)制的原理和運(yùn)用場(chǎng)景忆植。SDWebImage的原...
    LZM輪回閱讀 2,007評(píng)論 0 12
  • “教師資格證式" 試講方式 近來(lái)經(jīng)常面試前來(lái)應(yīng)聘的教師放可,他們大多是剛畢業(yè)沒(méi)有經(jīng)驗(yàn)或畢業(yè)不久講課水平不高的老師。...
    海納百川_b083閱讀 247評(píng)論 0 1
  • 四十四回寫賈璉在鳳姐生日之時(shí)偷腥朝刊,被鳳姐逮個(gè)正著耀里,平兒無(wú)故遭殃。四十六回寫賈赦老不正經(jīng)打老太太傭人鴛鴦的主意拾氓,邢夫...
    易恒40閱讀 660評(píng)論 0 2