關聯對象 Associated Object 的本質

上一篇文章分類category、load、initialize的本質和源碼分析介紹到,在objc4最新源碼objc4-818.2中,category結構體如下:

struct category_t {
    const char *name;   // 類名
    classref_t cls;
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;    // 實例方法列表
    WrappedPtr<method_list_t, PtrauthStrip> classMethods;   // 類方法列表
    struct protocol_list_t *protocols;  // 協議列表
    struct property_list_t *instanceProperties; // 屬性列表
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

使用clang命令,將 Objective-C 代碼轉換成 C++ 後,其結構與源碼中的類似。

category_t結構體中存儲了實例方法列表、類方法列表、協議列表、屬性列表,但沒有成員變量列表。因此,分類中不能直接添加成員變量,添加的屬性只會聲明getter、setter方法,不會生成對應的getter、setter實現和成員變量。

因此,想要在分類中實現屬性的效果,就需要額外步驟存儲成員變量值。

1. 分類添加成員變量方案

1.1 使用全局變量存儲

分類中沒有成員變量,屬性不會生成訪問器方法的實現。可以通過添加一個全局變量,保存屬性值。如下所示:

static NSString *_name;

@implementation Child (Test)

- (void)setName:(NSString *)name {
    _name = name;
}

- (NSString *)name {
    return _name;
}

@end

使用以下代碼驗證分類屬性存取值:

        Child *child1 = [[Child alloc] init];
        child1.name = @"pro648";
        
        Child *child2 = [[Child alloc] init];
        child2.name = @"It's Time";
        
        NSLog(@"child1 name:%@ -- child2 name:%@", child1.name, child2.name);

控制檯輸出如下:

child1 name:It's Time -- child2 name:It's Time

由於全局變量存儲在類中,內存中只有一個類對象,這就導致不同實例對象共用同一個全局變量,不能區分不同實例的值。

1.2 使用全局字典存儲

既然使用一個全局變量不能區分不同實例,我們可以添加一個全局字典,以實例作爲key,存儲其值。這樣可以區分不同實例:

NSMutableDictionary *nameDict;
#define key [NSString stringWithFormat:@"%p", self]

+ (void)initialize {
    if (self == Child.class) {
        nameDict = [NSMutableDictionary dictionary];
    }
}

- (void)setName:(NSString *)name {
    nameDict[key] = name;
}

- (NSString *)name {
    return nameDict[key];
}

由於不同實例要使用同一個字典,這裏將字典的初始化放到了+initialize方法。在Child類首次收到消息時初始化,既確保了延遲加載,也確保了只初始化一份。由於有多個分類時,只會調用一個分類的+initialize方法,此時應將其初始化放到+load方法。關於+load+initialize方法的區別,可以查看分類category、load、initialize的本質和源碼分析

執行後控制檯打印如下:

child1 name:pro648 -- child2 name:It's Time

如果有多個屬性,就需要添加多個字典,這樣會很麻煩。字典會長時間存在,造成內存泄漏。除此之外,字典並非線程安全,多線程存取時會遇到問題。

1.3 關聯對象 Associated Object

Objective-C 2.0 中 runtime 增加的 associated object,在運行時可以將任意值關聯到指定對象。其主要提供了以下 API:

  • objc_setAssociatedObject:爲指定對象、使用指定key關聯值。值爲nil時,用於清楚關聯對象。
  • objc_getAssociatedObject:取出指定對象使用指定key關聯的值。
  • objc_removeAssociatedObjects:清除指定對象的所有關聯對象。主要用於將對象還原爲「初始狀態」,不應用於從對象中刪除關聯對象,因爲它會刪除關聯到該對象的所有值。通常,爲objc_setAssociatedObject傳值nil以清除關聯。

添加關聯對象對象、取出關聯對象方法如下:

// 添加關聯對象
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
// 取出關聯對象
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

可以看到key是const void * _Nonnull類型的,也就是地址值,不需要變量值。因此,只定義就可以使用。

添加int類型的全局變量age,如下所示:

// 佔4個字節
int age = 0;

使用關聯對象存、取屬性值:

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

- (NSString *)name {
    return objc_getAssociatedObject(self, &age);
}
1.3.1 key 的幾種類型

除了使用int類型的全局變量作爲 key,還可以使用以下內容作爲 key:

  • 指針變量:一個指針變量佔用8個字節,指針變量聲明如下:

    static const void *NameKey = &NameKey;    // 傳 NameKey
    
  • char:一個char只佔用一個字節。

    static const char NameKey;    // 傳 &NameKey
    
  • 字符串:字符串存儲在數據常量區,多個相同字符串地址相同。直接爲 key 傳遞字符串時,就是在傳遞字符串的地址值。

  • @selector():就是指定字符串地址值。

