iOS Objective-C 關(guān)聯(lián)對象
1. 關(guān)聯(lián)對象簡介
對于關(guān)聯(lián)對象,我們熟悉它的地方就是給分類添加屬性娱仔。雖然我們可以在分類中通過@property
編寫代碼來聲明一個屬性讹躯,但是當(dāng)我們使用的時候就報(bào)方法找不到錯誤粉洼,其實(shí)缺失的方法就是屬性的getter
和setter
的實(shí)現(xiàn)按价,那么關(guān)聯(lián)對象就可以完美的解決這個問題餐茵。
官方定義:
Associative references, available starting in OS X v10.6, simulate the addition of object instance variables to an existing class. Using associative references, you can add storage to an object without modifying the class declaration. This may be useful if you do not have access to the source code for the class, or if for binary-compatibility reasons you cannot alter the layout of the object.
Associations are based on a key. For any object you can add as many associations as you want, each using a different key. An association can also ensure that the associated object remains valid for at least the lifetime of the source object.
譯: 從OS X v10.6開始可用的關(guān)聯(lián)引用模擬了將對象實(shí)例變量添加到現(xiàn)有類中炬转。使用關(guān)聯(lián)引用辆苔,可以在不修改類聲明的情況下將存儲添加到對象。如果您無權(quán)訪問該類的源代碼扼劈,或者由于二進(jìn)制兼容性原因而無法更改對象的布局驻啤,則這可能很有用。
關(guān)聯(lián)基于
key
荐吵。對于任何對象骑冗,您都可以根據(jù)需要添加任意數(shù)量的關(guān)聯(lián),每個關(guān)聯(lián)都使用不同的key
先煎。關(guān)聯(lián)還可以確保關(guān)聯(lián)的對象至少在源對象的生存期內(nèi)保持有效贼涩。
通過蘋果官方文檔我們可以知道,關(guān)聯(lián)引用不僅僅可以用在給分類添加屬性薯蝎。但是給分類添加屬性是我們最常用的場景遥倦。
關(guān)聯(lián)對象的兩個函數(shù)
- 設(shè)置關(guān)聯(lián)對象
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
SetAssocHook.get()(object, key, value, policy);
}
- 參數(shù)一:id object : 要關(guān)聯(lián)的對象
- 參數(shù)二:const void *key : 關(guān)聯(lián)使用的key值
- 參數(shù)三:id value : 關(guān)聯(lián)的值,也就是我們要設(shè)置的值占锯。
- 參數(shù)四:objc_AssociationPolicy policy : 策略屬性袒哥,以什么形式保存
- 獲取關(guān)聯(lián)對象
id
objc_getAssociatedObject(id object, const void *key)
{
return _object_get_associative_reference(object, key);
}
- 參數(shù)一:id object : 獲取哪個對象里面的關(guān)聯(lián)的值
- 參數(shù)二:const void *key : 關(guān)聯(lián)使用的key值,通過這個key取出對應(yīng)的值
關(guān)聯(lián)使用的策略:
/**
* Policies related to associative references.
* These are options to objc_setAssociatedObject()
*/
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. */
};
策略 | 對應(yīng) @property | 描述 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | (assign)或者(unsafe_unretained) | 指定一個關(guān)聯(lián)對象的弱引用 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | (nonatomic, strong) | 不能原子化的強(qiáng)引用 |
OBJC_ASSOCIATION_COPY_NONATOMIC | (nonatomic, copy) | copy引用消略,不能原子化 |
OBJC_ASSOCIATION_RETAIN | (atomic, strong) | 原子化的強(qiáng)引用 |
OBJC_ASSOCIATION_COPY | (atomic, copy) | 原子化的copy引用 |
2. 關(guān)聯(lián)對象的應(yīng)用
舉個例子堡称,說了半天關(guān)聯(lián)對象可以為分類添加屬性,那么我們就把這個例子寫一下艺演。
@interface CTObject (Category)
@property (nonatomic, copy) NSString *cate_p1;
@end
@implementation CTObject (Category)
- (void)setCate_p1:(NSString *)cate_p1{
objc_setAssociatedObject(self, @"cate_p1",cate_p1, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)cate_p1{
return objc_getAssociatedObject(self, @"cate_p1");
}
@end
3. 關(guān)聯(lián)對象的底層原理
上面兩節(jié)對關(guān)聯(lián)對象做了簡單的介紹和其使用的舉例却紧,下面我們來研究一下它的底層實(shí)現(xiàn)。
3.1 objc_setAssociatedObject
我們先看看objc_setAssociatedObject
的源碼胎撤,由于使用了各種C++的語法和嵌套晓殊,嵌套過程就不多說了,以下是嵌套的代碼:
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
SetAssocHook.get()(object, key, value, policy);
}
static void
_base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};
由以上代碼我們可以知道objc_setAssociatedObject
實(shí)際調(diào)用的是_object_set_associative_reference
函數(shù)哩照,下面我們就來到_object_set_associative_reference
看看它究竟是如何實(shí)現(xiàn)的挺物。
_object_set_associative_reference 源碼:
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
DisguisedPtr<objc_object> disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
if (value) {
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it's the first association we make */
object->setHasAssociatedObjects();
}
/* establish or replace the association */
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
association.swap(result.first->second);
}
} else {
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
auto it = refs.find(key);
if (it != refs.end()) {
association.swap(it->second);
refs.erase(it);
if (refs.size() == 0) {
associations.erase(refs_it);
}
}
}
}
}
// release the old value (outside of the lock).
association.releaseHeldValue();
}
acquireValue 源碼:
inline void acquireValue() {
if (_value) {
switch (_policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
_value = objc_retain(_value);
break;
case OBJC_ASSOCIATION_SETTER_COPY:
_value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
break;
}
}
}
- 首先還是做一些非空判斷防止一些空對象空值可能會引起的崩潰
- 判斷類是否禁用了關(guān)聯(lián)引用,如果是就打印錯誤信息
- 初始化一個
disguised
對象飘弧,是對object
按位取反 - 初始化一個
ObjcAssociation
對象用于持有關(guān)聯(lián)對象 - 通過
acquireValue
函數(shù)給我們的value
返回一個新值识藤,acquireValue
源碼在上面,主要是根據(jù)策略進(jìn)行不同的處理 - 接下來就是初始化一個
AssociationsManager
對象次伶,獲取一個AssociationsHashMap
哈希表 - 接下來分兩個流程一個是值存在此時是賦值
- 首先獲取到類的關(guān)聯(lián)表
- 如果沒獲取到說明我們是第一次給該類關(guān)聯(lián)痴昧,所以需要創(chuàng)建一個新的表
- 接下來獲取表的首地址,并判斷對應(yīng)的
key
是否已經(jīng)存在冠王,不存在就直接寫入 - 存在就用新值替換舊值
- 第二種是值為空赶撰,此時是刪除關(guān)聯(lián)對象
- 首先是獲取到該類對應(yīng)的哈希表
- 判斷表不為空
- 找到key對應(yīng)的節(jié)點(diǎn)
- 節(jié)點(diǎn)不為空判斷
- 替換節(jié)點(diǎn)值為空
- 清空節(jié)點(diǎn)
- 清空節(jié)點(diǎn)后,如果表也為空,則清空表
- 最后釋放舊值
3.2 objc_getAssociatedObject
objc_getAssociatedObject
就沒有那么多嵌套了豪娜,直接就可以看出是調(diào)用的_object_get_associative_reference
函數(shù)餐胀。
objc_getAssociatedObject(id object, const void *key)
{
return _object_get_associative_reference(object, key);
}
_object_get_associative_reference 源碼:
id
_object_get_associative_reference(id object, const void *key)
{
ObjcAssociation association{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
ObjectAssociationMap &refs = i->second;
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
association = j->second;
association.retainReturnedValue();
}
}
}
return association.autoreleaseReturnedValue();
}
_object_get_associative_reference
的實(shí)現(xiàn)也很簡單:
- 首先還是初始化一個
ObjcAssociation
對象,AssociationsManager
對象瘤载,獲取AssociationsHashMap
哈希表 - 獲取到當(dāng)前對象的關(guān)聯(lián)表
- 如果表不為空則通過key在表中查找數(shù)據(jù)
- 如果找到了并且不為空則調(diào)用
retainReturnedValue
函數(shù)根據(jù)策略賦值 - 最后返回通過
autoreleaseReturnedValue
函數(shù)根據(jù)策略處理的值
3.3 objc_removeAssociatedObjects
對于關(guān)聯(lián)對象其實(shí)還有一個函數(shù)objc_removeAssociatedObjects
否灾,只不過我們基本不用他,根據(jù)名字我們就可以知道該函數(shù)是移除關(guān)聯(lián)對象的鸣奔。這里也嵌套了一層代碼墨技,最終調(diào)用的是_object_remove_assocations
objc_removeAssociatedObjects 源碼:
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
_object_remove_assocations 源碼:
// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
void
_object_remove_assocations(id object)
{
ObjectAssociationMap refs{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
refs.swap(i->second);
associations.erase(i);
}
}
// release everything (outside of the lock).
for (auto &i: refs) {
i.second.releaseHeldValue();
}
}
根據(jù)注釋我們可以知道_object_remove_assocations
函數(shù)是會對性能有影響的。
- 這里還是要初始化
ObjectAssociationMap
對象挎狸,AssociationsManager
對象扣汪,AssociationsHashMap
對象。 - 找出要釋放對象的關(guān)聯(lián)表
- 判斷不為空锨匆,則將移除這些關(guān)聯(lián)關(guān)系崭别,并釋放
- 最后循環(huán)釋放類的所有關(guān)聯(lián)表
4. 總結(jié)
- 關(guān)聯(lián)對象實(shí)際上在底層是一個
ObjcAssociation
對象結(jié)構(gòu) - 全局由一個
AssociationsManager
管理類存儲了一個靜態(tài)的哈希表AssociationsHashMap
- 這個哈希表存儲的是以對象指針為鍵,以該對象所有關(guān)聯(lián)對象為值
- 關(guān)聯(lián)對象又是以
ObjectAssociationMap
來存儲的 -
ObjectAssociationMap
的存儲結(jié)構(gòu)以key
為鍵统刮,ObjcAssociation
為值 - 判斷一個對象是否存在關(guān)聯(lián)對象可以通過對象
isa
的has_assoc
至此我們的關(guān)聯(lián)對象就基本分析完畢了紊遵,但是由于本人才疏學(xué)淺,有些地方用詞不當(dāng)侥蒙,一些C++
語法不是很熟悉暗膜,有些表述不完整,不貼切鞭衩,但是我也想不出什么好詞的学搜,可能也會有些不準(zhǔn)確。如有問題歡迎指正论衍。