通過Runtime源碼了解關(guān)聯(lián)對象的實現(xiàn)

原文鏈接

在iOS開發(fā)中,Category是經(jīng)常使用到的一個特性,合理的使用Category能夠減少繁瑣代碼,提高開發(fā)效率。在使用Category時,有經(jīng)驗的開發(fā)者應(yīng)該都知道,在Category中是無法添加屬性的,如果想在Category中實現(xiàn)屬性的效果,需要使用關(guān)聯(lián)對象。關(guān)聯(lián)對象屬于Runtime的范疇,本篇文章結(jié)合Runtime源碼,分析下關(guān)聯(lián)對象的內(nèi)部實現(xiàn)。

Category中使用@property

上面提到了在Category中無法添加屬性,來驗證一下。倘若在Category中添加屬性,是會直接編譯錯誤?還是會警告?

定義一個Person類,代碼如下:

@interface Person : NSObject{
    NSString *_age;
}

- (void)printName;

@end

實現(xiàn)文件

@implementation Person

- (void)printName
{
    NSLog(@"my name is Person");
}

@end

為Person 添加一個Category MyPerson,Category中定義一個屬性 personName,代碼如下:

@interface Person (MyPerson)

@property (nonatomic, copy) NSString *personName;

@end

實現(xiàn)文件中暫時為空。

現(xiàn)在我們在Category中添加了@property,編譯一下,沒有問題,可以編譯成功。也就是說,Category中使用@property不會引起編譯錯誤。但是呢,Xcode會提示警告,警告信息如下:

Property 'personName' requires method 'personName' to be defined - use @dynamic or provide a method implementation in this category

Property 'personName' requires method 'setPersonName:' to be defined - use @dynamic or provide a method implementation in this category

大意就是需要為屬性personName實現(xiàn)get方法和set方法。

在繼續(xù)下一步之前,首先需要了解Objective-C中的@property到底是什么:

@property = 實例變量 + get方法 + set方法

關(guān)于@property的更詳細(xì)介紹,可以參考這篇文章。

也就是說,在普通文件中,定義一個屬性,編譯器會自動生成實例變量,以及該實例變量對應(yīng)的get/set方法。但是在Category中,根據(jù)Xcode的警告信息,是沒有生成get/set方法的。

既然Xcode沒有自動生成get/set方法,那么我們來手動實現(xiàn)一下get/set方法。

在Category的實現(xiàn)文件中加入以下代碼:

- (NSString *)personName
{
    return _personName;
}

- (void)setPersonName:(NSString *)personName
{
    _personName = personName;
}

警告信息確實沒了,直接提示error,編譯不通過,錯誤信息如下:

Use of undeclared identifier '_personName'

_personName沒有定義。看來在Category中使用@property,編譯器不僅不會自動生成set/get方法,連實例變量也不會生成。話說回來,沒有實例變量,自然也不會有set/get方法。

正是因為Category中的@property不會生成實例變量,get/set方法,所以如果在程序中使用Category的屬性,編譯不會有問題,但是在運行期間會直接崩潰。

Person *p = [[Person alloc] init];
[p printName];
    
p.personName = @"haha"; // 這里會直接崩潰

崩潰信息如下:

-[Person setPersonName:]: unrecognized selector sent to instance 0x60000300ab80

崩潰原因也是容易理解的,因為根本沒有setPersonName方法。

@property和關(guān)聯(lián)對象結(jié)合使用

既然在Category中無法直接使用@property,那有沒有什么辦法解決呢?答案就是關(guān)聯(lián)對象。

關(guān)聯(lián)對象其實是AssociatedObject的翻譯。需要注意的是,關(guān)聯(lián)對象并不是代替了Category中的屬性,而是在Category中@property和關(guān)聯(lián)對象結(jié)合使用,以達(dá)到正常使用@property的目的。

文章開頭也提到了,關(guān)聯(lián)對象屬于Runtime的范疇,因此使用關(guān)聯(lián)對象之前,首先導(dǎo)入runtime頭文件

#import <objc/runtime.h>

然后在實現(xiàn)屬性的get/set方法,get/set方法中使用關(guān)聯(lián)對象,代碼如下:

- (NSString *)personName
{
    return objc_getAssociatedObject(self, _cmd);
}

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

現(xiàn)在在程序中使用Category中的屬性,可以正常使用:

Person *p = [[Person alloc] init];
[p printName];
    
p.personName = @"haha";
NSLog(@"p.personName = %@",p.personName);

輸出:

my name is Person
p.personName = haha

這就是關(guān)聯(lián)對象的作用。Category中關(guān)聯(lián)對象和@property結(jié)合使用,能夠達(dá)到在主程序中正常使用Category中屬性的目的。

關(guān)聯(lián)對象在Runtime中的實現(xiàn)

