前言
Category能否添加成員變量?如果可以,如何給Category添加成員變量?
答:不能直接添加成員變量,但是可以通過runtime的方式間接實現添加成員變量的效果。
Category動態關聯對象
方法一:我們可以通過使用靜態全局變量給分類添加屬性
static NSString *_name;
-(void)setName:(NSString *)name
{
_name = name;
}
-(NSString *)name
{
return _name;
}
但是這樣_name靜態全局變量與類并沒有關聯,無論對象創建與銷毀,只要程序在運行_name變量就存在,并不是真正意義上的屬性。
方法二:使用runtime動態添加屬性
-(void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
return objc_getAssociatedObject(self, @"name");
}
關聯對象的實現原理
實現關聯對象技術的核心對象有
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
其中Map同我們平時使用的字典類似。通過key-value一一對應存值。
本文基于objc-723
版本,在Apple Github和Apple OpenSource上有源碼,來到runtime源碼,首先找到objc_setAssociatedObject函數,看一下其實現
objc_setAssociatedObject函數
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
objc_setAssociatedObject_non_gc(object, key, value, policy);
}
void objc_setAssociatedObject_non_gc(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函數中
_object_set_associative_reference函數
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
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);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
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 {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
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);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
_object_set_associative_reference函數內部我們可以全部找到我們上面說過的實現關聯對象技術的核心對象。接下來我們來一個一個看其內部實現原理探尋他們之間的關系。
AssociationsManager
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap.
public:
AssociationsManager() { _lock.lock(); }
~AssociationsManager() { _lock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
通過AssociationsManager內部源碼發現,AssociationsManager內部有一個AssociationsHashMap對象,還有個自旋鎖 spinlock_t。
AssociationsHashMap
通過AssociationsHashMap內部源碼我們發現AssociationsHashMap繼承自unordered_map首先來看一下unordered_map內的源碼
unordered_map內部分源碼
從unordered_map源碼中我們可以看出_Key和_Tp也就是前兩個參數對應著map中的Key和Value,那么對照上面AssociationsHashMap內源碼發現_Key中傳入的是disguised_ptr_t,_Tp中傳入的值則為ObjectAssociationMap。
緊接著我們來到ObjectAssociationMap中,上圖中ObjectAssociationMap已經標記出,我們發現ObjectAssociationMap中同樣以key、Value的方式存儲著ObjcAssociation。
接著我們來到ObjcAssociation中
ObjcAssociation
我們發現ObjcAssociation存儲著_policy、_value,而這兩個值我們可以發現正是我們調用objc_setAssociatedObject函數傳入的值,也就是說我們在調用objc_setAssociatedObject函數中傳入的value和policy這兩個值最終是存儲在ObjcAssociation中的。
現在我們已經對AssociationsManager、 AssociationsHashMap、 ObjectAssociationMap、ObjcAssociation四個對象之間的關系有了簡單的認識,那么接下來我們來細讀源碼,看一下objc_setAssociatedObject函數中傳入的四個參數分別放在哪個對象中充當什么作用。
細讀上述源碼我們可以發現,首先根據我們傳入的value經過acquireValue函數處理獲取new_value。acquireValue函數內部其實是通過對策略的判斷返回不同的值
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 manager;以及拿到manager內部的AssociationsHashMap即associations。
之后我們看到了我們傳入的第一個參數object
object經過 DISGUISE 函數被轉化為了disguised_ptr_t類型的disguised_object。
typedef uintptr_t disguised_ptr_t;
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
DISGUISE函數
DISGUISE函數其實僅僅對object做了位運算
- 我們看到被處理成new_value的value,同policy被存入了ObjcAssociation中。
- 而ObjcAssociation對應我們傳入的key被存入了ObjectAssociationMap中。
- disguised_object和ObjectAssociationMap則以key-value的形式對應存儲在associations中也就是AssociationsHashMap中。
value為nil
從上述代碼中可以看出,如果我們value設置為nil的話那么會執行下面的代碼,就會將關聯對象從ObjectAssociationMap中移除。
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);
}
}
最后我們通過一張圖可以很清晰的理清楚其中的關系
關聯對象底層對象關系
通過上圖我們可以總結為:
一個實例對象就對應一個ObjectAssociationMap,而ObjectAssociationMap中存儲著多個此實例對象的關聯對象的key以及ObjcAssociation,為ObjcAssociation中存儲著關聯對象的value和policy策略。
由此我們可以知道關聯對象并不是放在了原來的對象里面,而是自己維護了一個全局的map用來存放每一個對象及其對應關聯屬性表格。
objc_getAssociatedObject函數
objc_getAssociatedObject內部調用的是_object_get_associative_reference
id objc_getAssociatedObject(id object, const void *key) {
return objc_getAssociatedObject_non_gc(object, key);
}
id objc_getAssociatedObject_non_gc(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
_object_get_associative_reference函數
從_object_get_associative_reference函數內部可以看出,向set方法中那樣,反向將value一層一層取出最后return出去。
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);
> 1. //Hash表中查找disguised_object
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
> 2. //找到的話根據key取出對應的ObjcAssociation
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
> 3. //根據policy對取出的value做相應的操作
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_removeAssociatedObjects函數
objc_removeAssociatedObjects用來刪除所有的關聯對象,objc_removeAssociatedObjects函數內部調用的是_object_remove_assocations函數
void objc_removeAssociatedObjects(id object)
{
#if SUPPORT_GC
if (UseGC) {
auto_zone_erase_associative_refs(gc_zone, object);
} else
#endif
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
}
objc_removeAssociatedObjects函數
總結:
關聯對象并不是存儲在被關聯對象本身內存中,而是存儲在全局的統一的一個AssociationsManager中,如果設置關聯對象為nil,就相當于是移除關聯對象。
我們現在來看objc_AssociationPolicy
policy 參數: 屬性以什么形式保存的策略。
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一個弱引用相關聯的對象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相關對象的強引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相關的對象被復制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相關對象的強引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相關的對象被復制,原子性
};
我們會發現其中只有RETAIN和COPY而為什么沒有weak呢?
總過上面對源碼的分析我們知道,object經過DISGUISE函數被轉化為了disguised_ptr_t類型的disguised_object。
disguised_ptr_t disguised_object = DISGUISE(object);
weak修飾的屬性,當沒有擁有對象之后就會被銷毀,并且指針置位nil,那么在對象銷毀之后,雖然在map中既然存在值object對應的AssociationsHashMap,但是因為object地址已經被置位nil,會造成壞地址訪問而無法根據object對象的地址轉化為disguised_object了。
使用 OBJC_ASSOCIATION_ASSIGN
策略其實等于 assign/unsafe_unretained
,本質上是保存了對象的地址而不是真正的弱引用,在一些情定的情況下 屬性對象釋放時再調用方法會出現野指針異常
更安全的 association weak 屬性
從 DZNEmptyDataSet 中可以學習使用 strong + WeakContainer 的方式,實現對屬性對象的 weak 引用。
思路是:
- 聲明一個 WeakContainer 類對真實的屬性對象進行 weak 屬性引用
- 通過
OBJC_ASSOCIATION_RETAIN_NONATOMIC
策略對 WeakContainer 進行 retain association - 這樣在 get 關聯屬性對象時由于 WeakContainer 對真是屬性對象的 weak 引用,會返回 nil 而不是野指針
實現 WeakContainer 如下:
@interface XWeakObjectContainer : NSObject
@property (nonatomic, readonly, weak) id weakObject;
- (instancetype)initWithWeakObject:(id)object;
@end
@implementation XWeakObjectContainer
- (instancetype)initWithWeakObject:(id)object {
self = [super init];
if (self) {
_weakObject = object;
}
return self;
}
@end
從而變相地實現 weak association 如下:
@implementation NSObject (XAssociate)
- (void)setXProperty:(id)xProperty {
XWeakObjectContainer *container = [[XWeakObjectContainer alloc] initWithWeakObject:xProperty];
objc_setAssociatedObject(self, @selector(xProperty), container, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)xProperty {
XWeakObjectContainer *container = objc_getAssociatedObject(self, _cmd);
return container.weakObject;
}
@end
雖然 retain 了一個 WeakContainer,但是 WeakContainer 最終會隨著屬性的持有對象一起銷毀,不存在泄露。