關聯對象
前言
我們都知道ARC環境下, 在一個類中聲明一個屬性@property (nonatomic, assign) int age;
, 系統類似的幫我們生成如下代碼:
- 生成下劃線的成員變量
- 生成setter, getter方法的聲明
- 生成setter, getter方法的實現
@interface Person : NSObject
{
int _age;
}
- (void)setAge:(int)age;
- (int)age;
@end
@implementation Person
- (void)setAge:(int)age
{
_age = age;
}
- (int)age
{
return _age;
}
@end
Category中添加屬性
在category中添加屬性, 系統只會做一件事情, 生成setter, getter方法的聲明.
我們知道category中不可以添加實例變量, 因為category是一個結構體, 它只可以添加對象/類方法, 協議, 屬性
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
如果要讓我們實現可以類似
的可以添加實例變量的效果, 那該如何做呢?
-
方式一: 使用全局字典
因為我們想讓category實現每一個person對象有一個實例變量的效果, 所以我們可以想到在全局創建一個可變字典, 每個person對應一個實例變量, 如下實現:
NSMutableDictionary *ages_; @implementation Person (Test1) + (void)load { ages_ = [NSMutableDictionary dictionary]; } - (void)setAge:(int)age { NSString *key = [NSString stringWithFormat:@"%p", self]; ages_[key] = @(age); } - (int)age { NSString *key = [NSString stringWithFormat:@"%p", self]; return [ages_[key] intValue]; } @end
- person對象的實例變量是存儲在person對象的內部, 而這種實現方式, 將實例變量存在了全局字典中, 實例變量存儲的位置不同
- 因為是全局的字典, 所以存在線程安全的問題, 需要在setter方法中加鎖
-
方式二: 使用關聯對象
/** object: 需要關聯的對象 key: 指針 類似于字典的key void * value: 關聯的值 policy: 內存策略 */ objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>) objc_getAssociatedObject(self, <#const void * _Nonnull key#>)
內存策略:
objc_AssociationPolicy 對應的修飾符 OBJC_ASSOCIATION_ASSIGN assign OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic OBJC_ASSOCIATION_RETAIN strong, atomic OBJC_ASSOCIATION_COPY copy, atomic
-
key的定義方式一:
static const void *ageKey = &ageKey; - (void)setAge:(int)age { objc_setAssociatedObject(self, ageKey, @(age), OBJC_ASSOCIATION_ASSIGN); }
因為key類似于字典的key, 所以每個關聯的值的key是唯一的, 為了唯一性, 我們可以使用: static const void *ageKey = &ageKey; (ageKey這個指針變量存儲的是它自己這個變量的地址, 這樣寫可以保證如果有很多關聯的key的話, 可以確保每個key是唯一的)
static修飾也可以防止其他文件用extern
關鍵字獲取這個key
static 保證這個全局變量只在內部使用變量 內存 值 ageKey 0x10000 0x10000 nameKey 0x10008 0x10008 -
key的定義方式二:
//更加省事而且聲明的這個變量只占一個字節 char static const char ageKey; - (void)setAge:(int)age { objc_setAssociatedObject(self, &ageKey, @(age), OBJC_ASSOCIATION_ASSIGN); }
-
key的定義方式三:
知識點: NSString的內存分配// 使用@"age", NSString *str = @"age"; 字面量的字符串變量存儲在常量區, 所以@"age", 所以兩個方法中的@"age"字符串的內存地址都是一樣的. - (void)setAge:(int)age { objc_setAssociatedObject(self, @"age", @(age), OBJC_ASSOCIATION_ASSIGN); } - (int)age { objc_getAssociatedObject(self, @"age") }
-
key的定義方式四:
- (void)setAge:(int)age { objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN); } - (int)age { // _cmd表示當前方法的@selector, _cmd == @selector(age) objc_getAssociatedObject(self, _cmd); } /*OC的編譯器在編譯后會在每個方法中加兩個隱藏的參數: 一個是_cmd,當前方法的一個SEL指針。 一個是self,指向當前對象的一個指針 (id)self, (SEL)_cmd */ // 當然使用@seletor(setAge:)等其他方法也可以
關聯對象的原理
實現關聯對象技術的核心對象有:
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
可以通過蘋果的開源代碼 objc4: objc-references.mm //引用
class AssociationsManager {
static AssociationsHashMap *_map;
}
class AssociationsHashMap: public unordered_map<disguised_ptr_t, ObjectAssociationMap>
class ObjectAssociationMap: public std::map <void *, ObjcAssociation>
class ObjcAssociation {
uintptr_t _policy;
id _value;
}
舉例說明:
@implementation Person (Test)
- (void)setAge:(int)age
{
objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
}
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
上面的代碼給Person實例對象關聯的兩個值(age和name), 底層上是由全局的AssociationsManager
管理, AssociationsManager
中有一個AssociationsHashMap
(字典), 其中以(姑且認為)person為鍵, AssociationsHashMap
(字典)為值. AssociationsHashMap
(字典)中以關聯值傳入的key為鍵, 以ObjcAssociation
對象為值, ObjcAssociation
中包含內存策略和value值
開源代碼如下:
// setter
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
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());
// 傳入的對象object, 經過DISGUISE(object)函數, 進行內存操作, 作為key
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) { //如果
// break any existing association.
// 根據disguised_object, 找到該對象對應的AssociationsHashMap
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
// 根據i->second找到ObjectAssociationMap的指針
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
//如果key有對應的`ObjcAssociation`, 則替換
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else { //如果key沒有對應的`ObjcAssociation`, 則創建新的key, ObjcAssociation鍵值對
// 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.
// 如果傳入的value為nil值
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()) { //遍歷該對象對應的字典中所有的ObjectAssociationMap, 進行抹除操作
old_association = j->second;
refs->erase(j); // 抹除
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
// getter
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
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) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
總結:
1.關聯對象并不是存儲在被關聯對象本身內存中
2.關聯對象存儲在全局的統一的一個AssociationsManager
中
3.設置關聯對象為nil, 就相當于移除關聯對象
4.移除某個對象上的所有的關聯對象 void objc_removeAssociatedObjects(id object)
5.如果某個person對象被銷毀了, 則這個person對象所對應的ObjectAssociationMap
字典也會被銷毀
6.因為內存策略(objc_AssociationPolicy)中沒有weak,
Person *p = [[Person alloc] init];
{
Person *tmp = [[Person alloc] init];
objc_setAssociatedObject(p, @"tmp", tmp, OBJC_ASSOCIATION_ASSIGN);
}
NSLog(@"%@", objc_getAssociatedObject(p, @"tmp"));
// 上面代碼會報錯誤 EXC_BAD_ACCESS, 壞內存地址訪問, 因為使用的是OBJC_ASSOCIATION_ASSIGN的內存策略, 出了大括號tmp對象釋放
如何設置關聯值的時候使用weak策略呢?
iOS weak 關鍵字漫談
方式一: 使用block包裹
- (void)setContext:(CDDContext*)object {
id __weak weakObject = object;
id (^block)() = ^{ return weakObject; };
objc_setAssociatedObject(self, @selector(context), block, OBJC_ASSOCIATION_COPY);
}
- (CDDContext*)context {
id (^block)() = objc_getAssociatedObject(self, @selector(context));
id curContext = (block ? block() : nil);
return curContext;
}
方式二: 使用對象包裹
新建一個類,用于包裹weak對象
@interface GYCatagoryWeakWrapper : NSObject
@property (nonatomic, weak) id weakObj;
@end
@implementation GYCatagoryWeakWrapper
@end
在Catagory中使用
- (void)setWeakCurrentVC {
GYCatagoryWeakWrapper *wrapper = [[GYCatagoryWeakWrapper alloc] init];
wrapper.weakObj = [self getCurrentVC];
objc_setAssociatedObject(self, @selector(getWeakCurrentVC), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)getWeakCurrentVC {
GYCatagoryWeakWrapper *wrapper = objc_getAssociatedObject(self, @selector(getWeakCurrentVC));
return wrapper.weakObj;
}