紧接着上一篇传送门:
Runtime源码分析系列(三)之方法查找C/C++递归部分
下面直奔主题,开始本节分析内容:
1、源码分析:动态方法解析
2、源码分析:消息转发
3、项目实战:动态方法解析及消息转发项目实战
4、苹果未开源部分-消息转发objc_msgForward实现流程详细分析
一、源码分析:动态方法解析
先把上节步骤3的动态方法解析部分代码贴出来:(大概在Runtime源码中 objc-runtime-new.mm
文件的4696行,图示如下):
代码及代码注释如下:
retry:
// No implementation found. Try method resolver once.
//如果没有发现函数的实现imp,会尝试一次动态方法解析,注意,仅仅只会尝试一次
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
注意这段代码if里面的逻辑,triedResolver
这个变量是局部变量,lookUpImpOrForward
开始的时候:bool triedResolver = NO;
如果找到了,就会把这个变量赋值为YES
并且再次retry
,重试上一篇中说的C/C++
查找imp
; 也就是说,只要动态方法解析成功后,就不再次进入动态方法解析了,也对应上面注释的英文,这个动态方法解析只进行一次
。
那么问题来了,动态方法解析底层做了什么?
接下来我们继续深入探究代码部分,发现核心的方法是 _class_resolveMethod(cls, sel, inst);
,点击进到这个函数的底层,我们找到下面的实现部分:
/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
分析 _class_resolveMethod
的实现部分,不难发现,这个函数会判断class
是否是元类(metaClass)
,如果是不是元类,就会调用 _class_resolveInstanceMethod
;如果是元类,就会调用 _class_resolveClassMethod
.
再分析一下第二个逻辑下面的lookUpImpOrNil
函数,分析一下多出来的这个if逻辑
/***********************************************************************
* lookUpImpOrNil.
* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache
**********************************************************************/
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
从注释部分就已经很清楚了,这个方法lookUpImpOrNil
和 lookUpImpOrForward
一模一样,只是返回值为nil
。这部分代码的意思就是:如果cls
为nil
或者没有找到cls
的imp
,元类调用_class_resolveClassMethod
之后,还会再调用一次_class_resolveInstanceMethod
。
所以上面的代码经过整理后如下:
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {//1、判断是不是元类(metaClass)
// 不是元类,调用_class_resolveInstanceMethod
_class_resolveInstanceMethod(cls, sel, inst);
} else {
// 是元类,调用_class_resolveClassMethod
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO, YES, NO)) {
//如果cls为nil或者没有找到cls的imp,元类调用_class_resolveClassMethod之后,还会再调用一次_class_resolveInstanceMethod
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
再次点击进入这两个方法的内部,
/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
assert(cls->isMetaClass());
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_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 imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
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));
}
}
}
/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
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));
}
}
}
找到两个重点的方法如下:
//1、如果是元类
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
//2、如果不是元类
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
这两句代码合并起来分别是是
bool resolved = objc_msgSend(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
和
bool resolved = objc_msgSend(cls, SEL_resolveInstanceMethod, sel);
意思就是对通过objc_msgSend
消息转发,查看当前这个cls,看看是否实现SEL_resolveClassMethod(元类)
或者SEL_resolveInstanceMethod(非元类)
,这个是系统内部已经实现过的,在动态解析过程中自动发送的。据我猜测:这个就是专门提供预留出来的方法,给开发者用的,让开发者自己可以在class
里面处理找不到imp
的这种特殊情况。
下图是我对以上动态方法解析的这部分内容的总结:
二、源码分析:消息转发分析
接着上面的内容,如果imp
找不到,调用完动态方法解析后还是找不到,就会调用消息转发了
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
接下来我们进入_objc_msgForward_impcache
里面,发现死活找不到,不要着急在这个函数前多加一个下划线,搜索 __objc_msgForward_impcache
,在objc-msg-arm64.s
汇编文件的521行发现这样的代码信息:
STATIC_ENTRY __objc_msgForward_impcache
MESSENGER_START
nop
MESSENGER_END_SLOW
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr x17, [x17, __objc_forward_handler@PAGEOFF]
br x17
END_ENTRY __objc_msgForward
从上面汇编代码可以看出,__objc_msgForward_impcache
主要是调用了一个指令 __objc_msgForward
,而在调用__objc_msgForward
的时候,又调用了 __objc_forward_handler
这个指令。注意!此时 __objc_msgForward
突然点不进去了,找不到源码实现了,只有汇编调用,因为苹果把这个方法的实现闭源了!我们等下会实战分析,先暂时放过这个方法,接下来继续搜索 objc_forward_handler
,我们来瞅瞅这个回调信息里面处理了什么逻辑,在Runtime源码中搜索 objc_forward_handler
,我们在objc-runtime.mm
文件中的大致450行发现了这个回调的实现部分,我把代码贴出来:
#if !__OBJC2__
// Default forward handler (nil) goes to forward:: dispatch.
void *_objc_forward_handler = nil;
void *_objc_forward_stret_handler = nil;
#else
// Default forward handler halts the process.
__attribute__((noreturn)) 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_forward_handler
等价于objc_defaultForwardHandler
,而 objc_defaultForwardHandler
的实现部分就是下面这部分:
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);
}
是不是很熟悉这句报错信息? unrecognized selector sent to instance
这不就是方法没实现,线程崩溃后打印台打印的这个报错文案吗?和我们下面这种日常报错信息完全一致!
'-[BMPerson run]: unrecognized selector sent to instance 0x600000004200'
我把objc_defaultForwardHandler的实现也放在这里,大家对比下便一目了然了
"%c[%s %s]: unrecognized selector sent to instance %p ", class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self)
此时我们就明白了,上面汇编里面 _objc_forward_handler
这个回调主要是用来处理打印信息的。
到目前为止,除了苹果闭源的 __objc_msgForward
消息转发部分,其他流程我们一个不放过全部分析完毕。下面我们先进入实战验证部分,这个闭源的部分我们能结合实战其他的工具分析出来。
三、动态方法解析以及消息转发项目实战部分
下面我们通过一个项目实例,来结合理解上面这部分的内容,新建工程,创建一个类BMPerson
,声明实例方法run
,然后并不实现,代码如下
新建工程选择commandTool
栏,如图3
.h文件
#import <Foundation/Foundation.h>
@interface BMPerson : NSObject
- (void)run;
@end
.m文件
#import "BMPerson.h"
#import <objc/runtime.h>
@implementation BMPerson
//- (void)run{
// NSLog(@"%s 跑🍅",__func__);
//}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"🍅🍅来了🍅🍅 %s ",__func__);
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"🍅🍅🍅 %s ",__func__);
return [super resolveClassMethod:sel];
}
直接进入到main.m
文件,调用
#import <Foundation/Foundation.h>
#import "BMPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
[[BMPerson alloc] run];
}
return 0;
}
直接在main.m
里面调用,然后运行
2020-03-10 21:08:32.189661+0800 Runtime[7218:1181862] 🍅🍅来了🍅🍅
2020-03-10 21:08:32.190285+0800 Runtime[7218:1181862] 🍅🍅来了🍅🍅
2020-03-10 21:08:32.190478+0800 Runtime[7218:1181862] -[BMPerson run]: unrecognized selector sent to instance 0x60000000c3d0
2020-03-10 21:08:32.191230+0800 Runtime[7218:1181862] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[BMPerson run]: unrecognized selector sent to instance 0x60000000c3d0'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff4ce80acd __exceptionPreprocess + 256
1 libobjc.A.dylib 0x00007fff77582a17 objc_exception_throw + 48
2 CoreFoundation 0x00007fff4cefa8d6 -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff4ce2293f ___forwarding___ + 1485
4 CoreFoundation 0x00007fff4ce222e8 _CF_forwarding_prep_0 + 120
5 Runtime 0x0000000100000e3b main + 59
6 libdyld.dylib 0x00007fff78d513d5 start + 1
7 ??? 0x0000000000000003 0x0 + 3
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
这里我们发现报错信息如之前分析的流程,走进了系统方法resolveInstanceMethod
,但是重点来了!有木有发现resolveInstanceMethod
调用了两次!!!这是为什么呢?我们来断点调试一下两次打印函数的调用:
第一次断点进入resolveInstanceMethod
这个方法时打印的信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000100000eab Runtime`+[BMPerson resolveInstanceMethod:](self=BMPerson, _cmd="resolveInstanceMethod:", sel="run") at BMPerson.m:25:5
frame #1: 0x00007fff7758c011 libobjc.A.dylib`resolveInstanceMethod(objc_class*, objc_selector*, objc_object*) + 104
frame #2: 0x00007fff7757579e libobjc.A.dylib`lookUpImpOrForward + 498
frame #3: 0x00007fff77575114 libobjc.A.dylib`_objc_msgSend_uncached + 68
frame #4: 0x0000000100000e3b Runtime`main(argc=3, argv=0x00007ffeefbff470) at main.m:16:9
frame #5: 0x00007fff78d513d5 libdyld.dylib`start + 1
(lldb)
结合我们之前底层的源码,我把注释补充一下,第一次打印的信息如下:(由于方法调用是堆栈结构,所以打印信息是从下往上读取的)
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 //主线程断点,断点组号1.1
*
* frame #0: 0x0000000100000eab Runtime`+[BMPerson resolveInstanceMethod:](self=BMPerson, _cmd="resolveInstanceMethod:", sel="run") at BMPerson.m:25:5 //第一次找到resolveInstanceMethod
frame #1: libobjc.A.dylib`resolveInstanceMethod(objc_class*, objc_selector*, objc_object*) //由于run是实例方法,所以会进入_class_resolveInstanceMethod里面,通过objc_msgSend,调用BMPerson类的方法resolveInstanceMethod
frame #2: libobjc.A.dylib`lookUpImpOrForward //开始进入C/C++递归retry查找
frame #3: libobjc.A.dylib`_objc_msgSend_uncached //在汇编里面没有找到BMPerson实例方法run实现的imp,从汇编中跳出来到_objc_msgSend_uncached
frame #4: Runtime`main(argc=3, argv=0x00007ffeefbff470) at main.m:16:9 //进入main函数,执行代码
frame #5: libdyld.dylib`start //首先程序开始
第二次断点进入resolveInstanceMethod
这个方法时打印的信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000100000eab Runtime`+[BMPerson resolveInstanceMethod:](self=BMPerson, _cmd="resolveInstanceMethod:", sel="run") at BMPerson.m:25:5
frame #1: 0x00007fff7758c011 libobjc.A.dylib`resolveInstanceMethod(objc_class*, objc_selector*, objc_object*) + 104
frame #2: 0x00007fff7757579e libobjc.A.dylib`lookUpImpOrForward + 498
frame #3: 0x00007fff7757c9c0 libobjc.A.dylib`class_getInstanceMethod + 54
frame #4: 0x00007fff4ce313ca CoreFoundation`__methodDescriptionForSelector + 269
frame #5: 0x00007fff4ce6fd93 CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
frame #6: 0x00007fff4ce224f7 CoreFoundation`___forwarding___ + 389
frame #7: 0x00007fff4ce222e8 CoreFoundation`__forwarding_prep_0___ + 120
frame #8: 0x0000000100000e3b Runtime`main(argc=3, argv=0x00007ffeefbff470) at main.m:16:9
frame #9: 0x00007fff78d513d5 libdyld.dylib`start + 1
(lldb)
第二次到断点函数调用堆栈我们先不分析,下面最后再仔细分析。因为经过之前Runtime
源码的深究,看懂第一个断点调用堆栈注释应该没有问题了,但是第二个可能小伙伴还是有点难度,因为苹果闭源的 __objc_msgForward
部分我们并没有分析!下面重点来了:
四、苹果未开源部分-消息转发objc_msgForward实现流程详细分析
注意!!!尽管 __objc_msgForward
这个消息转发方法苹果没有提供源码,我们
还是能借助源码中苹果提供的其他工具人去尝试分析这个方法的底层实现,这个工具人就是苹果提供的函数调用堆栈打印函数:instrumentObjcMessageSends
全局搜索 instrumentObjcMessageSends
,找到objc-class.mm
文件夹里面这个函数的实现底层,我贴在下面:
/***********************************************************************
* instrumentObjcMessageSends
**********************************************************************/
// Define this everywhere even if it isn't used to simplify fork() safety code.
spinlock_t objcMsgLogLock;
#if !SUPPORT_MESSAGE_LOGGING
void instrumentObjcMessageSends(BOOL flag)
{
}
#else
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;
}
// SUPPORT_MESSAGE_LOGGING
#endif
从这个方法的实现部分我们可以读出,这个函数是用来打印log
的(一般在系统内部使用)
,从里面的 logMessageSend
实现的部分,我们发现,这个打印log
会在tmp
文件夹下生成一个打印日志文件,这里我贴一下这个打印日志的完整的路径是:(其中的占位%d
是pid
,可以理解成:只是一串数字)
Macintosh HD/private/tmp/msgSends-%d
instrumentObjcMessageSends
使用的话,需要在目标文件引入 #import <objc/message.h>
,继续进入刚才的项目实战部分,我们在main.m
里面使用这个函数
#import <Cocoa/Cocoa.h>
#import "BMPerson.h"
#import <objc/message.h>
extern void instrumentObjcMessageSends(BOOL);
int main(int argc, const char * argv[]) {
@autoreleasepool {
instrumentObjcMessageSends(YES);
[[BMPerson alloc] run];
instrumentObjcMessageSends(NO);
}
return 0;
}
加完后运行工程,然后工程因为-(void)run
没实现,还是会崩溃,然后在刚才那个路径下找到log
文件,如下图4:
然后我把log
文件中的重要调用堆栈给贴出来, 注意因为是BMPerson
调用的,注意文件中 BMPerson
开头的才是,应该是能够找到两处
//第一处在文件顶部
+ BMPerson NSObject initialize
+ BMPerson NSObject alloc
+ BMPerson BMPerson resolveInstanceMethod:
+ BMPerson BMPerson resolveInstanceMethod:
//第二次在文件中部
+ NSObject NSObject resolveInstanceMethod:
- BMPerson NSObject forwardingTargetForSelector:
- BMPerson NSObject forwardingTargetForSelector:
- BMPerson NSObject methodSignatureForSelector:
- BMPerson NSObject methodSignatureForSelector:
- BMPerson NSObject class
- BMPerson NSObject doesNotRecognizeSelector:
- BMPerson NSObject doesNotRecognizeSelector:
这两处调用堆栈信息和我们上面的断点调试结果是完全对应的,第一次的我们已经分析的比较明白了,接下来我们来分析第二次的调用堆栈:
显示的调用堆栈里面明显有两个方法:methodSignatureForSelector
以及 forwardingTargetForSelector
,下面我们在BMPerson.m
文件里面实现一下:
#import "BMPerson.h"
#import <objc/runtime.h>
@implementation BMPerson
//- (void)run{
// NSLog(@"%s 跑🍅",__func__);
//}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"🍅🍅来了🍅🍅 %s ",__func__);
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"🍅🍅来了🍅🍅 %s ",__func__);
return [super resolveClassMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s",__func__);
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s",__func__);
return [super methodSignatureForSelector:aSelector];
}
@end
打印结果发现:系统确实调用了这两个函数
2020-03-11 21:40:29.602788+0800 RuntimeCode[14615:2242308] 🍅🍅来了🍅🍅 +[BMPerson resolveInstanceMethod:]
2020-03-11 21:40:29.604752+0800 RuntimeCode[14615:2242308] -[BMPerson forwardingTargetForSelector:]
2020-03-11 21:40:29.605355+0800 RuntimeCode[14615:2242308] -[BMPerson methodSignatureForSelector:]
2020-03-11 21:40:29.606224+0800 RuntimeCode[14615:2242308] -[BMPerson run]: unrecognized selector sent to instance 0x101830500
2020-03-11 21:40:29.610530+0800 RuntimeCode[14615:2242308] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[BMPerson run]: unrecognized selector sent to instance 0x101830500'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff4ce80acd __exceptionPreprocess + 256
1 libobjc.A.dylib 0x00007fff77582a17 objc_exception_throw + 48
2 CoreFoundation 0x00007fff4cefa8d6 -[NSObject(NSObject) __retain_OA] + 0
3 CoreFoundation 0x00007fff4ce2293f ___forwarding___ + 1485
4 CoreFoundation 0x00007fff4ce222e8 _CF_forwarding_prep_0 + 120
5 RuntimeCode 0x0000000100000bbc main + 76
6 libdyld.dylib 0x00007fff78d513d5 start + 1
7 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
给大家看一下官方提供的消息转发流程图,如下图6
结合这个图,以及上面的内容我们就明白了第二次报错的含义了,我把第二次断点的堆栈信息加一下详细的注释,方便深入理解
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: Runtime`+[BMPerson resolveInstanceMethod:](self=BMPerson, _cmd="resolveInstanceMethod:", sel="run") at BMPerson.m:25:5 //动态方法解析,第二次进入resolveInstanceMethod
frame #1: libobjc.A.dylib`resolveInstanceMethod(objc_class*, objc_selector*, objc_object*) + 104 //找不到imp,进入动态方法解析
frame #2: libobjc.A.dylib`lookUpImpOrForward + 498 //查找imp以及找不到imp会继续消息转发
frame #3: libobjc.A.dylib`class_getInstanceMethod + 54 //再次查找方法实现
frame #4: CoreFoundation`__methodDescriptionForSelector + 269
frame #5: CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38 //消息转发-方法签名
frame #6: CoreFoundation`___forwarding___ + 389 //消息转发开始
frame #7: CoreFoundation`__forwarding_prep_0___ + 120 //消息转发准备
frame #8: Runtime`main(argc=3, argv=0x00007ffeefbff470) at main.m:16:9 //进入main函数
frame #9: libdyld.dylib`start + 1 //开始执行
(lldb)
通官方的消息转发简图以及以上步骤的分析,如果我们想修正BMPerson
的run
方法没有实现而导致的bug,可以利用消息重定向的forwardInvocation
方法里面,给run
进行实现,就能解决imp
找不到闪退的问题。 methodSignatureForSelector
以及 forwardingTargetForSelector
的作用也有很多,也是可以用来做crash
收集以及防崩溃处理的。下面是一个简单的实现:
新建一个类BMStudent
,继承自BMPerson
,下面是BMPerson
的.m及.h
.h文件
#import "BMPerson.h"
@interface BMStudent : BMPerson
+ (void)eat:(NSString *)str;
@end
.m文件
@implementation BMStudent
+ (void)eat:(NSString *)str {
NSLog(@"%s 吃🍅 %@",__func__, str);
}
@end
在BMPerson.m
中引入BMStudent.h
,并用方法签名methodSignatureForSelector
拦截未实现的实例方法run
,然后重新指定run
的实现为BMStudent
的eat:
方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s",__func__);
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s",__func__);
//在方法签名中拦截,如果run没有实现,就会进入下面的forwardInvocation,我们就可以重新指定方法实现
if (aSelector == @selector(run)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s",__func__);
NSString * newSelString = @"添加新的实现部分";
anInvocation.target = [BMStudent class];
[anInvocation setArgument:&newSelString atIndex:2];
NSLog(@"%@", anInvocation.methodSignature);
anInvocation.selector = @selector(eat:);
[anInvocation invoke];
}
然后运行,发现不崩了,即使BMPerson
中没有实现,由于重新指定了run
的实现部分,就不存在imp
找不到的情况了
2020-03-11 22:29:44.881404+0800 RuntimeCode[15289:2294502] 🍅🍅来了🍅🍅 +[BMPerson resolveInstanceMethod:]
2020-03-11 22:29:44.882713+0800 RuntimeCode[15289:2294502] -[BMPerson forwardingTargetForSelector:]
2020-03-11 22:29:49.004952+0800 RuntimeCode[15289:2294502] -[BMPerson methodSignatureForSelector:]
2020-03-11 22:29:49.006246+0800 RuntimeCode[15289:2294502] 🍅🍅来了🍅🍅 +[BMPerson resolveInstanceMethod:]
2020-03-11 22:29:49.007766+0800 RuntimeCode[15289:2294502] -[BMPerson forwardInvocation:]
2020-03-11 22:29:57.684618+0800 RuntimeCode[15289:2294502] <NSMethodSignature: 0x10070e1b0>
2020-03-11 22:29:58.546054+0800 RuntimeCode[15289:2294502] +[BMStudent eat:] 吃🍅 添加新的实现部分
Program ended with exit code: 0
这样就完美解决了imp
找不到而崩溃的bug
了。
至此Runtime
源码分析的动态方法决议以及消息转发机制,以及其中底层调用流程原理完美分析完成,但是大家回过头来有没有发现
动态方法决议只会执行一次,为什么resolveInstanceMethod
这个方法会调用两次?
这里我就不分析了,直接告诉答案啦,因为方法查找会从当前的类一直找元类
,元类
会一直往上找到根元类
,而根元类
的父类
就是NSObject
,所以两次的resolveInstanceMethod
,第一次是当前类
调用的,最后一次的resolveInstanceMethod
是NSObject
调用的。
好啦,本节分析就到这里了,如果对你有帮助的话,点个赞再走呗~