前俩篇objc_msgSend快速查找和objc_msgSend慢速查找的流程,主要分析了经过汇编流程快速查找缓存,经过类的方法列表慢速查找,本章着重接着上俩章深刻分析没有找到方法的状况下,
苹果
给开发者提供了二个建议。c++
动态方法解析
: 在慢速查找过程当中,未找到IMP
,会执行一次动态方法解析
消息转发
: 若是动态方法决议仍是没有找到IMP
,则开始消息转发
若是以上俩步都没有作相应的操做,就会报平常开发常见的错误
方法未实现的崩溃报错
objective-c
以下示例代码:windows
@interface Student : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayNB;
- (void)sayMaster;
- (void)say666;
- (void)sayHello;
+ (void)sayNB;
+ (void)lgClassMethod;
@end
@implements Student
- (void)sayHello{
NSLog(@"%s",__func__);
}
- (void)sayNB{
NSLog(@"%s",__func__);
}
- (void)sayMaster{
NSLog(@"%s",__func__);
}
+ (void)lgClassMethod{
NSLog(@"%s",__func__);
}
@end
复制代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [Student alloc];
[stu say666];
//[Student performSelector:@selector(sayNB)];
}
return 0;
}
复制代码
在main
方法中分别调用实例方法
和类方法
,缓存
慢速查找
的源码中,IMP
未找到,会赋值称为forward_imp=(IMP)_objc_msgForward_impcache;
,经过搜索_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
,根据以前总结的规则, 去掉一个下划线来搜索。markdown
// 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;
复制代码
实际的本质都是调用objc_defaultForwardHandler
,这就是咱们平常中常常见到的崩溃错误。 下面深刻分析崩溃发生以前的补救方法
架构
在
lookUpImpOrForward
方法里,方法慢速
查找走完以后,会开始走方法动态解析
流程,给开发者提供第一次
机会,来处理找不到消息
的错误。函数
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
复制代码
经过注释
也能够得知, 这个实在IMP
没有找到的时候,会走这里解决,而且只走一次。工具
/*********************************************************************** * resolveMethod_locked * Call +resolveClassMethod or +resolveInstanceMethod. * * Called with the runtimeLock held to avoid pressure in the caller * Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb **********************************************************************/
static NEVER_INLINE IMP 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);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
复制代码
主要分如下几步:oop
先是判断cls
是不是元类
?post
类
,调用对象方法
的动态解析resolveInstanceMethod
元类
,调用类方法
的动态解析resolveClassMethod
来处理,而后判断是否能找到sel
,找不到接着再调用一次resolveInstanceMethod
,由于类方法,即带+号的方法
相对于元类
来讲也是实例方法, 调用resolveInstanceMethod
,参数第一个是inst=类
,第二个查找是sel方法名字
,第三个cls=元类
,if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
复制代码
若是这里查找的是类方法, 是在cls->ISA
根元类里找这个解析方法的实现, 找到就去发送消息, 找不到返回默认实现。
+ (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
,消息崩溃以前, 会执行一次实例方法动态解析
,在这个方法里,经过runtime
把没找到的sel
指向一个存在的imp
上,打印结果
这里会看到这个方法打印里俩次
,这个问题留在文章末尾分析。
发送类方法消息
找不到imp
致使的崩溃修复,与实例方法
相似方法修复, 重写resolveClassMethod
来解决,在该方法中, 把崩溃的sel
指向一个能够找到的imp
。
+ (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];
}
复制代码
⚠️ 这里要注意获取类方法是要到
元类
,添加类方法
也要到元类中
,可使用objc_getMetaClass
获取元类。
经过上边的方法的动态解析
分析, 获得这样的结论
类 -> 父类 -> 根类 -> nil
类方法(resolveClassMethod) 元类 -> 父元类 -> 根元类 -> 根类 -> nil
类方法(resolveInstanceMethod) 根元类 -> 根类 -> nil
以前的修复崩溃都是在对应的类中重写resolveInstanceMethod
或者resolveClassMethod
,经过上边这三条路线,能够根类NSObject
中重写resolveInstanceMethod
统一处理实例方法
和类方法
的崩溃处理。
resolveInstanceMethod
在NSObject
有默认实现
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
复制代码
以下,建立一个NSObject
的分类,统一处理以下,由于有默认实现,因此返回NO
,不能调用[super resolveInstanceMethod:sel]
:
+ (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;
}
复制代码
固然这种统一处理的方式,仍是会有一些问题, 一些系统的方法会走进这里, 能够针对类中的方法名统一前缀
,根据前缀
判断对应的模块来处理,好比mine
模块, 属于这个模块的崩溃统一跳转到mine
模块首页, 也能够作一些错误上报的操做。
在
快速查找
+慢速查找
没有找到以及动态消息解析
也未处理,就会进入消息转发过程
在
lookUpImpOrForward
的函数末尾, 在log_and_fill_cache
有这么一个控制条件objcMsgLogEnabled
,经过它能够控制日志保存到本地,经过日志能够看到调用流程
控制这个objcMsgLogEnabled
的是这个函数instrumentObjcMessageSends
,给它传入true
,控制开启本地日志保存
经过lookUpImpOrForward -> log_and_fill_cache -> 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
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
void instrumentObjcMessageSends(BOOL flag) {
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
复制代码
由于这个instrumentObjcMessageSends
是内部函数,在外部使用须要使用extern
外部声明
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);
}
return 0;
}
复制代码
经过以上源码了解到日志的保存路径在/tmp/msgSends
目录中,运行代码,就能够看到以下内容
在目录中打开msgSends
开头的文件, 调用完resolveInstanceMethod
方法,并无在方法动态解析
处理,因此来到forwardingTargetForSelector
的快速转发
以及后续 的慢速转发
Hopper
和IDA
是一个能够帮助咱们静态分析的反汇编工具,将可执行文件反汇编为伪代码 和流程图形式,帮助咱们去分析,因为IDA
在mac上不稳定,能够在windows
系统上测试, 如下使用Hopper
来分析。
运行崩溃后,经过bt
看堆栈信息,
经过汇编查看,__forwarding___
也是在CoreFoundation
中。
经过image list
调试命令查看CoreFoundation image
的位置
找到CoreFoundation
后,用Hopper
打开它
打开Hopper
, 选择Try the Demo
,将CoreFoundation
拖入里边
点击OK
默认点击Next
等待加载完成,
搜索__forwarding_prep_0___
,查看伪代码, 跳转到___forwarding___
里边的伪代码
首先判断是否实现forwardingTargetForSelector
loc_64a67
loc_649fc
,经过forwardingTargetForSelector
获取接受对象给rax
, 再对rax
做容错处理,有错误跳到loc_64e3c
loc_64a67
伪代码跳到这里后,首先判断是否为僵尸对象
,在下边继续判断是否响应 methodSignatureForSelector
,
loc_64dd7
, 直接报错loc_64e3c
loc_64dd7
伪代码和loc_64e3c
伪代码经过获取methodSignatureForSelector
方法签名为nil
也直接报错
上边的流程获取到方法签名
,开始在forwardInvocation
方法中进行处理
因此经过以上分析, 消息转发有俩种
forwardingTargetForSelector
methodSignatureForSelector
+forwardingTargetForSelector
实现在lookUpImpOrForward
中,慢速也没有找到imp
,
方法的动态解析
处理,这步未处理, 即走消息转发
消息转发
第一步开始forwardingTargetForSelector
,即快速消息转发
,将消息转发给别等对象处理,这步未处理,交给慢速转发
慢速转发
使用methodSignatureForSelector
返回方法签名,不能够返回nil
或者签名内容为空
,使用方法签名生成NSInvocation
对象, 因此须要重写forwardInvocation
进行消息转发。resolveInstanceMethod
为何执行俩次?解决以前遗留的问题, 在实例动态方法解析的时候, 只重写了, 并未对未找到的
sel
做处理, 会调用俩次
在实例动态方法解析
的时候, 会走到lookUpImpOrForward
-> resolveMethod_locked
-> resolveInstanceMethod
,是经过这里触发
在IMP imp = lookUpImpOrNil(inst, sel, cls);
加个断点, 当sel
是say666
停下来,打印了了say66 来了
经过bt
查看堆栈,
第一次打印的信息, 经过堆栈能够看出是第一次经过方法动态解析
执行打印的。
经过第二次打印, 经过[NSObject(NSObject) methodSignatureForSelector:]
-> __methodDescriptionForSelector
-> class_getInstanceMethod
再次来到方法的动态解析
并打印了第二次,经过堆栈分析, 能够经过Hopper
反汇编CoreFoundation
文件,查看methodSignatureForSelector
的伪代码
在跳进到___methodDescriptionForSelector
看它的实现
结合以前的堆栈信查看, 这里调用了objc 的方法 class_getInstanceMethod
,在源码工程查看
/*********************************************************************** * class_getInstanceMethod. Return the instance method for the * specified class and selector. **********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel) {
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
#warning fixme build and search caches
// Search method lists, try method resolver, etc.
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}
复制代码
经过源码查看,这里又调用了lookUpImpOrForward
, 又走了一次方法动态解析
,系统在调用完methodSignatureForSelector
,返回方法签名,在调用invocation
以前,又去调用class_getInstanceMethod
,因此又走了一遍lookUpImpOrForward
,查询一遍sel
,没查到再走方法动态解析
和消息转发
流程。
由于在源码工程里探索, 因此有上帝视角, 若是没有环境, 如何验证上边的流程?
在普通工程
里重写resolveInstanceMethod
,在方法里解决sel
找不到的错误,使用class_addMethod
添加一个IMP
, 看看这个方法是否会走俩次?
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 来了", NSStringFromSelector(sel));
//获取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayHello));
//获取sayMaster的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
//获取sayMaster的方法签名
const char *type = method_getTypeEncoding(sayMethod);
//将sel的实现指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
复制代码
经过结果看,经过动态方法解析
,赋值了IMP
, 只执行了一次,说明第二次不在这里。按照消息转发流程
, 把resolveInstanceMethod
里的imp
去掉,重写forwardingTargetForSelector
,并指定[LGStudent alloc]
,从新运行, 看是否resolveInstanceMethod
打印俩次, 打印俩次,说明在forwardingTargetForSelector
以前执行了方法动态解析
,反之,则在以后执行的方法动态解析
。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%s -- %@ 来了",__func__, NSStringFromSelector(sel));
// //获取sayMaster方法的imp
// IMP imp = class_getMethodImplementation(self, @selector(sayHello));
// //获取sayMaster的实例方法
// Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
// //获取sayMaster的方法签名
// const char *type = method_getTypeEncoding(sayMethod);
// //将sel的实现指向sayMaster
// return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
return [LGStudent alloc];
}
复制代码
经过运行结果看, 并无在以前答应俩次, 说明在forwardingTargetForSelector
以后执行的方法动态解析
接着根据流程,重写methodSignatureForSelector
和forwardInvocation
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%s -- %@ 来了",__func__, NSStringFromSelector(sel));
if (sel == @selector(say666)) {
// //获取sayMaster方法的imp
// IMP imp = class_getMethodImplementation(self, @selector(sayHello));
// //获取sayMaster的实例方法
// Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
// //获取sayMaster的方法签名
// const char *type = method_getTypeEncoding(sayMethod);
// //将sel的实现指向sayMaster
// return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
NSLog(@"%p", [NSMethodSignature signatureWithObjCTypes:"v@:@"]);
return [NSMethodSignature signatureWithObjCTypes:"v@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
// GM sayHello - anInvocation - 漂流瓶 - anInvocation
anInvocation.target = [LGStudent alloc];
// anInvocation 保存 - 方法
[anInvocation invoke];
}
复制代码
通过上边的分析,第二次动态决议是在methodSignatureForSelector
和forwardInvocation
之间调用的,第二种分析方法验证结果和第一种反汇编
的结果是同样的。获得以下的图
本篇是消息流程分析
,方法动态解析
,消息转发
的最后一篇
消息
经过汇编流程
快速查找,没有找到跳到lookupImpOrForward
开始慢速查找慢速查找消息
也没有找到,开始方法动态决议
方法动态决议
根据消息是类方法
仍是实例方法
重写resolveInstanceMethod
和resolveClassMethod
方法,开始第一次补救方法动态决议
也没有处理, 开始进行消息转发即【快速转发】快速转发
, 即重写forwardingTargetForSelector
方法, 将消息甩给能够处理的对象
,进行第二次补救慢速转发
使用methodSignatureForSelector
返回方法签名,不能够返回nil
或者签名内容为空
,使用方法签名生成NSInvocation
对象, 因此须要重写forwardInvocation
进行消息转发。iOS开发·runtime原理与实践: 消息转发篇(Message Forwarding) (消息机制,方法未实现+API不兼容奔溃,模拟多继承)