Runtime最全總結
本系列詳細講解Runtime知識點,由于運行時的內容較多,所以將內容拆分成以下幾個方面,可以自行選擇想要查看的部分
- OC運行時機制Runtime(一):從isa指針開始初步結識Runtime
- OC運行時機制Runtime(二):探索Runtime的消息轉發機制和分類Category
- OC運行時機制Runtime(三):關聯對象Associated Object和分類Category
- OC運行時機制Runtime(四):嘗試使用黑魔法 Method Swizzling
本文主要分析Runtime的消息機制,探究OC類和對象發送消息的原理。
理解 objc_msgSend 的作用
調用方法是面向對象語言常用的功能,用Objective-C的術語來說這叫“傳遞消息”,消息有“名稱(name)”和“選擇子(selector)”,而Runtime賦予了OC動態語言的特性,用一串偽代碼簡單介紹下靜態語言和動態語言調用方法的區別。
if(bool){ function1(); }
else{ function2(); }
//函數地址硬編碼在指令中,這是函數調用方式
void (*fnc)();
if(bool){ fnc = function1; }
else{ fnc = function2; }
fnc();
//需要的函數在運行時才能確定,動態綁定
并非編譯時就會確定要調用的方法,而是交由運行時決定,這是消息機制的特點,OC基本調用方法為
id returnValue = [someObject messageName:parameter];
在這里someObject
叫做接受者(receiver)
,messageName
叫做選擇子(selector)
,選擇子和參數一同組成一條消息,編譯器將其轉為C語言函數調用,函數原型為void objc_msgSend(id self,SEL cmd, ...)
,這里函數的參數個數可變,那么上面的例子會變成以下這段代碼
id returnValue = objc_msgSend(someObject, @selector(messageName:),parameter);
下面還以上一篇的Father和Son為例子,講解一下消息傳遞的函數方法,在main.m中輸入以下代碼。
Son * son = [[Son alloc] init];
接下來用clang將其轉為.cpp文件clang -rewrite-objc main.m
,代碼如下:
Son * son = ((Son *(*)(id, SEL))(void *)objc_msgSend)((id)((Son *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Son"), sel_registerName("alloc")), sel_registerName("init"));
這里將類型等簡化為以下代碼
Son * son = (objc_msgSend)((objc_msgSend)(objc_getClass("Son"), sel_registerName("alloc")), sel_registerName("init"));
解釋下這段代碼,首先使用objc_getClass獲取當前類對象,然后通過objc_msgSend,向這個類對象發送消息,消息名為alloc,完成實例對象分配內存,再次調用objc_msgSend向這個實例對象發送消息,消息名為init,表示實例對象的初始化。
根據上一篇文章 OC運行時機制Runtime(一):從isa指針開始初步結識Runtime 我們了解了OC方法的繼承體系
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
每個方法從所屬類的“方法列表”中查找,找不到則沿繼承體系向上查找,如果還是找不到,執行“完整的消息轉發機制”
,這里有個問題,如果每次都執行同一個方法都要查找一次,那么這個耗時還是比較長的,所以每次找到方法后會將方法存入“快速映射表”
中以便下次快速查找。“快速映射表”
結構體代碼如下
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
那么這個cache
到底有什么作用,OC許多類方法多到幾十種,但是只有10%-20%的方法會經常調用,可能產生的調用次數超過80%,所以緩存的作用就體現出來了,首次調用方法從緩存中查找如果找不到再從方法列表中查找,極大的提升了查詢效率。
這里已經看到了在Runtime的作用下,objc_msgSend
函數完成了面向對象消息傳遞向面向過程函數調用的轉化,這也是消息機制的基礎,那么就存在一個問題了,如果一條消息發送失敗,runtime會如何處理呢?
消息轉發機制:unrecognized selector出現后的三次機會
首先看一個最典型的案例
id string = @"hello";
[string addObject:@"world"];
這里用id類型聲明了一個變量string,這時string默認為NSString類型,那我們調用NSMutableArray的方法,就會報錯,[__NSCFConstantString addObject:]: unrecognized selector sent to instance 0x100001040
,我們知道oc方法繼承體系是通過isa指針向上查找,直到NSObject依然找不到方法,就會調用doesNotRecognizeSelector:
方法,報unrecognized selector
的錯誤,當然這不是結束,oc在每個消息發送失敗后會給出我們三次機會來解決這條消息,這就是消息轉發機制
。
消息轉發分為兩大階段,第一階段看接受者是否能動態添加方法,這叫做動態方法解析
,如果運行期第一階段已經被系統執行完畢,那么執行第二階段分為兩小步,先看其他有沒有對象能處理這條消息,如果有備援的接收者
,將消息轉發給這個對象,如果沒有,則會啟動完整的消息轉發機制
,將所有信息封裝到 NSInvocation
對象中,給最后一次解決這個問題的機會。
下面舉一個例子,首先如果一個屬性執行了@dynamic property;
,那么如果外界調用這個屬性的setter或getter都會crash
這里用動態方法解析的方式解決一下這個問題
+ (BOOL)resolveInstanceMethod:(SEL)selector; //當遇到無法解讀的實例方法時調用這個方法
+ (BOOL)resolveClassMethod:(SEL)selector; //當遇到無法解讀的類方法時調用這個方法
Son * son = [Son new];
son.name = @"Tom";
NSLog(@"%@", son.name);
void dynamicNameSetter (id self,SEL _cmd, id value) {
Son *son = (Son *)self;
son.resolveName = (NSString *)value;
}
id dynamicNameGetter (id self,SEL _cmd) {
Son *son = (Son *)self;
return son.resolveName;
}
@implementation Son
@dynamic name;
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(setName:)) {
class_addMethod(self, sel, (IMP)dynamicNameSetter, "v@:@");
return true;
}else if(sel == @selector(name)) {
class_addMethod(self, sel, (IMP)dynamicNameGetter, "@@:");
return true;
}
return [super resolveClassMethod:sel];
};
這里看到,當我們調用son的setter方法,因為實現了@dynamic;
關鍵字,所以首先消息發送失敗,走到消息轉發的第一步resolveInstanceMethod
,通過添加方法的方式,將值綁定到resolveName上,實現setter和getter方法。
那么如果上一步沒有操作,會走到第二步
- (id)forwardingTargetForSelector:(SEL)selector;
//如果找到接收者,返回那個對象,否則返回nil,這一步轉發的消息不能進行操作,可以通過完整的消息轉發機制操作。
@implementation Son
- (id)forwardingTargetForSelector:(SEL)selector {
return [[Daughter alloc] init];
}
@end
@implementation Daughter
- (void)substitute {
NSLog(@"I can realize");
}
@end
這里外部調用
Son * son = [Son new];
[son performSelector:@selector(substitute)];
雖然son找不到substitute方法,但是它的備援接收者daughter可以,所以不會報錯,并有成功打印。
如果以上操作都沒有做,會執行最后一步完整的消息轉發機制
。
- (void)forwardInvocation:(NSInvocation*)invocation;
//封裝了NSInvocation對象,將target,selector,和parameters全部封裝其中
總結
ObjC是一門動態語言,以消息機制
方式代替函數調用方式,運行時
才會決定消息的發送成功與失敗,即使發送失敗也有一套消息轉發機制來避免crash,總共分為動態方法解析
,備援接收者
,完整的消息轉發
三步,如果全部沒有操作才會拋出異常。
后續
到這里分析了基礎的兩個部分,分別是運行時結構和消息機制,感興趣的朋友們可以移步下一篇文章 OC運行時機制Runtime(三):關聯對象Associated Object和分類Category,如果覺得本文對您有些作用,請在下方點個贊再走哈~