上一篇文章《iOS-底層原理09-msgSend消息轉發》中提到,如果慢速查找在父類的緩存中沒有找到,則傳入父類的class,進而重新進行父類的慢速查找流程...一層層遞歸循環,直到找到方法的Imp后返回。實質上并不是走的這個遞歸循環,因為在父類的緩存中走匯編代碼快速查找找不到之后,并不會進入__objc_msgSend_uncached
,就不會走入MethodTableLookup
從父類的緩存中查找的流程imp = cache_getImp(curClass, sel);
走到匯編,CacheLookup GETIMP, _cache_getImp,CacheLookup將參數GETIMP帶入到寄存器p0中,
當父類的緩存中一直沒有找到此方法時,則進入JumpMiss/CheckMiss $0,
從而判斷$0是否等于GETIMP,沒有找到就是GETIMP,進入cbz p9, LGetImpMiss,將空值0存入寄存器p0位置,并直接返回ret = nil。并沒有進行從匯編開始的慢速查找遞歸循環。
故以上根本沒有再次進入父類的方法慢速查找流程,遞歸循環,正確的流程圖如下
方法查找流程:先進入本類的快速查找流程->本類的多線程緩存中查找->本類的慢速查找(二分查找)->父類的緩存中查找->父類的慢速查找(二分查找),如果還沒找到呢?且不做任何處理。程序將會崩潰報錯。
通過代碼調試能夠看到進入了父類的實例方法慢速查找流程,新建LGMankind為LGPerson的父類,本類實例方法慢速查找流程LGPerson->LGMankind->NSObject->nil
類中沒有實現對象方法或類方法,調用的對象方法或類方法的時候會報錯,最常見的錯誤,最熟悉的陌生人
unrecognized selector sent to instance 0x10102c040
- 調用沒有實現的對象方法
-[LGPerson say666]: unrecognized selector sent to instance 0x10102c040
2020-11-03 20:25:47.362425+0800 KCObjc[2999:669876] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[LGPerson say666]: unrecognized selector sent to instance 0x10102c040'
- 調用沒有實現的類方法
沒有實現對象方法或類方法,則會返回forward_imp,forward_imp = (IMP)_objc_msgForward_impcache; _objc_msgForward_impcache是匯編實現,匯編到C++多一個下劃線_,C++到C少一個下劃線_。
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
沒有實現實例方法或類方法,系統提供一次挽救的機會實現:動態方法決議
1.實例方法的動態方法決議
根據isa的走位圖,實例方法慢速查找流程LGPerson->LGMankind->NSObject->nil
重寫類方法+ (BOOL)resolveInstanceMethod:(SEL)sel,給類添加一個sayMaster的方法,在編譯類加載的時候,+ (BOOL)resolveInstanceMethod:(SEL)sel就已經加載進內存了,將sayMaster的實現Imp寫進sel中。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 來了哦",NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *type = method_getTypeEncoding(sayMMethod);
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
若動態方法決議+ (BOOL)resolveInstanceMethod:(SEL)sel里面,沒有對sel方法say666進行重新指定imp,則此動態決議方法+ (BOOL)resolveInstanceMethod:(SEL)sel會走兩次,為什么會走兩次呢???
- 第一次動態方法決議后,方法返回,是什么時候進入第二次動態方法決議方法的呢???通過打印第二進入前的堆棧情況,獲取堆棧信息如下,得到第二次觸發是在CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:],后面探索實質為消息的慢速轉發流程。
- 第二次動態方法決議還沒找到imp,程序報錯_objc_msgForward_impcache
2.類方法的動態方法決議
根據isa的走位圖,查找類方法流程元類(metaClass)->根元類(rootMetaClass:NSObject)->根類(NSObject)->nil,根元類繼承于NSObject。
- 查找類方法,先傳入LGPerson的元類的地址0x0000000100002270,進行查找類方法sayNB,獲取類方法,類方法在元類中是以實例方法的形式存在的,類中存在兩個類方法+ (void)lgClassMethod和+ (BOOL)resolveInstanceMethod:(SEL)sel
- 1 通過lldb獲取LGPerson元類中的方法,相當于獲取類方法的個數兩個+ (void)lgClassMethod和+ (BOOL)resolveInstanceMethod:(SEL)sel
- 方法列表中不存在sayNB的類方法,此時繼續for循環在LGPerson元類的父類,也就是根元類NSObject中查找sayNB方法,curClass = curClass->superclass,curClass的地址為0x00000001003340f0
- 2 通過lldb查看根元類NSObject中的方法個數
- 3 根元類中沒有找到sayNB方法,繼續往上查找,在根元類的父類NSObject中繼續查找sayNB方法
- 4 根元類的父類中沒有找到sayNB方法,進入類方法的動態方法決議
- curClass = curClass->superclass,curClass的地址為0x0000000100334140,沒有找到進入動態方法決議return resolveMethod_locked(inst, sel, cls, behavior);此時cls傳入的是LGPerson的元類,會走入resolveClassMethod方法,能通過實現此方法,對方法進行重新添加,先看一看沒有實現此方法的情況下,流程接下來往哪里走?
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
沒有實現resolveClassMethod:類方法,此時lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)一層一層的往上找,LGPerson的元類0x0000000100002270中無法找到,繼續往上一層LGPerson的根元類(NSObject)0x00000001003340f0中繼續查找,LGPerson的根元類是NSObject的元類,存放了NSObject的類方法,正好resolveClassMethod:也是NSObject的類方法,所以下面的return語句永遠都不會走。
前面如果沒有實現,后面在NSObject的元類中也就是LGPerson的根元類中一定能找到系統NSObject的類方法resolveClassMethod:的實現,下面return永遠不會走。
if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
流程繼續往下bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
返回false,因為resolveClassMethod:方法并沒有在LGPerson中實現,sayNB方法更沒有添加。往下執行IMP imp = lookUpImpOrNil(inst, sel, cls);查找流程,仍然找不到sayNB方法。
- 進入
resolveInstanceMethod(inst, sel, cls);
,查看根元類cls->ISA()中是否有實例方法resolveInstanceMethod:存在,是有的,根元類是NSObject的元類,NSObject的類方法resolveInstanceMethod:在NSObjct的元類中以實例方法存在,能找到,不會走return。 但bool resolved = msg(cls, resolve_sel, sel);為false,因為LGPerson的元類中并不存在resolveInstanceMethod:和sayNB。
若實現了resolveInstanceMethod:方法,添加了sayNB方法還會報錯嗎???等到后面分析
- IMP imp = lookUpImpOrNil(inst, sel, cls);查找sayNB方法,找不到返回imp為nil,此時,動態方法決議結束,再次查找一遍sayNB方法,確認有沒有添加。
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
,未找到sayNB,返回(IMP) imp = 0x00000001002c262c (libobjc.A.dylib`_objc_msgForward_impcache),報錯unrecognized selector sent to instance 0x10102c040
第一次動態方法決議結束
- 后面的流程往哪里走呢???后面再探索,下面先看下常規操作,類方法sayNB找不到,防止報錯的動態方法決議的處理
查詢LGPerson的對象方法傳入的cls是類LGPerson
(lldb) p cls
(Class) $11 = LGPerson
(lldb) p/x cls
(Class) $12 = 0x00000001000032f0 LGPerson
查詢LGPerson的類方法傳入的cls是元類LGPerson
類方法的動態方法決議
1.重寫resolveClassMethod:方法
sayNB方法找不到,根據前文分析會進入resolveClassMethod(inst, sel, cls);
,此時的cls為LGPerson的元類LGPerson,進入resolveClassMethod(inst, sel, cls);
,查詢LGPerson有沒有實現resolveClassMethod:
方法,雖然目前LGPerson中沒有實現resolveClassMethod:類方法,但LGPerson元類的父類,也就是根元類NSObject中,存在resolveClassMethod:方法,為什么呢?
因為系統類NSObject的類方法resolveClassMethod:存在NSObject的元類中以實例方法形式存在,即根元類NSObject中。
- 1.如果啥也不做,必定會報錯,最熟悉的陌生人
- 2.動態方法決議重寫resolveClassMethod:方法,但不增加sayNB的Imp,動態方法決議中能迅速找到方法resolveClassMethod(inst, sel, cls);
沒有添加sayNB的imp,bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);仍然為false
添加sayNB的imp之后,看resolved的值bool resolved = msg(nonmeta, @selector(resolveClassMethod:),此時為true
此時能找到LGPerson的類方法sayNB:,為什么能找到呢???
先看編譯時的,sayNB方法添加過程
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"%@ 來了",NSStringFromSelector(sel));
if (sel == @selector(sayNB)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
const char *type = method_getTypeEncoding(sayMMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
往元類中添加方法class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
走入m = getMethodNoSuper_nolock(cls, name),發現和方法的慢速查找過程中是用一個方法,傳入的cls也都是元類,
BOOL
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return NO;
mutex_locker_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}
static IMP
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
IMP result = nil;
runtimeLock.assertLocked();
checkIsKnownClass(cls);
ASSERT(types);
ASSERT(cls->isRealized());
method_t *m;
if ((m = getMethodNoSuper_nolock(cls, name))) {//表示在LGPerson的元類中已經存在name
// already exists
if (!replace) {//不用傳入的imp進行替換
result = m->imp;
} else {//用傳入的imp進行替換
result = _method_setImplementation(cls, m, imp);
}
} else {
auto rwe = cls->data()->extAllocIfNeeded();
// fixme optimize
method_list_t *newlist;
newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
newlist->entsizeAndFlags =
(uint32_t)sizeof(method_t) | fixed_up_method_list;
newlist->count = 1;
newlist->first.name = name;
newlist->first.types = strdupIfMutable(types);
newlist->first.imp = imp;
prepareMethodLists(cls, &newlist, 1, NO, NO);
rwe->methods.attachLists(&newlist, 1);
flushCaches(cls);
result = nil;
}
return result;
}
傳入cls在LGPerson的元類中進行查找,若查找到,是否替換,替換用傳入的imp替換,
result = _method_setImplementation(cls, m, imp); 不替換返回原來的m->imp,
若沒找到則在LGPerson的元類cls的data()中進行增加內存空間,重新添加auto rwe = cls->data()->extAllocIfNeeded();rwe->methods.attachLists(&newlist, 1);實時更新類的信息flushCaches(cls);方便下次查找,所以能找到。不會再報unrecognized selector sent to instance 0x10102c040找不到方法的錯了。
以上是通過重寫resolveClassMethod:方法,添加Imp的方式對類方法進行動態方法決議,類方法還能通過重寫+ (BOOL)resolveInstanceMethod:(SEL)sel的方式進行動態方法決議嗎???
2.重寫+ (BOOL)resolveInstanceMethod:(SEL)sel
理所當然的會想到重寫+ (BOOL)resolveInstanceMethod:(SEL)sel方法,將上面添加元類方法的Imp寫入 (BOOL)resolveInstanceMethod:(SEL)sel中
發現添加的方法沒有生效,依然報錯,為什么會報錯呢?已經往元類中添加過方法了,為什么沒有找到呢?
2020-11-13 18:10:21.604474+0800 KCObjc[50067:1891886] +[LGPerson sayNB]: unrecognized selector sent to class 0x1000032d8
2020-11-13 18:10:21.607479+0800 KCObjc[50067:1891886] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[LGPerson sayNB]: unrecognized selector sent to class 0x1000032d8'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff374a8b57 __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00000001001318fa objc_exception_throw + 42
2 CoreFoundation 0x00007fff37527b37 __CFExceptionProem + 0
3 CoreFoundation 0x00007fff3740d3bb ___forwarding___ + 1427
4 CoreFoundation 0x00007fff3740cd98 _CF_forwarding_prep_0 + 120
5 KCObjc 0x0000000100001a53 main + 67
6 libdyld.dylib 0x00007fff71497cc9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
通過斷點調試,查看進入resolveInstanceMethod方法中的cls為LGPerson的元類,通過lldb查看LGPerson的元類中存在lgClassMethod和resolveInstanceMethod:兩個方法,并不存在sayNB方法
這句代碼為falsebool resolved = msg(cls, resolve_sel, sel);
為什么LGPerson的類方法resolveInstanceMethod:中添加的元類方法沒有生效呢?
因為LGPerson的元類是一個虛擬的類,在代碼中并不存在,所以在LGPerson的.m文件中增加resolveInstanceMethod:方法,程序并不會真正進入,就不會執行,所以sayNB方法就沒有添加進去,從而報錯,此時沒有生效的話,可以根據繼承鏈的關系LPerson元類->LGPerson根元類->NSObject->nil,NSObject不是一個虛擬的類,是一個實實在在的的類,可以新建類別NSObject+LG,將方法寫入到NSObject+LG中,增加resolveInstanceMethod:,斷點調試看是否能生效。
NSObject中本身存在resolveInstanceMethod:方法,重寫此方法,返回值保持一致。return NO。
說明NSObject+LG中的resolveInstanceMethod:方法生效,已經將sayNB的替代方法lgClassMethod添加到LGPerson的元類中,不會再報錯。
結論:
NSObject+LG是系統的分類,可能會多一些系統的方法會受影響
可以通過過濾特定命名規則的代碼找不到的情況,做上傳服務器操作或跳轉到首頁或特定的頁面,防止程序崩潰
AOP封裝成SDK,一般在這一層不作處理,而進行消息轉發
消息轉發
1.快速轉發流程
instrumentObjcMessageSends輔助分析
在消息慢速轉發流程中IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
,若找到了則進入log_and_fill_cache,滿足條件slowpath(objcMsgLogEnabled && implementer)進入logMessageSend,對方法進行打印記錄,詳細記錄到文件路徑下/tmp/msgSends
通過instrumentObjcMessageSends改變objcMsgLogEnabled的值為true,從而記錄調用流程,在外部使用內部的方法需要添加關鍵字extern。extern void instrumentObjcMessageSends(BOOL flag);
在LGPerson中添加一個沒有實現的對象方法sayHello,查看方法調用情況,在/tmp/msgSends路徑下會生成一個名為msgSends-41550的文件,查看文件中的內容即為方法的調用流程,發現調用了- LGPerson NSObject forwardingTargetForSelector:方法
此時可以在LGPerson中重寫forwardingTargetForSelector:方法,程序崩潰之前打印了sayHello方法說明可以在此方法內將有sayHello方法實現的類返回或在此方法內添加sayHello的Imp。
- 1.forwardingTargetForSelector:方法中將有sayHello方法實現的類返回
// 1: 快速轉發
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
//return [super forwardingTargetForSelector:aSelector];
return [LGStudent alloc];
}
- 2.forwardingTargetForSelector:方法中動態添加sayHello的Imp,結果程序崩潰,為什么呢???
2.慢速轉發流程
在msgSends-41550的文件中發現在forwardingTargetForSelector:方法之后,還調用了- LGPerson NSObject methodSignatureForSelector:
方法
查閱官方文檔- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector和- (void)forwardInvocation:(NSInvocation *)anInvocation要搭配使用,有兩種方式,一種是重寫方法,但是不做處理,另一種是進行事務的重新賦值
- 1.重寫- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector和- (void)forwardInvocation:(NSInvocation *)anInvocation方法,不做處理,僅僅防止奔潰
- 2.事務重新賦值
anInvocation.target = [LGStudent alloc];[anInvocation invoke];
對事物進行revoke。
大膽做個假設可以將事務進行本類方法指定嗎,是可以的
消息轉發流程圖如下
以上是以上帝視角探索消息轉發流程,有沒有更好的辦法呢?
反匯編探索消息轉發流程
程序崩潰后通過bt查看堆棧信息,在程序崩潰之前調用了CoreFoundation中的forwarding_prep_0和forwarding,尋找CoreFoundation的源碼,在官網源碼查找并下載CF-1151.16.tar,在源碼中查找forwarding_prep_0,發現無法找到。
- 讀取CorFoundation鏡像文件,路徑為/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation,找到CoreFoundation的可執行文件
- 通過工具Hopper.Demo.dmg查看編譯后的代碼,付費軟件使用試用版try the demo
- 全局搜索
__forwarding_prep_0___
,點擊跳轉到函數入口
- 進入
__forwarding__
流程圖如下,和前面探索的消息轉發流程不謀而合。