Runtime簡介
==== Runtime的特性主要是消息(方法)傳遞機制,當向?qū)ο蟀l(fā)送一條消息時,如果能在對象的類或所繼承類中的方法列表中找到對應的SEL,即進行方法調(diào)用,否則進行消息轉(zhuǎn)發(fā)。底層主要用C和匯編代碼編寫,使得動態(tài)系統(tǒng)變得高效。
消息傳遞
關鍵函數(shù):
id objc_msgSend(id self, SEL op, ...);
self : 指向接收消息的類的實例的指針地址,可以理解成調(diào)用方法的對象
op : 處理消息的方法選擇器
(SEL:類成員(方法)的指針,不同于C中的函數(shù)指針直接保存了方法的地址,
在這只是 方法的編號)
首先介紹下消息傳遞時一個非常重要的家伙,指向 類的數(shù)據(jù)結(jié)構(gòu)體 的指針 isa :
它是一個class類型的指針,實例對象的isa指向它的類對象,類對象的isa指向元類對象(meta class),元類對象isa指向根元類(root class),根元類isa指向自身。

/* isa結(jié)構(gòu)體:這其中包含了指向其父類的isa指針
及Dispatch table(key為SEL value為IMP)
其中結(jié)構(gòu)體 objc_method_list 中包含結(jié)構(gòu)體成員 objc_method,
objc_method中 包含成員 SEL和IMP(可在runtime.h中查找)*/
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;
例如對象調(diào)用方法 [obj callAndy],編譯器轉(zhuǎn)成消息發(fā)送時是這樣 objc_msgSend(obj, callAndy),Runtime時執(zhí)行的流程是:
1 .通過obj的isa指針找到它的數(shù)據(jù)結(jié)構(gòu)體 ;
2 .根據(jù)方法名callAndy生成的SEL(方法編號)查詢緩存 objc_cache,若命中則直接調(diào)用,否則繼續(xù)下一步
3 .在 class 的 Dispatch table(調(diào)度表)中查找對應的SEL ; 若class 中沒找到,繼續(xù)往它的 superclass 中找 ;
一旦找到就去執(zhí)行它的IMP(函數(shù)指針,方法實現(xiàn)的地址) 。
否則就進行消息轉(zhuǎn)發(fā)。
注:objc_cache:查找結(jié)束時為了高效處理,objc_cache會把已被調(diào)用的函數(shù)緩存下來,提高函數(shù)查詢的效率。 --找到callAndy 之后,把callAndy 的SEL 作為key ,IMP作為value 給存起來。當再次收到callAndy 消息的時候,可以直接在objc_cache 里找到,避免再次遍歷。
如果你剛才去找Andy,問了他dad,問了他dad的dad,又問了他dad的dad的dad...,還是找不到人,這個時候他可能鬼混去了,但是鬼混歸鬼混,他還不叫你,哼,也別顧及兄弟情義了,去報警吧,報人口失蹤,轉(zhuǎn)交給警察叔叔處理...
哈哈,扯淡歸扯淡,咱言歸正傳
消息轉(zhuǎn)發(fā)
獻上流程圖

大家看它們很不害臊的給自己標紅了,就知道其中有四個關鍵方法了:
1. + (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel
實例方法和類方法分別調(diào)用這兩個,返回NO才繼續(xù)執(zhí)行轉(zhuǎn)發(fā)流程 執(zhí)行方法2
在此方法中若動態(tài)添加方法給接收者,則返回YES,并且執(zhí)行所添加的方法
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically))
{
/* v@: v表示返回值為void類型
固定的兩個參數(shù): @表示第一個參數(shù)類型是id也就是當前對象self
:表示第二個參數(shù)類型是SEL即@selector(...)
*/
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSel];
}
2. - (id)forwardingTargetForSelector:(SEL)aSelector
//在此可以返回一個已知對象,此對象實現(xiàn)了未知消息,使得消息可以轉(zhuǎn)發(fā)給這個對象
若在此返回轉(zhuǎn)發(fā)目標,則執(zhí)行轉(zhuǎn)發(fā)目標的方法,結(jié)束轉(zhuǎn)發(fā)流程;
若返回nil ,執(zhí)行方法3
3. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
返回nil則會被標記為消息無法處理 系統(tǒng)則會crash :unrecognized selector...
返回方法簽名則執(zhí)行方法4 ;
4. - (void)forwardInvocation:(NSInvocation *)anInvocation
為了響應對象本身不認識的方法,必須覆蓋 methodSignatureForSelector:
以及 forwardInvocation 方法
(詳細內(nèi)容可參閱開發(fā)者文檔 Xcode -> Window -> Developer Documentation)
創(chuàng)建一個NSNull分類,避免當對NSNull對象發(fā)送一個ClassArray所包含類中實例 存在的消息時,應用直接Crash,而是打印錯誤調(diào)用的堆棧信息,在保證應用繼續(xù)運行的情況下幫助開發(fā)者更好的定位代碼的問題。當然這只是做編碼時千慮一失最后的一塊盾牌,更重要的還是要讓自己做好千慮這件事。(此處要注意nil和null的區(qū)別,nil是不占內(nèi)存的,向它發(fā)送消息不會crash,而null占有內(nèi)存是會進行消息傳遞的,所以會crash,這個值一般由服務器下發(fā)的情況較多)
(分類創(chuàng)建:新建Objective-C File,F(xiàn)ile Type:Category,Class:NSNull)
#define ClassArray @[[NSMutableArray class],[NSMutableDictionary class],[NSMutableString class],[NSNumber class],[NSDate class],[NSData class]]
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature *signature;
if (!signature)
{
for (Class someClass in ClassArray)
{
@try {
//若某個類(它的實例)能夠響應selector 設置相應的方法簽名
if ([someClass instancesRespondToSelector:selector])
{
signature = [someClass instanceMethodSignatureForSelector:selector];
break;
}
} @catch (NSException *exception) {}
}
}
if (signature){
return signature;
}
return [[self superclass] methodSignatureForSelector:selector];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
invocation.target = nil;
[invocation invoke];
//獲取方法調(diào)用堆棧信息 彈窗提示
NSArray *stack = [NSThread callStackSymbols];
NSString *infoStr = [NSString stringWithFormat:@"%@ .. %@ -- stackInfo: %@",NSStringFromClass([invocation.target class]),NSStringFromSelector(invocation.selector),stack];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"請聯(lián)系開發(fā)人員反映問題!" message:infoStr preferredStyle:UIAlertControllerStyleAlert];
UIViewController *vCtrl = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
UIAlertAction *action = [UIAlertAction actionWithTitle:@"復制錯誤信息" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = infoStr;
}];
[alert addAction:action];
if (vCtrl){
[vCtrl presentViewController:alert animated:YES completion:nil];
}
}
學習多篇博客以及實踐之后作此記錄,不準確的地方希望能得到各位Coder指教,感謝。