來看一下關(guān)聯(lián)對象在Runtime中到底是怎么實現(xiàn)的。我們主要通過追蹤Runtime開放給我們的接口來探索。上面已經(jīng)用到了兩個接口,分別是:

objc_getAssociatedObject
objc_setAssociatedObject

除了這兩個接口外,還有一個接口:

objc_removeAssociatedObjects

也就是說,Runtime主要提供了三個方法供我們使用關(guān)聯(lián)對象:

// 根據(jù)key獲取關(guān)聯(lián)對象
id objc_getAssociatedObject(id object, const void *key);
// 以key、value的形式設(shè)置關(guān)聯(lián)對象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 移出對象所有的關(guān)聯(lián)對象
void objc_removeAssociatedObjects(id object);

接下來依次分析每個方法。

objc_setAssociatedObject

objc_setAssociatedObject方法位于objc-runtime.mm文件中,該方法的實現(xiàn)比較簡單,調(diào)用了_object_set_associative_reference函數(shù)。

// 設(shè)置關(guān)聯(lián)對象的方法
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

_object_set_associative_reference函數(shù)完成了設(shè)置關(guān)聯(lián)對象的操作。在看_object_set_associative_reference函數(shù)源碼之前,先了解幾個結(jié)構(gòu)體代表的含義。

ObjcAssociation

ObjcAssociation就是關(guān)聯(lián)對象,在應(yīng)用層設(shè)置、獲取關(guān)聯(lián)對象,在Runtime中都被表示成了ObjcAssociation??匆幌翺bjcAssociation的定義:

// ObjcAssociation就是關(guān)聯(lián)對象類
class ObjcAssociation {
    uintptr_t _policy;
    // 值
    id _value;
public:
    // 構(gòu)造函數(shù)
    ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
    // 默認(rèn)構(gòu)造函數(shù),參數(shù)分別為0和nil
    ObjcAssociation() : _policy(0), _value(nil) {}
};

關(guān)聯(lián)對象中定義了_value和_policy兩個變量。_policy之后再說,_value就是關(guān)聯(lián)對象的值,比如上面賦值為@"haha"。

AssociationsManager

AssociationsManager可以理解成一個Manager類,看一下AssociationsManager的實現(xiàn)

class AssociationsManager {
    // AssociationsManager中只有一個變量AssociationsHashMap
    static AssociationsHashMap *_map;
public:
    // 構(gòu)造函數(shù)中加鎖
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    // 析構(gòu)函數(shù)中釋放鎖
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    // 構(gòu)造函數(shù)、析構(gòu)函數(shù)中加鎖、釋放鎖的操作,保證了AssociationsManager是線程安全的
    
    AssociationsHashMap &associations() {
        // AssociationsHashMap 的實現(xiàn)可以理解成單例對象
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

AssociationsManager中只有一個變量,AssociationsHashMap,通過源碼可以看到,AssociationsManager中的AssociationsHashMap的實現(xiàn)可以理解成是單例的。而且AssociationsManager的構(gòu)造函數(shù)和析構(gòu)函數(shù)分別做了加鎖、釋放鎖的操作。也就是說,同一時刻,只能有一個線程操作AssociationsManager中的AssociationsHashMap。

AssociationsHashMap

AssociationsHashMap,看名字可以猜到是hashMap類型,那么里面的key、value到底是什么呢?看下AssociationsHashMap的定義:

// AssociationsHashMap是字典,key是對象的disguised_ptr_t值,value是ObjectAssociationMap
    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); }
    };

key是對象的DISGUISE()值,value是ObjectAssociationMap。DISGUISE()可以是一個函數(shù),每個對象的DISGUISE()值不同,作為了AssociationsHashMap的key。

ObjectAssociationMap

ObjectAssociationMap是map類型,里面也是以key、value的形式存儲??匆幌翺bjectAssociationMap的定義

// ObjectAssociationMap是字典,key是從外面?zhèn)鬟^來的key,例如@selector(hello),value是關(guān)聯(lián)對象,也就是
    // ObjectAssociation
    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); }
    };

key是從外面?zhèn)鬟^來的,比如我們上面用到的@selector(personName),value是上面提到的ObjcAssociation對象,也就是關(guān)聯(lián)對象。終于看到了關(guān)聯(lián)對象,通過下面一整圖看一下整個是如何存儲的

[圖片上傳失敗...(image-bb1f1f-1554867497085)]

_object_set_associative_reference源碼

_object_set_associative_reference函數(shù)中根據(jù)所傳的參數(shù)value是否為nil,分成了不同的邏輯。value為nil的邏輯比較簡單,我們首先看一下value為nil所做的處理。

value = nil

value為nil時的代碼:

// 初始化一個manager
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 獲取對象的DISGUISE值,作為AssociationsHashMap的key
disguised_ptr_t disguised_object = DISGUISE(object);

