一、Runtime基石:Objective-C對象模型
1、對象
- 每一個對象都是類的執行個體, 類中儲存對象的方法清單;當一個對象方法被調用時,類會首先查找它本身是否有該方法的實作,如果沒有,則會向它的父類查找該方法,直到NSObject(根類);
- 類是元類 (metaclass) 的執行個體;元類儲存類方法清單;當一個類方法被調用時,元類會首先查找它本身是否有該類方法的實作,如果沒有,則會向它的父類查找該方法,直到NSObject(根類);
2、isa指針
- 對象的isa指針指向所屬的類,類的isa指針指向所屬的元類;所有的元類的 isa指針都會指向一個根元類 (root metaclass)。根元類的 isa指針指向自己,行成了一個閉環。
- 在64 位 CPU 下,isa 的内部結構有變化。具體檢視用 isa 承載對象的類資訊
- 對象、isa指針、類、元類、根元類的關系如下圖:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZlBna9QXbm9Fe39DMvwVUk5kWIdGdKNEdONWaB9mT4ADZ2JUT440ak10TKFnbwYDVGdWQjVzRkhzN39UN5ondwF2aERmdilGRIhUYLVVYplnYkpleil2SVRXYpVWULp2YpRVbyoUYphmUkZ2LcdGcq9lepJWbt9CXuNmLvd2bsFnL6lmYt12Lc9CX6MHc0RHaiojIsJye.jpg)
3、對象布局
- 執行個體變量(包括父類)都儲存在對象本身的存儲空間内;執行個體方法儲存在類中,類方法儲存在元類中;父類的執行個體方法儲存在各級 super class 中,父類的類方法儲存在各級 super meta class;
//對象組成 --start-- isa pointer rootClass's vars penultimate superClass's vars ... superClass's vars Class's vars //對象組成 --end-- typedef struct objc_class *Class; //類的結構 struct objc_class{ struct objc_class* isa; //指向元類 struct objc_class* super_class; //指向父類 const char* name; long version; long info; long instance_size; struct objc_ivar_list* ivars; //執行個體變量清單 struct objc_method_list** methodLists; //方法清單 struct objc_cache* cache; struct objc_protocol_list* protocols; //協定清單 }; //執行個體變量的結構 struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; char *ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }
說明1:對象中儲存指向類的isa指針 以及 各級的 執行個體變量(ivar),這個記憶體結構在編譯時就确定下來了,不能在編譯時給對象增加執行個體變量。
說明2:類的記憶體布局有isa指針、super_class指針、執行個體變量清單、方法清單和協定清單,其中執行個體變量(var)包含了變量的名稱、類型、偏移等。
二、Runtime核心:消息發送和轉發
Runtime賦予了OC了諸多動态特性,使其可以在運作時可以做一些事情;主要表現為:動态類型(在運作時才檢查對象類型)和 動态綁定(接到消息後,由運作環境決定執行哪部分代碼)
1、消息發送(Message)
- Objective-C 中的方法調用,實質上是在底層用objc_msgSend()實作消息發送,其核心在于:根據SEL(選擇器)開始找到IMP;其中SEL是執行個體方法的指針,可以看做方法名字元串;IMP是函數指針,指向方法實作的位址。
//調用方法 [obj doSomething]; //在編譯時候轉換 objc_msgSend(obj,@selector(doSomething))
- objc_msgSend的定義如下:
// self是接收者,接收該消息的類的執行個體 // _cmd是選擇器,要處理的消息的selector // ... 是需傳入的參數,參數個數不定 objc_msgSend(id self, SEL _cmd, ...)
- objc_msgSend的發送流程:先在Class中的緩存查找imp(沒緩存則初始化緩存),如果沒找到,則向父類的Class查找。如果一直查找到根類仍舊沒有實作,就走消息轉發(_objc_msgForward)了。
- 給nil發送消息不會有什麼作用,但是傳回值有些差別,具體如下:
a) 如果方法傳回值是 對象,傳回nil b) 如果方法傳回值是 指針類型,其指針大小為小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量 c) 如果方法傳回值是 結構體,發送給 nil 的消息将傳回0。結構體中各個字段的值将都是0。 d) 如果方法傳回值不是 上述提到的幾種情況,那麼發送給 nil 的消息的傳回值将是未定義的。
2-1、消息轉發(Message Forwarding)
消息轉發解決的是:查找IMP(方法實作)失敗後的處理;經曆動态方法解析、 備用接收者和 完整的消息轉發三個過程,其流程如下圖:
- 動态方法解析:接收到未知消息時,Runtime向目前類發送+resolveInstanceMethod:或+resolveClassMethod:消息,在這裡可以添加缺失的方法,傳回YES,重新發送消息,否則繼續下一步;
- 備用接收者:動态方法解析中沒能處理,Runtime會向forwardingTargetForSelector:發消息,如果該方法傳回了一個非nil或非self對象,恰好該對象實作了這個方法,那麼該對象就成了消息的接收者,消息就被分發到該對象。
- 完整消息轉發:前兩個都沒能處理好,Runtime發送methodSignatureForSelector:消息,擷取selector對應方法的簽名;如果有方法簽名傳回,則根據方法簽名建立描述消息的NSInvocation,向目前對象發送forwardInvocation:消息;如果沒有方法簽名傳回,傳回nil,向目前對象發送doesNotRecognizeSelector:消息,應用Crash退出。
2-2、避免消息轉發的辦法
在消息轉發三個過程中,未知消息的處理過程越往後,代價越大;一般我們可以這麼做 盡可能避免消息轉發,可以這麼做:
- 調用delegate 方法前檢查方法是否實作(respondsToSelector:), 隻有實作了(respondsToSelector:傳回YES) ,才去真正調用delegate 方法。
if([self.delegate respondsToSelector: @selector(sayHello)]) { [self.delegate sayHello]; }
- 直接調用方法,少用performSelector:;因為在直接調用方法時,編譯自動校驗,如果方法不存在,編譯器會直接報錯;而使用performSelector:的話一定是在運作時候才能發現,如果此方法不存在就會崩潰。
//直接使用方法調用,少使用performSelector [dog sayHello]; // [dog performSelector:@selector(sayHello) withObject:nil];
- 使用performSelector:,最好先判斷方法是否實作(respondsToSelector:),隻有實作了(respondsToSelector:傳回YES) ,才去調用performSelector:方法。
//respondsToSelector:和performSelector:組合使用 if ([dog respondsToSelector:@selector(sayHello)]) { [dog performSelector:@selector(sayHello)]; }
- 強制類型轉換,先判斷對象是否屬于強制轉換後的類
if([data isKindOfClass:[NSDictionary class]]){ // }
三、Runtime特性和應用
1、分類(Category)
- 原理:對象的方法定義都儲存在類的可變區域中,修改methodLists指針指向的指針的值,就可以實作動态地為某一個類增加成員方法。(但是對象布局在編譯時候就固定了,結構體的大小并不能動态變化,在運作時不能增加執行個體變量)。
- 通過關聯objc_setAssociatedObject 和 objc_getAssociatedObject方法可以變相地給對象增加執行個體變量,并不會真正改變了對象的記憶體結構。
- 通過Category新增的方法,會插入到方法清單的前部;如果有和原來方法重名,在運作時,順序查找時,一旦找到對應名字的方法,就不再查找,導緻原來方法得不到機會,這是Category新增的方法和原方法重名,原有方法失效的原因。
- 作用:給現有的類添加方法;将一個類的實作拆分成多個獨立的源檔案;聲明私有的方法。
2、關聯對象(Associated Objects)
- 原理:Category不能給一個已有類添加執行個體變量,但是可以通過關聯對象添加屬性;但是關聯對象不會改變對象的記憶體布局,新增的屬性是添加到和對象位址關聯的哈希表中;
- Associated Objects 相關的三個方法
objc_setAssociatedObject //添加關聯對象 objc_getAssociatedObject //擷取關聯對象 objc_removeAssociatedObjects // 删除所有關聯對象
- 作用:為現有的類添加私有變量以幫助實作細節;為現有的類添加公有屬性;為 KVO 建立一個關聯的觀察者
3、方法混寫(Method Swizzling)
- 原理:在運作時交換方法實作(IMP)
- 作用:可以利用它hook原有的方法,插入自己的業務需求,
4、鍵值觀察(KVO)
觀察者模式在Objective-C的應用之一,借助Runtime特性,實作自動鍵值觀察;使用了isa swizzling機制。具體描述如下:
- 當某個類的對象第一次被觀察時,系統就會在運作期動态地建立該類的一個子類,在這個子類中重寫基類中被觀察屬性的 setter 方法,實作真正的通知機制;
- 派生類還重寫了 class 方法以“欺騙”外部調用者,系統将對象的 isa 指針指向這個新誕生的子類,實質上這個對象就成為該派生類的對象了,因而在該對象上對 setter 的調用就會調用重寫的 setter,進而激活鍵值通知機制。
- 此外,派生類還重寫了 dealloc 方法來釋放資源。
說明:KVC(鍵值編碼)是不通過存取方法,而通過屬性名稱字元串間接通路屬性的機制,沒有用到isa swizzling機制。
5、NSProxy
- OC是單繼承的,但是可以利用NSProxy實作一下“僞多繼承”,具體參考NSProxy——少見卻神奇的類
- 項目中,主要是利用NSProxy做消息轉發的代理類,如弱引用代理類,可以打破循環引用。
說明: NSProxy非常适合做消息轉發的代理類,能自動轉發中定義的接口和NSObject的Category中定義的方法,如果使用NSObject來做,不能自動轉發NSObject的Category中定義、respondsToSelector:、isKindOfClass:這兩個方法。@interface FLWeakProxy : NSProxy + (instancetype)weakProxyForObject:(id)targetObject; @end @interface FLWeakProxy () @property (nonatomic, weak) id target; @end @implementation FLWeakProxy #pragma mark Life Cycle //類沒有定義預設的init方法. + (instancetype)weakProxyForObject:(id)targetObject{ FLWeakProxy *weakProxy = [FLWeakProxy alloc]; weakProxy.target = targetObject; return weakProxy; } #pragma mark Forwarding Messages - (id)forwardingTargetForSelector:(SEL)selector{ // Keep it lightweight: access the ivar directly return _target; } - (void)forwardInvocation:(NSInvocation *)invocation{ void *nullPointer = NULL; [invocation setReturnValue:&nullPointer]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector{ return [NSObject instanceMethodSignatureForSelector:@selector(init)]; } @end