Runtime源码分析系列(三)之动态方法决议及消息转发流程

紧接着上一篇传送门:
Runtime源码分析系列(三)之方法查找C/C++递归部分

下面直奔主题,开始本节分析内容:

1、源码分析:动态方法解析 
2、源码分析:消息转发
3、项目实战:动态方法解析及消息转发项目实战
4、苹果未开源部分-消息转发objc_msgForward实现流程详细分析

一、源码分析:动态方法解析


先把上节步骤3的动态方法解析部分代码贴出来:(大概在Runtime源码中 objc-runtime-new.mm文件的4696行,图示如下):

1、动态方法解析和消息转发在源码中位置图.png

代码及代码注释如下:

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;
}

从注释部分就已经很清楚了,这个方法lookUpImpOrNillookUpImpOrForward一模一样,只是返回值为nil。这部分代码的意思就是:如果clsnil或者没有找到clsimp,元类调用_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的这种特殊情况。

下图是我对以上动态方法解析的这部分内容的总结:

图2、动态方法解析错误.png

二、源码分析:消息转发分析


接着上面的内容,如果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

图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文件夹下生成一个打印日志文件,这里我贴一下这个打印日志的完整的路径是:(其中的占位%dpid,可以理解成:只是一串数字)

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:

图4、log路径.png
图5、log内容.png

然后我把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

图6、消息转发流程图.png

结合这个图,以及上面的内容我们就明白了第二次报错的含义了,我把第二次断点的堆栈信息加一下详细的注释,方便深入理解

(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) 

通官方的消息转发简图以及以上步骤的分析,如果我们想修正BMPersonrun方法没有实现而导致的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的实现为BMStudenteat:方法


- (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,第一次是当前类调用的,最后一次的resolveInstanceMethodNSObject调用的。

好啦,本节分析就到这里了,如果对你有帮助的话,点个赞再走呗~

溪浣双鲤的技术摸爬滚打之路

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容