上一篇说到了慢速查找,若在慢速查找中还是没有找到,则进入动态决议做补救,流程如下:
1、找自己的methodList
2、找父类的methodList
3、找到返回imp
4、未找到,进行消息转发..
查找结束后,第一步:动态决议 通过走resolveInstanceMethod / resolveClassMethod 进行动态补救,如第一步返回nil,则进行第二步:快速转发 forwardingTargetForSelector方法,若成功返回recevier,若返回nil则 进行第三步:慢速转发 methodSignatureForSelector 及 forwardingInvocation

一、动态决议
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 动态方法决议 : 给一次机会 重新查询
if (! cls->isMetaClass()) { // 对象 - 类
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else { // 类方法 - 元类
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) { // 为什么要有这行代码
resolveInstanceMethod(inst, sel, cls);
}
}
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
1、判断类是否是元类
2、如果是类,执行实例方法的动态方法决议resolveInstanceMethod
3、如果是元类,执行类方法的动态决议resolveClassMethod,如果在元类中没有找到或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
4、如果动态方法决议中,将其实现指向其他方法,则继续查找指定的imp,即继续慢速查找lookupImpOrForword 流程。

实例方法的动态方法决议
针对动态方法决议可以对奔溃的修改,可以通过在类中重写resolveInstanceMethod类方法,并将其指向其他方法的实现,在其父类 CLPerson中重写rwsolveInstanceMethod类方法,将实例方法say666的实现指向sayMaster方法实现。
如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
//获取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
//获取sayMaster的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
//获取sayMaster的丰富签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
运行结果如下:

从结果中可以发现,resolveInstanceMethod动态决议方法中“来了”打印了两次,这是为什么呢?
原因:
第一次动态决议:第一次的“来了”是在查找say666方法时会进入动态方法决议
第二次动态决议:第二次的“来了”是在慢速转发流程中调用了 CoreFoundation框架中的NSObject(NSObject) methodSignatureForSelector:后,会再次进入动态决议
类方法的动态方法决议
+ (BOOL)resolveClassMethod:(SEL)sel{
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 [super resolveClassMethod:sel];
}
注意:resolveClassMethod类方法的重写需要注意一点,传入 cls不在是类,而是元类,可以通过objc_getMetaClass方法获取类的元类,原因是因为:类方法在元类中是实例方法
实例方法:类 -- 父类 -- 根类 -- nil
类方法:元类 -- 根元类 -- 根类 -- nil
类方法和实例方法,如果前面没有找到,都会来到根类 即NSObject中查找,所以我们是否将上述的两个方法统一整合在一起呢? 其实是可以的,可以通过NSObject添加分类的方式来实现统一处理,而且由于类方法的查找,在其基础链,查找的也是实例方法 和 类方法的统一处理放在resolveInstanceMethod方法中,如下:
+ (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;
}
当然 ,上面这种写法还会有其他问题,比如系统方法也会被更改,针对这一点,我们 可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否自定义方法,如何 统一处理自定义方法,例如可以在崩溃前 提示框 在首页等,主要是用于** app线上防崩溃的处理**,提升用户体验.
一、消息转发
在动态方法决议补救也不行,就使用消息转发...
通过以下方法来了解一下,方法调用崩溃前都都走了哪些方法???
1、通过instrumentObjcMessagesSends 方式打印发送消息的日志。
通过lookupImpOrForward - >log_and_fill_cache - >logMessageSend,在logMessageSend源码下方法找到instrumentObjcMessagesSends的源码实现,所以,在main中调用,instrumentObjcMessagesSends打印方法调用的日志信息,有以下两点:
1、打开 objcMsgLogEnabled 开关,即调用instrumentObjcMessageSends方法时,传入YES
2、在main中通过extern 声明instrumentObjcMessageSends方法
如:
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);
NSLog(@"Hello, World!");
}
return 0;
}
通过logMessageSend源码,了解到消息发送打印信息存储在/tmp/msgSends 目录,如下

运行代码,前往/tmp/msgSends 目录,发现有msgSends开头的日志文件,打开发现在崩溃前,执行了以下方法:
两次动态方法决议:resolveInstanceMethod方法
两次消息快速转发:forwardingTargetForSelector方法
两次消息慢速转发:methodSignatureForSelector + resolveInstanceMethod

2、通过hopper / IDA反编译
Hopper 和 IDA 是一个可以帮助我们静态分析可视化文件的工具,可以将执行文件反编译成伪代码、控制流程图等,下面以hopper 为例(hopper高级版本是一款收费软件,针对比较简单的反汇编需求来说,demo 版本足够使用..)
运行程序崩溃,查看堆栈信息

发现forwarding来自CoreFoundation

通过image list,读取整个镜像文件,然后搜索CoreFoundation,查看其可执行文件的路径

通过文件路径,找到CoreFoundation的可执行文件

打开hopper,选择Try the Demo,然后将上一步的可执行文件拖入hopper进行反汇编,选择x86(64 bits)


以下是反汇编后的界面,主要使用上面的三个功能,分别是 汇编、流程图、伪代码

通过左侧的搜索框搜索forwarding_prep_0,然后选择伪代码
以下是forwarding_prep_0的汇编伪代码,跳转至forwarding

以下是forwarding的伪代码实现,首先是查看是否实现forwardingTargetForSelector方法,如果没有响应,跳转至loc_6459b即快速转发没有响应,进入慢速转发流程:

跳转至loc_6459b,在其下方判断是否响应methodSignatureForSelector方法

如果没有响应,跳转至loc_6490b,则直接报错
如果获取methodSignatureForSelector的方法签名为nil,也是直接报错

如果methodSignatureForSelector返回值不为空,则在forwardInvocation方法中对invocation进行处理

由上所得:
快速转发:forwardingTargetForSelector
慢速转发:methodSignatureForSelector & forwardInvocation
针对前文的崩溃问题,如果动态方法决议也没有找到实现,则需要在LGPerson中重写forwardingTargerForSelector方法,将LGPerson的实例方法的接收者指定为LGStudent的对象,(LGStudent类中有say666的具体实现),如下:
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
//将消息的接收者指定为LGStudent,在LGStudent中查找say666的实现
return [LGStudent alloc];
}

也可以直接不指定消息接收者,直接调用父类的该方法,如果还是没有找到,则直接报错

针对快速转发 中还没有找到,则进入最后一次挽救机会,即在LGPerson中重写methodSignatureForSelector 如下:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
}

从打印结果,发现forwardInvocation方法中不对forwardInvocation:进行处理,也不会崩溃报错,也可以处理invocation事务,修改invocation的target为[LGStudent alloc],调用[anInvocation invoke] 触发,即LGPerson类的say666实例方法的调用 会调用 LGStudent 的say666,如下:
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
anInvocation.target = [LGStudent alloc];
[anInvocation invoke];
}
所以,由上述可知,无论在forwardInvocation方法中是否处理invocation事务,程序都不会崩溃。
补充:方法未实现报错源码
根据慢速查找的源码,我们发现,其报错最后都是走到__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_forward_handler,并没有找到,在源码中去掉一个下划线进行全局搜索_objc_forward_handler,有如下实现,本质是调用的objc_defaultForwardHandler方法
// 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;
这就是我们在日常开发中最常见的错误:没有实现函数,运行程序,崩溃时报的错误提示。
网友评论