通过Runtime源码了解关联对象的实现
原文鏈接
在iOS開發中,Category是經常使用到的一個特性,合理的使用Category能夠減少繁瑣代碼,提高開發效率。在使用Category時,有經驗的開發者應該都知道,在Category中是無法添加屬性的,如果想在Category中實現屬性的效果,需要使用關聯對象。關聯對象屬于Runtime的范疇,本篇文章結合Runtime源碼,分析下關聯對象的內部實現。
Category中使用@property
上面提到了在Category中無法添加屬性,來驗證一下。倘若在Category中添加屬性,是會直接編譯錯誤?還是會警告?
定義一個Person類,代碼如下:
@interface Person : NSObject{NSString *_age; }- (void)printName;@end復制代碼實現文件
@implementation Person- (void)printName {NSLog(@"my name is Person"); }@end 復制代碼為Person 添加一個Category MyPerson,Category中定義一個屬性 personName,代碼如下:
@interface Person (MyPerson)@property (nonatomic, copy) NSString *personName;@end 復制代碼實現文件中暫時為空。
現在我們在Category中添加了@property,編譯一下,沒有問題,可以編譯成功。也就是說,Category中使用@property不會引起編譯錯誤。但是呢,Xcode會提示警告,警告信息如下:
Property 'personName' requires method 'personName' to be defined - use @dynamic or provide a method implementation in this categoryProperty 'personName' requires method 'setPersonName:' to be defined - use @dynamic or provide a method implementation in this category 復制代碼大意就是需要為屬性personName實現get方法和set方法。
在繼續下一步之前,首先需要了解Objective-C中的@property到底是什么:
@property = 實例變量 + get方法 + set方法
關于@property的更詳細介紹,可以參考這篇文章。
也就是說,在普通文件中,定義一個屬性,編譯器會自動生成實例變量,以及該實例變量對應的get/set方法。但是在Category中,根據Xcode的警告信息,是沒有生成get/set方法的。
既然Xcode沒有自動生成get/set方法,那么我們來手動實現一下get/set方法。
在Category的實現文件中加入以下代碼:
- (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和關聯對象結合使用
既然在Category中無法直接使用@property,那有沒有什么辦法解決呢?答案就是關聯對象。
關聯對象其實是AssociatedObject的翻譯。需要注意的是,關聯對象并不是代替了Category中的屬性,而是在Category中@property和關聯對象結合使用,以達到正常使用@property的目的。
文章開頭也提到了,關聯對象屬于Runtime的范疇,因此使用關聯對象之前,首先導入runtime頭文件
#import <objc/runtime.h> 復制代碼然后在實現屬性的get/set方法,get/set方法中使用關聯對象,代碼如下:
- (NSString *)personName {return objc_getAssociatedObject(self, _cmd); }- (void)setPersonName:(NSString *)personName {objc_setAssociatedObject(self, @selector(personName), personName, OBJC_ASSOCIATION_COPY_NONATOMIC); } 復制代碼現在在程序中使用Category中的屬性,可以正常使用:
Person *p = [[Person alloc] init]; [p printName];p.personName = @"haha"; NSLog(@"p.personName = %@",p.personName); 復制代碼輸出:
my name is Person p.personName = haha 復制代碼這就是關聯對象的作用。Category中關聯對象和@property結合使用,能夠達到在主程序中正常使用Category中屬性的目的。
關聯對象在Runtime中的實現
來看一下關聯對象在Runtime中到底是怎么實現的。我們主要通過追蹤Runtime開放給我們的接口來探索。上面已經用到了兩個接口,分別是:
objc_getAssociatedObject objc_setAssociatedObject 復制代碼除了這兩個接口外,還有一個接口:
objc_removeAssociatedObjects 復制代碼也就是說,Runtime主要提供了三個方法供我們使用關聯對象:
// 根據key獲取關聯對象 id objc_getAssociatedObject(id object, const void *key); // 以key、value的形式設置關聯對象 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); // 移出對象所有的關聯對象 void objc_removeAssociatedObjects(id object); 復制代碼接下來依次分析每個方法。
objc_setAssociatedObject
objc_setAssociatedObject方法位于objc-runtime.mm文件中,該方法的實現比較簡單,調用了_object_set_associative_reference函數。
// 設置關聯對象的方法 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函數完成了設置關聯對象的操作。在看_object_set_associative_reference函數源碼之前,先了解幾個結構體代表的含義。
ObjcAssociation
ObjcAssociation就是關聯對象,在應用層設置、獲取關聯對象,在Runtime中都被表示成了ObjcAssociation。看一下ObjcAssociation的定義:
// ObjcAssociation就是關聯對象類 class ObjcAssociation {uintptr_t _policy;// 值id _value; public:// 構造函數ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}// 默認構造函數,參數分別為0和nilObjcAssociation() : _policy(0), _value(nil) {} }; 復制代碼關聯對象中定義了_value和_policy兩個變量。_policy之后再說,_value就是關聯對象的值,比如上面賦值為@"haha"。
AssociationsManager
AssociationsManager可以理解成一個Manager類,看一下AssociationsManager的實現
class AssociationsManager {// AssociationsManager中只有一個變量AssociationsHashMapstatic AssociationsHashMap *_map; public:// 構造函數中加鎖AssociationsManager() { AssociationsManagerLock.lock(); }// 析構函數中釋放鎖~AssociationsManager() { AssociationsManagerLock.unlock(); }// 構造函數、析構函數中加鎖、釋放鎖的操作,保證了AssociationsManager是線程安全的AssociationsHashMap &associations() {// AssociationsHashMap 的實現可以理解成單例對象if (_map == NULL)_map = new AssociationsHashMap();return *_map;} }; 復制代碼AssociationsManager中只有一個變量,AssociationsHashMap,通過源碼可以看到,AssociationsManager中的AssociationsHashMap的實現可以理解成是單例的。而且AssociationsManager的構造函數和析構函數分別做了加鎖、釋放鎖的操作。也就是說,同一時刻,只能有一個線程操作AssociationsManager中的AssociationsHashMap。
AssociationsHashMap
AssociationsHashMap,看名字可以猜到是hashMap類型,那么里面的key、value到底是什么呢?看下AssociationsHashMap的定義:
// AssociationsHashMap是字典,key是對象的disguised_ptr_t值,value是ObjectAssociationMapclass 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()可以是一個函數,每個對象的DISGUISE()值不同,作為了AssociationsHashMap的key。
ObjectAssociationMap
ObjectAssociationMap是map類型,里面也是以key、value的形式存儲。看一下ObjectAssociationMap的定義
// ObjectAssociationMap是字典,key是從外面傳過來的key,例如@selector(hello),value是關聯對象,也就是// ObjectAssociationclass 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是從外面傳過來的,比如我們上面用到的@selector(personName),value是上面提到的ObjcAssociation對象,也就是關聯對象。終于看到了關聯對象,通過下面一整圖看一下整個是如何存儲的
_object_set_associative_reference源碼
_object_set_associative_reference函數中根據所傳的參數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對應的關聯對象 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;// 調用erase()方法刪除對應的關聯對象refs->erase(j);} }// 釋放舊的關聯對象 if (old_association.hasValue()) ReleaseValue()(old_association); 復制代碼通過代碼可以看到,當value'為nil時,Runtime做的操作就是找到原來該key所對應的關聯對象,并且將該關聯對象刪除。也就是說,value為nil,實際上就是釋放一個key對應的關聯對象。
value != nil
value不為nil,實際上就是為某個對象添加關聯對象。為某個對象添加關聯對象,又分為該對象之前已經添加過關聯對象和該對象是第一次添加關聯對象的邏輯。
通過代碼可以看到,若該對象是第一次添加關聯對象,則先生成新的ObjectAssociationMap,并根據policy、value初始化ObjcAssociation對象,以外部傳的key、生成的ObjcAssociation分別作為ObjectAssociationMap的key、value。以DISGUISE(object)、生成的ObjectAssociationMap分別作為AssociationsHashMap的key、value。 2. 該對象不是第一次添加關聯對象 若該對象不是第一次添加關聯對象,根據原來是否有該key對應的關聯對象進行邏輯區分。 1. 原來有該key對應的關聯對象 代碼如下: ``` // 初始化一個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是關聯對象類ObjcAssociation) ObjectAssociationMap *refs = i->second; // ObjectAssociationMap::iterator 類型的迭代器 ObjectAssociationMap::iterator j = refs->find(key);// 原來該key對應的有關聯對象 // 將原關聯對象的值存起來,并且賦新值 old_association = j->second; j->second = ObjcAssociation(policy, new_value);// 釋放舊的關聯對象 if (old_association.hasValue()) ReleaseValue()(old_association); ``` 原來有該key所對應的關聯對象,所做的處理就是將原來的值存下來,并且賦新的值。最后將原來的值釋放。 2. 原來沒有該key對應的關聯對象 代碼如下: ``` // 初始化一個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是關聯對象類ObjcAssociation) ObjectAssociationMap *refs = i->second; // ObjectAssociationMap::iterator 類型的迭代器 ObjectAssociationMap::iterator j = refs->find(key);// 無該key對應的關聯對象,直接賦值即可 // ObjcAssociation(policy, new_value)提供了這樣的構造函數 (*refs)[key] = ObjcAssociation(policy, new_value); ``` 原來沒有該key所對應的關聯對象,直接賦值即可。 復制代碼_object_set_associative_reference流程
看完了_object_set_associative_reference的源碼,介紹的比較復雜,其實流程相對來說是比較簡單的,整個流程可以用下面的流程圖來表示:
policy參數
上面已經多次看到了policy參數,policy參數到底代表什么呢?通過上面的介紹,應該可以猜到了policy的作用。在定義一個屬性時,需要使用各種各樣的修飾符,如nonatomic,copy,strong等,既然關聯對象是為了達到和屬性相同的效果,那么關聯對象是否也應該有對應的修飾符呢?
正是如此,構造關聯對象的policy參數,就是類似于屬性的修飾符。
我們在應用層設置關聯對象時,之前代碼用到的值是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. */ }; 復制代碼根據其注釋,可以得出objc_AssociationPolicy與屬性修飾符之間的一個對應關系,如下:
這也是為何我們之前的代碼,設置關聯對象時,使用OBJC_ASSOCIATION_COPY_NONATOMIC的原因。
關于各種屬性修飾符之間的區別,以及什么情景下使用哪種修飾符,可以參考這篇文章。
objc_getAssociatedObject
objc_getAssociatedObject方法位于objc-runtime.mm文件中,該方法的實現比較簡單,內部直接調用了_object_get_associative_reference函數,代碼如下:
// 獲取關聯對象的方法 id objc_getAssociatedObject(id object, const void *key) {return _object_get_associative_reference(object, (void *)key); } 復制代碼_object_get_associative_reference函數
獲取關聯對象的操作都在函數_object_get_associative_reference中。其主要流程是,獲取對象的DISGUISE()值,根據該值獲取到ObjectAssociationMap。根據外部所傳的key,在ObjectAssociationMap中找到key所對應的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);// 獲取到關聯對象ObjcAssociation ObjcAssociation &entry = j->second; // 獲取到value value = entry.value();// 返回關聯對像的值 return value; 復制代碼objc_removeAssociatedObject
objc_removeAssociatedObject位于objc-runtime.mm文件中。注意,objc_removeAssociatedObject函數的作用是移除某個對象的所有關聯對象。倘若想要移除對象某個key所對應的關聯對象,需要使用objc_setAssociatedObject函數,value傳nil。
objc_removeAssociatedObject的實現比較簡單,內部調用了_object_remove_associations函數,代碼如下:
// 移除對象object的所有關聯對象 void objc_removeAssociatedObjects(id object) {if (object && object->hasAssociatedObjects()) {_object_remove_assocations(object);} } 復制代碼_object_remove_associations函數
_object_remove_associations函數的邏輯也比較簡單,根據對象的DISGUISE()值找到ObjectAssociationMap,然后將該map中的所有值刪除。刪除時需要先將值存起來,然后再刪除,_object_remove_associations函數中使用了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()); 復制代碼總結
至此,關于關聯對象的使用、在Runtime源碼中的實現已經全部介紹完畢。實際上,日常的工作中是很難涉及到關聯對象的內部實現的。只要掌握Runtime提供給我們的三個接口,使用Category以及關聯對象就足以勝任工作項目。不過,對于想要了解Runtime源碼的同學來說,掌握關聯對象在Runtime源碼中的實現,是有很大幫助的。
參考文章
關聯對象 AssociatedObject 完全解析
轉載于:https://juejin.im/post/5cad64a35188251b2a2cf96a
總結
以上是生活随笔為你收集整理的通过Runtime源码了解关联对象的实现的全部內容,希望文章能夠幫你解決所遇到的問題。