分類、擴展、關聯對象
- 前言
- 分類
- 擴展
- 擴展和分類的區別
- 關聯對象
- key的幾種用法
- 流程
- 總結
前言
最近的學習中筆者發現自己對于分類、擴展相關知識并不是很熟悉,剛好看源碼類的加載過程中發現有類擴展與關聯對象詳解。本篇我們來探索一下這部分相關知識,首先我們要記住擴展是編譯時就被添加在類中,而分類是在運行時才被整合到類信息中來的。
分類
這里我們先來看看使用Clang
編譯之后,分類的底層結構struct category_t
:
這里我們來看看其中的內容,根據名稱我們可以發現其中存儲了類指針、實例方法表、類方法表、協議表、屬性列表,但是并沒有類中有的成員變量表。其實看到這里我們就可以明白,我們不可以在分類中定義成員變量,原因很簡單,這里面都沒有成員變量表。
這里還有一個結論:分類可以聲明屬性,并可以生成對應的set、get方法,但沒有去實現該方法
分類加載流程:
- 在編譯階段將分類中的方法、屬性等編譯到一個數據結構
category_t
- 將分類中的方法、屬性等合并到一個大數組中去,而后參加編譯的分類就會在數組前面
- 將合并后的分類數據插入到原有數據的前面
故而當分類中的方法與原始類中方法重名的時候,會先去調用分類中實現的方法。
擴展
@interface Person ()@property (nonatomic, assign) NSInteger age; // 私有屬性- (BOOL)validateAge; // 私有方法聲明@end
這里我們將一個擴展直接使用Clang
轉化位cpp文件,我們可以看到其直接被存儲到了成員變量表中,同時方法也直接被添加到了metholist
中:
故而擴展是在編譯階段與該類同時編譯的,是類的一部分。擴展中聲明的方法只能在該類的@implementation中實現。所以這也就意味著我們無法對系統的類使用擴展。
擴展和分類的區別
類別、分類
- 專門用來給類添加新的方法
- 不能給類添加成員屬性,添加了成員屬性也無法取到
- 注意:其實可以通過
runtime
給分類添加屬性,即屬性關聯
,重寫setter
、getter
方法
- 注意:其實可以通過
- 分類中用
@property
定義變量,只會生成變量的setter
、getter
方法的聲明
,不能生成方法實現和帶下劃線的成員變量
擴展
- 可以說成是
特殊的分類
,也可稱作匿名分類
- 可以
給類添加成員屬性
,但是是私有變量
- 可以
給類添加方法
,也是私有方法
關聯對象
這里我們來講解一下如何通過runtime
來給分類添加屬性,這里主要分為兩部分:
- 通過
objc_setAssociatedObject
設值流程 - 通過
objc_getAssociatedObject
取值流程
流程如上所示
我們先來看看取值流程:
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)
- 參數一:要關聯的對象,即為誰添加關聯屬性
- 參數二:標識符,方便下次查找
- 參數三:value
- 參數四:屬性的策略,即
nonatomic、atomic、assign
等,下面展示一下所有關聯對象的屬性類型:
下面我們來看看objc_setAssociatedObject
的源碼實現:
下面我們進入_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/44094390if (!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));//object封裝成一個數組結構類型,類型為DisguisedPtrDisguisedPtr<objc_object> disguised{(objc_object *)object};//相當于包裝了一下 對象object,便于使用// 包裝一下 policy - valueObjcAssociation association{policy, value};// retain the new value (if any) outside the lock.association.acquireValue();//根據策略類型進行處理bool isFirstAssociation = false;{//初始化manager變量,相當于自動調用AssociationsManager的析構函數進行初始化AssociationsManager manager;//并不是全場唯一,構造函數中加鎖只是為了避免重復創建,在這里是可以初始化多個AssociationsManager變量的AssociationsHashMap &associations(manager.get());//AssociationsHashMap 全場唯一if (value) {auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//返回結構為一個類對if (refs_result.second) {//判斷第二個存不存在,即bool值是否為true/* it's the first association we make */isFirstAssociation = true;}/* establish or replace the association */auto &refs = refs_result.first->second;//得到一個空的桶子,找到引用對象類型,即第一個元素的second值auto result = refs.try_emplace(key, std::move(association));//查找當前的key是否有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);}}}}}// Call setHasAssociatedObjects outside the lock, since this// will call the object's _noteAssociatedObjects method if it// has one, and this may trigger +initialize which might do// arbitrary stuff, including setting more associated objects.if (isFirstAssociation)object->setHasAssociatedObjects();// release the old value (outside of the lock).association.releaseHeldValue();//釋放
}
我們來看看這段源碼的實現過程:
- 首先檢查對象所屬類是否禁止關聯對象(系統類就不可以),若禁止則直接觸發崩潰
- 創建一個全局管理關聯對象的
AssociationsManager
管理類,并獲取唯一的全局靜態哈希Map:AssociationsHashMap
- value是否存在:
- 若存在,通過
try_emplace
方法,創建一個空的ObjectAssociationMap
去取查詢鍵值對 - 如果發現沒有這個
key
就插入一個空的BucketT
進去并返回true
- 通過
setHasAssociatedObjects
方法標記對象存在關聯對象
即置isa
指針的has_assoc
屬性為true
- 用當前
policy 和 value
組成了一個ObjcAssociation
替換原來BucketT
中的空 - 標記一下
ObjectAssociationMap
的第一次為false
AssociationsManager
我們先來看看其源碼實現
這里我們可以看到AssociationsHashMap
從靜態變量中取出,所以全場唯一
下面我們來看看這AssociationsHashMap
以及ObjectAssociationMap
的定義
這里先說一下
DenseMap
,這個東西時LLVM實現的高性能哈希表,支持快速插入、查找、刪除(筆者具體也不會)
- 先來看看
ObjectAssociationMap
,他對應的是一個對象的關聯屬性集合,通過健快速定位到具體的ObjcAssociation
。
? 這里展示一下該結構體內部包含的內容:關聯值的引用計數策略與實際值
- 再來看看
AssociationsHashMap
,這是一個全局管理所有對象的關聯屬性的集合,這里鍵為偽裝指針(DisguisedPtr
),值為該對象關聯屬性表ObjectAssociationMap
這里附一張圖來講解這幾個表之間的關系
下面來說一下這幾個map之間的聯系與不同:
AssociationsManager
可以有很多個,但是AssociationsHashMap
類型的map只能有一個,是通過AssociationsManager
來獲取的- 這個map中有很多個
ObjectAssociationMap
類型的map,在上文中的講解中,我們可以明白每個對象都有一個ObjcAssociation
,所以每個對象都會有一個自己的ObjectAssociationMap
類型的map
key的幾種用法
- 使用的get方法的@selector作為key
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
- 使用指針的地址作為key
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
- 使用static作為key
static char MyKey;objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
- 使用屬性名作為key
objc_setAssociatedObject(obj, @“property”, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @“property”);
流程
-
設置關聯對象:
-
調用
objc_setAssociatedObject
-
AssociationsManager
查找或創建與目標對象相關的ObjectAssociationMap
-
在
ObjectAssociationMap
中查找或創建對應的ObjcAssociation
。 -
將關聯值和存儲策略設置到
ObjcAssociation
中。
-
-
獲取關聯對象:
-
調用
objc_setAssociatedObject
-
AssociationsManager
查找與目標對象相關的ObjectAssociationMap
-
在
ObjectAssociationMap
中查找對應的ObjcAssociation
。 -
返回
ObjcAssociation
中存儲的關聯值
-
-
移除關聯對象:
- 調用
objc_removeAssociatedObjects
或objc_setAssociatedObject
設置為 nil。 AssociationsManager
查找與目標對象相關的ObjectAssociationMap
。- 從
ObjectAssociationMap
中移除對應的ObjcAssociation
。 - 如果
ObjectAssociationMap
為空,可能會移除整個映射以釋放資源。
- 調用
這個流程其實也就是上文中_object_set_associative_reference
的流程,筆者認為這樣理解更好一些,下面再附一張圖幫助理解
總結
關聯對象就是一個二層哈希的處理,存取的時候都是兩層處理,類似于二維數組: