原文連接git
在iOS開發中,Category是常用到的一個特性,合理的使用Category可以減小繁瑣代碼,提升開發效率。在使用Category時,有經驗的開發者應該都知道,在Category中是沒法添加屬性的,若是想在Category中實現屬性的效果,須要使用關聯對象。關聯對象屬於Runtime的範疇,本篇文章結合Runtime源碼,分析下關聯對象的內部實現。github
上面提到了在Category中沒法添加屬性,來驗證一下。假若在Category中添加屬性,是會直接編譯錯誤?仍是會警告?安全
定義一個Person類,代碼以下:bash
@interface Person : NSObject{
NSString *_age;
}
- (void)printName;
@end
複製代碼
實現文件ide
@implementation Person
- (void)printName
{
NSLog(@"my name is Person");
}
@end
複製代碼
爲Person 添加一個Category MyPerson,Category中定義一個屬性 personName,代碼以下:函數
@interface Person (MyPerson)
@property (nonatomic, copy) NSString *personName;
@end
複製代碼
實現文件中暫時爲空。ui
如今咱們在Category中添加了@property,編譯一下,沒有問題,能夠編譯成功。也就是說,Category中使用@property不會引發編譯錯誤。可是呢,Xcode會提示警告,警告信息以下:this
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實現get方法和set方法。atom
在繼續下一步以前,首先須要瞭解Objective-C中的@property究竟是什麼:spa
@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方法。
既然在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開放給咱們的接口來探索。上面已經用到了兩個接口,分別是:
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-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就是關聯對象,在應用層設置、獲取關聯對象,在Runtime中都被表示成了ObjcAssociation。看一下ObjcAssociation的定義:
// ObjcAssociation就是關聯對象類
class ObjcAssociation {
uintptr_t _policy;
// 值
id _value;
public:
// 構造函數
ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
// 默認構造函數,參數分別爲0和nil
ObjcAssociation() : _policy(0), _value(nil) {}
};
複製代碼
關聯對象中定義了_value和_policy兩個變量。_policy以後再說,_value就是關聯對象的值,好比上面賦值爲@"haha"。
AssociationsManager能夠理解成一個Manager類,看一下AssociationsManager的實現
class AssociationsManager {
// AssociationsManager中只有一個變量AssociationsHashMap
static 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,看名字能夠猜到是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()能夠是一個函數,每一個對象的DISGUISE()值不一樣,做爲了AssociationsHashMap的key。
ObjectAssociationMap是map類型,裏面也是以key、value的形式存儲。看一下ObjectAssociationMap的定義
// ObjectAssociationMap是字典,key是從外面傳過來的key,例如@selector(hello),value是關聯對象,也就是
// 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是從外面傳過來的,好比咱們上面用到的@selector(personName),value是上面提到的ObjcAssociation對象,也就是關聯對象。終於看到了關聯對象,經過下面一整圖看一下整個是如何存儲的
_object_set_associative_reference函數中根據所傳的參數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,實際上就是爲某個對象添加關聯對象。爲某個對象添加關聯對象,又分爲該對象以前已經添加過關聯對象和該對象是第一次添加關聯對象的邏輯。
// 初始化一個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
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
// 賦值
(*refs)[key] = ObjcAssociation(policy, new_value);
// 設置該對象的有關聯對象,調用的是setHasAssociatedObjects()方法
object->setHasAssociatedObjects();
複製代碼
經過代碼能夠看到,若該對象是第一次添加關聯對象,則先生成新的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的源碼,介紹的比較複雜,其實流程相對來講是比較簡單的,整個流程能夠用下面的流程圖來表示:
上面已經屢次看到了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-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中。其主要流程是,獲取對象的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-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函數的邏輯也比較簡單,根據對象的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源碼中的實現,是有很大幫助的。