OC-關聯對象AssociatedObject

關聯對象

前言

我們都知道ARC環境下, 在一個類中聲明一個屬性@property (nonatomic, assign) int age;, 系統類似的幫我們生成如下代碼:

  1. 生成下劃線的成員變量
  2. 生成setter, getter方法的聲明
  3. 生成setter, getter方法的實現
@interface Person : NSObject
{
    int _age;
}

- (void)setAge:(int)age;
- (int)age;

@end


@implementation Person

- (void)setAge:(int)age
{
    _age = age;
}

- (int)age
{
    return _age;
}
@end

Category中添加屬性

在category中添加屬性, 系統只會做一件事情, 生成setter, getter方法的聲明.
我們知道category中不可以添加實例變量, 因為category是一個結構體, 它只可以添加對象/類方法, 協議, 屬性

struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

如果要讓我們實現可以類似的可以添加實例變量的效果, 那該如何做呢?

  • 方式一: 使用全局字典

    因為我們想讓category實現每一個person對象有一個實例變量的效果, 所以我們可以想到在全局創建一個可變字典, 每個person對應一個實例變量, 如下實現:

    NSMutableDictionary *ages_;
    
    @implementation Person (Test1)
    
    + (void)load {
        ages_ = [NSMutableDictionary dictionary];
    }
    
    - (void)setAge:(int)age
    {
        NSString *key = [NSString stringWithFormat:@"%p", self];
        ages_[key] = @(age);
    }
    
    - (int)age {
        NSString *key = [NSString stringWithFormat:@"%p", self];
        return [ages_[key] intValue];
    }
    
    @end
    
    
    1. person對象的實例變量是存儲在person對象的內部, 而這種實現方式, 將實例變量存在了全局字典中, 實例變量存儲的位置不同
    2. 因為是全局的字典, 所以存在線程安全的問題, 需要在setter方法中加鎖
  • 方式二: 使用關聯對象

    /**
        object: 需要關聯的對象
        key: 指針 類似于字典的key void *
        value: 關聯的值
        policy: 內存策略
    */
    objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)
    
    objc_getAssociatedObject(self, <#const void * _Nonnull key#>)
    
    

    內存策略:

    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
  • key的定義方式一:

    static const void *ageKey = &ageKey;
    - (void)setAge:(int)age
    {
        objc_setAssociatedObject(self, ageKey, @(age), OBJC_ASSOCIATION_ASSIGN);
    }
    
    

    因為key類似于字典的key, 所以每個關聯的值的key是唯一的, 為了唯一性, 我們可以使用: static const void *ageKey = &ageKey; (ageKey這個指針變量存儲的是它自己這個變量的地址, 這樣寫可以保證如果有很多關聯的key的話, 可以確保每個key是唯一的)
    static修飾也可以防止其他文件用extern關鍵字獲取這個key
    static 保證這個全局變量只在內部使用

    變量 內存
    ageKey 0x10000 0x10000
    nameKey 0x10008 0x10008
  • key的定義方式二:

    //更加省事而且聲明的這個變量只占一個字節 char
    static const char ageKey;
    - (void)setAge:(int)age
    {
        objc_setAssociatedObject(self, &ageKey, @(age), OBJC_ASSOCIATION_ASSIGN);
    }
    
  • key的定義方式三:
    知識點: NSString的內存分配

    // 使用@"age", NSString *str = @"age"; 字面量的字符串變量存儲在常量區, 所以@"age", 所以兩個方法中的@"age"字符串的內存地址都是一樣的. 
    - (void)setAge:(int)age
    {
        objc_setAssociatedObject(self, @"age", @(age), OBJC_ASSOCIATION_ASSIGN);
    }
    
    - (int)age {
        objc_getAssociatedObject(self, @"age")
    }
    
  • key的定義方式四:

    - (void)setAge:(int)age
    {
        objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
    }
    
    - (int)age {
        // _cmd表示當前方法的@selector, _cmd == @selector(age)
        objc_getAssociatedObject(self, _cmd);
    }
    /*OC的編譯器在編譯后會在每個方法中加兩個隱藏的參數:
    一個是_cmd,當前方法的一個SEL指針。
    一個是self,指向當前對象的一個指針
    (id)self, (SEL)_cmd
    */
    // 當然使用@seletor(setAge:)等其他方法也可以
    

關聯對象的原理

實現關聯對象技術的核心對象有:
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
可以通過蘋果的開源代碼 objc4: objc-references.mm //引用

class AssociationsManager {
    static AssociationsHashMap *_map;
}

class AssociationsHashMap: public unordered_map<disguised_ptr_t, ObjectAssociationMap>

class ObjectAssociationMap: public std::map <void *, ObjcAssociation>

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
}