// value無值,也就是釋放一個key對應(yīng)的關(guān)聯(lián)對象
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;
        // 調(diào)用erase()方法刪除對應(yīng)的關(guān)聯(lián)對象
        refs->erase(j);
    }
}

// 釋放舊的關(guān)聯(lián)對象
if (old_association.hasValue()) ReleaseValue()(old_association);

通過代碼可以看到,當(dāng)value'為nil時,Runtime做的操作就是找到原來該key所對應(yīng)的關(guān)聯(lián)對象,并且將該關(guān)聯(lián)對象刪除。也就是說,value為nil,實際上就是釋放一個key對應(yīng)的關(guān)聯(lián)對象

value != nil

value不為nil,實際上就是為某個對象添加關(guān)聯(lián)對象。為某個對象添加關(guān)聯(lián)對象,又分為該對象之前已經(jīng)添加過關(guān)聯(lián)對象和該對象是第一次添加關(guān)聯(lián)對象的邏輯。

  1. 該對象第一次添加關(guān)聯(lián)對象
    看一下該對象第一次添加關(guān)聯(lián)對象的代碼:
// 初始化一個manager
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 獲取對象的DISGUISE值,作為AssociationsHashMap的key
disguised_ptr_t disguised_object = DISGUISE(object);

// AssociationsHashMap::iterator 類型的迭代器
AssociationsHashMap::iterator i = associations.find(disguised_object);

// 執(zhí)行到這里,說明該對象是第一次添加關(guān)聯(lián)對象
 // 初始化ObjectAssociationMap
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
// 賦值
(*refs)[key] = ObjcAssociation(policy, new_value);
// 設(shè)置該對象的有關(guān)聯(lián)對象,調(diào)用的是setHasAssociatedObjects()方法
object->setHasAssociatedObjects();

通過代碼可以看到,若該對象是第一次添加關(guān)聯(lián)對象,則先生成新的ObjectAssociationMap,并根據(jù)policy、value初始化ObjcAssociation對象,以外部傳的key、生成的ObjcAssociation分別作為ObjectAssociationMap的key、value。以DISGUISE(object)、生成的ObjectAssociationMap分別作為AssociationsHashMap的key、value。

  1. 該對象不是第一次添加關(guān)聯(lián)對象
    若該對象不是第一次添加關(guān)聯(lián)對象,根據(jù)原來是否有該key對應(yīng)的關(guān)聯(lián)對象進(jìn)行邏輯區(qū)分。
    1. 原來有該key對應(yīng)的關(guān)聯(lián)對象
      代碼如下:
    // 初始化一個manager
    AssociationsManager manager;
    AssociationsHashMap &associations(manager.associations());
    // 獲取對象的DISGUISE值,作為AssociationsHashMap的key
    disguised_ptr_t disguised_object = DISGUISE(object);
    
    // AssociationsHashMap::iterator 類型的迭代器
    AssociationsHashMap::iterator i = associations.find(disguised_object);
    
    // 獲取到ObjectAssociationMap(key是外部傳來的key,value是關(guān)聯(lián)對象類ObjcAssociation)
    ObjectAssociationMap *refs = i->second;
    // ObjectAssociationMap::iterator 類型的迭代器
    ObjectAssociationMap::iterator j = refs->find(key);
    
    // 原來該key對應(yīng)的有關(guān)聯(lián)對象
    // 將原關(guān)聯(lián)對象的值存起來,并且賦新值
    old_association = j->second;
    j->second = ObjcAssociation(policy, new_value);
    
    // 釋放舊的關(guān)聯(lián)對象
    if (old_association.hasValue()) ReleaseValue()(old_association);
    
    原來有該key所對應(yīng)的關(guān)聯(lián)對象,所做的處理就是將原來的值存下來,并且賦新的值。最后將原來的值釋放。
    1. 原來沒有該key對應(yīng)的關(guān)聯(lián)對象
      代碼如下:
    // 初始化一個manager
    AssociationsManager manager;
    AssociationsHashMap &associations(manager.associations());
    // 獲取對象的DISGUISE值,作為AssociationsHashMap的key
    disguised_ptr_t disguised_object = DISGUISE(object);
    
    // AssociationsHashMap::iterator 類型的迭代器
    AssociationsHashMap::iterator i = associations.find(disguised_object);
    
    // 獲取到ObjectAssociationMap(key是外部傳來的key,value是關(guān)聯(lián)對象類ObjcAssociation)
    ObjectAssociationMap *refs = i->second;
    // ObjectAssociationMap::iterator 類型的迭代器
    ObjectAssociationMap::iterator j = refs->find(key);
    
    // 無該key對應(yīng)的關(guān)聯(lián)對象,直接賦值即可
    // ObjcAssociation(policy, new_value)提供了這樣的構(gòu)造函數(shù)
    (*refs)[key] = ObjcAssociation(policy, new_value);
    
    原來沒有該key所對應(yīng)的關(guān)聯(lián)對象,直接賦值即可。
