我們知道,分類無法添加成員變量,在分類中定義了屬性,系統沒有生成對應的成員變量,也沒有實現set和get方法。那我們如何實現為分類添加屬性呢?
通過runtime中提供的關聯對象相關API我們可以實現以上功能。
添加關聯對象
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)
關聯API對象參數說明:
參數一:id object
: 給哪個對象添加屬性。
參數二:const void * key
: 關聯對象中屬性值存取過程中對應唯一標識,根據key來設置和取值。
參數三:id value
: 關聯的值,也就是set方法傳入的值給屬性去保存。
參數四: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 // 指定相關的對象被復制,原子性
};
提供一個實例,給大家看看關聯對象的基本使用。
定義Person類,以及Person+Test分類,在Person+Test中定義name ,weight屬性,然后通過關聯對象實現set和get方法,看外部能不能正常使用?
@interface Person : NSObject
@end
@implementation Person
@end
@interface Person (Test)
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int weight;
@end
@implementation Person (Test)
/*
給key設值有三種比較好的的方法
因為key值類型為const void *,任何類型的值,其實就是給一個唯一的標識
1.我們針對每個屬性,定義一個全局的key名,然后取其地址,著一定是唯一的
加上static,只在文件內部有效
static const void *NameKey = &NameKey;
static const void *WeightKey = &WeightKey;
2.針對每個屬性,因為類中的屬性名是唯一的,直接拿屬性名作為key
#define NameKey = @"name";
#define WeightKey = @"weight";
3.使用@seletor作為key
直接用屬性名對應的get方法的selector,有提示不容易寫錯
并且get方法隱藏參數_cmd可以直接用,看上去就會更加簡潔
以下實例代碼就是用的第三種方式
*/
- (void)setName:(NSString *)name{
//通過一個key給對象關聯一個值
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name{
//通過一個key,取出對象所關聯的值
//隱式參數 _cmd = @selector(name)
return objc_getAssociatedObject(self, _cmd);
}
- (void)setWeight:(int)weight{
objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (int)weight{
return [objc_getAssociatedObject(self, _cmd) intValue];
}
@end
外部調用
Person *person = [[Person alloc] init];
person.name = @"jack";
person.weight = 20;
NSLog(@"name:%@ weight:%d",person.name,person.weight);
打印結果
2019-07-15 17:52:27.471526+0800 給分類添加屬性-關聯對象[19511:8022622] name:jack weight:20
通過以上實例可以看出通過關聯對象是可以成功為分類添加屬性的。
那么使用關聯對象是將屬性添加到原有類的內存中,還是另外通過其他方式保存呢?如果通過其他方式,又是如何保存,如何銷毀的呢?
關聯對象原理
實現關聯對象的核心對象有
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
其中Map就相當于我們平時使用的字典,也是key-value存取值。
我們通過runtime源碼來分析底層原理,來到objc-references.mm,搜索objc_setAssociatedObject
,objc_getAssociatedObject
,objc_removeAssociatedObjects
,分別來看看設置,取值,移除所有關聯對象的底層邏輯。
/**********************************************************************
* Associative Reference Support
**********************************************************************/
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
_object_set_associative_reference函數
_object_set_associative_reference函數內部我們可以找到上面說過的實現關聯對象技術的核心對象。接下來我們來AssociationManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation之間的關系。
AssociationManager
AssociationsHashMap、ObjectAssociationMap、ObjcAssociation
通過AssociationsHashMap內部源碼我們發現AssociationsHashMap繼承自unordered_map首先來看一下unordered_map內的源碼
從unordered_map源碼中可以看出前兩個參數_key
,_tp
對應的就是map中的key和value,參照傳入參數,_key對應的值就是disguised_ptr_t
,_Tp 對應的值就是ObjectAssociationMap *
。
接著我們查看ObjectAssociationMap,繼承至map,和unordered_map類似,同樣以key,value方式存儲著ObjectAssociation。_key對應著void *
,_Tp 對應著ObjectAssociation
。
再接著來查看ObjcAssociation。
我們發現ObjcAssociation存儲著_policy
、_value
,而這兩個值我們可以發現正是我們調用objc_setAssociatedObject函數傳入的值,也就是說我們在調用objc_setAssociatedObject函數中傳入的value和policy這兩個值最終是存儲在ObjcAssociation中的。
現在我們已經對AssociationsManager、 AssociationsHashMap、 ObjectAssociationMap、ObjcAssociation四個對象之間的關系有了簡單的認識,那么接下來我們來細讀源碼。
最后通過一張圖整理其中關系,這樣就看的更加清晰明了。
關聯對象并不是存儲在關聯對象本身內存中
關聯對象存儲在全局的一個AssociationsManager中
設置關聯對象為nil,就相當于移除了關聯對象
_object_get_associative_reference函數
_object_remove_assocations函數
結合以上總結的關聯對象原理圖,就能很好理解_object_get_associative_reference
和_object_remove_assocations
的邏輯。
總結:
關聯對象并不是存儲在關聯對象本身內存中
關聯對象存儲在全局的一個AssociationsManager中
設置關聯對象為nil,就相當于移除了關聯對象