在iOS開(kāi)發(fā)中,Category是經(jīng)常使用到的一個(gè)特性,合理的使用Category能夠減少繁瑣代碼,提高開(kāi)發(fā)效率。在使用Category時(shí),有經(jīng)驗(yàn)的開(kāi)發(fā)者應(yīng)該都知道,在Category中是無(wú)法添加屬性的,如果想在Category中實(shí)現(xiàn)屬性的效果,需要使用關(guān)聯(lián)對(duì)象。關(guān)聯(lián)對(duì)象屬于Runtime的范疇,本篇文章結(jié)合Runtime源碼,分析下關(guān)聯(lián)對(duì)象的內(nèi)部實(shí)現(xiàn)。
Category中使用@property
上面提到了在Category中無(wú)法添加屬性,來(lái)驗(yàn)證一下。倘若在Category中添加屬性,是會(huì)直接編譯錯(cuò)誤?還是會(huì)警告?
定義一個(gè)Person類,代碼如下:
@interface Person : NSObject{
NSString *_age;
}
- (void)printName;
@end
實(shí)現(xiàn)文件
@implementation Person
- (void)printName
{
NSLog(@"my name is Person");
}
@end
為Person 添加一個(gè)Category MyPerson,Category中定義一個(gè)屬性 personName,代碼如下:
@interface Person (MyPerson)
@property (nonatomic, copy) NSString *personName;
@end
實(shí)現(xiàn)文件中暫時(shí)為空。
現(xiàn)在我們?cè)贑ategory中添加了@property,編譯一下,沒(méi)有問(wèn)題,可以編譯成功。也就是說(shuō),Category中使用@property不會(huì)引起編譯錯(cuò)誤。但是呢,Xcode會(huì)提示警告,警告信息如下:
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實(shí)現(xiàn)get方法和set方法。
在繼續(xù)下一步之前,首先需要了解Objective-C中的@property到底是什么:
@property = 實(shí)例變量 + get方法 + set方法
關(guān)于@property的更詳細(xì)介紹,可以參考這篇文章。
也就是說(shuō),在普通文件中,定義一個(gè)屬性,編譯器會(huì)自動(dòng)生成實(shí)例變量,以及該實(shí)例變量對(duì)應(yīng)的get/set方法。但是在Category中,根據(jù)Xcode的警告信息,是沒(méi)有生成get/set方法的。
既然Xcode沒(méi)有自動(dòng)生成get/set方法,那么我們來(lái)手動(dòng)實(shí)現(xiàn)一下get/set方法。
在Category的實(shí)現(xiàn)文件中加入以下代碼:
- (NSString *)personName
{
return _personName;
}
- (void)setPersonName:(NSString *)personName
{
_personName = personName;
}
警告信息確實(shí)沒(méi)了,直接提示error,編譯不通過(guò),錯(cuò)誤信息如下:
Use of undeclared identifier '_personName'
_personName沒(méi)有定義。看來(lái)在Category中使用@property,編譯器不僅不會(huì)自動(dòng)生成set/get方法,連實(shí)例變量也不會(huì)生成。話說(shuō)回來(lái),沒(méi)有實(shí)例變量,自然也不會(huì)有set/get方法。
正是因?yàn)镃ategory中的@property不會(huì)生成實(shí)例變量,get/set方法,所以如果在程序中使用Category的屬性,編譯不會(huì)有問(wèn)題,但是在運(yùn)行期間會(huì)直接崩潰。
Person *p = [[Person alloc] init];
[p printName];
p.personName = @"haha"; // 這里會(huì)直接崩潰
崩潰信息如下:
-[Person setPersonName:]: unrecognized selector sent to instance 0x60000300ab80
崩潰原因也是容易理解的,因?yàn)楦緵](méi)有setPersonName方法。
@property和關(guān)聯(lián)對(duì)象結(jié)合使用
既然在Category中無(wú)法直接使用@property,那有沒(méi)有什么辦法解決呢?答案就是關(guān)聯(lián)對(duì)象。
關(guān)聯(lián)對(duì)象其實(shí)是AssociatedObject的翻譯。需要注意的是,關(guān)聯(lián)對(duì)象并不是代替了Category中的屬性,而是在Category中@property和關(guān)聯(lián)對(duì)象結(jié)合使用,以達(dá)到正常使用@property的目的。
文章開(kāi)頭也提到了,關(guān)聯(lián)對(duì)象屬于Runtime的范疇,因此使用關(guān)聯(lián)對(duì)象之前,首先導(dǎo)入runtime頭文件
#import <objc/runtime.h>
然后在實(shí)現(xiàn)屬性的get/set方法,get/set方法中使用關(guān)聯(lián)對(duì)象,代碼如下:
- (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)對(duì)象的作用。Category中關(guān)聯(lián)對(duì)象和@property結(jié)合使用,能夠達(dá)到在主程序中正常使用Category中屬性的目的。
關(guān)聯(lián)對(duì)象在Runtime中的實(shí)現(xiàn)
來(lái)看一下關(guān)聯(lián)對(duì)象在Runtime中到底是怎么實(shí)現(xiàn)的。我們主要通過(guò)追蹤Runtime開(kāi)放給我們的接口來(lái)探索。上面已經(jīng)用到了兩個(gè)接口,分別是:
objc_getAssociatedObject
objc_setAssociatedObject
除了這兩個(gè)接口外,還有一個(gè)接口:
objc_removeAssociatedObjects
也就是說(shuō),Runtime主要提供了三個(gè)方法供我們使用關(guān)聯(lián)對(duì)象:
// 根據(jù)key獲取關(guān)聯(lián)對(duì)象
id objc_getAssociatedObject(id object, const void *key);
// 以key、value的形式設(shè)置關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 移出對(duì)象所有的關(guān)聯(lián)對(duì)象
void objc_removeAssociatedObjects(id object);
接下來(lái)依次分析每個(gè)方法。
objc_setAssociatedObject
objc_setAssociatedObject方法位于objc-runtime.mm文件中,該方法的實(shí)現(xiàn)比較簡(jiǎn)單,調(diào)用了_object_set_associative_reference函數(shù)。
// 設(shè)置關(guān)聯(lián)對(duì)象的方法
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)對(duì)象的操作。在看_object_set_associative_reference函數(shù)源碼之前,先了解幾個(gè)結(jié)構(gòu)體代表的含義。
ObjcAssociation
ObjcAssociation就是關(guān)聯(lián)對(duì)象,在應(yīng)用層設(shè)置、獲取關(guān)聯(lián)對(duì)象,在Runtime中都被表示成了ObjcAssociation。看一下ObjcAssociation的定義:
// ObjcAssociation就是關(guān)聯(lián)對(duì)象類
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)對(duì)象中定義了_value和_policy兩個(gè)變量。_policy之后再說(shuō),_value就是關(guān)聯(lián)對(duì)象的值,比如上面賦值為@"haha"。
AssociationsManager
AssociationsManager可以理解成一個(gè)Manager類,看一下AssociationsManager的實(shí)現(xiàn)
class AssociationsManager {
// AssociationsManager中只有一個(gè)變量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 的實(shí)現(xiàn)可以理解成單例對(duì)象
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
AssociationsManager中只有一個(gè)變量,AssociationsHashMap,通過(guò)源碼可以看到,AssociationsManager中的AssociationsHashMap的實(shí)現(xiàn)可以理解成是單例的。而且AssociationsManager的構(gòu)造函數(shù)和析構(gòu)函數(shù)分別做了加鎖、釋放鎖的操作。也就是說(shuō),同一時(shí)刻,只能有一個(gè)線程操作AssociationsManager中的AssociationsHashMap。
AssociationsHashMap
AssociationsHashMap,看名字可以猜到是hashMap類型,那么里面的key、value到底是什么呢?看下AssociationsHashMap的定義:
// AssociationsHashMap是字典,key是對(duì)象的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是對(duì)象的DISGUISE()值,value是ObjectAssociationMap。DISGUISE()可以是一個(gè)函數(shù),每個(gè)對(duì)象的DISGUISE()值不同,作為了AssociationsHashMap的key。
ObjectAssociationMap
ObjectAssociationMap是map類型,里面也是以key、value的形式存儲(chǔ)??匆幌翺bjectAssociationMap的定義
// ObjectAssociationMap是字典,key是從外面?zhèn)鬟^(guò)來(lái)的key,例如@selector(hello),value是關(guān)聯(lián)對(duì)象,也就是
// 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)鬟^(guò)來(lái)的,比如我們上面用到的@selector(personName),value是上面提到的ObjcAssociation對(duì)象,也就是關(guān)聯(lián)對(duì)象。終于看到了關(guān)聯(lián)對(duì)象,通過(guò)下面一整圖看一下整個(gè)是如何存儲(chǔ)的
[圖片上傳失敗...(image-bb1f1f-1554867497085)]
_object_set_associative_reference源碼
_object_set_associative_reference函數(shù)中根據(jù)所傳的參數(shù)value是否為nil,分成了不同的邏輯。value為nil的邏輯比較簡(jiǎn)單,我們首先看一下value為nil所做的處理。
value = nil
value為nil時(shí)的代碼:
// 初始化一個(gè)manager
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 獲取對(duì)象的DISGUISE值,作為AssociationsHashMap的key
disguised_ptr_t disguised_object = DISGUISE(object);
// value無(wú)值,也就是釋放一個(gè)key對(duì)應(yīng)的關(guān)聯(lián)對(duì)象
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()方法刪除對(duì)應(yīng)的關(guān)聯(lián)對(duì)象
refs->erase(j);
}
}
// 釋放舊的關(guān)聯(lián)對(duì)象
if (old_association.hasValue()) ReleaseValue()(old_association);
通過(guò)代碼可以看到,當(dāng)value'為nil時(shí),Runtime做的操作就是找到原來(lái)該key所對(duì)應(yīng)的關(guān)聯(lián)對(duì)象,并且將該關(guān)聯(lián)對(duì)象刪除。也就是說(shuō),value為nil,實(shí)際上就是釋放一個(gè)key對(duì)應(yīng)的關(guān)聯(lián)對(duì)象。
value != nil
value不為nil,實(shí)際上就是為某個(gè)對(duì)象添加關(guān)聯(lián)對(duì)象。為某個(gè)對(duì)象添加關(guān)聯(lián)對(duì)象,又分為該對(duì)象之前已經(jīng)添加過(guò)關(guān)聯(lián)對(duì)象和該對(duì)象是第一次添加關(guān)聯(lián)對(duì)象的邏輯。
- 該對(duì)象第一次添加關(guān)聯(lián)對(duì)象
看一下該對(duì)象第一次添加關(guān)聯(lián)對(duì)象的代碼:
// 初始化一個(gè)manager
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 獲取對(duì)象的DISGUISE值,作為AssociationsHashMap的key
disguised_ptr_t disguised_object = DISGUISE(object);
// AssociationsHashMap::iterator 類型的迭代器
AssociationsHashMap::iterator i = associations.find(disguised_object);
// 執(zhí)行到這里,說(shuō)明該對(duì)象是第一次添加關(guān)聯(lián)對(duì)象
// 初始化ObjectAssociationMap
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
// 賦值
(*refs)[key] = ObjcAssociation(policy, new_value);
// 設(shè)置該對(duì)象的有關(guān)聯(lián)對(duì)象,調(diào)用的是setHasAssociatedObjects()方法
object->setHasAssociatedObjects();
通過(guò)代碼可以看到,若該對(duì)象是第一次添加關(guān)聯(lián)對(duì)象,則先生成新的ObjectAssociationMap,并根據(jù)policy、value初始化ObjcAssociation對(duì)象,以外部傳的key、生成的ObjcAssociation分別作為ObjectAssociationMap的key、value。以DISGUISE(object)、生成的ObjectAssociationMap分別作為AssociationsHashMap的key、value。
- 該對(duì)象不是第一次添加關(guān)聯(lián)對(duì)象
若該對(duì)象不是第一次添加關(guān)聯(lián)對(duì)象,根據(jù)原來(lái)是否有該key對(duì)應(yīng)的關(guān)聯(lián)對(duì)象進(jìn)行邏輯區(qū)分。- 原來(lái)有該key對(duì)應(yīng)的關(guān)聯(lián)對(duì)象
代碼如下:
原來(lái)有該key所對(duì)應(yīng)的關(guān)聯(lián)對(duì)象,所做的處理就是將原來(lái)的值存下來(lái),并且賦新的值。最后將原來(lái)的值釋放。// 初始化一個(gè)manager AssociationsManager manager; AssociationsHashMap &associations(manager.associations()); // 獲取對(duì)象的DISGUISE值,作為AssociationsHashMap的key disguised_ptr_t disguised_object = DISGUISE(object); // AssociationsHashMap::iterator 類型的迭代器 AssociationsHashMap::iterator i = associations.find(disguised_object); // 獲取到ObjectAssociationMap(key是外部傳來(lái)的key,value是關(guān)聯(lián)對(duì)象類ObjcAssociation) ObjectAssociationMap *refs = i->second; // ObjectAssociationMap::iterator 類型的迭代器 ObjectAssociationMap::iterator j = refs->find(key); // 原來(lái)該key對(duì)應(yīng)的有關(guān)聯(lián)對(duì)象 // 將原關(guān)聯(lián)對(duì)象的值存起來(lái),并且賦新值 old_association = j->second; j->second = ObjcAssociation(policy, new_value); // 釋放舊的關(guān)聯(lián)對(duì)象 if (old_association.hasValue()) ReleaseValue()(old_association);- 原來(lái)沒(méi)有該key對(duì)應(yīng)的關(guān)聯(lián)對(duì)象
代碼如下:
原來(lái)沒(méi)有該key所對(duì)應(yīng)的關(guān)聯(lián)對(duì)象,直接賦值即可。// 初始化一個(gè)manager AssociationsManager manager; AssociationsHashMap &associations(manager.associations()); // 獲取對(duì)象的DISGUISE值,作為AssociationsHashMap的key disguised_ptr_t disguised_object = DISGUISE(object); // AssociationsHashMap::iterator 類型的迭代器 AssociationsHashMap::iterator i = associations.find(disguised_object); // 獲取到ObjectAssociationMap(key是外部傳來(lái)的key,value是關(guān)聯(lián)對(duì)象類ObjcAssociation) ObjectAssociationMap *refs = i->second; // ObjectAssociationMap::iterator 類型的迭代器 ObjectAssociationMap::iterator j = refs->find(key); // 無(wú)該key對(duì)應(yīng)的關(guān)聯(lián)對(duì)象,直接賦值即可 // ObjcAssociation(policy, new_value)提供了這樣的構(gòu)造函數(shù) (*refs)[key] = ObjcAssociation(policy, new_value); - 原來(lái)有該key對(duì)應(yīng)的關(guān)聯(lián)對(duì)象
_object_set_associative_reference流程
看完了_object_set_associative_reference的源碼,介紹的比較復(fù)雜,其實(shí)流程相對(duì)來(lái)說(shuō)是比較簡(jiǎn)單的,整個(gè)流程可以用下面的流程圖來(lái)表示:
[圖片上傳失敗...(image-b69509-1554867497085)]
policy參數(shù)
上面已經(jīng)多次看到了policy參數(shù),policy參數(shù)到底代表什么呢?通過(guò)上面的介紹,應(yīng)該可以猜到了policy的作用。在定義一個(gè)屬性時(shí),需要使用各種各樣的修飾符,如nonatomic,copy,strong等,既然關(guān)聯(lián)對(duì)象是為了達(dá)到和屬性相同的效果,那么關(guān)聯(lián)對(duì)象是否也應(yīng)該有對(duì)應(yīng)的修飾符呢?
正是如此,構(gòu)造關(guān)聯(lián)對(duì)象的policy參數(shù),就是類似于屬性的修飾符。
我們?cè)趹?yīng)用層設(shè)置關(guān)聯(lián)對(duì)象時(shí),之前代碼用到的值是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與屬性修飾符之間的一個(gè)對(duì)應(yīng)關(guān)系,如下:
[圖片上傳失敗...(image-61610-1554867497085)]
這也是為何我們之前的代碼,設(shè)置關(guān)聯(lián)對(duì)象時(shí),使用OBJC_ASSOCIATION_COPY_NONATOMIC的原因。
關(guān)于各種屬性修飾符之間的區(qū)別,以及什么情景下使用哪種修飾符,可以參考這篇文章。
objc_getAssociatedObject
objc_getAssociatedObject方法位于objc-runtime.mm文件中,該方法的實(shí)現(xiàn)比較簡(jiǎn)單,內(nèi)部直接調(diào)用了_object_get_associative_reference函數(shù),代碼如下:
// 獲取關(guān)聯(lián)對(duì)象的方法
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)對(duì)象的操作都在函數(shù)_object_get_associative_reference中。其主要流程是,獲取對(duì)象的DISGUISE()值,根據(jù)該值獲取到ObjectAssociationMap。根據(jù)外部所傳的key,在ObjectAssociationMap中找到key所對(duì)應(yīng)的ObjcAssociation對(duì)象,然后得到ObjcAssociation的value。代碼如下:
id value = nil;
AssociationsManager manager;
// 獲取到manager中的AssociationsHashMap
AssociationsHashMap &associations(manager.associations());
// 獲取對(duì)象的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)對(duì)象ObjcAssociation
ObjcAssociation &entry = j->second;
// 獲取到value
value = entry.value();
// 返回關(guān)聯(lián)對(duì)像的值
return value;
objc_removeAssociatedObject
objc_removeAssociatedObject位于objc-runtime.mm文件中。注意,objc_removeAssociatedObject函數(shù)的作用是移除某個(gè)對(duì)象的所有關(guān)聯(lián)對(duì)象。倘若想要移除對(duì)象某個(gè)key所對(duì)應(yīng)的關(guān)聯(lián)對(duì)象,需要使用objc_setAssociatedObject函數(shù),value傳nil。
objc_removeAssociatedObject的實(shí)現(xiàn)比較簡(jiǎn)單,內(nèi)部調(diào)用了_object_remove_associations函數(shù),代碼如下:
// 移除對(duì)象object的所有關(guān)聯(lián)對(duì)象
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
_object_remove_associations函數(shù)
_object_remove_associations函數(shù)的邏輯也比較簡(jiǎn)單,根據(jù)對(duì)象的DISGUISE()值找到ObjectAssociationMap,然后將該map中的所有值刪除。刪除時(shí)需要先將值存起來(lái),然后再刪除,_object_remove_associations函數(shù)中使用了vector來(lái)存儲(chǔ)值。之后再將找到的ObjectAssociationMap刪除,代碼如下:
// 聲明了一個(gè)vector
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 獲取對(duì)象的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)對(duì)象的使用、在Runtime源碼中的實(shí)現(xiàn)已經(jīng)全部介紹完畢。實(shí)際上,日常的工作中是很難涉及到關(guān)聯(lián)對(duì)象的內(nèi)部實(shí)現(xiàn)的。只要掌握Runtime提供給我們的三個(gè)接口,使用Category以及關(guān)聯(lián)對(duì)象就足以勝任工作項(xiàng)目。不過(guò),對(duì)于想要了解Runtime源碼的同學(xué)來(lái)說(shuō),掌握關(guān)聯(lián)對(duì)象在Runtime源碼中的實(shí)現(xiàn),是有很大幫助的。