關注倉庫,及時獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github
我們在 iOS 開發中經常需要使用分類(Category),為已經存在的類添加屬性的需求,但是使用 @property
并不能在分類中正確創建實例變量和存取方法。
不過,通過 Objective-C 運行時中的關聯對象,也就是 Associated Object,我們可以實現上述需求。
寫在前面
這篇文章包含了兩方面的內容:
注:如果你剛剛入門 iOS 開發,筆者相信了解第一部分的內容會對你的日常開發中有所幫助,不過第二部分的內容可能有些難以理解。
如果你對關聯對象的使用非常熟悉,可以直接跳過第一部分的內容,從這里開始深入了解其底層實現。
關聯對象的應用
關于關聯對象的使用相信已經成為了一個老生常談的問題了,不過為了保證這篇文章的完整性,筆者還是會在這里為各位介紹這部分的內容的。
分類中的 @property
@property
可以說是一個 Objective-C 編程中的“宏”,它有元編程的思想。
@interface DKObject : NSObject
@property (nonatomic, strong) NSString *property;
@end
在使用上述代碼時會做三件事:
- 生成實例變量
_property
- 生成
getter
方法- property
- 生成
setter
方法- setProperty:
@implementation DKObject {
NSString *_property;
}
- (NSString *)property {
return _property;
}
- (void)setProperty:(NSString *)property {
_property = property;
}
@end
這些代碼都是編譯器為我們生成的,雖然你看不到它,但是它確實在這里,我們既然可以在類中使用 @property
生成一個屬性,那么為什么在分類中不可以呢?
我們來做一個小實驗:創建一個 DKObject
的分類 Category
,并添加一個屬性 categoryProperty
:
@interface DKObject (Category)
@property (nonatomic, strong) NSString *categoryProperty;
@end
看起來還是很不錯的,不過 Build 一下這個 Demo,會發現有這么一個警告:
在這里的警告告訴我們 categoryProperty
屬性的存取方法需要自己手動去實現,或者使用 @dynamic
在運行時實現這些方法。
換句話說,分類中的 @property
并沒有為我們生成實例變量以及存取方法,而需要我們手動實現。
使用關聯對象
Q:我們為什么要使用關聯對象?
A:因為在分類中 @property
并不會自動生成實例變量以及存取方法,所以一般使用關聯對象為已經存在的類添加『屬性』。
上一小節的內容已經給了我們需要使用關聯對象的理由。在這里,我們會介紹 ObjC 運行時為我們提供的與關聯對象有關的 API,并在分類中實現一個偽屬性:
#import "DKObject+Category.h"
#import <objc/runtime.h>
@implementation DKObject (Category)
- (NSString *)categoryProperty {
return objc_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
是干什么的?
關于第一個問題,我們需要看一下這兩個方法的原型:
id objc_getAssociatedObject(id object, const void *key);
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
@selector(categoryProperty)
也就是參數中的 key
,其實可以使用靜態指針 static void *
類型的參數來代替,不過在這里,筆者強烈推薦使用 @selector(categoryProperty)
作為 key
傳入。因為這種方法省略了聲明參數的代碼,并且能很好地保證 key
的唯一性。
OBJC_ASSOCIATION_RETAIN_NONATOMIC
又是什么呢?如果我們使用 Command
加左鍵查看它的定義:
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_AssociationPolicy | modifier |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, strong |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
OBJC_ASSOCIATION_RETAIN | atomic, strong |
OBJC_ASSOCIATION_COPY | atomic, copy |
而我們在代碼中實現的屬性 categoryProperty
就相當于使用了 nonatomic
和 strong
修飾符。
關于屬性修飾符的區別,并不是這篇文章的主要內容,如果你需要了解它們的區別,Google 是一個很好的選擇。
到這里,我們已經完成了對關聯對象應用的介紹,再來回顧一下小節的內容。
@property` 其實有元編程的思想,它能夠為我們自動生成實例變量以及存取方法,而這三者構成了屬性這個類似于語法糖的概念,為我們提供了更便利的點語法來訪問屬性:
self.property <=> [self property]
self.property = value <=> [self setProperty:value]
在分類中,因為類的實例變量的布局已經固定,使用 @property
已經無法向固定的布局中添加新的實例變量(這樣做可能會覆蓋子類的實例變量),所以我們需要使用關聯對象以及兩個方法來模擬構成屬性的三個要素。
如果你是一個 iOS 開發方面的新手,我相信這篇文章的前半部分對已經足夠使用了,不過,如果你還對關聯對象的實現非常感興趣,也可以嘗試閱讀下面的內容。
關聯對象的實現
探索關聯對象的實現一直是我想要做的一件事情,直到最近,我才有足夠的時間來完成這篇文章,希望能夠對各位讀者有所幫助。
這一部分會從三個 objc 運行時的方法為入口來對關聯對象的實現一探究竟,其中兩個方法是上一部分使用到的方法:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);
三個方法的作用分別是:
- 以鍵值對形式添加關聯對象
- 根據
key
獲取關聯對象 - 移除所有關聯對象
而接下來的內容自然就是圍繞這三個方法進行的,我們會對它們的實現進行分析。
objc_setAssociatedObject
首先是 objc_setAssociatedObject
方法,這個方法的調用棧并不復雜:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
└── void objc_setAssociatedObject_non_gc(id object, const void *key, id value, objc_AssociationPolicy policy)
└── void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy)
調用棧中的 _object_set_associative_reference
方法實際完成了設置關聯對象的任務:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
ObjcAssociation old_association(0, nil);
id new_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
在源代碼中的定義是這樣的:
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_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
的映射:
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); }
};
而 ObjectAssociationMap
則保存了從 key
到關聯對象 ObjcAssociation
的映射,這個數據結構保存了當前對象對應的所有關聯對象:
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); }
};
最關鍵的 ObjcAssociation
包含了 policy
以及 value
:
class ObjcAssociation {
uintptr_t _policy;
id _value;
public:
ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
ObjcAssociation() : _policy(0), _value(nil) {}
uintptr_t policy() const { return _policy; }
id value() const { return _value; }
bool hasValue() { return _value != nil; }
};
舉一個簡單的例子來說明關聯對象在內存中以什么形式存儲的,以下面的代碼為例:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [NSObject new];
objc_setAssociatedObject(obj, @selector(hello), @"Hello", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return 0;
}
這里的關聯對象 ObjcAssociation(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @"Hello")
在內存中是這么存儲的:
接下來我們可以重新回到對 objc_setAssociatedObject
方法的分析了。
在這里會將方法的執行分為兩種情況:
-
new_value != nil
設置/更新關聯對象的值 -
new_value == nil
刪除一個關聯對象
new_value != nil
先來分析在 new_value != nil
的情況下,該方法的執行是什么樣的:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
ObjcAssociation old_association(0, nil);
id new_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
static id acquireValue(id value, uintptr_t policy) { switch (policy & 0xFF) { case OBJC_ASSOCIATION_SETTER_RETAIN: return ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain); case OBJC_ASSOCIATION_SETTER_COPY: return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy); } return value; }
-
初始化一個
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()
釋放關聯對象的值struct ReleaseValue { void operator() (ObjcAssociation &association) { releaseValue(association.value(), association.policy()); } }; static void releaseValue(id value, 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(id object, void *key, id value, uintptr_t policy) {
ObjcAssociation old_association(0, nil);
id new_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()
方法的作用是什么?
inline void objc_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)) goto retry;
}
它會將 isa
結構體中的標記位 has_assoc
標記為 true
,也就是表示當前對象有關聯對象,在這里我還想祭出這張圖來介紹 isa
中的各個標記位都是干什么的。
如果想要了解關于 isa 的知識,可以閱讀從 NSObject 的初始化了解 isa
objc_getAssociatedObject
我們既然已經對 objc_setAssociatedObject
的實現已經比較熟悉了,相信對于 objc_getAssociatedObject
的理解也會更加容易。
方法的調用棧和 objc_setAssociatedObject
非常相似:
id objc_getAssociatedObject(id object, const void *key)
└── id objc_getAssociatedObject_non_gc(id object, const void *key);
└── id _object_get_associative_reference(id object, void *key)
而 _object_get_associative_reference
相比于前面方法的實現更加簡單。
id _object_get_associative_reference(id object, void *key) {
id value = 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);
}
return value;
}
代碼中尋找關聯對象的邏輯和 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
方法,其實現也相對簡單,這是方法的調用棧:
void objc_removeAssociatedObjects(id object)
└── void _object_remove_assocations(id object)
這是簡化版本的 objc_removeAssociatedObjects
方法實現:
void objc_removeAssociatedObjects(id object) {
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
為了加速移除對象的關聯對象的速度,我們會通過標記位 has_assoc
來避免不必要的方法調用,在確認了對象和關聯對象的存在之后,才會調用 _object_remove_assocations
方法移除對象上所有的關聯對象:
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > 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
指示對象是否含有關聯對象
關注倉庫,及時獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github
原文鏈接: http://draveness.me/ao