美文网首页
消息动态决议

消息动态决议

作者: Wayne_Wang | 来源:发表于2021-07-19 19:34 被阅读0次
1.jpeg

我们上一章针对[消息的慢速查找](https://www.jianshu.com/p/f03392ffe0b3)进行了探索,在最后我们留下了一个疑问就是当所有消息查找都没找到的时候我们看到系统对imp进行了赋值forward_imp。那我们今天就来探索下在消息查找没有找到后系统到底做了什么。

准备工作

我们在objc源码里创建这样几个类。ZYPerson 继承于 NSObject , ZYIoser 继承于ZYPersonNSObject+(ZY)NSObject的分类。代码如下:

ZYPerson:

//.h文件
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface ZYPerson : NSObject

- (void)zyEatSugar;

+ (void)sayHappy;

@end

NS_ASSUME_NONNULL_END

//.m文件

#import "ZYPerson.h"

@implementation ZYPerson

@end

ZYIoser:

//.h文件
#import "ZYPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface ZYIoser : ZYPerson

- (void)zySayHello;

+ (void)zySayGo;

@end

NS_ASSUME_NONNULL_END

//.m文件
#import "ZYIoser.h"
#import <objc/message.h>
@implementation ZYIoser

- (void)zySayHello
{
    NSLog(@"%s",__func__);
}

+ (void)zySayGo
{
    NSLog(@"%s",__func__);
}
@end

动态方法决议-对象方法

我们回到main.m文件写这样一句代码:

#import <Foundation/Foundation.h>
#import "ZYIoser.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        ZYIoser *ioser = [ZYIoser alloc];
        //调用父类ZYPerson 的zyEatSugar 方法 ,该方法未实现
        [ioser zyEatSugar];
       
    }
    return 0;
}

运行结果:

2021-07-16 15:28:44.672794+0800 KCObjcBuild[3378:55054] -[ZYIoser zyEatSugar]: unrecognized selector sent to instance 0x10123b7a0
2021-07-16 15:28:44.675195+0800 KCObjcBuild[3378:55054] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ZYIoser zyEatSugar]: unrecognized selector sent to instance 0x10123b7a0'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff205e398b __exceptionPreprocess + 242
    1   libobjc.A.dylib                     0x00000001002fbec0 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff206663ad -[NSObject(NSObject) __retain_OA] + 0
    3   CoreFoundation                      0x00007fff2054b9bb ___forwarding___ + 1448
    4   CoreFoundation                      0x00007fff2054b388 _CF_forwarding_prep_0 + 120
    5   KCObjcBuild                         0x0000000100003b20 main + 64
    6   libdyld.dylib                       0x00007fff2048bf3d start + 1
    7   ???                                 0x0000000000000001 0x0 + 1
)
libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ZYIoser zyEatSugar]: unrecognized selector sent to instance 0x10123b7a0'
terminating with uncaught exception of type NSException
(lldb) 

这个错误在开发过程中我们经常能看到,就是找不到方法的报错。那么我们不禁有想法,他这个报错到底是怎么出来的?或者说他是经过怎么样的查找和处理后才报出这个错误的呢?

我们回到我们之前在消息慢速查找流程中的一个函数lookUpImpOrForward。我们上一章探索到的当慢速查找imp找不到的时候就直接给imp赋值了一个forward_imp。然后就跳出了循环查找了。

lookUpImpOrForward:

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
//省略部分代码
    for (unsigned attempts = unreasonableClassCount();;) {
       //省略部分代码
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
//省略部分代码
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

从第一行代码

const IMP forward_imp = (IMP)_objc_msgForward_impcache;

我们就看到forward_imp就是_objc_msgForward_impcache,那我们看看_objc_msgForward_impcache到底里面有什么。

2.jpeg 3.png 4.png

到这里发现TailCallFunctionPointer方法直接返回了,所以还得返回上一句代码图3__objc_forward_handler去看看:

5.png

到这里就发现调用这个方法就直接提示出错误信息了。那在这个过程之前系统到底对消息做了哪些处理呢?我们返回前面的源码lookUpImpOrForward:

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

  //省略大量代码

    // No implementation found. Try method resolver once.
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
   //省略大量代码
   return imp;
}

在这个方法查找流程的for循环之后有上面这样一段代码,看注释我们可以知道是当imp找不到的时候尝试去resolver method 一次。我们跑一下源码验证下是不是真的当imp找不到的时候走这里。我们lldb调试下:

