談Objective-C關聯(lián)對象

#前言

前不久剛寫了 談Objective-C類成員變量 髓需,分析了成員變量的實現(xiàn)原理以及不能動態(tài)添加的原因奕枝,在這篇文章里我們來根據(jù) objc4-646.tar.gz版本 源碼來談一下 Objective-C 關聯(lián)對象的實現(xiàn)原理。

關聯(lián)對象(Associated Objects)是 Objective-C 2.0運行時的一個特性齿尽,起始于OS X Snow Leopard和iOS 4锌介。它允許開發(fā)者對已經(jīng)存在的類在擴展中添加自定義的屬性令哟。相關參考可以查看 <objc/runtime.h> 中定義的三個允許你將任何鍵值在運行時關聯(lián)到對象上的函數(shù):

  • void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) 用于給對象添加關聯(lián)屬性心傀,傳入nil則移除已有的關聯(lián)對象
  • id objc_getAssociatedObject(id object, const void *key) 用于獲取關聯(lián)屬性
  • void objc_removeAssociatedObjects(id object) 移除一個對象所有的關聯(lián)屬性屈暗,但不建議手動調(diào)用這個函數(shù),因為這可能會導致其它人對其添加的屬性也被移除了。你可以調(diào)用objc_setAssociatedObject方法并傳入nil來指定移除某個關聯(lián)

下面分析一下 objc_setAssociatedObject 兩個參數(shù) keypolicy

#key

通常來說該屬性應該是常量养叛、唯一的种呐,在getter和setter方法中都可以訪問到。這里有兩種常見的添加方式:

第一種是添加 static char 類型的變量一铅,當然更推薦是指針型的陕贮。

