Objective-C(十二)關聯對象

本文是Objective-C系列的第12篇,主要講述了關聯對象的底層結構和使用。c++

1、爲何須要關聯對象?

1. Category能添加成員變量嗎?

咱們在Objective-C(十)Category中講過,Category能添加協議、方法、屬性等,參考下面的結構體。git

//from objc-runtime-new.h
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    ....
};
複製代碼

既然能夠添加屬性,那是否是能夠添加成員變量呢?github

答案是:不能夠app

以下面的實例,調用後,Crash異步

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[BFPerson setAge:]: unrecognized selector sent to instance 0x60000202c4b0'ide

//分類聲明
@interface BFPerson (BFBoy)
@property (nonatomic, assign) NSInteger age;
@end
    
//調用
BFPerson *person = [[BFPerson alloc] init];
person.age = 28;
複製代碼

因此,咱們得知:函數

Category能夠添加屬性,但不會添加對應的成員變量,也不會實現對應的setter、getter方法。工具

2. 如何實現給Category添加成員變量?

實例源碼參考01-Category成員變量post

既然沒法直接給分類中添加,咱們能夠經過全局變量的方式來保存成員變量的值,且實現對應的setter、getter來模擬爲Category完整添加屬性(成員變量)的效果。ui

@interface BFPerson (BFBoy)
@property (nonatomic, assign) NSInteger age;
@end

@implementation BFPerson (BFBoy)
NSMutableDictionary *ages_;
+ (void)load
{
    ages_ = [[NSMutableDictionary alloc] init];
}
- (void)setAge:(NSInteger)age
{
    NSString *key = [NSString stringWithFormat:@"%p", self];
    ages_[key] = @(age);
}
- (NSInteger)age
{
    NSString *key = [NSString stringWithFormat:@"%p", self];
    return [ages_[key] integerValue];
}
@end
複製代碼

上述方案有一些缺點:

  • 每添加一個屬性,就須要建立一個字典來保存對應成員變量
    • 抑或共用一個字典,但必需要保證不一樣屬性對應不一樣的key;
  • 實現繁瑣。

3. 什麼是關聯對象?

關聯是指把兩個對象相互關聯起來,使得其中的一個對象做爲另一個對象的一部分。 關聯特性只有在Mac OS X V10.六、iOSV3.1以及之後的版本上纔是可用的。

使用關聯,咱們能夠不用修改類的定義而爲其對象增長存儲空間。這在咱們沒法訪問到類的源碼的時候或者是考慮到二進制兼容性的時候是很是有用。

關聯是基於關鍵字的,所以,咱們能夠爲任何對象增長任意多的關聯,每一個都使用不一樣的關鍵字便可。關聯是能夠保證被關聯的對象在關聯對象的整個生命週期都是可用的(在垃圾自動回收環境下也不會致使資源不可回收)。

2、關聯對象的使用

示例代碼02-關聯對象

1. 關聯對象API

image-20181202190626994

2. key

  • 關鍵字是一個void類型的指針,必須惟一

  • 經常使用靜態變量來做爲key。一般推薦key使用static char類型——使用指針或許更好,並只在getter和setter方法內部使用。

  • 更簡單的方案是:直接使用選擇器(selector),由於SEL生成的時候就是一個惟一的常量。

image-20181202191224174

3. policy

  • policy代表了value內存語義,是經過賦值,保留引用仍是複製的方式進行關聯的;

  • policy還代表原子的仍是非原子的。

這裏的關聯策略和聲明屬性時的很相似。這種關聯策略是經過使用預先定義好的常量來表示的。

associated_object_policy

4. 應用

在本文開始就提出,關聯對象做爲給Category添加成員變量是一種高效可行的方案

可是,咱們仍然須要提醒你

  • 關聯對象應該被當作最後的手段來使用(不得不用時才用),而不是爲了尋求一個解決方案就行。
  • category自己就不該該是解決問題優先選擇的工具

說到這裏,咱們在開發中經常看到一種,或者經歷過一種現象——某個開發者,在博客或其餘方式中得知某一個技術點,這個技術點具有一些特定——是巧妙的伎倆、hack手段或者是變通的解決方案,開發者這時候躍躍欲試,爲了用而用,在不徹底吃透該技術點或原理時,就貿然使用。