6.png 7.png

按照上图的验证发现确实是当调用了没实现的方法zyEatSugar的时候调用了resolveMethod_locked方法。在这之前走了这样一句代码:

 if (slowpath(behavior & LOOKUP_RESOLVER)) {
     behavior ^= LOOKUP_RESOLVER;
/* method lookup */
enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
};

这句代码的意思是类似执行单例,在相同的流程里表示方法只会执行一次。这里的behavior是这个lookUpImpOrForward参数传进来的,并且溯源找到等于behavior = 3 (如图8)。

8.png

代码解析:if 判断behavior & LOOKUP_RESOLVER = 3 & 2 = 2; behavior ^= LOOKUP_RESOLVER = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 0`;

继续,所以我们跟过去看看到底对method做了怎样的处理。

resolveMethod_locked:

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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

方法解析:进入这个方法 先判断是否是元类,不是元类进入resolveInstanceMethod(inst, sel, cls);方法,是元类就进入eles,其实我们看到在eles里也是调用了 resolveClassMethod(inst, sel, cls);resolveInstanceMethod(inst, sel, cls);。处理完成后最后调用lookUpImpOrForwardTryCache再次进行一次方法查找。

其实在进入这个方法前我们已经查找消息的快速查找流程和慢速查找流程。都没有找到这个方法,按理说已经知道找不到这个方法了,为了不直接进行报错处理,这里相当于给了我们再一次容错的处理。

接下来我们先看看resolveInstanceMethod

resolveInstanceMethod:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, 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 = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved  &&  PrintResolving) {
//省略部分展示信息的代码
}
   
}

代码解析:
SEL resolve_sel = @selector(resolveInstanceMethod:);
定义一个方法。resolveInstanceMethod
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; bool resolved = msg(cls, resolve_sel, sel);
然后系统自动给cls 这个类 发送一个消息resolve_sel。如果这个消息发送成功 表示你在你的类里 对resolve_sel这个消息有做处理的话就再次走下面的代码
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
在当前类的这个方法列表里去查找一遍方法。

接下来我们就用代码来验证一下resolveInstanceMethod这个方法。因为这里我们发现msg(cls, resolve_sel, sel);是往cls类里直接发送消息所以这个方法应该是一个+方法然后运行:

9.png

从上图我们看到在报错前调用了我们的这个方法。意味着我们能在报错前对这个报错的方法进行处理。比如给这个方法动态添加一个imp 或者实现。

10.png

发现不报错了,而且调用了我们的实例方法zySayHello。所以这就给了我们一次容错的机会。

我们跟踪下resolveInstanceMethod方法,发现系统自动实现了返回的是NO。所以这就解释了在上面的static void resolveInstanceMethod(id inst, SEL sel, Class cls)里第一个if判断,如果来的是resolveInstanceMethod就不会进if而是直接往下走。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
 //省略部分代码 
//这里的if 不会走
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }
//省略部分代码
}

动态方法决议-类方法

resolveMethod_locked 这个方法 在上面对象方法的动态决议中看到有对类方法的动态决议处理即eles流程。下面我们再去看看 类方法的处理。

resolveMethod_locked:

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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

我们跟踪下类方法的动态决议方法resolveClassMethod

** resolveClassMethod**

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(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 = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
       //省略部分错误信息代码
    }
}

代码解析:
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
判断resolveClassMethod是否存在
Class nonmeta;{}
因为类方法存在元类里,所以这里针对元类方法做些处理。
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
然后系统自动给cls 这个元类 发送一个消息resolveClassMethod。如果这个消息发送成功 表示你在你的元类里 对resolveClassMethod这个消息有做处理的话就再次走下面的代码
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
在当前元类的这个方法列表里去查找一遍方法。

所以我们接下来仿照我们在对象方法决议的过程中一样,代码验证和实现下。流程如下:

直接调用父类ZYPerson未实现的类方法+ (void)sayHappy报错如下:

11.png

在子类ZYIoser里实现resolveClassMethod

12.png

对方法进行实现,利用ZYIoser类的类方法zySayGo来替代未找到的方法sayHappy,因为类方法存在元类里所以我们取方法的imp 以及添加方法的时候都从元类里获取和处理。

13.png

这样我们就实现了类方法的动态决议。

拓展:

但是到这里我们有一个疑问,就是在对类方法动态决议调用之后又进行了一次lookUpImpOrNilTryCache判断,然后又走了一次对象方法决议,这个的作用是什么呢?

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
}
}

其实这里就会再次回归到苹果官方的isa走位图。类方法是以对象方法的形式存在元类里,所以类方法还有一个走位图就是元类(对象方法)->根元类(对象方法)-> 根元类(对象方法)-> NSObject。**

所以我们就可以直接去NSObject类里处理动态决议,单独以对象方法的动态决议处理所有动态决议(对象方法、类方法)。我们去NSObject+ZY文件去实现:

14.png 15.png

动态方法决议之后的流程探索:

我们在lookUpImpOrForward方法里看到当我们动态决议后是没有接续流程的。所以我们并没有办法直接从这里找到下一步系统是如何进行以下流程的。我们这个时候可以看看系统为我们保存的一些消息发送的处理日志。我们到objc源码全局搜索instrumentObjcMessageSends

16.png 17.png

这样我们就找到系统对消息发送保存的日志。我们创建一个工程然后调用一次没有实现的方法使他报错然后进去这个日志文件看看。

工程ZYProjectTenth000:

ZYPerson.h:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
@interface ZYPerson : NSObject
- (void)sayHello;
+ (void)say666;
@end
NS_ASSUME_NONNULL_END

ZYPerson.m:

#import "ZYPerson.h"
@implementation ZYPerson
@end

main.m:

#import <Foundation/Foundation.h>
#import "ZYPerson.h"

// 慢速查找
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        instrumentObjcMessageSends(YES);
        ZYPerson *person = [ZYPerson alloc];
        [person sayHello];
        instrumentObjcMessageSends(NO);
        
        NSLog(@"Hello, World!");
    }
    return 0;
}

我们运行代码会报错,这时候我们commad+shift+G全局搜索刚查到的文件路径/tmp/msgSends找到我们这次报错的消息日志缓存文件。

18.png 19.png 20.png

从上面的日志我们发现,在调用resolveInstanceMethod方法后又调用了forwardingTargetForSelectormethodSignatureForSelector。甚至后面的doesNotRecognizeSelector.这样是不是我们可以查找下这几个方法看看是否是我们想要看到的东西呢?我们下一篇接着探索。

文章到此就告一段落,后期如果有新的问题或者补充会在后期加上来。

遇事不决,可问春风。站在巨人的肩膀上学习,如有疏忽或者错误的地方还请多多指教。谢谢!

相关文章

  • 消息动态决议

    前言 前面我们分析了消息的查找流程,快速查找流程,慢速查找流程。如果这两种方式都没找到方法的实现,苹果给了两个建议...

  • 消息动态决议

    我们上一章针对[消息的慢速查找](https://www.jianshu.com/p/f03392ffe0b3[h...

  • 消息动态决议

    我们先从lookUpImpOrForward看起 realizeAndInitializeIfNeeded_loc...

  • 消息的动态决议

    当lookupImpOrForward函数从cache和methodTable中找不到对应Method,继续向下执...

  • runtime简单笔记

    动态特性: 动态类型、动态绑定、动态方法决议、动态加载、内省 编译器会把[接收器 消息]形式的对象消息,转换为含有...

  • Runtime — 动态方法决议

    前言 在 消息发送 中,当查不到方法时,会进行动态方法决议,下面我们就来具体分析一下动态方法决议过程中,系统如果操...

  • 九、消息流程之动态决议

    在消息经过慢速查找之后还没有找到,就会走到resolveMethod_locked进行消息动态决议,看一下reso...

  • 消息转发之动态方法决议 & 消息转发

    前言 前面的两篇文章我们已经探索了消息的快速查找和慢速查找的流程。 objc_msgSend 流程之缓存查找[ht...

  • 十、消息流程—动态方法决议 & 消息转发

    主要内容:objc_msgSend的快速查找和慢速查找,都没有找到方法时,苹果给了挽救的机会:一、动态方法决议1....

  • iOS Objective-C 消息的转发

    iOS Objective-C 消息的转发 1.动态方法决议(解析) 在上一篇消息查找的文章中我们在消息查找中没有...

网友评论

      本文标题:消息动态决议

      本文链接:https://www.haomeiwen.com/subject/lyrmpltx.html