前言
OC調(diào)用方法,底層是調(diào)用 objc_msgSend 發(fā)送消息。在發(fā)送消息時(shí)會經(jīng)過一系列的快速 查找、慢速查找,如果查找到對應(yīng)的 IMP,直接返回;如果沒有找到,就會進(jìn)入到方法的動態(tài)方法決議和消息轉(zhuǎn)發(fā)流程。
這篇文章就是深入探索
動態(tài)方法決議
和消息轉(zhuǎn)發(fā)
準(zhǔn)備工作
- 復(fù)習(xí):iOS 底層探索:objc_msgSend 慢速查找
一 、 動態(tài)方法決議
接著上一篇,在慢速查找流程未找到方法實(shí)現(xiàn)時(shí),首先會嘗試一次動態(tài)方法決議:
if (slowpath(behavior & LOOKUP_RESOLVER)) {
//動態(tài)方法決議的控制條件
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
進(jìn)入動態(tài)方法決議階段,源碼如下
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
//判斷是否是元類
if (! cls->isMetaClass()) {
resolveInstanceMethod(inst, sel, cls);//類執(zhí)行解析方式
}
else {
resolveClassMethod(inst, sel, cls); //元類執(zhí)行解析方式
// -- 如果resolveClassMethod找到了,就不會走這里了
//-- 如果沒找到,這個(gè)if必然會走,之前調(diào)用lookUpImpOrForward,已經(jīng)給該sel的方法緩存了imp = forward_imp
// -- 必然會走到done_nolock,返回一個(gè)nil
// -- 類方法在元類中也是以實(shí)例方法的形式存在,所以還需再走一遍實(shí)例方法的動態(tài)方法決議流程
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);//類執(zhí)行解析方式
}
}
//重新進(jìn)入 lookUpImpOrForward 根據(jù) behavior | LOOKUP_CACHE進(jìn)行判斷執(zhí)行方式
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
在resolveInstanceMethod方法中對實(shí)例方法動態(tài)解析,源碼如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 1\. 判斷系統(tǒng)是否實(shí)現(xiàn)SEL_resolveInstanceMethod方法
// 即+(BOOL)resolveInstanceMethod:(SEL)sel,
// 繼承自NSObject的類,默認(rèn)實(shí)現(xiàn),返回NO
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA()))
{
// Resolver not implemented.
// 不是NSObject的子類,也未實(shí)現(xiàn)+(BOOL)resolveInstanceMethod:(SEL)sel,
// 直接返回,沒有動態(tài)解析的必要
return;
}
// 2\. 系統(tǒng)給你一次機(jī)會 - 你要不要針對 sel 來操作一下下
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// 3\.-- 再去查一次imp,如果在cls的繼承鏈上自定義實(shí)現(xiàn)了`resolveInstanceMethod`方法并在里面添加了imp,就可以找到imp
IMP imp = lookUpImpOrNil(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
在resolveClassMethod方法中對實(shí)例方法動態(tài)解析,源碼如下:
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//: -- 容錯(cuò)處理,判斷該元類的繼承鏈中是否有resolveClassMethod方法
//: -- 如果自定義沒實(shí)現(xiàn),則會找到NSObject
if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
//: -- 得到元類/類的對應(yīng)的類
//: -- 因?yàn)槲覀冎荒茉陬惱飳?shí)現(xiàn)resolveClassMethod方法,無法去元類實(shí)現(xiàn),所以這里把消息接受者設(shè)置為當(dāng)前類
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
//: -- 發(fā)送消息,調(diào)用nonmeta中的`resolveClassMethod `方法
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
//: -- 再去查詢一次imp,如果上面調(diào)用用nonmeta中的`resolveClassMethod `方法里面給元類添加了imp,就會直接找到
IMP imp = lookUpImpOrNil(inst, sel, cls);
//: --異常情況,打印錯(cuò)誤信息
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
在經(jīng)過
resolveMethod_locked
方法后,進(jìn)行resolveInstanceMethod
,重新進(jìn)行一遍lookUpImpOrForward
;即: 如果動態(tài)方法決議中,將其實(shí)現(xiàn)指向了其他方法,則繼續(xù)查找指定的imp,即繼續(xù)慢速查找lookUpImpOrForward流程-
使用動態(tài)方法決議流程:resolveMethod_locked的流程圖
使用動態(tài)方法決議,代碼舉例:
//對象使用
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 來了",NSStringFromSelector(sel));
/**
添加方法
@param self 調(diào)用該方法的對象
@param sel 方法的名
@param IMP 新添加的方法,是c語言實(shí)現(xiàn)的
@param type :方法簽名, 新添加的方法的類型,包含函數(shù)的返回值以及參數(shù)內(nèi)容類型,eg:void xxx(NSString *name, int size),類型為:v@i
*/
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)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];
}
就算沒有實(shí)現(xiàn)方法say666 ,但是class_addMethod,依然不會崩潰。
優(yōu)化:根據(jù)resolveMethod_locked的流程圖,如果是元類調(diào)用,最后還是會走實(shí)例方法,根據(jù)isa走位圖,可知元類繼承鏈,最終繼承NSObject,所以我們可以給NSObject 寫個(gè)分類,直接在分類中綜合兩個(gè)方法如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *type = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, imp, type);
}else if (sel == @selector(sayNB)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
const char *type = method_getTypeEncoding(lgClassMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return NO;
}
風(fēng)險(xiǎn):使用這方法的風(fēng)險(xiǎn)可能造成其他SDK 或者系統(tǒng)方法也使用了類似的方法,造成沖突。所以我們使用的類名LGPerson一定要特殊唯一。
注 :
使用這種辦法的前提是:相關(guān)方法代碼已經(jīng)實(shí)現(xiàn),只是在運(yùn)行時(shí)將改方法動態(tài)添加到目標(biāo)類中。。
二、 消息轉(zhuǎn)發(fā)機(jī)制
在方法查找過程中,經(jīng)過緩存查找,方法列表查找和動態(tài)方法解析,如果經(jīng)歷慢速查找都沒有查找到IMP,也沒有進(jìn)行方法動態(tài)解析,那么我們還有辦法進(jìn)行方法實(shí)現(xiàn): 消息轉(zhuǎn)發(fā)機(jī)制
。但是我們只知道有消息轉(zhuǎn)發(fā)這個(gè)機(jī)制,但是我們始終找不到 消息轉(zhuǎn)發(fā)的實(shí)現(xiàn)方法和實(shí)現(xiàn)。這個(gè)時(shí)候:
開啟凡人的視角:
- lldb方式斷點(diǎn)查看: 通過進(jìn)入 [person sayHello]之后的斷點(diǎn)查看在 class 為 LGPerson的時(shí)候 Sel ,可以發(fā)現(xiàn)
forwardingTargetForSelector
和methodSignatureForSelector
。如圖:
image.png
開啟上帝視角:
方式1:通過
instrumentObjcMessageSends
方式打印發(fā)送消息的日志方式2:通過
hopper/IDA反編譯
方式1 :instrumentObjcMessageSends
通過lookUpImpOrForward
--> log_and_fill_cache
--> logMessageSend
,在logMessageSend
源碼下方找到instrumentObjcMessageSends
的源碼實(shí)現(xiàn)如下:
/***********************************************************************
* instrumentObjcMessageSends
**********************************************************************/
// Define this everywhere even if it isn't used to simplify fork() safety code.
spinlock_t objcMsgLogLock;
#if !SUPPORT_MESSAGE_LOGGING
///開啟instrumentObjcMessageSends
void instrumentObjcMessageSends(BOOL flag)
{
}
#else
///將方法執(zhí)行記錄通過打印出來
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
.........
}
///結(jié)束instrumentObjcMessageSends
void instrumentObjcMessageSends(BOOL flag)
{
.........
// SUPPORT_MESSAGE_LOGGING
#endif
所以,在main
中調(diào)用instrumentObjcMessageSends
打印方法調(diào)用的日志信息猶如上面的格式:
- 1、在main中通過extern 聲明instrumentObjcMessageSends方法
- 2、打開 objcMsgLogEnabled 開關(guān),即調(diào)用instrumentObjcMessageSends方法時(shí),傳入YES
不帶源碼的工程中這樣實(shí)現(xiàn):
///extern 修飾 我們可以拿來調(diào)用 蘋果提供了打印方法流程的方法
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
//開啟
instrumentObjcMessageSends(YES);
//去logMessageSend找打印信息
[person sayHello];
//關(guān)閉
instrumentObjcMessageSends(NO);
}
return 0;
}
我們在objc4源碼工程再去找logMessageSend方法:
/***********************************************************************
* logMessageSend
**********************************************************************/
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
........
return false;
}
運(yùn)行不帶源碼的工程,并前往/tmp/msgSends 目錄,發(fā)現(xiàn)有msgSends開頭的日志文件;
快捷鍵:command + shift + g ---> 輸入 /tmp/msgSends -->回車
打開msgSends.文件:
可以看出 ,崩潰前執(zhí)行了
兩次動態(tài)方法決議:resolveInstanceMethod方法
兩次消息快速轉(zhuǎn)發(fā):forwardingTargetForSelector方法
兩次消息慢速轉(zhuǎn)發(fā):methodSignatureForSelector + resolveInstanceMethod
方式2:通過hopper/IDA反編譯
找到forwardingTargetForSelector
等轉(zhuǎn)發(fā)方法的實(shí)現(xiàn)
Hopper和IDA是一個(gè)可以幫助我們靜態(tài)分析可視性文件的工具,可以將可執(zhí)行文件反匯編成偽代碼、控制流程圖等,下面以mac端Hopper為例(Hopper :要收費(fèi)的,IDA:是Windows端的)。
通過打印崩潰的堆棧信息進(jìn)行查看崩潰信息如下:
通過路徑/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 獲取到CoreFoundation的可執(zhí)行文件:
CoreFoundation
- 通過 hopper 進(jìn)行反編譯,步驟如下:
打開hopper,選擇Try the Demo,然后將上一步的可執(zhí)行文件拖入hopper進(jìn)行反匯編,選擇x86(64 bits)如圖:
next:先搜索__forwarding_prep_0___
image.png
跳轉(zhuǎn)至:___forwarding___
-
終于找到了forwardingTargetForSelector:方法了:咋們再來看看 其執(zhí)行流程,簡單分析如下:___forwarding___簡單流程
開啟上帝視角之后,我們通過匯編分析,可以清晰看到轉(zhuǎn)發(fā)流程方法:
【快速轉(zhuǎn)發(fā)】forwardingTargetForSelector
【慢速轉(zhuǎn)發(fā)】methodSignatureForSelector —>forwardInvocation
注:forwardInvocation :消息重定向,以后再分析。
動態(tài)方法決議和消息轉(zhuǎn)發(fā)整體的流程如下:注: 不論是否進(jìn)行動態(tài)決議都會重新lookUpImpOrForward,這里會有behavior的判斷,是否經(jīng)歷過動態(tài)決議處理。所以開始有一次動態(tài)決議處理方法的機(jī)會。
三、 實(shí)際舉例驗(yàn)證:
快速轉(zhuǎn)發(fā)實(shí)現(xiàn)
慢速轉(zhuǎn)發(fā)實(shí)現(xiàn)
注:如果不進(jìn)行快速消息轉(zhuǎn)發(fā) 也不進(jìn)行慢速轉(zhuǎn)發(fā)就會崩潰,可以試試。
四、 總結(jié)
- 消息轉(zhuǎn)發(fā)機(jī)制是在匯編中實(shí)現(xiàn)的,并且屬于CoreFoundation框架中,不開源的。我們可以通過
反匯編
的方式去查看;
- 消息轉(zhuǎn)發(fā)機(jī)制是在匯編中實(shí)現(xiàn)的,并且屬于CoreFoundation框架中,不開源的。我們可以通過
-
- 在我們沒有實(shí)現(xiàn)方法的時(shí)候,慢速查找也找不到方法的時(shí)候,我們有三次機(jī)會去實(shí)現(xiàn)方法:
- 1). 動態(tài)方法決議:實(shí)現(xiàn)
resolveInstanceMethod
; - 2). 快速消息轉(zhuǎn)發(fā):實(shí)現(xiàn)
forwardingTargetForSelector
; - 3). 慢速消息轉(zhuǎn)發(fā):實(shí)現(xiàn)
methodSignatureForSelector —>forwardInvocation
;
- 如果objc_msgSend快速查找和慢速查找失敗,未實(shí)現(xiàn)動態(tài)方法決議和消息轉(zhuǎn)發(fā),則程序直接報(bào)錯(cuò)崩潰
unrecognized selector sent to instance
;
- 如果objc_msgSend快速查找和慢速查找失敗,未實(shí)現(xiàn)動態(tài)方法決議和消息轉(zhuǎn)發(fā),則程序直接報(bào)錯(cuò)崩潰
五、 拓展
- objc_msgForward_impcache 的轉(zhuǎn)換 ;
objc_msgForward_impcache調(diào)用
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
......
_objc_msgForward_impcache源碼:
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
_objc_msgForward_impcache
只是個(gè)內(nèi)部的函數(shù)指針,只存儲于類的方法緩存中,需要被轉(zhuǎn)化為_objc_msgForward
才能被外部調(diào)用。