上一篇文章分類category、load、initialize的本質和源碼分析介紹到,在objc4最新源碼objc4-818.2中,category結構體如下:
struct category_t {
const char *name; // 類名
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods; // 實例方法列表
WrappedPtr<method_list_t, PtrauthStrip> classMethods; // 類方法列表
struct protocol_list_t *protocols; // 協議列表
struct property_list_t *instanceProperties; // 屬性列表
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
使用clang命令,將 Objective-C 代碼轉換成 C++ 後,其結構與源碼中的類似。
category_t
結構體中存儲了實例方法列表、類方法列表、協議列表、屬性列表,但沒有成員變量列表。因此,分類中不能直接添加成員變量,添加的屬性只會聲明getter、setter方法,不會生成對應的getter、setter實現和成員變量。
因此,想要在分類中實現屬性的效果,就需要額外步驟存儲成員變量值。
1. 分類添加成員變量方案
1.1 使用全局變量存儲
分類中沒有成員變量,屬性不會生成訪問器方法的實現。可以通過添加一個全局變量,保存屬性值。如下所示:
static NSString *_name;
@implementation Child (Test)
- (void)setName:(NSString *)name {
_name = name;
}
- (NSString *)name {
return _name;
}
@end
使用以下代碼驗證分類屬性存取值:
Child *child1 = [[Child alloc] init];
child1.name = @"pro648";
Child *child2 = [[Child alloc] init];
child2.name = @"It's Time";
NSLog(@"child1 name:%@ -- child2 name:%@", child1.name, child2.name);
控制檯輸出如下:
child1 name:It's Time -- child2 name:It's Time
由於全局變量存儲在類中,內存中只有一個類對象,這就導致不同實例對象共用同一個全局變量,不能區分不同實例的值。
1.2 使用全局字典存儲
既然使用一個全局變量不能區分不同實例,我們可以添加一個全局字典,以實例作爲key,存儲其值。這樣可以區分不同實例:
NSMutableDictionary *nameDict;
#define key [NSString stringWithFormat:@"%p", self]
+ (void)initialize {
if (self == Child.class) {
nameDict = [NSMutableDictionary dictionary];
}
}
- (void)setName:(NSString *)name {
nameDict[key] = name;
}
- (NSString *)name {
return nameDict[key];
}
由於不同實例要使用同一個字典,這裏將字典的初始化放到了+initialize
方法。在Child
類首次收到消息時初始化,既確保了延遲加載,也確保了只初始化一份。由於有多個分類時,只會調用一個分類的+initialize
方法,此時應將其初始化放到+load
方法。關於+load
、+initialize
方法的區別,可以查看分類category、load、initialize的本質和源碼分析。
執行後控制檯打印如下:
child1 name:pro648 -- child2 name:It's Time
如果有多個屬性,就需要添加多個字典,這樣會很麻煩。字典會長時間存在,造成內存泄漏。除此之外,字典並非線程安全,多線程存取時會遇到問題。
1.3 關聯對象 Associated Object
Objective-C 2.0 中 runtime 增加的 associated object,在運行時可以將任意值關聯到指定對象。其主要提供了以下 API:
- objc_setAssociatedObject:爲指定對象、使用指定key關聯值。值爲
nil
時,用於清楚關聯對象。 - objc_getAssociatedObject:取出指定對象使用指定key關聯的值。
- objc_removeAssociatedObjects:清除指定對象的所有關聯對象。主要用於將對象還原爲「初始狀態」,不應用於從對象中刪除關聯對象,因爲它會刪除關聯到該對象的所有值。通常,爲
objc_setAssociatedObject
傳值nil
以清除關聯。
添加關聯對象對象、取出關聯對象方法如下:
// 添加關聯對象
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
// 取出關聯對象
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
可以看到key是const void * _Nonnull
類型的,也就是地址值,不需要變量值。因此,只定義就可以使用。
添加int
類型的全局變量age
,如下所示:
// 佔4個字節
int age = 0;
使用關聯對象存、取屬性值:
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, &age, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, &age);
}
1.3.1 key 的幾種類型
除了使用int
類型的全局變量作爲 key,還可以使用以下內容作爲 key:
-
指針變量:一個指針變量佔用8個字節,指針變量聲明如下:
static const void *NameKey = &NameKey; // 傳 NameKey
-
char:一個char只佔用一個字節。
static const char NameKey; // 傳 &NameKey
字符串:字符串存儲在數據常量區,多個相同字符串地址相同。直接爲 key 傳遞字符串時,就是在傳遞字符串的地址值。
@selector():就是指定字符串地址值。
_cmd
:方法默認有兩個參數,第一個(id)self
,第二個(SEL)_cmd
。因此@selector()
可以使用_cmd
替換。
比較之後,最爲便捷的還是@selector()
方式,如下所示:
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, @selector(name));
}
1.3.2 內存管理
objc_AssociationPolicy
枚舉類型決定關聯值存儲類型。
objc_AssociationPolicy | 對應property | 描述 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property(assign) | 對關聯對象弱引用。 |
OBJC_ASSOCIATION_COPY | @property(nonatomic, strong) | 對關聯對象強引用,非原子性。 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property(nonatomic, copy) | 複製關聯對象,非原子性。 |
OBJC_ASSOCIATION_RETAIN | @property(atomic, strong) | 對關聯對象強引用,原子性。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property(atomic, copy) | 複製關聯對象,原子性。 |
2. 關聯對象的本質
RunTime 主要由以下對象實現關聯對象技術:
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjectAssociation
2.1 objc_setAssociatedObject源碼
在objc4中搜索objc_setAssociatedObject()
函數,如下所示:
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
其調用了_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));
// 用object拿到AssociationsHashMap中的key
DisguisedPtr<objc_object> disguised{(objc_object *)object};
// 使用傳入的policy、value生成association。
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
bool isFirstAssociation = false;
{
AssociationsManager manager;
// manager關聯了AssociationsHashMap
AssociationsHashMap &associations(manager.get());
if (value) {
// 使用object生成的disguised取出字典數據
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it's the first association we make */
isFirstAssociation = true;
}
/* establish or replace the association */
// 取出ObjectAssociationMap
auto &refs = refs_result.first->second;
// 使用key取出ObjectAssociation
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
// 交換關聯的association
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);
// 如果關聯對象value是空,就移除關聯對象。
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
中關聯了AssociationsHashMap
,使用參數object
計算出 key disguised
,利用disguised
從AssociationsManager
中取出ObjectAssociationMap
,利用傳入的 key 取出ObjectAssociation
。之前使用參數value
、policy
已經生成了association
,這裏直接與 key 取出的ObjectAssociation
交換。另外,也可以看到,association 不會引用傳進來的 object 對象,只使用其計算出一個值,但引用了傳進來的 value。對象釋放時,會自動移除關聯對象關聯的所有值。
如果參數value
是空的,就移除關聯對象。
AssociationsManager
源碼如下:
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
// AssociationsManager關聯了ObjectAssociationMap。
AssociationsHashMap &get() {
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
};
ObjectAssociationMap
和AssociationsHashMap
源碼如下:
// key是 const void *, value是ObjcAssociation
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
// key: DisguisePtr value: ObjectAssociationMap
// 就是value是另一個map
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
關聯對象原理如下圖:
2.2 objc_getAssociatedObject源碼
RunTime 中的objc_getAssociatedObject()
函數如下:
id
objc_getAssociatedObject(id object, const void *key)
{
return _object_get_associative_reference(object, key);
}
其調用了_object_get_associative_reference()
函數,_object_get_associative_reference()
如下:
id
_object_get_associative_reference(id object, const void *key)
{
ObjcAssociation association{};
{
AssociationsManager manager;
// manager關聯了AssociationsHashMap
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
// 取出ObjectAssociationMap
ObjectAssociationMap &refs = i->second;
// 使用參數 key,取出ObjectAssociation
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
// ObjectAssociation包含要取出的 value 和 policy。
association = j->second;
association.retainReturnedValue();
}
}
}
return association.autoreleaseReturnedValue();
}
objc_getAssociatedObject()
取出關聯對象步驟與objc_setAssociatedObject()
設值關聯對象步驟類似,都是使用傳入的object
作爲 key,取出ObjectAssociationMap
,即ObjectAssociationMap
包含了該實例的所有關聯對象。使用參數 key 從ObjectAssociationMap
中取出ObjectAssociation
對象。ObjectAssociation
就是要取出、設值的關聯對象。
2.3 objc_removeAssociatedObjects
RunTime 中objc_removeAssociatedObjects()
函數如下:
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object, /*deallocating*/false);
}
}
objc_removeAssociatedObjects()
函數調用了_object_remove_associations()
函數,如下所示:
// 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, bool deallocating)
{
ObjectAssociationMap refs{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
refs.swap(i->second);
// If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
bool didReInsert = false;
if (!deallocating) {
for (auto &ref: refs) {
if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
i->second.insert(ref);
didReInsert = true;
}
}
}
if (!didReInsert)
// 移除 AssociationsMap
associations.erase(i);
}
}
// Associations to be released after the normal ones.
SmallVector<ObjcAssociation *, 4> laterRefs;
// release everything (outside of the lock).
for (auto &i: refs) {
if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
// If we are not deallocating, then RELEASE_LATER associations don't get released.
if (deallocating)
laterRefs.append(&i.second);
} else {
i.second.releaseHeldValue();
}
}
for (auto *later: laterRefs) {
later->releaseHeldValue();
}
}
_object_remove_associations()
函數根據object
對象,查找到AssociationMap
後直接移除。
3. 面試題
3.1 分類可以添加成員變量嗎?
分類不能添加成員變量。可以添加屬性,但系統只會生成屬性getter、setter方法的聲明,不會生成其實現,也不會生成屬性的成員變量。想要在分類中使用屬性,需在getter、setter實現中手動使用關聯對象實現。
Demo名稱:AssociatedObject
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/AssociatedObject
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/關聯對象%20Associated%20Object%20的本質.md