Associated Object(關聯對象)使用及原理

一 基本使用
二 底層原理
三 應用場景

分類(category)與關聯對象(Associated Object)作為objective-c的擴展機制的兩個特性:分類:可以通過它來擴展方法,Associated Object:可以通過它來擴展屬性。
在iOS開發中,可能Category比較常見,相對的Associated Object,就用的比較少,要用它之前,必須導入<objc/runtime.h>的頭文件。

一 基本使用

關聯對象提供了以下API:

// 1.添加關聯對象:
void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
// 2.獲得關聯對象:
id objc_getAssociatedObject(id object, const void * key)
// 3.移除所有的關聯對象
void objc_removeAssociatedObjects(id object)

在添加關聯對象的方法中有一個policy屬性,它是一個枚舉值,對應我們平時定義屬性時設置的修飾詞

objc_AssociationPolicy 對應的修飾符
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

下面是使用示例:

#import <Foundation/Foundation.h>

@interface Person : NSObject

@end

#import "Person.h"
#import <objc/runtime.h>
@implementation Person

@end
#import "Person.h"
@interface Person (Baba)

@property(assign,nonatomic) int age;
@property(copy,nonatomic) NSString *name;

@end

#import "Person+Baba.h"
#import <objc/runtime.h>
@implementation Person (Baba)

-(void)setAge:(int)age {
    
    objc_setAssociatedObject(self, @selector(age),@(age), OBJC_ASSOCIATION_ASSIGN);
}

- (int)age{
   return [objc_getAssociatedObject(self, @selector(age)) intValue];
}

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

- (NSString *)name {
   return objc_getAssociatedObject(self, @selector(name));
}

@end
#include<stdio.h>
#include "Person.h"
#import <Foundation/Foundation.h>
#import "Person+Baba.h"
 int main()
{
    Person *person = [[Person alloc] init];
    person.age = 10;
    person.name = @"張三";
    NSLog(@"姓名:%@,年齡:%d",person.name,person.age);
    
}
output:
2019-02-11 13:08:18.630262+0800 test[7310:121043] 姓名:張三,年齡:10

我們利用關聯對象給分類添加屬性,使用是很簡單的,就三個方法,下面我們看一下它的底層實現原理,當然了,我們主要研究三個關聯對象方法的底層實現。

二 底層原理

我們先來看一下 void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)底層實現,我們在objc源代碼,objc-runtime.mm可以看到


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

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

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

我們跟進去看一下

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
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
它們之間的關系圖如下:

根據文初示例代碼 objc_setAssociatedObject(self, @selector(age),@(age), OBJC_ASSOCIATION_ASSIGN);我們給出對應結構圖如下:


關聯對象底層就是這幾個對象相互配合實現的
AssociationsManager:操作所有的關聯屬性 和 獲取關聯屬性 移除關聯屬性
AssociationsHashMap:存儲這通過傳遞進來的對象地址作為key, ObjectAssociationMap為value的映射對象,可以存儲不同對象的關聯,擴展性強。
ObjectAssociationMap:傳遞進來的key為key, ObjcAssociation作為value
ObjcAssociation:存儲這關聯策略和關聯的值。
下面我們來看具體的源碼分析:

2.1 AssociationsManager
class AssociationsManager {
    static spinlock_t _lock;
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap. 這個_ map 里邊存儲的有關聯列表
public:
    AssociationsManager()   { _lock.lock(); }
    ~AssociationsManager()  { _lock.unlock(); }

    AssociationsHashMap &associations() { //可以看成是只初始化一次 類似與單例
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

AssociationsManager 是一個 C++的類 用來進行對關聯對象的屬性添加 和 查找 移除等操作,它里邊有個 spinlock_t鎖 對 _map 這個全局唯一的實例 進行加鎖和解鎖 ,由于懶漢模式的單例 需要在多個線程訪問 _map 時候進行加鎖保護

2.2 AssociationsHashMap
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };

關聯列表是一個 hashMap 類似于 OC 的 NSDictionary ,其中用 disguised_ptr_t 作為key, ObjectAssociationMap *作為一個 value disguised_ptr_tuintptr_t 的類型 intptr_tuintptr_t類型用來存放指針地址。它們提供了一種可移植且安全的方法聲明指針,而且和系統中使用的指針長度相同,對于把指針轉化成整數形式來說很有用。可以把disguised_ptr_t理解為一個指針類型的變量

2.3 ObjectAssociationMap
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };

ObjectAssociationMap也是一個 HashMap 存放的是 一個void * key就是關聯屬性時傳進來的 key , ObjcAssociation 存放的關聯屬性策略和值的信息

2.4 ObjcAssociation
class ObjcAssociation {
        uintptr_t _policy;
        id _value;
    public:
        ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
        ObjcAssociation() : _policy(0), _value(nil) {}

        uintptr_t policy() const { return _policy; }
        id value() const { return _value; }

        bool hasValue() { return _value != nil; }
    };

ObjcAssociation 關聯屬性信息類 存放了關聯策略 和 傳遞進來關聯的值 id 類型


2.5 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.
   /// 舊的關聯對象 因為關聯屬性時如果傳 nil 可能會替換舊的關聯屬性 ,這就是移除某個關聯屬性時傳 nil 的原因
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
       ///獲取關聯屬性列表 ,取出來的列表是以對象為單位的 ,即某個對象的關聯列表 ,這樣就可以單獨的關聯某個對象的關聯屬性 而不與其他對象隔離開
        AssociationsHashMap &associations(manager.associations()); 
      /// 將要添加關聯屬性的對象產生一個內存地址 做 key 存儲 它的關聯屬性
        disguised_ptr_t disguised_object = DISGUISE(object);
      /// 如果要關聯的值不為空 ,不為空時 就需要判斷這個屬性和 key 是不是第一天添加 ,即  void *key, id value 都是第一次傳遞進來 
        if (new_value) {
            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; //取出舊值 后邊對這個舊值進行 release 操作
                   ///將新值存放到 key 對應的字典中去 
                    j->second = ObjcAssociation(policy, new_value);
                } else { ///沒有舊值直接將新值添加到字典里
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else { 
                  如果 key 對象的字典不存在 就創建一個字典 (hashMap 類似于字典的功能,本文為了方便理解將它稱為字典)
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs; 
              ///將要關聯屬性和策略封裝到一個ObjcAssociation類里邊 并根據 key 添加到這個字典里
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
          ///如果添加關聯的屬性為空時 就需要取出之前關聯的值 并把它擦除掉 相當于removeObjectForKey 
        ///還是根據對象內存地址找到它的關聯屬性列表 ,然后通過 key 找到它關聯屬性的實體(ObjcAssociation這個類) 最后擦除掉 相當于 free 從內存中移除
            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);
}

objc_setAssociatedObject 添加關聯屬性的 API

2.6 objc_getAssociatedObject
id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN; 
    {
      ///還是通過 AssociationsManager 找到所有關聯對象類別 ,然后通過傳入 object 找到某個對象的關聯列表 ,然后通過 key 找到這個對象關聯屬性列表的某個實體(ObjcAssociation) 最后根據關聯策略返回這個屬性的值 
        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();
                ///取出關聯值和策略 發送消息 類似與 [obj retain]
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
   /// 如果這個對象是延時釋放的類型 類似與 OC Array String 這些不是 alloc 來的對象 都要執行 [obj autorelease]來釋放 
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;
}

objc_getAssociatedObject 關聯對象取值的操作

2.7 objc_removeAssociatedObjects
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);
       ///如果這個對象有關聯的屬性列表 那么久便利它關聯的屬性列表 然后通過便利將這些關聯內容 一個個從字典里邊擦除  先擦除對象列表關聯的屬性列表 然后將這個對象關聯屬性的 hashMap 擦除掉 相當于 [dict removeAllObjects] 然后再從全局 AssociationsManager 移除 這個對象關聯的字典 , 又相當于 從一個全局大字典里 把 dict這個對象的小字典 給移除了 
        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());
}

objc_removeAssociatedObjects 移除該對象所有的關聯屬性列表
關聯對象原理就講完了,下面我們來看它的具體應用場景

三 應用場景
3.1 給系統提供的類添加屬性

通過給分類添加屬性,可以衍生到,當我們遇到某個系統提供的類,我們想要給它添加一個屬性,那么我們就可以創建這個類的分類,然后使用關聯對象添加屬性,比如我們給NSString 添加一個isEmail 屬性,標示它是不是一個郵箱地址。

#import <Foundation/Foundation.h>

@interface NSString (Email)

@property(assign,nonatomic) BOOL isEmail;

@end

#import "NSString+Email.h"
#import "objc/runtime.h"

@implementation NSString (Email)

-(void)setIsEmail:(BOOL)isEmail{
    
    objc_setAssociatedObject(self, @selector(isEmail),@(isEmail), OBJC_ASSOCIATION_ASSIGN);
}

- (BOOL)isEmail{
    
   return [objc_getAssociatedObject(self, @selector(isEmail)) boolValue];
}

@end
#include<stdio.h>
#import <Foundation/Foundation.h>
#import "NSString+Email.h"
 int main()
{
    NSString *string = @"56885688@163.com";
    string.isEmail = true;
    if(string.isEmail){
        NSLog(@"這是一個郵箱地址");
    } else {
        NSLog(@"這是不是郵箱地址");
    }
    
}
output:
2019-02-11 16:21:35.993353+0800 test[12438:215884] 這是一個郵箱地址
3.2 關聯block(關聯回調,關聯執行邏輯)

UIButton為例,使用關聯對象完成一個功能函數:為UIButton增加一個分類,定義一個方法,使用block去實現button的點擊回調

UIButton+Handle.h

#import <UIKit/UIKit.h>
#import <objc/runtime.h>    // 導入頭文件

// 聲明一個button點擊事件的回調block
typedef void(^ButtonClickCallBack)(UIButton *button);

@interface UIButton (Handle)

// 為UIButton增加的回調方法
- (void)handleClickCallBack:(ButtonClickCallBack)callBack;

@end

UIButton+Handle.m
#import "UIButton+Handle.h"

// 聲明一個靜態的索引key,用于獲取被關聯對象的值
static char *buttonClickKey;

@implementation UIButton (Handle)

- (void)handleClickCallBack:(ButtonClickCallBack)callBack {
    // 將button的實例與回調的block通過索引key進行關聯:
    objc_setAssociatedObject(self, &buttonClickKey, callBack, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // 設置button執行的方法
    [self addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];
}

- (void)buttonClicked {
    // 通過靜態的索引key,獲取被關聯對象(這里就是回調的block)
    ButtonClickCallBack callBack = objc_getAssociatedObject(self, &buttonClickKey);
    
    if (callBack) {
        callBack(self);
    }
}

@end
viewController 里使用

[self.testButton handleClickCallBack:^(UIButton *button) {
        NSLog(@"block --- click 回調");
    }];
3.3 更多使用示例

關聯對象開發中有哪些應用場景,我們不能窮盡,但是我們可以多學習別人的使用場景,怎么學習呢?很簡單,github上直接搜索objc_getAssociatedObject 選中code,就有很多別人寫的示例:


這種查找資料的方法是不是很簡單呢,呵呵。OK 關聯對象就講完了。

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