關聯是指把兩個對象相互關聯起來,使得其中的一個對象作為另外一個對象的一部分。 ? ?關聯特性只有在Mac OS X V10.6以及以后的版本上才是可用的。
? ? 使用關聯,我們可以不用修改類的定義而為其對象增加存儲空間。這在我們無法訪問到類的源碼的時候或者是考慮到二進制兼容性的時候是非常有用。 關聯是基于關鍵字的,因此,我們可以為任何對象增加任意多的關聯,每個都使用不同的關鍵字即可。關聯是可以保證被關聯的對象在關聯對象的整個生命周期都是可用的(在垃圾自動回收環境下也不會導致資源不可回收)。
? ? 創建關聯要使用到Objective-C的運行時函數:objc_setAssociatedObject來把一個對象與另外一個對象進行關聯。該函數需要四個參數:源對象,關鍵字,關聯的對象和一個關聯策略。當然,此處的關鍵字和關聯策略是需要進一步討論的。
? ■ ?關鍵字是一個void類型的指針。每一個關聯的關鍵字必須是唯一的。通常都是會采用靜態變量來作為關鍵字。
? ■ ?關聯策略表明了相關的對象是通過賦值,保留引用還是復制的方式進行關聯的;還有這種關聯是原子的還是非原子的。這里的關聯策略和聲明屬性時的很類似。這種關聯策略是通過使用預先定義好的常量來表示的。
? ? 我們在 iOS 開發中經常需要使用分類(Category),為已經存在的類添加屬性的需求,但是使用@property并不能在分類中正確創建實例變量和存取方法。
不過,通過 Objective-C 運行時中的關聯對象,也就是 Associated Object,我們可以實現上述需求。
? ? 關于關聯對象的使用相信已經成為了一個老生常談的問題了,不過為了保證這篇文章的完整性,筆者還是會在這里為各位介紹這部分的內容的。
分類中的 @property
@property可以說是一個 Objective-C 編程中的“宏”,它有元編程的思想。
@interfaceDKObject:NSObject@property(nonatomic,strong)NSString*property;@end
在使用上述代碼時會做三件事:
生成實例變量_property
生成getter方法- property
生成setter方法- setProperty:
@implementationDKObject{NSString*_property;}- (NSString*)property {return_property;}- (void)setProperty:(NSString*)property {? ? _property = property;}@end
這些代碼都是編譯器為我們生成的,雖然你看不到它,但是它確實在這里,我們既然可以在類中使用@property生成一個屬性,那么為什么在分類中不可以呢?
我們來做一個小實驗:創建一個DKObject的分類Category,并添加一個屬性categoryProperty:
@interfaceDKObject(Category)@property(nonatomic,strong)NSString*categoryProperty;@end
看起來還是很不錯的,不過 Build 一下這個 Demo,會發現有這么一個警告:
objc-ao-warning-category-property
在這里的警告告訴我們categoryProperty屬性的存取方法需要自己手動去實現,或者使用@dynamic在運行時實現這些方法。
換句話說,分類中的@property并沒有為我們生成實例變量以及存取方法,而需要我們手動實現。
Q:我們為什么要使用關聯對象?
A:因為在分類中@property并不會自動生成實例變量以及存取方法,所以一般使用關聯對象為已經存在的類添加『屬性』。
上一小節的內容已經給了我們需要使用關聯對象的理由。在這里,我們會介紹 ObjC 運行時為我們提供的與關聯對象有關的 API,并在分類中實現一個偽屬性:
#import"DKObject+Category.h"#import<objc/runtime.h>@implementationDKObject(Category)- (NSString*)categoryProperty {returnobjc_getAssociatedObject(self, _cmd);}- (void)setCategoryProperty:(NSString*)categoryProperty {? ? objc_setAssociatedObject(self,@selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}@end
這里的_cmd代指當前方法的選擇子,也就是@selector(categoryProperty)。
我們使用了兩個方法objc_getAssociatedObject以及objc_setAssociatedObject來模擬『屬性』的存取方法,而使用關聯對象模擬實例變量。
在這里有必要解釋兩個問題:
為什么向方法中傳入@selector(categoryProperty)?
OBJC_ASSOCIATION_RETAIN_NONATOMIC是干什么的?
關于第一個問題,我們需要看一下這兩個方法的原型:
idobjc_getAssociatedObject(idobject,constvoid*key);voidobjc_setAssociatedObject(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy);
@selector(categoryProperty)也就是參數中的key,其實可以使用靜態指針static void *類型的參數來代替,不過在這里,筆者強烈推薦使用@selector(categoryProperty)作為key傳入。因為這種方法省略了聲明參數的代碼,并且能很好地保證key的唯一性。
OBJC_ASSOCIATION_RETAIN_NONATOMIC又是什么呢?如果我們使用Command加左鍵查看它的定義:
typedefOBJC_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_AssociationPolicymodifier
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICnonatomic, strong
OBJC_ASSOCIATION_COPY_NONATOMICnonatomic, copy
OBJC_ASSOCIATION_RETAINatomic, strong
OBJC_ASSOCIATION_COPYatomic, copy
而我們在代碼中實現的屬性categoryProperty就相當于使用了nonatomic和strong修飾符。
關于屬性修飾符的區別,并不是這篇文章的主要內容,如果你需要了解它們的區別,Google是一個很好的選擇。
到這里,我們已經完成了對關聯對象應用的介紹,再來回顧一下小節的內容。
@property` 其實有元編程的思想,它能夠為我們自動生成實例變量以及存取方法,而這三者構成了屬性這個類似于語法糖的概念,為我們提供了更便利的點語法來訪問屬性:
self.property <=> [selfproperty]self.property = value <=> [selfsetProperty:value]
在分類中,因為類的實例變量的布局已經固定,使用@property已經無法向固定的布局中添加新的實例變量(這樣做可能會覆蓋子類的實例變量),所以我們需要使用關聯對象以及兩個方法來模擬構成屬性的三個要素。
如果你是一個 iOS 開發方面的新手,我相信這篇文章的前半部分對已經足夠使用了,不過,如果你還對關聯對象的實現非常感興趣,也可以嘗試閱讀下面的內容。
? ? 探索關聯對象的實現一直是我想要做的一件事情,直到最近,我才有足夠的時間來完成這篇文章,希望能夠對各位讀者有所幫助。
這一部分會從三個 objc 運行時的方法為入口來對關聯對象的實現一探究竟,其中兩個方法是上一部分使用到的方法:
voidobjc_setAssociatedObject(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy);idobjc_getAssociatedObject(idobject,constvoid*key);voidobjc_removeAssociatedObjects(idobject);
三個方法的作用分別是:
以鍵值對形式添加關聯對象
根據key獲取關聯對象
移除所有關聯對象
而接下來的內容自然就是圍繞這三個方法進行的,我們會對它們的實現進行分析。
objc_setAssociatedObject
首先是objc_setAssociatedObject方法,這個方法的調用棧并不復雜:
voidobjc_setAssociatedObject(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy) └──voidobjc_setAssociatedObject_non_gc(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy)? ? └──void_object_set_associative_reference(idobject,void*key,idvalue, uintptr_t policy)
調用棧中的_object_set_associative_reference方法實際完成了設置關聯對象的任務:
void_object_set_associative_reference(idobject,void*key,idvalue, uintptr_t policy) {? ? ObjcAssociation old_association(0,nil);idnew_value = value ? acquireValue(value, policy) :nil;? ? {? ? ? ? AssociationsManager manager;? ? ? ? AssociationsHashMap &associations(manager.associations());? ? ? ? ObjectAssociationMap *refs = i->second;? ? ? ? ...? ? }if(old_association.hasValue()) ReleaseValue()(old_association);}
在這里的實現省略了大多的實現代碼,而且忽略了很多邏輯上的順序,不過不要在意這里的代碼能否執行。
我們需要注意其中的幾個類和數據結構,因為在具體分析這個方法的實現之前,我們需要了解其中它們的作用:
AssociationsManager
AssociationsHashMap
ObjcAssociationMap
ObjcAssociation
AssociationsManager
AssociationsManager在源代碼中的定義是這樣的:
classAssociationsManager {staticspinlock_t _lock;staticAssociationsHashMap *_map;public:? ? AssociationsManager()? { _lock.lock(); }? ? ~AssociationsManager()? { _lock.unlock(); }? ? ? ? AssociationsHashMap &associations() {if(_map ==NULL)? ? ? ? ? ? _map = new AssociationsHashMap();return*_map;? ? }};spinlock_t AssociationsManager::_lock;AssociationsHashMap *AssociationsManager::_map =NULL;
它維護了spinlock_t和AssociationsHashMap的單例,初始化它的時候會調用lock.lock()方法,在析構時會調用lock.unlock(),而associations方法用于取得一個全局的AssociationsHashMap單例。
也就是說AssociationsManager通過持有一個自旋鎖spinlock_t保證對AssociationsHashMap的操作是線程安全的,即每次只會有一個線程對 AssociationsHashMap 進行操作。
如何存儲 ObjcAssociation
ObjcAssociation就是真正的關聯對象的類,上面的所有數據結構只是為了更好的存儲它。
首先,AssociationsHashMap用與保存從對象的disguised_ptr_t到ObjectAssociationMap的映射:
classAssociationsHashMap : public unordered_map {public:void*operator new(size_t n) {return::malloc(n); }voidoperator delete(void*ptr) { ::free(ptr); }};
而ObjectAssociationMap則保存了從key到關聯對象ObjcAssociation的映射,這個數據結構保存了當前對象對應的所有關聯對象:
classObjectAssociationMap : public std::map {public:void*operator new(size_t n) {return::malloc(n); }voidoperator delete(void*ptr) { ::free(ptr); }};
最關鍵的ObjcAssociation包含了policy以及value:
classObjcAssociation {? ? uintptr_t _policy;id_value;public:? ? ObjcAssociation(uintptr_t policy,idvalue) : _policy(policy), _value(value) {}? ? ObjcAssociation() : _policy(0), _value(nil) {}? ? uintptr_t policy()const{return_policy; }idvalue()const{return_value; }boolhasValue() {return_value !=nil; }};
舉一個簡單的例子來說明關聯對象在內存中以什么形式存儲的,以下面的代碼為例:
intmain(intargc,constchar* argv[]) {@autoreleasepool{NSObject*obj = [NSObjectnew];? ? ? ? objc_setAssociatedObject(obj,@selector(hello),@"Hello", OBJC_ASSOCIATION_RETAIN_NONATOMIC);? ? }return0;}
這里的關聯對象ObjcAssociation(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @"Hello")在內存中是這么存儲的:
objc-ao-associateobjcect
接下來我們可以重新回到對objc_setAssociatedObject方法的分析了。
在這里會將方法的執行分為兩種情況:
new_value != nil設置/更新關聯對象的值
new_value == nil刪除一個關聯對象
new_value != nil
先來分析在new_value != nil的情況下,該方法的執行是什么樣的:
void_object_set_associative_reference(idobject,void*key,idvalue, uintptr_t policy) {? ? ObjcAssociation old_association(0,nil);idnew_value = value ? acquireValue(value, policy) :nil;? ? {? ? ? ? AssociationsManager manager;? ? ? ? AssociationsHashMap &associations(manager.associations());? ? ? ? disguised_ptr_t disguised_object = DISGUISE(object);? ? ? ? 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;? ? ? ? ? ? ? ? j->second = ObjcAssociation(policy, new_value);? ? ? ? ? ? }else{? ? ? ? ? ? ? ? (*refs)[key] = ObjcAssociation(policy, new_value);? ? ? ? ? ? }? ? ? ? }else{? ? ? ? ? ? ObjectAssociationMap *refs = new ObjectAssociationMap;? ? ? ? ? ? associations[disguised_object] = refs;? ? ? ? ? ? (*refs)[key] = ObjcAssociation(policy, new_value);? ? ? ? ? ? object->setHasAssociatedObjects();? ? ? ? }? ? }if(old_association.hasValue()) ReleaseValue()(old_association);}
使用old_association(0, nil)創建一個臨時的ObjcAssociation對象(用于持有原有的關聯對象,方便在方法調用的最后釋放值)
調用acquireValue對new_value進行retain或者copy
staticidacquireValue(idvalue, uintptr_t policy) {switch(policy &0xFF) {caseOBJC_ASSOCIATION_SETTER_RETAIN:return((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);caseOBJC_ASSOCIATION_SETTER_COPY:return((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);? ? }returnvalue;}
初始化一個AssociationsManager,并獲取唯一的保存關聯對象的哈希表AssociationsHashMap
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
先使用DISGUISE(object)作為 key 尋找對應的ObjectAssociationMap
如果沒有找到,初始化一個ObjectAssociationMap,再實例化ObjcAssociation對象添加到 Map 中,并調用setHasAssociatedObjects方法,表明當前對象含有關聯對象
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
如果找到了對應的ObjectAssociationMap,就要看key是否存在了,由此來決定是更新原有的關聯對象,還是增加一個
ObjectAssociationMap *refs = i->second;ObjectAssociationMap::iterator j = refs->find(key);if(j != refs->end()) {? ? old_association = j->second;? ? j->second = ObjcAssociation(policy, new_value);}else{? ? (*refs)[key] = ObjcAssociation(policy, new_value);}
最后的最后,如果原來的關聯對象有值的話,會調用ReleaseValue()釋放關聯對象的值
structReleaseValue {voidoperator() (ObjcAssociation &association) {? ? ? ? releaseValue(association.value(), association.policy());? ? }};staticvoidreleaseValue(idvalue, uintptr_t policy) {if(policy & OBJC_ASSOCIATION_SETTER_RETAIN) {? ? ? ? ((id(*)(id, SEL))objc_msgSend)(value, SEL_release);? ? }}
到這里,該條件下的方法實現就結束了。
new_value == nil
如果new_value == nil,就說明我們要刪除對應key的關聯對象,實現如下:
void_object_set_associative_reference(idobject,void*key,idvalue, uintptr_t policy) {? ? ObjcAssociation old_association(0,nil);idnew_value = value ? acquireValue(value, policy) :nil;? ? {? ? ? ? AssociationsManager manager;? ? ? ? AssociationsHashMap &associations(manager.associations());? ? ? ? disguised_ptr_t disguised_object = DISGUISE(object);? ? ? ? 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;? ? ? ? ? ? ? ? refs->erase(j);? ? ? ? ? ? }? ? ? ? }? ? }if(old_association.hasValue()) ReleaseValue()(old_association);}
這種情況下方法的實現與前面的唯一區別就是,我們會調用erase方法,擦除ObjectAssociationMap中key對應的節點。
setHasAssociatedObjects()
其實上面的兩種情況已經將objc_setAssociatedObject方法的實現分析得很透徹了,不過,這里還有一個小問題來等待我們解決,setHasAssociatedObjects()方法的作用是什么?
inlinevoidobjc_object::setHasAssociatedObjects() {if(isTaggedPointer())return; retry:? ? isa_t oldisa = LoadExclusive(&isa.bits);? ? isa_t newisa = oldisa;if(!newisa.indexed)return;if(newisa.has_assoc)return;? ? newisa.has_assoc =true;if(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))gotoretry;}
它會將isa結構體中的標記位has_assoc標記為true,也就是表示當前對象有關聯對象,在這里我還想祭出這張圖來介紹isa中的各個標記位都是干什么的。
objc-ao-isa-struct
如果想要了解關于 isa 的知識,可以閱讀從 NSObject 的初始化了解 isa
objc_getAssociatedObject
我們既然已經對objc_setAssociatedObject的實現已經比較熟悉了,相信對于objc_getAssociatedObject的理解也會更加容易。
方法的調用棧和objc_setAssociatedObject非常相似:
idobjc_getAssociatedObject(idobject,constvoid*key)└──idobjc_getAssociatedObject_non_gc(idobject,constvoid*key);? ? └──id_object_get_associative_reference(idobject,void*key)
而_object_get_associative_reference相比于前面方法的實現更加簡單。
id_object_get_associative_reference(idobject,void*key) {idvalue =nil;? ? uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;? ? {? ? ? ? AssociationsManager manager;? ? ? ? AssociationsHashMap &associations(manager.associations());? ? ? ? disguised_ptr_t disguised_object = DISGUISE(object);? ? ? ? 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()) {? ? ? ? ? ? ? ? ObjcAssociation &entry = j->second;? ? ? ? ? ? ? ? value = entry.value();? ? ? ? ? ? ? ? policy = entry.policy();if(policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);? ? ? ? ? ? }? ? ? ? }? ? }if(value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {? ? ? ? ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);? ? }returnvalue;}
代碼中尋找關聯對象的邏輯和objc_setAssociatedObject差不多:
獲取靜態變量AssociationsHashMap
以DISGUISE(object)為 key 查找AssociationsHashMap
以void *key為 key 查找ObjcAssociation
根據policy調用相應的方法
if(policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);if(value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {? ? ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);}
返回關聯對象ObjcAssociation的值
objc_removeAssociatedObjects
關于最后的objc_removeAssociatedObjects方法,其實現也相對簡單,這是方法的調用棧:
voidobjc_removeAssociatedObjects(idobject)└──void_object_remove_assocations(idobject)
這是簡化版本的objc_removeAssociatedObjects方法實現:
voidobjc_removeAssociatedObjects(idobject) {if(object && object->hasAssociatedObjects()) {? ? ? ? _object_remove_assocations(object);? ? }}
為了加速移除對象的關聯對象的速度,我們會通過標記位has_assoc來避免不必要的方法調用,在確認了對象和關聯對象的存在之后,才會調用_object_remove_assocations方法移除對象上所有的關聯對象:
void_object_remove_assocations(idobject) {? ? vector< ObjcAssociation,ObjcAllocator > elements;? ? {? ? ? ? AssociationsManager manager;? ? ? ? AssociationsHashMap &associations(manager.associations());if(associations.size() ==0)return;? ? ? ? disguised_ptr_t disguised_object = DISGUISE(object);? ? ? ? AssociationsHashMap::iterator i = associations.find(disguised_object);if(i != associations.end()) {? ? ? ? ? ? ObjectAssociationMap *refs = i->second;for(ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {? ? ? ? ? ? ? ? elements.push_back(j->second);? ? ? ? ? ? }? ? ? ? ? ? delete refs;? ? ? ? ? ? associations.erase(i);? ? ? ? }? ? }? ? for_each(elements.begin(), elements.end(), ReleaseValue());}
方法會將對象包含的所有關聯對象加入到一個vector中,然后對所有的ObjcAssociation對象調用ReleaseValue()方法,釋放不再被需要的值。
小結
關于應用
本來在這個系列的文章中并不會涉及關聯對象這個話題,不過,有人問過我這么一個問題:在分類中到底能否實現屬性?其實在回答這個問題之前,首先要知道到底屬性是什么?而屬性的概念決定了這個問題的答案。
如果你把屬性理解為通過方法訪問的實例變量,我相信這個問題的答案是不能,因為分類不能為類增加額外的實例變量。
不過如果屬性只是一個存取方法以及存儲值的容器的集合,那么分類是可以實現屬性的。
分類中對屬性的實現其實只是實現了一個看起來像屬性的接口而已。
關于實現
關聯對象又是如何實現并且管理的呢:
關聯對象其實就是ObjcAssociation對象
關聯對象由AssociationsManager管理并在AssociationsHashMap存儲
對象的指針以及其對應ObjectAssociationMap以鍵值對的形式存儲在AssociationsHashMap中
ObjectAssociationMap則是用于存儲關聯對象的數據結構
每一個對象都有一個標記位has_assoc指示對象是否含有關聯對象
鏈接:http://www.lxweimin.com/p/79479a09a8c0#%E5%85%B3%E8%81%94%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%BA%94%E7%94%A8