舉例說明:

@implementation Person (Test)

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

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

@end

上面的代碼給Person實例對象關聯的兩個值(age和name), 底層上是由全局的AssociationsManager管理, AssociationsManager中有一個AssociationsHashMap(字典), 其中以(姑且認為)person為鍵, AssociationsHashMap(字典)為值. AssociationsHashMap(字典)中以關聯值傳入的key為鍵, 以ObjcAssociation對象為值, ObjcAssociation中包含內存策略和value值

開源代碼如下:

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

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());
        // 傳入的對象object, 經過DISGUISE(object)函數, 進行內存操作, 作為key
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) { //如果
            // break any existing association.
            // 根據disguised_object, 找到該對象對應的AssociationsHashMap
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                // 根據i->second找到ObjectAssociationMap的指針
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    //如果key有對應的`ObjcAssociation`, 則替換
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else { //如果key沒有對應的`ObjcAssociation`, 則創建新的key, ObjcAssociation鍵值對
                // 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.
            // 如果傳入的value為nil值
            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()) { //遍歷該對象對應的字典中所有的ObjectAssociationMap, 進行抹除操作
                    old_association = j->second;
                    refs->erase(j); // 抹除
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}


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

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) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}


總結:

1.關聯對象并不是存儲在被關聯對象本身內存中
2.關聯對象存儲在全局的統一的一個AssociationsManager
3.設置關聯對象為nil, 就相當于移除關聯對象
4.移除某個對象上的所有的關聯對象 void objc_removeAssociatedObjects(id object)
5.如果某個person對象被銷毀了, 則這個person對象所對應的ObjectAssociationMap字典也會被銷毀
6.因為內存策略(objc_AssociationPolicy)中沒有weak,

    Person *p = [[Person alloc] init];
        
    {
        Person *tmp = [[Person alloc] init];
        objc_setAssociatedObject(p, @"tmp", tmp, OBJC_ASSOCIATION_ASSIGN);
    }
    NSLog(@"%@", objc_getAssociatedObject(p, @"tmp"));
    // 上面代碼會報錯誤 EXC_BAD_ACCESS, 壞內存地址訪問, 因為使用的是OBJC_ASSOCIATION_ASSIGN的內存策略, 出了大括號tmp對象釋放

如何設置關聯值的時候使用weak策略呢?
iOS weak 關鍵字漫談

方式一: 使用block包裹

- (void)setContext:(CDDContext*)object {
    id __weak weakObject = object;
    id (^block)() = ^{ return weakObject; };
    objc_setAssociatedObject(self, @selector(context), block, OBJC_ASSOCIATION_COPY);
}

- (CDDContext*)context {
    id (^block)() = objc_getAssociatedObject(self, @selector(context));
    id curContext = (block ? block() : nil);
    return curContext;
}

方式二: 使用對象包裹

新建一個類,用于包裹weak對象
@interface GYCatagoryWeakWrapper : NSObject
@property (nonatomic, weak) id weakObj;
@end

@implementation GYCatagoryWeakWrapper
@end

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