前俩篇objc_msgSend快速查找和objc_msgSend慢速查找的流程,主要分析了通过汇编流程快速查找缓存,通过类的方法列表慢速查找,本章着重接着上俩章深入分析没有找到方法的情况下,
苹果
给开发者提供了二个建议。
动态方法解析
: 在慢速查找过程中,未找到IMP
,会执行一次动态方法解析
消息转发
: 如果动态方法决议还是没有找到IMP
,则开始消息转发
0x00 - forward_imp
如果以上俩步都没有做相应的操作,就会报日常开发常见的错误
方法未实现的崩溃报错
如下示例代码:
@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
,根据之前总结的规则, 去掉一个下划线来搜索。
// 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
,这就是我们日常中经常见到的崩溃错误。
下面深入分析崩溃发生之前的补救方法
0x01 - 方法的动态解析
在
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);
}
-
主要分以下几步:
-
先是判断
cls
是否是元类
?- 如果是
类
,调用对象方法
的动态解析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
模块首页, 也可以做一些错误上报的操作。
0x03 - 消息转发流程
在
快速查找
+慢速查找
没有找到以及动态消息解析
也未处理,就会进入消息转发过程
在
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
的快速转发
以及后续 的慢速转发
0x04 - Hopper/IDA 反汇编看流程
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
进行消息转发。
0x05 - 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不兼容奔溃,模拟多继承)