static char kAssociatedObjectKey;
- (void)setMenber:(NSString *)menber {
    objc_setAssociatedObject(self, &kAssociatedObjectKey, menber, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

當然更推薦的是使用更簡單的方式實現(xiàn):用 selector(getter方法):

- (void)setMenber:(NSString *)menber {
    objc_setAssociatedObject(self, @selector(menber), menber, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

#關聯(lián)策略 policy

關聯(lián)策略跟屬性修飾符的使用方法差不多堕油,屬性可以根據(jù)定義在 objc_AssociationPolicy 上的類型被關聯(lián)到對象上:

關聯(lián)策略 等價屬性 說明
OBJC_ASSOCIATION_ASSIGN @property (assign)或 @property (unsafe_unretained) 弱引用關聯(lián)對象
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 強引用關聯(lián)對象潘飘,且為非原子操作
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 復制關聯(lián)對象,且為非原子操作
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 強引用關聯(lián)對象掉缺,且為原子操作
OBJC_ASSOCIATION_COPY @property (atomic, copy) 復制關聯(lián)對象卜录,且為原子操作

#關聯(lián)對象實現(xiàn)

下面讓我們具體來分析一下這幾個函數(shù)的具體實現(xiàn)吧!

分析objc_setAssociatedObject實現(xiàn)

objc_setAssociatedObject的實現(xiàn)被定義在objc-auto.mm文件 467 行

GC_RESOLVER(objc_setAssociatedObject)

#define GC_RESOLVER(name)                                       \
    OBJC_EXPORT void *name##_resolver(void) __asm__("_" #name); \
    void *name##_resolver(void)                                 \
    {                                                           \
        __asm__(".symbol_resolver _" #name);                    \
        if (UseGC) return (void*)name##_gc;                     \
        else return (void*)name##_non_gc;                       \
    }
  • ## 符號: 連接宏眶明。舉個例子:#define COMMAND(A, B) A##B 艰毒, int COMMAND(temp, Int) = 10 等同于 int tempInt = 10
  • UseGC 是否使用垃圾回收,在 iPhone 平臺上被定義為 NO
    所以這個宏展開來為下面的代碼
   void GC_RESOLVER(name)                                 
   {                                                           
      return (void*)objc_setAssociatedObject_non_gc();                       
   }

objc_setAssociatedObject_non_gc的實現(xiàn)在objc-runtime.m文件搜囱,再經(jīng)過一些跳轉丑瞧,可以發(fā)現(xiàn) objc_setAssociatedObject 最終會調(diào)用 _object_set_associative_reference方法 (objc-runtime.m 268行)

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);
}
  • AssociationsManager manager;, 會創(chuàng)建一個AssociationsManager結構體的變量 manager,在調(diào)用它的構造函數(shù)時會上鎖蜀肘,調(diào)用析構函數(shù)時解鎖绊汹。結構體內(nèi)有一個靜態(tài)變量 AssociationsHashMap, 懶加載該變量。
  • DISGUISE(object) 用來獲取 object 的指針地址
  • AssociationsHashMap是一個無序的哈希表扮宠,維護了從對象地址到 ObjectAssociationMap 的映射
  • ObjectAssociationMap 是一個map西乖,維護了從 key 到 ObjcAssociation 的映射
  • ObjcAssociation 是一個 C++ 類, 主要包括兩個成員變量:uintptr_t _policy(關聯(lián)策略) id _value(關聯(lián)對象的值)

簡單的講解上面那個函數(shù)的流程:

  1. 新建一個 AssociationsManager 實例 manager坛增,同時上鎖获雕。通過 manager 得到 AssociationsHashMap 關聯(lián)哈希表 associations,通過 DISGUISE()函數(shù)得到 object 的指針 disguised_object收捣。在哈希表 associations 中 根據(jù) disguised_object 查找 ObjectAssociationMap届案,如果沒有則新建一個 refs。
  2. 新建一個 ObjcAssociation 實例 new_association罢艾,存儲在 refs 中
  3. 如果傳入的value是nil萝玷,則在 refs 移除該映射關系
  4. 釋放掉舊的 old_association
  5. 作用域結束釋放掉 manager,解鎖
添加關聯(lián)對象流程圖

分析objc_getAssociatedObject實現(xiàn)

按照上一節(jié)的流程昆婿,我們首先找到 objc_getAssociatedObject 的最終實現(xiàn)源碼:

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

代碼量比上一節(jié)少了還挺多哈球碉,過程也類似,就不講的很細了

  1. 先得到 AssociationsHashMap 實例 associations(靜態(tài)變量)仓蛆。根據(jù) object 的指針地址睁冬,在 associations 得到映射的 ObjectAssociationMap refs。
  2. 在 refs 根據(jù) key 得到映射的 ObjcAssociation 實例 entry,在 entry 中可以得到成員變量 _value豆拨,也就是我們所關聯(lián)屬性的值直奋。
  3. 根據(jù)關聯(lián)策略 policy 進行相應的操作(autorelease, retain)后返回 value

分析objc_removeAssociatedObjects實現(xiàn)

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

其實不看代碼應該也能夠猜出個大概了吧.

  1. 根據(jù) object地址 找到映射的 refs,遍歷 refs施禾,將保存著的 value 保存在 vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements
  2. 刪除 refs脚线, 然后一個個的釋放 elements 里面的值

#給類對象關聯(lián)對象

看完源代碼后,我們知道實例對象地址與 ObjectAssociationMap map是一一對應的弥搞。那么是否可以給類對象添加關聯(lián)對象呢邮绿?
答案是可以,因為Class也是一個對象攀例,我們完全可以用同樣的方式給類對象添加關聯(lián)對象船逮,只不過我們一般情況下不會這樣做,因為更多時候可以通過 static 變量來實現(xiàn)類級別的變量粤铭。

你可以通過下面的代碼這樣操作

@implementation NSObject (AssociatedObject)
+ (NSString *)associatedObject {
    return objc_getAssociatedObject(self, @selector(associatedObject));
}
+ (void)setAssociatedObject:(NSString *)associatedObject {
    objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

- (void) foo {
    NSObject.associatedObject = @"associatedObject";
}

#何時釋放關聯(lián)對象

探究ARC下dealloc實現(xiàn) 中我們研究過挖胃,當對象引用計數(shù)變?yōu)?時會調(diào)用 dealloc 方法,然后最終調(diào)用 objc_destructInstance 方法來執(zhí)行釋放所有__weak修飾的指向該對象的指針梆惯,釋放關聯(lián)對象酱鸭,釋放該對象成員變量的操作

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = !UseGC && obj->hasAssociatedObjects();
        bool dealloc = !UseGC;

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        if (dealloc) obj->clearDeallocating();
    }
    return obj;
}

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

是不是有點熟悉呢,在上上節(jié)中我們剛剛分析過這個方法垛吗。當對象 dealloc 時凹髓,會自動調(diào)用 objc_removeAssociatedObjects 方法來釋放所有的關聯(lián)對象。

#總結一下

  • 類實例跟關聯(lián)對象(關聯(lián)的屬性)并沒有直接的存儲關系职烧,關聯(lián)對象在創(chuàng)建時后存儲在一個靜態(tài)哈希表中扁誓,根據(jù)類實例的指針映射到該關聯(lián)對象
  • 當類實例 dealloc 后,會從哈希表中釋放該實例的所有的關聯(lián)對象
  • 關聯(lián)對象的關聯(lián)策略跟屬性的修飾符非常的相似蚀之,要合理使用避免 crash
  • 比起其他解決問題的方法蝗敢,關聯(lián)對象應該被視為最后的選擇

#引用

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市足删,隨后出現(xiàn)的幾起案子寿谴,更是在濱河造成了極大的恐慌,老刑警劉巖失受,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讶泰,死亡現(xiàn)場離奇詭異,居然都是意外死亡拂到,警方通過查閱死者的電腦和手機痪署,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來兄旬,“玉大人狼犯,你說我怎么就攤上這事。” “怎么了悯森?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵宋舷,是天一觀的道長。 經(jīng)常有香客問我瓢姻,道長祝蝠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任幻碱,我火速辦了婚禮绎狭,結果婚禮上,老公的妹妹穿的比我還像新娘收班。我一直安慰自己坟岔,他們只是感情好谒兄,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布摔桦。 她就那樣靜靜地躺著,像睡著了一般承疲。 火紅的嫁衣襯著肌膚如雪邻耕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天燕鸽,我揣著相機與錄音兄世,去河邊找鬼。 笑死啊研,一個胖子當著我的面吹牛御滩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播党远,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼削解,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沟娱?” 一聲冷哼從身側響起氛驮,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎济似,沒想到半個月后矫废,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡砰蠢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年蓖扑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片台舱。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡律杠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情俩功,我是刑警寧澤幻枉,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站诡蜓,受9級特大地震影響熬甫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蔓罚,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一椿肩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧豺谈,春花似錦郑象、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至丽惭,卻和暖如春击奶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背责掏。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工柜砾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人换衬。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓痰驱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瞳浦。 傳聞我的和親對象是個殘疾皇子担映,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345