我給出的建議就是,**吃透原理,分析場景,**這些小伎倆再用到平時開發中。

下面給出了一些其餘開源庫中用到的場景:

4.1 類增長狀態

添加私有變量來幫助實現細節 。當拓展一個內置類時,可能有必要跟蹤一些額外的狀態,這是關聯對象最廣泛的應用場景。

例如:AFNetworking中在UIImageView的分類中使用關聯對象來存儲一個請求操做對象(operation object),用於異步的從遠程獲取圖片。

4.2 解耦

使用關聯對象來代替X,其中X表明下面的一些項:

  • 子類化,當使用繼承比使用組合更合適的時候。
  • target-action給響應者添加交互事件。
    • 按鈕
    • 手勢識別,當target-action模式不夠用的時候。
  • 代理,當事件能夠委託給其餘對象。
  • 消息 & 消息中心使用低耦合的方式來廣播消息。

3、原理

1.核心對象

對應的API爲:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) 
複製代碼

1.1 AssociationsManager

全局關聯對象管理類,其擁有一個map字典用於存放全部被關聯對象object的關聯對象。

class AssociationsManager {
    static AssociationsHashMap *_map;

    AssociationsHashMap &associations() {
        if (_map == NULL)
        _map = new AssociationsHashMap();
        return *_map;
    }
 };
複製代碼

1.2 AssociationsHashMap

AssociationsHashMap就是存放一個被關聯對象object的全部管理對象的字典,根據下面的類定義:

class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    };
複製代碼
  • 繼承於unordered_map,後面三個參數,分別是hashequalallocator函數的實現。

其中該字典對應的:

  • key:disguised_ptr_t類型
  • value:ObjectAssociationMap類型

1.3 ObjectAssociationMap

ObjectAssociationMap是真正存儲object中關聯對象的載體。

class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
};
複製代碼

其中該字典:

  • key:void *指針類型,即傳進來的key。
  • value:ObjcAssociation類型,該類型存儲了傳進來的valuepolicy

1.4 ObjcAssociation

ObjcAssociation用於存放關聯對象的valuepolicy

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
};
複製代碼

2. 關聯對象的結構

image-20181202214605229

3.核心流程

3.1 設置關聯對象

  • object不能爲nil
  • value可爲nil,nil表示清除該關聯對象
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
  if (new_value) {
        // break any existing association.
        // 1.value有值
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // 1.1.1 找到該object對應的ObjectAssociationMap
            ObjectAssociationMap *refs = i->second;
            // 1.1.2 在ObjectAssociationMap尋找key對應的ObjcAssociation
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                // 1.1.2.1 找到ObjcAssociation,將舊值保存在old_association
                // 1.1.2.2 從新設新值
                old_association = j->second;
                j->second = ObjcAssociation(policy, new_value);
            } else {
                // 1.1.2.3 未找到對應ObjcAssociation,直接設新值
                (*refs)[key] = ObjcAssociation(policy, new_value);
            }
        } else {
            // 1.2.1 未找到ObjectAssociationMap,即以前object未設置過關聯對象
            // ObjectAssociationMap新增,ObjcAssociation新增
            ObjectAssociationMap *refs = new ObjectAssociationMap;
            associations[disguised_object] = refs;
            (*refs)[key] = ObjcAssociation(policy, new_value);
            object->setHasAssociatedObjects();
        }
    } else {
        // 2. value爲空
        // 2.1 尋找object對應的ObjectAssociationMap
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i !=  associations.end()) {
            ObjectAssociationMap *refs = i->second;
            // 2.1.1 尋找key對應的ObjcAssociation
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                // 2.2.1.1 找到ObjcAssociation,將舊值保存在old_association
                // 2.2.1.2 從新設新值
                old_association = j->second;
                refs->erase(j);
            }
        }
    }
}
複製代碼

3.2 獲取關聯對象

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;
}
複製代碼

3.3 移除關聯對象

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. objc源碼
  2. 示例代碼01-Category成員變量
  3. 示例代碼02-關聯對象
相關文章
相關標籤/搜索