前言
OC调用方法,底层是调用 objc_msgSend 发送消息。在发送消息时会经过一系列的快速 查找、慢速查找,如果查找到对应的 IMP,直接返回;如果没有找到,就会进入到方法的动态方法决议和消息转发流程。
这篇文章就是深入探索
动态方法决议
和消息转发
准备工作
一 、 动态方法决议
接着上一篇,在慢速查找流程未找到方法实现时,首先会尝试一次动态方法决议:
if (slowpath(behavior & LOOKUP_RESOLVER)) {
//动态方法决议的控制条件
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
进入动态方法决议阶段,源码如下
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);//类执行解析方式
}
else {
resolveClassMethod(inst, sel, cls); //元类执行解析方式
// -- 如果resolveClassMethod找到了,就不会走这里了
//-- 如果没找到,这个if必然会走,之前调用lookUpImpOrForward,已经给该sel的方法缓存了imp = forward_imp
// -- 必然会走到done_nolock,返回一个nil
// -- 类方法在元类中也是以实例方法的形式存在,所以还需再走一遍实例方法的动态方法决议流程
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);//类执行解析方式
}
}
//重新进入 lookUpImpOrForward 根据 behavior | LOOKUP_CACHE进行判断执行方式
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
在resolveInstanceMethod方法中对实例方法动态解析,源码如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 1\. 判断系统是否实现SEL_resolveInstanceMethod方法
// 即+(BOOL)resolveInstanceMethod:(SEL)sel,
// 继承自NSObject的类,默认实现,返回NO
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA()))
{
// Resolver not implemented.
// 不是NSObject的子类,也未实现+(BOOL)resolveInstanceMethod:(SEL)sel,
// 直接返回,没有动态解析的必要
return;
}
// 2\. 系统给你一次机会 - 你要不要针对 sel 来操作一下下
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// 3\.-- 再去查一次imp,如果在cls的继承链上自定义实现了`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方法中对实例方法动态解析,源码如下:
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//: -- 容错处理,判断该元类的继承链中是否有resolveClassMethod方法
//: -- 如果自定义没实现,则会找到NSObject
if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
//: -- 得到元类/类的对应的类
//: -- 因为我们只能在类里实现resolveClassMethod方法,无法去元类实现,所以这里把消息接受者设置为当前类
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);
}
}
//: -- 发送消息,调用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,如果上面调用用nonmeta中的`resolveClassMethod `方法里面给元类添加了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 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));
}
}
}
在经过
resolveMethod_locked
方法后,进行resolveInstanceMethod
,重新进行一遍lookUpImpOrForward
;即: 如果动态方法决议中,将其实现指向了其他方法,则继续查找指定的imp,即继续慢速查找lookUpImpOrForward流程-
使用动态方法决议流程:
使用动态方法决议,代码举例:
//对象使用
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 来了",NSStringFromSelector(sel));
/**
添加方法
@param self 调用该方法的对象
@param sel 方法的名
@param IMP 新添加的方法,是c语言实现的
@param type :方法签名, 新添加的方法的类型,包含函数的返回值以及参数内容类型,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];
}
就算没有实现方法say666 ,但是class_addMethod,依然不会崩溃。
优化:根据resolveMethod_locked的流程图,如果是元类调用,最后还是会走实例方法,根据isa走位图,可知元类继承链,最终继承NSObject,所以我们可以给NSObject 写个分类,直接在分类中综合两个方法如下:
+ (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;
}
风险:使用这方法的风险可能造成其他SDK 或者系统方法也使用了类似的方法,造成冲突。所以我们使用的类名LGPerson一定要特殊唯一。
注 :
使用这种办法的前提是:相关方法代码已经实现,只是在运行时将改方法动态添加到目标类中。。
二、 消息转发机制
在方法查找过程中,经过缓存查找,方法列表查找和动态方法解析,如果经历慢速查找都没有查找到IMP,也没有进行方法动态解析,那么我们还有办法进行方法实现: 消息转发机制
。但是我们只知道有消息转发这个机制,但是我们始终找不到 消息转发的实现方法和实现。这个时候:
开启凡人的视角:
- lldb方式断点查看: 通过进入 [person sayHello]之后的断点查看在 class 为 LGPerson的时候 Sel ,可以发现
forwardingTargetForSelector
和methodSignatureForSelector
。如图:
开启上帝视角:
方式1:通过
instrumentObjcMessageSends
方式打印发送消息的日志方式2:通过
hopper/IDA反编译
方式1 :instrumentObjcMessageSends
通过lookUpImpOrForward
--> log_and_fill_cache
--> logMessageSend
,在logMessageSend
源码下方找到instrumentObjcMessageSends
的源码实现如下:
/***********************************************************************
* 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
///将方法执行记录通过打印出来
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
.........
}
///结束instrumentObjcMessageSends
void instrumentObjcMessageSends(BOOL flag)
{
.........
// SUPPORT_MESSAGE_LOGGING
#endif
所以,在main
中调用instrumentObjcMessageSends
打印方法调用的日志信息犹如上面的格式:
- 1、在main中通过extern 声明instrumentObjcMessageSends方法
- 2、打开 objcMsgLogEnabled 开关,即调用instrumentObjcMessageSends方法时,传入YES
不带源码的工程中这样实现:
///extern 修饰 我们可以拿来调用 苹果提供了打印方法流程的方法
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
//开启
instrumentObjcMessageSends(YES);
//去logMessageSend找打印信息
[person sayHello];
//关闭
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;
}
运行不带源码的工程,并前往/tmp/msgSends 目录,发现有msgSends开头的日志文件;
快捷键:command + shift + g ---> 输入 /tmp/msgSends -->回车
打开msgSends.文件:
可以看出 ,崩溃前执行了
两次动态方法决议:resolveInstanceMethod方法
两次消息快速转发:forwardingTargetForSelector方法
两次消息慢速转发:methodSignatureForSelector + resolveInstanceMethod
方式2:通过hopper/IDA反编译
找到forwardingTargetForSelector
等转发方法的实现
Hopper和IDA是一个可以帮助我们静态分析可视性文件的工具,可以将可执行文件反汇编成伪代码、控制流程图等,下面以mac端Hopper为例(Hopper :要收费的,IDA:是Windows端的)。
通过打印崩溃的堆栈信息进行查看崩溃信息如下:
通过路径/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 获取到CoreFoundation的可执行文件:
- 通过 hopper 进行反编译,步骤如下:
打开hopper,选择Try the Demo,然后将上一步的可执行文件拖入hopper进行反汇编,选择x86(64 bits)如图:
next:先搜索__forwarding_prep_0___
跳转至:___forwarding___
-
终于找到了forwardingTargetForSelector:方法了:咋们再来看看 其执行流程,简单分析如下:
开启上帝视角之后,我们通过汇编分析,可以清晰看到转发流程方法:
【快速转发】forwardingTargetForSelector
【慢速转发】methodSignatureForSelector —>forwardInvocation
注:forwardInvocation :消息重定向,以后再分析。
动态方法决议和消息转发整体的流程如下:注: 不论是否进行动态决议都会重新lookUpImpOrForward,这里会有behavior的判断,是否经历过动态决议处理。所以开始有一次动态决议处理方法的机会。
三、 实际举例验证:
注:如果不进行快速消息转发 也不进行慢速转发就会崩溃,可以试试。
四、 总结
- 消息转发机制是在汇编中实现的,并且属于CoreFoundation框架中,不开源的。我们可以通过
反汇编
的方式去查看;
- 消息转发机制是在汇编中实现的,并且属于CoreFoundation框架中,不开源的。我们可以通过
-
- 在我们没有实现方法的时候,慢速查找也找不到方法的时候,我们有三次机会去实现方法:
- 1). 动态方法决议:实现
resolveInstanceMethod
; - 2). 快速消息转发:实现
forwardingTargetForSelector
; - 3). 慢速消息转发:实现
methodSignatureForSelector —>forwardInvocation
;
- 如果objc_msgSend快速查找和慢速查找失败,未实现动态方法决议和消息转发,则程序直接报错崩溃
unrecognized selector sent to instance
;
- 如果objc_msgSend快速查找和慢速查找失败,未实现动态方法决议和消息转发,则程序直接报错崩溃
五、 拓展
- objc_msgForward_impcache 的转换 ;
objc_msgForward_impcache调用
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
只是个内部的函数指针,只存储于类的方法缓存中,需要被转化为_objc_msgForward
才能被外部调用。