iOS Objective-C 關聯對象

iOS Objective-C 關聯對象

1. 關聯對象簡介

對于關聯對象,我們熟悉它的地方就是給分類添加屬性。雖然我們可以在分類中通過@property編寫代碼來聲明一個屬性,但是當我們使用的時候就報方法找不到錯誤,其實缺失的方法就是屬性的gettersetter的實現,那么關聯對象就可以完美的解決這個問題。

官方定義:

Associative references, available starting in OS X v10.6, simulate the addition of object instance variables to an existing class. Using associative references, you can add storage to an object without modifying the class declaration. This may be useful if you do not have access to the source code for the class, or if for binary-compatibility reasons you cannot alter the layout of the object.

Associations are based on a key. For any object you can add as many associations as you want, each using a different key. An association can also ensure that the associated object remains valid for at least the lifetime of the source object.

譯: 從OS X v10.6開始可用的關聯引用模擬了將對象實例變量添加到現有類中。使用關聯引用,可以在不修改類聲明的情況下將存儲添加到對象。如果您無權訪問該類的源代碼,或者由于二進制兼容性原因而無法更改對象的布局,則這可能很有用。

關聯基于key。對于任何對象,您都可以根據需要添加任意數量的關聯,每個關聯都使用不同的key。關聯還可以確保關聯的對象至少在源對象的生存期內保持有效。

通過蘋果官方文檔我們可以知道,關聯引用不僅僅可以用在給分類添加屬性。但是給分類添加屬性是我們最常用的場景。

關聯對象的兩個函數

  • 設置關聯對象
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}
  1. 參數一:id object : 要關聯的對象
  2. 參數二:const void *key : 關聯使用的key值
  3. 參數三:id value : 關聯的值,也就是我們要設置的值。
  4. 參數四:objc_AssociationPolicy policy : 策略屬性,以什么形式保存
  • 獲取關聯對象
id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}
  1. 參數一:id object : 獲取哪個對象里面的關聯的值
  2. 參數二:const void *key : 關聯使用的key值,通過這個key取出對應的值

關聯使用的策略:

/**
 * 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. */
};
策略 對應 @property 描述
OBJC_ASSOCIATION_ASSIGN (assign)或者(unsafe_unretained) 指定一個關聯對象的弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC (nonatomic, strong) 不能原子化的強引用
OBJC_ASSOCIATION_COPY_NONATOMIC (nonatomic, copy) copy引用,不能原子化
OBJC_ASSOCIATION_RETAIN (atomic, strong) 原子化的強引用
OBJC_ASSOCIATION_COPY (atomic, copy) 原子化的copy引用

2. 關聯對象的應用

舉個例子,說了半天關聯對象可以為分類添加屬性,那么我們就把這個例子寫一下。

@interface CTObject (Category)

@property (nonatomic, copy) NSString *cate_p1;

@end
@implementation CTObject (Category)
- (void)setCate_p1:(NSString *)cate_p1{
    
    objc_setAssociatedObject(self, @"cate_p1",cate_p1, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cate_p1{
    
    return  objc_getAssociatedObject(self, @"cate_p1");
}
@end

3. 關聯對象的底層原理

上面兩節對關聯對象做了簡單的介紹和其使用的舉例,下面我們來研究一下它的底層實現。

3.1 objc_setAssociatedObject

我們先看看objc_setAssociatedObject的源碼,由于使用了各種C++的語法和嵌套,嵌套過程就不多說了,以下是嵌套的代碼:

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}

static void
_base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}

static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};

由以上代碼我們可以知道objc_setAssociatedObject實際調用的是_object_set_associative_reference函數,下面我們就來到_object_set_associative_reference看看它究竟是如何實現的。

_object_set_associative_reference 源碼:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

acquireValue 源碼:

inline void acquireValue() {
        if (_value) {
            switch (_policy & 0xFF) {
            case OBJC_ASSOCIATION_SETTER_RETAIN:
                _value = objc_retain(_value);
                break;
            case OBJC_ASSOCIATION_SETTER_COPY:
                _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
                break;
            }
        }
    }
  • 首先還是做一些非空判斷防止一些空對象空值可能會引起的崩潰
  • 判斷類是否禁用了關聯引用,如果是就打印錯誤信息
  • 初始化一個disguised對象,是對object按位取反
  • 初始化一個ObjcAssociation對象用于持有關聯對象
  • 通過acquireValue函數給我們的value返回一個新值,acquireValue源碼在上面,主要是根據策略進行不同的處理
  • 接下來就是初始化一個AssociationsManager對象,獲取一個AssociationsHashMap哈希表
  • 接下來分兩個流程一個是值存在此時是賦值
    • 首先獲取到類的關聯表
    • 如果沒獲取到說明我們是第一次給該類關聯,所以需要創建一個新的表
    • 接下來獲取表的首地址,并判斷對應的key是否已經存在,不存在就直接寫入
    • 存在就用新值替換舊值
  • 第二種是值為空,此時是刪除關聯對象
    • 首先是獲取到該類對應的哈希表
    • 判斷表不為空
    • 找到key對應的節點
    • 節點不為空判斷
    • 替換節點值為空
    • 清空節點
    • 清空節點后,如果表也為空,則清空表
  • 最后釋放舊值

3.2 objc_getAssociatedObject

objc_getAssociatedObject就沒有那么多嵌套了,直接就可以看出是調用的_object_get_associative_reference函數。

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

_object_get_associative_reference 源碼:

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

_object_get_associative_reference的實現也很簡單:

  • 首先還是初始化一個ObjcAssociation對象,AssociationsManager對象,獲取AssociationsHashMap哈希表
  • 獲取到當前對象的關聯表
  • 如果表不為空則通過key在表中查找數據
  • 如果找到了并且不為空則調用retainReturnedValue函數根據策略賦值
  • 最后返回通過autoreleaseReturnedValue函數根據策略處理的值

3.3 objc_removeAssociatedObjects

對于關聯對象其實還有一個函數objc_removeAssociatedObjects,只不過我們基本不用他,根據名字我們就可以知道該函數是移除關聯對象的。這里也嵌套了一層代碼,最終調用的是_object_remove_assocations

objc_removeAssociatedObjects 源碼:

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

_object_remove_assocations 源碼:

// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
void
_object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);
            associations.erase(i);
        }
    }

    // release everything (outside of the lock).
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}

根據注釋我們可以知道_object_remove_assocations函數是會對性能有影響的。

  • 這里還是要初始化ObjectAssociationMap對象,AssociationsManager對象,AssociationsHashMap對象。
  • 找出要釋放對象的關聯表
  • 判斷不為空,則將移除這些關聯關系,并釋放
  • 最后循環釋放類的所有關聯表

4. 總結

  • 關聯對象實際上在底層是一個ObjcAssociation對象結構
  • 全局由一個AssociationsManager管理類存儲了一個靜態的哈希表AssociationsHashMap
  • 這個哈希表存儲的是以對象指針為鍵,以該對象所有關聯對象為值
  • 關聯對象又是以ObjectAssociationMap來存儲的
  • ObjectAssociationMap的存儲結構以key為鍵,ObjcAssociation為值
  • 判斷一個對象是否存在關聯對象可以通過對象isahas_assoc

至此我們的關聯對象就基本分析完畢了,但是由于本人才疏學淺,有些地方用詞不當,一些C++語法不是很熟悉,有些表述不完整,不貼切,但是我也想不出什么好詞的,可能也會有些不準確。如有問題歡迎指正。

5. 參考資料

**Apple Associative References
**

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374