  • _cmd:方法默認有兩個參數,第一個(id)self,第二個(SEL)_cmd。因此@selector()可以使用_cmd替換。

比較之後,最爲便捷的還是@selector()方式,如下所示:

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

- (NSString *)name {
    return objc_getAssociatedObject(self, @selector(name));
}
1.3.2 內存管理

objc_AssociationPolicy枚舉類型決定關聯值存儲類型。

objc_AssociationPolicy 對應property 描述
OBJC_ASSOCIATION_ASSIGN @property(assign) 對關聯對象弱引用。
OBJC_ASSOCIATION_COPY @property(nonatomic, strong) 對關聯對象強引用,非原子性。
OBJC_ASSOCIATION_COPY_NONATOMIC @property(nonatomic, copy) 複製關聯對象,非原子性。
OBJC_ASSOCIATION_RETAIN @property(atomic, strong) 對關聯對象強引用,原子性。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property(atomic, copy) 複製關聯對象,原子性。

2. 關聯對象的本質

RunTime 主要由以下對象實現關聯對象技術:

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjectAssociation

2.1 objc_setAssociatedObject源碼

在objc4中搜索objc_setAssociatedObject()函數,如下所示:

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

其調用了_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));

    // 用object拿到AssociationsHashMap中的key
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    // 使用傳入的policy、value生成association。
    ObjcAssociation association{policy, value};

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

    bool isFirstAssociation = false;
    {
        AssociationsManager manager;
        // manager關聯了AssociationsHashMap
        AssociationsHashMap &associations(manager.get());

        if (value) {
            // 使用object生成的disguised取出字典數據
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                isFirstAssociation = true;
            }

            /* establish or replace the association */
            // 取出ObjectAssociationMap
            auto &refs = refs_result.first->second;
            
            // 使用key取出ObjectAssociation
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                // 交換關聯的association
                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);
                    // 如果關聯對象value是空,就移除關聯對象。
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // Call setHasAssociatedObjects outside the lock, since this
    // will call the object's _noteAssociatedObjects method if it
    // has one, and this may trigger +initialize which might do
    // arbitrary stuff, including setting more associated objects.
    if (isFirstAssociation)
        object->setHasAssociatedObjects();

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

AssociationsManager中關聯了AssociationsHashMap,使用參數object計算出 key disguised,利用disguisedAssociationsManager中取出ObjectAssociationMap,利用傳入的 key 取出ObjectAssociation。之前使用參數valuepolicy已經生成了association,這裏直接與 key 取出的ObjectAssociation交換。另外,也可以看到,association 不會引用傳進來的 object 對象,只使用其計算出一個值,但引用了傳進來的 value。對象釋放時,會自動移除關聯對象關聯的所有值。

如果參數value是空的,就移除關聯對象。

AssociationsManager源碼如下:

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

    // AssociationsManager關聯了ObjectAssociationMap。
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }

    static void init() {
        _mapStorage.init();
    }
};

ObjectAssociationMapAssociationsHashMap源碼如下:

// key是 const void *, value是ObjcAssociation
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
// key: DisguisePtr value: ObjectAssociationMap
// 就是value是另一個map
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

關聯對象原理如下圖:

2.2 objc_getAssociatedObject源碼

RunTime 中的objc_getAssociatedObject()函數如下:

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

其調用了_object_get_associative_reference()函數,_object_get_associative_reference()如下:

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

    {
        AssociationsManager manager;
        // manager關聯了AssociationsHashMap
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            // 取出ObjectAssociationMap
            ObjectAssociationMap &refs = i->second;
            // 使用參數 key,取出ObjectAssociation
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                // ObjectAssociation包含要取出的 value 和 policy。
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

objc_getAssociatedObject()取出關聯對象步驟與objc_setAssociatedObject()設值關聯對象步驟類似,都是使用傳入的object作爲 key,取出ObjectAssociationMap,即ObjectAssociationMap包含了該實例的所有關聯對象。使用參數 key 從ObjectAssociationMap中取出ObjectAssociation對象。ObjectAssociation就是要取出、設值的關聯對象。

2.3 objc_removeAssociatedObjects

RunTime 中objc_removeAssociatedObjects()函數如下:

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object, /*deallocating*/false);
    }
}

objc_removeAssociatedObjects()函數調用了_object_remove_associations()函數,如下所示:

// 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, bool deallocating)
{
    ObjectAssociationMap refs{};

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

            // If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
            bool didReInsert = false;
            if (!deallocating) {
                for (auto &ref: refs) {
                    if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
                        i->second.insert(ref);
                        didReInsert = true;
                    }
                }
            }
            if (!didReInsert)
                // 移除 AssociationsMap
                associations.erase(i);
        }
    }

    // Associations to be released after the normal ones.
    SmallVector<ObjcAssociation *, 4> laterRefs;

    // release everything (outside of the lock).
    for (auto &i: refs) {
        if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
            // If we are not deallocating, then RELEASE_LATER associations don't get released.
            if (deallocating)
                laterRefs.append(&i.second);
        } else {
            i.second.releaseHeldValue();
        }
    }
    for (auto *later: laterRefs) {
        later->releaseHeldValue();
    }
}

_object_remove_associations()函數根據object對象,查找到AssociationMap後直接移除。

3. 面試題

3.1 分類可以添加成員變量嗎?

分類不能添加成員變量。可以添加屬性,但系統只會生成屬性getter、setter方法的聲明,不會生成其實現,也不會生成屬性的成員變量。想要在分類中使用屬性,需在getter、setter實現中手動使用關聯對象實現。

Demo名稱:AssociatedObject
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/AssociatedObject

歡迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/關聯對象%20Associated%20Object%20的本質.md

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章