面試題
問: Category能否新增成員變數?如果可以,如何給Category新增成員變數?
答:不能直接新增成員變數,但是可以通過runtime的方式間接實現新增成員變數的效果。
RunTime為Category動態關聯物件
使用RunTime給系統的類新增屬性,首先需要了解物件與屬性的關係。我們通過之前的學習知道,物件一開始初始化的時候其屬性為nil,給屬性賦值其實就是讓屬性指向一塊儲存內容的記憶體,使這個物件的屬性跟這塊記憶體產生一種關聯。
那麼如果想動態的新增屬性,其實就是動態的產生某種關聯就好了。而想要給系統的類新增屬性,只能通過分類。
這裡給NSObject新增name屬性,建立NSObject的分類 我們可以使用@property給分類新增屬性
@property(nonatomic,strong)NSString *name;
複製程式碼
通過探尋Category的本質我們知道,雖然在分類中可以寫@property 新增屬性,但是不會自動生成私有屬性,也不會生成set,get方法的實現,只會生成set,get的宣告,需要我們自己去實現。
方法一:我們可以通過使用靜態全域性變數給分類新增屬性
static NSString *_name;
-(void)setName:(NSString *)name
{
_name = name;
}
-(NSString *)name
{
return _name;
}
複製程式碼
但是這樣_name靜態全域性變數與類並沒有關聯,無論物件建立與銷燬,只要程式在執行_name變數就存在,並不是真正意義上的屬性。
方法二:使用RunTime動態新增屬性
RunTime提供了動態新增屬性和獲得屬性的方法。
-(void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
return objc_getAssociatedObject(self, @"name");
}
複製程式碼
- 動態新增屬性
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
複製程式碼
引數一:id object
: 給哪個物件新增屬性,這裡要給自己新增屬性,用self。
引數二:void * == id key
: 屬性名,根據key獲取關聯物件的屬性的值,在**objc_getAssociatedObject
中通過次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 // 指定相關的物件被複制,原子性
};
複製程式碼
key值只要是一個指標即可,我們可以傳入@selector(name)
- 獲得屬性
objc_getAssociatedObject(id object, const void *key);
複製程式碼
引數一:id object
: 獲取哪個物件裡面的關聯的屬性。
引數二:void * == id key
: 什麼屬性,與**objc_setAssociatedObject
**中的key相對應,即通過key值取出value。
- 移除所有關聯物件
- (void)removeAssociatedObjects
{
// 移除所有關聯物件
objc_removeAssociatedObjects(self);
}
複製程式碼
此時已經成功給NSObject新增name屬性,並且NSObject物件可以通過點語法為屬性賦值。
NSObject *objc = [[NSObject alloc]init];
objc.name = @"xx_cc";
NSLog(@"%@",objc.name);
複製程式碼
可以看出關聯物件的使用非常簡單,接下來我們來探尋關聯物件的底層原理
關聯物件實現原理
實現關聯物件技術的核心物件有
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
其中Map同我們平時使用的字典類似。通過key-value一一對應存值。
對關聯物件技術的核心物件有了一個大概的意識,我們通過原始碼來探尋這些物件的存在形式以及其作用
objc_setAssociatedObject函式
來到runtime原始碼,首先找到objc_setAssociatedObject函式,看一下其實現
我們看到其實內部呼叫的是_object_set_associative_reference函式,我們來到_object_set_associative_reference函式中
_object_set_associative_reference函式
_object_set_associative_reference函式內部我們可以全部找到我們上面說過的實現關聯物件技術的核心物件。接下來我們來一個一個看其內部實現原理探尋他們之間的關係。AssociationsManager
通過AssociationsManager內部原始碼發現,AssociationsManager內部有一個AssociationsHashMap物件。
AssociationsHashMap
我們來看一下AssociationsHashMap內部的原始碼。
通過AssociationsHashMap內部原始碼我們發現AssociationsHashMap繼承自unordered_map首先來看一下unordered_map內的原始碼
從unordered_map原始碼中我們可以看出_Key
和_Tp
也就是前兩個引數對應著map中的Key和Value,那麼對照上面AssociationsHashMap內原始碼發現_Key
中傳入的是disguised_ptr_t
,_Tp
中傳入的值則為ObjectAssociationMap*
。
緊接著我們來到ObjectAssociationMap
中,上圖中ObjectAssociationMap已經標記出,我們發現ObjectAssociationMap中同樣以key、Value的方式儲存著ObjcAssociation。
接著我們來到ObjcAssociation中
我們發現ObjcAssociation儲存著_policy
、_value
,而這兩個值我們可以發現正是我們呼叫objc_setAssociatedObject函式傳入的值,也就是說我們在呼叫objc_setAssociatedObject函式中傳入的value和policy這兩個值最終是儲存在ObjcAssociation中的。
現在我們已經對AssociationsManager、 AssociationsHashMap、 ObjectAssociationMap、ObjcAssociation四個物件之間的關係有了簡單的認識,那麼接下來我們來細讀原始碼,看一下objc_setAssociatedObject函式中傳入的四個引數分別放在哪個物件中充當什麼作用。
重新回到_object_set_associative_reference函式實現中
細讀上述原始碼我們可以發現,首先根據我們傳入的value經過acquireValue函式處理獲取new_value。acquireValue函式內部其實是通過對策略的判斷返回不同的值
之後建立AssociationsManager manager;以及拿到manager內部的AssociationsHashMap即associations。 之後我們看到了我們傳入的第一個引數object object經過DISGUISE函式被轉化為了disguised_ptr_t型別的disguised_object。
DISGUISE函式其實僅僅對object做了位運算
之後我們看到被處理成new_value的value,同policy被存入了ObjcAssociation中。 而ObjcAssociation對應我們傳入的key被存入了ObjectAssociationMap中。 disguised_object和ObjectAssociationMap則以key-value的形式對應儲存在associations中也就是AssociationsHashMap中。
如果我們value設定為nil的話那麼會執行下面的程式碼
從上述程式碼中可以看出,如果我們設定value為nil時,就會將關聯物件從ObjectAssociationMap中移除。
最後我們通過一張圖可以很清晰的理清楚其中的關係
通過上圖我們可以總結為:一個例項物件就對應一個ObjectAssociationMap,而ObjectAssociationMap中儲存著多個此例項物件的關聯物件的key以及ObjcAssociation,為ObjcAssociation中儲存著關聯物件的value和policy策略。
由此我們可以知道關聯物件並不是放在了原來的物件裡面,而是自己維護了一個全域性的map用來存放每一個物件及其對應關聯屬性表格。
objc_getAssociatedObject函式
objc_getAssociatedObject內部呼叫的是_object_get_associative_reference
_object_get_associative_reference函式
從_object_get_associative_reference函式內部可以看出,向set方法中那樣,反向將value一層一層取出最後return出去。
objc_removeAssociatedObjects函式
objc_removeAssociatedObjects用來刪除所有的關聯物件,objc_removeAssociatedObjects函式內部呼叫的是_object_remove_assocations函式
_object_remove_assocations函式
上述原始碼可以看出_object_remove_assocations函式將object物件向對應的所有關聯物件全部刪除。
總結:
關聯物件並不是儲存在被關聯物件本身記憶體中,而是儲存在全域性的統一的一個AssociationsManager中,如果設定關聯物件為nil,就相當於是移除關聯物件。
此時我們我們在回過頭來看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 // 指定相關的物件被複制,原子性
};
複製程式碼
我們會發現其中只有RETAIN和COPY而為什麼沒有weak呢? 總過上面對原始碼的分析我們知道,object經過DISGUISE函式被轉化為了disguised_ptr_t型別的disguised_object。
disguised_ptr_t disguised_object = DISGUISE(object);
複製程式碼
而同時我們知道,weak修飾的屬性,當沒有擁有物件之後就會被銷燬,並且指標置位nil,那麼在物件銷燬之後,雖然在map中既然存在值object對應的AssociationsHashMap,但是因為object地址已經被置位nil,會造成壞地址訪問而無法根據object物件的地址轉化為disguised_object了。
本文是對底層原理學習的總結,如果有不對的地方請指正,歡迎大家一起交流學習 xx_cc 。需要這套視訊一起交流學習的可以加我Q:2336684744