_object_set_associative_reference流程

看完了_object_set_associative_reference的源碼,介紹的比較復(fù)雜,其實流程相對來說是比較簡單的,整個流程可以用下面的流程圖來表示:

[圖片上傳失敗...(image-b69509-1554867497085)]

policy參數(shù)

上面已經(jīng)多次看到了policy參數(shù),policy參數(shù)到底代表什么呢?通過上面的介紹,應(yīng)該可以猜到了policy的作用。在定義一個屬性時,需要使用各種各樣的修飾符,如nonatomic,copy,strong等,既然關(guān)聯(lián)對象是為了達(dá)到和屬性相同的效果,那么關(guān)聯(lián)對象是否也應(yīng)該有對應(yīng)的修飾符呢?

正是如此,構(gòu)造關(guān)聯(lián)對象的policy參數(shù),就是類似于屬性的修飾符。

我們在應(yīng)用層設(shè)置關(guān)聯(lián)對象時,之前代碼用到的值是OBJC_ASSOCIATION_COPY_NONATOMIC,OBJC_ASSOCIATION_COPY_NONATOMIC是枚舉類型,其取值有以下幾種:

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. */
};

根據(jù)其注釋,可以得出objc_AssociationPolicy與屬性修飾符之間的一個對應(yīng)關(guān)系,如下:

[圖片上傳失敗...(image-61610-1554867497085)]

這也是為何我們之前的代碼,設(shè)置關(guān)聯(lián)對象時,使用OBJC_ASSOCIATION_COPY_NONATOMIC的原因。

關(guān)于各種屬性修飾符之間的區(qū)別,以及什么情景下使用哪種修飾符,可以參考這篇文章。

objc_getAssociatedObject

objc_getAssociatedObject方法位于objc-runtime.mm文件中,該方法的實現(xiàn)比較簡單,內(nèi)部直接調(diào)用了_object_get_associative_reference函數(shù),代碼如下:

// 獲取關(guān)聯(lián)對象的方法
id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}
_object_get_associative_reference函數(shù)

獲取關(guān)聯(lián)對象的操作都在函數(shù)_object_get_associative_reference中。其主要流程是,獲取對象的DISGUISE()值,根據(jù)該值獲取到ObjectAssociationMap。根據(jù)外部所傳的key,在ObjectAssociationMap中找到key所對應(yīng)的ObjcAssociation對象,然后得到ObjcAssociation的value。代碼如下:

id value = nil;
AssociationsManager manager;
// 獲取到manager中的AssociationsHashMap
AssociationsHashMap &associations(manager.associations());
// 獲取對象的DISGUISE值
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);

// 獲取ObjectAssociationMap
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);

// 獲取到關(guān)聯(lián)對象ObjcAssociation
ObjcAssociation &entry = j->second;
// 獲取到value
value = entry.value();

// 返回關(guān)聯(lián)對像的值
return value;

objc_removeAssociatedObject

objc_removeAssociatedObject位于objc-runtime.mm文件中。注意,objc_removeAssociatedObject函數(shù)的作用是移除某個對象的所有關(guān)聯(lián)對象。倘若想要移除對象某個key所對應(yīng)的關(guān)聯(lián)對象,需要使用objc_setAssociatedObject函數(shù),value傳nil。

objc_removeAssociatedObject的實現(xiàn)比較簡單,內(nèi)部調(diào)用了_object_remove_associations函數(shù),代碼如下:

// 移除對象object的所有關(guān)聯(lián)對象
void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}
_object_remove_associations函數(shù)

_object_remove_associations函數(shù)的邏輯也比較簡單,根據(jù)對象的DISGUISE()值找到ObjectAssociationMap,然后將該map中的所有值刪除。刪除時需要先將值存起來,然后再刪除,_object_remove_associations函數(shù)中使用了vector來存儲值。之后再將找到的ObjectAssociationMap刪除,代碼如下:

// 聲明了一個vector
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;

AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 獲取對象的DISGUISE值
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);

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);

for_each(elements.begin(), elements.end(), ReleaseValue());

總結(jié)

至此,關(guān)于關(guān)聯(lián)對象的使用、在Runtime源碼中的實現(xiàn)已經(jīng)全部介紹完畢。實際上,日常的工作中是很難涉及到關(guān)聯(lián)對象的內(nèi)部實現(xiàn)的。只要掌握Runtime提供給我們的三個接口,使用Category以及關(guān)聯(lián)對象就足以勝任工作項目。不過,對于想要了解Runtime源碼的同學(xué)來說,掌握關(guān)聯(lián)對象在Runtime源碼中的實現(xiàn),是有很大幫助的。

參考文章

關(guān)聯(lián)對象 AssociatedObject 完全解析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容