iOS 底層探索系列html
- iOS 底層探索 - alloc & init
- iOS 底層探索 - calloc 和 isa
- iOS 底層探索 - 類
- iOS 底層探索 - cache_t
- iOS 底層探索 - 方法
- iOS 底層探索 - 消息查找
- iOS 底層探索 - 消息轉發
- iOS 底層探索 - 應用加載
- iOS 底層探索 - 類的加載
- iOS 底層探索 - 分類的加載
- iOS 底層探索 - 類拓展和關聯對象
- iOS 底層探索 - KVC
- iOS 底層探索 - KVO
iOS 查漏補缺系列objective-c
前面咱們探索了 iOS
中類和分類的加載,關於類這一塊的內容,咱們還有一些坑沒有填,好比類拓展和關聯對象,今天讓咱們一塊兒填下這塊的坑。bash
關於類拓展的具體定義,你們能夠直接參考 Apple 對於類拓展的說明。markdown
A class extension bears some similarity to a category, but it can only be added to a class for which you have the source code at compile time (the class is compiled at the same time as the class extension).架構
類拓展和分類很類似,可是前提是你擁有原始類的源碼,而且是在編譯時被附加到類上的。(類和類擴展同時編譯)app
類拓展的結構:ide
@interface ClassName ()
@end
複製代碼
Because no name is given in the parentheses, class extensions are often referred to as anonymous categories. 由於括號中沒有填寫任何內容,因此類擴展也被稱爲匿名的分類。oop
咱們在 Xcode
中建立 Objective
類型的文件的時候,能夠選擇空文件、分類、協議以及類擴展。佈局
若是咱們選擇 Extension
選項,Xcode
會幫咱們生成一個 NSObject + 擴展名
的頭文件出來,也就是說類擴展的命名方式爲 類名_擴展名.h
post
而這樣的操做其實咱們不多作,咱們通常都是在 .m
文件中聲明一下當前類的拓展,基本上咱們都會在類擴展去聲明一些私有的屬性、方法。好比在 .h
文件中聲明一個只讀的屬性,而後在 .m
文件的類拓展中去重寫這個屬性爲可讀可寫。
咱們不妨使用 LLDB
打印看一下類拓展到底是不是在編譯時就被附加到了類上面了呢?
咱們在 objc-756
源碼中的 objc-debug
項目下新建一個類 Person
,而後給這個類添加一個屬性 name
,而後在 .m
文件中的類拓展中添加一個屬性 mName
和方法 extM_method
,接着再建立一個 Person
的類拓展 Person+Extension.h
文件:
// Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
// Person.m
#import "Person.h"
#import "Person+Extension.h"
@interface Person ()
@property (nonatomic, copy) NSString *mName;
- (void)extM_method;
@end
@implementation Person
+ (void)load{
NSLog(@"%s",__func__);
}
- (void)extM_method{
NSLog(@"%s",__func__);
}
- (void)extH_method{
NSLog(@"%s",__func__);
}
@end
// Person+Extension.h
#import <AppKit/AppKit.h>
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, copy) NSString *ext_subject;
- (void)extH_method;
@end
NS_ASSUME_NONNULL_END
複製代碼
接着咱們在 main.m
中來測試一下:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
NSLog(@"%@ - %p", p, p);
}
return 0;
}
複製代碼
咱們在 Person
實例化對象 p
這一行打上斷點,而後運行項目。接着在控制檯進行 LLDB
打印:
由於對象的屬性以及方法都存儲在類對象上面,而因爲類結構裏面的
ro
是編譯時就肯定了其內容,因此咱們只須要打印出類對象的ro
結構中 是否有類拓展中的mName
屬性和extM_method
方法 是否有類擴展中的ext_name
和ext_subject
屬性以及extH_method
方法
x/4gx
命令打印出 LGPerson
類對象的內存地址,以 16 進制方式打印,打印 4 段isa
,緊接着是 superclass
,而後是 cache_t
。咱們前面已經分析過,在默認的 arm64
處理器架構下,isa
佔 8 個字節,superclass
佔 8 個字節,而 cache_t
的三個屬性加起來是 8 + 4 + 4 = 16 個字節,因此要想拿到 bits
須要進行 8 + 8 + 16 = 32 字節的內存平移,可是這裏是 16 進制,因此須要移動 0x20 個內存地址,也就是 0x100002420 + 0x20 = 0x100002440
data()
屬性會返回 bits.data()
,因此這裏直接打印剛纔取到的 bits
的 data()
屬性,而 bits
的 data()
屬性其實返回的是 rw
。struct objc_class : objc_object { class_rw_t *data() { return bits.data(); } } struct class_data_bits_t { class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); } } 複製代碼
rw
的屬性 ro
,而後咱們先嚐試讀取 baseMethodList
屬性,該屬性存儲的是編譯時肯定的類的全部的方法。baseMethodList
屬性是一個 List
類型的容器,咱們直接使用 get(index)
來獲取其 index
處的值,結果咱們所要尋找的 extH_method
和 extM_method
出現了,不過還沒結束,咱們還沒驗證類拓展中聲明的兩個屬性,讓咱們打印一下 ro
的 baseProperties
mName
,ext_name
和 ext_subject
都被找到了,那麼是否是就是說類拓展就是編譯時肯定的了呢?咱們還漏掉了這三個屬性的 getter
和 setter
了,讓咱們回過頭再去 baseMethodList
中查找一下getter
和 setter
方法也生成了,至此,咱們就徹底肯定了類拓展在編譯時就會被加載到類的 ro
中。這裏有個注意點,就是若是咱們沒有在類的頭文件或者源文件中引入單獨的類拓展頭文件,那麼這個單獨的類拓展的頭文件裏面的屬性和方法將不會被加載到類上面來。
研究對象 | 加載時機 | 操做對象 | 可否經過@property聲明屬性生成 getter 和 setter |
---|---|---|---|
分類(實現了load方法) | 運行時 | rw | 不能,須要藉助關聯對象來實現 |
分類(沒有實現load方法) | 編譯時 | ro | 不能,須要藉助關聯對象來實現 |
類拓展 | 編譯時 | ro | 能夠 |
上一節咱們探索了類拓展以及類拓展與分類的區別,咱們知道,類拓展中能夠聲明屬性,編譯器會幫助咱們生成屬性對應的 getter
和 setter
方法,可是分類經過 @property
的方式來聲明屬性卻不能生成 getter
和 setter
方法。而其實 iOS
中有一種方式能夠爲分爲增長具備 getter
和 setter
的屬性,那就是 - 關聯對象 Associated Objects
。
關聯對象的官方定義能夠在 蘋果官方文檔 上找到。
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.
關聯引用,是從
OS X 10.6
開始啓用的,模擬了將對象實例變量添加到已經存在的類中。經過使用關聯引用,你能夠在不修改類聲明的前提下爲對象添加內容。若是你無權訪問該類的源代碼,或者因爲二進制兼容性緣由而沒法更改該對象的佈局,則這可能頗有用。
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.
關聯引用機制基於
key
。對於任何對象,你均可以根據須要添加任意數量的關聯引用,每一個關聯都使用不一樣的key
。關聯引用還能夠確保關聯的對象至少在源對象的聲明週期內保持有效。
而關於關聯對象的最佳實踐能夠參考 NSHipster - Associated Objects 一文。
從蘋果官方文檔能夠看到,關聯引用其實不是隻能在分類中使用,只不過對於咱們平常開發來講,分類中使用關聯引用仍是更經常使用的場景。相信大多數開發者都知道怎麼使用關聯引用,的確,關聯引用使用起來很簡單,不外乎兩個方法:
// 設置關聯對象
objc_setAssociatedObject()
// 獲取關聯對象
objc_getAssociatedObject()
複製代碼
咱們若是要給一個分類中的屬性設置關聯對象,須要重寫屬性的 setter
方法,而後使用 objc_setAssociatedObject
:
- (void)setXXX:(關聯值數據類型)關聯值
objc_setAssociatedObject(self, 關聯的key, 關聯值, 關聯對象內存管理策略);
}
複製代碼
而後還須要重寫 getter
方法,而後使用 objc_getAssociatedObject
:
- (關聯值數據類型)關聯值{
return objc_getAssociatedObject(self, 關聯的key);
}
複製代碼
這其中的關聯對象內存管理策略以下表所示:
關聯策略 | 等同的 @property | 描述 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) 或 @property (unsafe_unretained) 指定一個關聯對象的弱引用。 | 指定一個關聯對象的弱引用。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | 指定一個關聯對象的強引用,不能被原子化使用。 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 指定一個關聯對象的copy引用,不能被原子化使用。 |
OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 指定一個關聯對象的強引用,能被原子化使用。 |
OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 指定一個關聯對象的copy引用,能被原子化使用。 |
關於關聯對象的底層原理,這裏有一篇燈塔
draveness
的博文 關聯對象 AssociatedObject 徹底解析 十分值得一讀。
固然,若是也能夠跟隨筆者一塊兒探索下關聯對象的底層原理。咱們不妨從最直觀的 objc_setAssociatedObject
方法開始切入:
// objc-runtime.mm
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
複製代碼
objc_setAssociatedObject
方法的實現又包裹了一層,其實現爲 _object_set_associative_reference
而 _object_set_associative_reference
方法的實現很是長,這裏就分段來進行探索吧。
// 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; 複製代碼
根據註釋咱們能夠知道,當傳入的
object
和key
同時爲nil
的時候,直接返回。這樣的處理是爲了不傳入空值時而致使崩潰。
// objc-references.mm 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)); // objc-runtime-new.h bool forbidsAssociatedObjects() { return (data()->flags & RW_FORBIDS_ASSOCIATED_OBJECTS); } 複製代碼
判斷要進行關聯的對象是否禁用掉了關聯引用,這裏是經過對象的
isa
的rw
的flags
屬性與上一個宏RW_FORBIDS_ASSOCIATED_OBJECTS
來判斷的。
// retain the new value (if any) outside the lock. ObjcAssociation old_association(0, nil); 複製代碼
初始化一個
ObjcAssociation
對象,用於持有原有的關聯對象
id new_value = value ? acquireValue(value, policy) : nil;
複製代碼
判斷傳入的關聯對象值是否存在,若是存在就調用
acquireValue
方法來獲取值,咱們能夠進入acquireValue
方法看一下:
static id acquireValue(id value, uintptr_t policy) { switch (policy & 0xFF) { case OBJC_ASSOCIATION_SETTER_RETAIN: return objc_retain(value); case OBJC_ASSOCIATION_SETTER_COPY: return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy); } return value; } 複製代碼
能夠看到
acquireValue
會根據關聯策略來進行retain
或copy
消息的發送
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
複製代碼
初始化一個
AssociationsManager
對象,而後獲取一個AssociationsHashMap
哈希表,而後經過DISGUISE
方法做爲去哈希表查找的key
。這裏的DISGUISE
其實進行了按位取反的操做。
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); } 複製代碼
若是傳入的關聯對象值存在,說明是進行賦值操做;若是傳入的關聯對象值不存在,說明是進行置空操做。這裏咱們先看一下賦值操做的流程:
if (new_value) { // break any existing association. AssociationsHashMap::iterator i = associations.find(disguised_object); if (i != associations.end()) { // secondary table exists ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find(key); if (j != refs->end()) { old_association = j->second; j->second = ObjcAssociation(policy, new_value); } else { (*refs)[key] = ObjcAssociation(policy, new_value); } } else { // create the new association (first time). ObjectAssociationMap *refs = new ObjectAssociationMap; associations[disguised_object] = refs; (*refs)[key] = ObjcAssociation(policy, new_value); object->setHasAssociatedObjects(); } } 複製代碼
1.經過上一步按位取反以後的結果,在
AssociationsHashMap
哈希表中查詢,這裏是經過迭代器的方式進行查詢,查詢的結果是ObjcAssociation
對象,這個結構也是一個哈希表,其內部存儲的是_object_set_associative_reference
方法傳入的key
爲鍵,ObjcAssociation
對象爲值的鍵值對 2.若是沒有查詢到,說明以前在當前類上沒有設置過關聯對象。則須要初始化一個ObjectAssociationMap
出來,而後經過setHasAssociatedObjects
設置當前對象的isa
的has_assoc
屬性爲true
3.若是查詢到了,說明以前在當前類上設置過關聯對象,接着須要看key
是否存在,若是key
存在,那麼就須要更新原有的關聯對象;若是key
不存在,則須要新增一個關聯對象
// setting the association to nil breaks the association. 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()) { old_association = j->second; refs->erase(j); } } 複製代碼
由於來到這裏的條件是
new_value
爲nil
,也就表明着要刪除關聯對象,內部的邏輯和上面的流程大同小異,不過最後多了一步在ObjectAssociationMap
擦除key
對應的節點
// release the old value (outside of the lock). if (old_association.hasValue()) ReleaseValue()(old_association); 複製代碼
最後會判斷
old_association
是否有值,若是有的話就釋放掉,固然前提是舊的關聯對象的策略是OBJC_ASSOCIATION_SETTER_RETAIN
struct ReleaseValue { 複製代碼
void operator() (ObjcAssociation &association) {
releaseValue(association.value(), association.policy());
}
複製代碼
}; static void releaseValue(id value, uintptr_t policy) { if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) { return objc_release(value); } }
複製代碼
objc_setAssociatedObject
方法分析完了,咱們接着看另一個重要的方法 objc_getAssociatedObject
:
id objc_getAssociatedObject(id object, const void *key) { return _object_get_associative_reference(object, (void *)key); } 複製代碼
能夠看到,跟
objc_setAssociatedObject
同樣,objc_getAssociatedObject
這裏又包裹了一層,其實現爲_object_get_associative_reference
,而這個方法相比於上一節的_object_set_associative_reference
要簡單一些,咱們就直接貼出完整的代碼
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.先初始化一個空的
value
,以及一個策略爲OBJC_ASSOCIATION_ASSIGN
的policy
2.初始化一個AssociationsManager
關聯對象管理類,接着拿到AssociationsHashMap
對象,這個對象在AssociationsManager
底層是靜態的 3.而後以DISGUISE(object)
按位取反以後的結果爲鍵去查詢AssociationsHashMap
4.若是在AssociationsHashMap
中扎到了,接着以key
爲鍵去ObjectAssociationMap
中查詢ObjcAssociation
若是在ObjectAssociationMap
中查詢到了ObjcAssociation
,則把值和策略賦值給方法入口聲明的兩個臨時變量,而後判斷獲取到的關聯對象的策略是否爲OBJC_ASSOCIATION_GETTER_RETAIN
,若是是的話,須要對關聯值進行retain
操做 5.最後判斷若是關聯值是否存在且策略爲OBJC_ASSOCIATION_GETTER_AUTORELEASE
,是的話就須要調用objc_autorelease
來釋放關聯值 6.最後返回關聯值
objc_removeAssociatedObjects
方法咱們平時可能用的很少,從字面含義來看,這個方法應該是用來刪除關聯對象。咱們來到它的定義處:
void objc_removeAssociatedObjects(id object) { if (object && object->hasAssociatedObjects()) { _object_remove_assocations(object); } } 複製代碼
這裏會判斷
object
存在且有關聯對象纔會進入真正的實現_object_remove_assocations
,該實現也不是很複雜,咱們仍是直接貼出代碼
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}
複製代碼
這裏會將對象包含的全部關聯對象加入到一個
vector
中,而後對全部的ObjcAssociation
對象調用ReleaseValue()
方法,釋放再也不被須要的值。
getter
和 setter
,並且分類不能聲明實例變量ObjcAssociation
對象的結構AssociationsManager
管理類存儲了一個靜態的哈希表 AssociationsHashMap
,這個哈希表存儲的是以對象指針爲鍵,以該對象全部的關聯對象爲值,而對象全部的關聯對象又是以 ObjectAssociationMap
來存儲的ObjectAssociationMap
存儲結構爲 key
爲鍵,ObjcAssociation
爲值isa
的 has_assoc