在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)對象的邏輯。
- 該對象第一次添加關(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。
- 該對象不是第一次添加關(guān)聯(lián)對象
若該對象不是第一次添加關(guān)聯(lián)對象,根據(jù)原來是否有該key對應(yīng)的關(guān)聯(lián)對象進(jìn)行邏輯區(qū)分。- 原來有該key對應(yīng)的關(guān)聯(lián)對象
代碼如下:
原來有該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)對象
代碼如下:
原來沒有該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),是有很大幫助的。