美文网首页iOS
Objctive-C方法调用实质(源码分析)

Objctive-C方法调用实质(源码分析)

作者: 顶级蜗牛 | 来源:发表于2021-12-04 21:17 被阅读0次

    前言

    我们知道Objctive-C方法调用实际是给一个对象发送消息。从这句话我们不难看出必要的条件有消息的接收者,和怎么发送消息。
    消息的发送过程是通过sel找到imp

    Runtime框架是一套运行时api,底层是用c/c++/汇编写的,提供给Objective-C/swift等使用

    探究编译后的.cpp文件

    工程准备:新建一个工程

    新建工程.png

    工程开启消息发送配置:target -> Build Settings -> Apple Clang - Preprocessing -> Enable Strict Checking of objc_msgSend Call 设置为NO

    声明一个WJPerson类

    #import <Foundation/Foundation.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface WJPerson : NSObject
    - (void)run;
    - (void)walk;
    + (void)say;
    @end
    NS_ASSUME_NONNULL_END
    
    #import "WJPerson.h"
    @implementation WJPerson
    - (void)run{
        NSLog(@"%s",__func__);
    }
    - (void)walk{
        NSLog(@"%s",__func__);
    }
    + (void)say {
        NSLog(@"%s",__func__);
    }
    @end
    

    main.m

    #import <Foundation/Foundation.h>
    #import "WJStudent.h"
    #import <objc/message.h>
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            WJPerson *p = [[WJPerson alloc] init];
            [p run];
        }
        return 0;
    }
    

    得到编译后的main.cpp
    打开终端cdmain.m路径

    // 编译
    clang -rewrite-objc main.m -o main.cpp
    
    main.cpp文件.png

    可以看到main函数编译后的代码,方法调用其实就是objc_msgSend函数给对象发送消息。

    核心疑问:怎么通过sel找到imp

    objc_msgSend两种查找imp方式:
    1.快速 -> 缓存里找 汇编cache_t 方法实现imp哈希表
    2.慢速 -> c/c++ 找缓存

    我们打开源码 objc4-756.2

    第一部分“汇编环节”

    objc_msgSend 底层进入到汇编的源码 entry _objc_msgSend

    _objc_msgSend.png

    再到LNilOrTagged都做了些什么

    LNilOrTagged.png

    我们先看LReturnZero

    LReturnZero.png

    检测指针如果为空,就立马返回。结论:给nil发送消息不会做处理。

    再看LGetIsaDone

    image.png

    透过isa找到当前对象的objc_class对象,从而找到Cache_t进行缓存查找

    LGetIsaDone.png

    CacheLookup是一个宏定义

    CacheLookup.png

    看出来CacheLookup里有三步操作:
    1.CacheHit: 缓存击中,即找到了imp
    2.CheckMiss: CacheHit没有的情况下做的操作
    3.add: 在别的地方(如函数表里)找到了imp,就添加到缓存里。

    第一步CacheHit的三种方式:
    1.NORMAL:
    2.GETIMP
    3.LOOKUP

    CacheHit.png

    自行看注释就好,我们重点在call imp。所以objc_msgSend在汇编部分是从缓存中获取imp。实际情况是objc_class结构体里的Cache_t里拿到imp。

    第二步CheckMiss的三种方式:
    1.NORMAL:
    2.GETIMP
    3.LOOKUP

    image.png

    请看__objc_msgSend_uncached是不是有一点熟悉的感觉了,我们再看里面做什么操作:

    __objc_msgSend_uncached.png MethodTableLookup.png

    我们再搜一下__class_lookupMethodAndLoadCache3就找不到了,至此查找imp汇编部分就结束了,接下来到了漫长查找过程:c/c++环节。

    第三步add是在MethodTableLookup对类进行查找方法列表找到后进行添加到缓存操作。

    第二部分“c/c++环节”

    然后我们全局搜索这个class_lookupMethodAndLoadCache3就找到了这个c函数,诶嘿!在混编环节在缓存Cache_t里找不到imp,继续进入c/c++环节继续找imp。

    class_lookupMethodAndLoadCache3.png

    注意这里的参数为什么是填 YES NO YES呢?
    看参数名对应的是:初始化、读缓存、解析器
    大概不难猜到我们这里cache已经在汇编阶段做过判断了,就不需要再设置为YES了。

    接着我们来看一下这个lookUpImpOrForward函数到底做了啥事儿(分段截图,截图不完):

    lookUpImpOrForward1.png

    这里又查了一次缓存里的imp,注意是一定查不到imp的,因为我们上面就已经做了同样的查找了。
    接着继续看:

    lookUpImpOrForward2.png

    这里判断类是否存在,如果不存在要做初始化。

    lookUpImpOrForward3.png

    做的操作是递归!干嘛呢?
    1.又做了一次缓存查询,这一次主要做的工作是remap哈希表 重映射;
    2.对类方法列表查询,对父类的方法列表查询,return imp并且缓存到汇编环节的Cache_t吧。 这里的log_and_fill_cache

    请看isa查找方法流程:

    方法查找流程.png

    如果类和父类的方法列表里都找不到imp呢?
    请往下看代码

    lookUpImpOrForward4.png

    如果类和父类的方法列表里都找不到imp!就进入动态方法解析消息转发
    并且动态方法解析只会执行一次。
    至此c/c++查询imp环节就结束了。

    第三部分动态方法解析

    看到上一张截图的函数resolveMethod。它会去
    call两个方法,看下面截图:

    resolveMethod.png

    案例:我声明一个WJPerson类,给予其.h文件指定一个+(void)walk;声明,但没有在.m文件实现方法。在main函数体里面进行调用 [WJPerson walk];
    运行这段代码,会产生崩溃。崩溃原因呢是没有找到这个walk方法:unrecognized selector sent to class 0x100004378'
    方法调用走到这里,就是经历过上面两个查找imp环节都没有找到imp。
    那我们是否可以救一下呢?答案是,可以的。因为我们还有动态方法解析和消息转发。

    此时我在WJPerson去实现一个方法:

    + (BOOL)resolveClassMethod:(SEL)sel {
        NSLog(@"来了");
        return [super resolveClassMethod:sel];
    }
    

    再次运行调用[WJPerson walk]; 会发现我们捕获到了方法解析流程。并且来了!会打印两次!!

    image.png

    为什么是两次呢?我们打点点到NSLog那行,重新运行。第一次断点处调试查看堆栈:

    bt1.png

    从堆栈可以看到,调用顺序和我们之前分析的一毛一样:会进入到源码里的resolveClassMethod

    image.png

    再看源码,这里可以看出为什么会打印两次了。系统自动给我们发了一条消息。

    然后再看第二次log的堆栈

    image.png

    看一下这个方法调用结构,就是著名的消息转发流程

    消息转发流程.png

    上面的补救措施:

    + (BOOL)resolveClassMethod:(SEL)sel {
        if (sel == @selector(walk)) {
            // 我们动态解析我们的 对象方法
            NSLog(@"类方法解析走这里");
            SEL hellowordSEL = @selector(helloWord);
    //        Method hellowordM1= class_getClassMethod(self, hellowordSEL);
            Method hellowordM= class_getInstanceMethod(object_getClass(self), hellowordSEL);
            IMP hellowordImp = method_getImplementation(hellowordM);
            const char *type = method_getTypeEncoding(hellowordM);
            NSLog(@"%s",type);
            return class_addMethod(object_getClass(self), sel, hellowordImp, type);
        }
        return [super resolveClassMethod:sel];
    }
    

    特别注意:
    上面有一段代码注释了我们拿出来说:

    Method hellowordM1= class_getClassMethod(self, hellowordSEL);
    Method hellowordM= class_getInstanceMethod(object_getClass(self), hellowordSEL);
    

    第一句的self指的是WJPerson类对象,意在给类对象添加类方法。
    第二句object_getClass(self)是WJPerson的元类,意在给元类添加实例方法。
    这两句代码其实是等价的
    因为类方法以实例方法的形态存储于元类的方法列表

    举例:

    /*
     * 1.WJPerson声明一个类方法+(void)walk,但是在.m没有实现它
     * 2.而在NSObject的分类里 实现了实例方法-(void)walk
     * 3.此时调用[WJPerson walk];是不会崩溃的!!!
     */ 
    [WJPerson walk]; 
    // 因为类方法以实例方法的形态存储于元类的方法列表
    // 如果元类的方法列表没有找到,则去找根元类,直到找到NSObject为止。
    
    

    我们再来看一下isa的走位图:

    isa走位流程.png

    再回来看我们的动态方法解析的源码:

    image.png

    以上我们差不多得出动态方法解析的大致流程,然后我们分实例方法和类方法的动态方法解析做分析。

    动态实例方法解析

    案例:声明一个WJPerson类继承NSObject,给它声明一个实例方法 -(void)say; 不写say方法实现
    分析方法调用:[[WJPerson alloc] say];

    1.缓存查找,没有找到
    2.类查找/父类查找,
    3.动态方法解析_class_resolveMethod,再去找cls的元类是否实现了+(Bool)resolveInstacenMethod:(SEL)sel
    4.如果还是没有实现,再去找根元类,直到找到NSObject的+(Bool)resolveInstacenMethod:(SEL)sel方法实现
    5.系统主动给类对象发送一条消息,为了补救imp没有找到的错误,可以在我们的类里的+(Bool)resolveInstacenMethod动态给类添加imp去绑定sel与imp
    6.接着还会往类里通过sel查找一次imp,最终向上做缓存

    接下来看源码:

    如果cls类对象不是元类,就会进入_class_resolveInstanceMethod方法里

    image.png

    如果当前类实现了+(Bool)resloveInstanceMethod:(SEL)sel再次调用一次方法列表递归查找imp lookUpImpOrNil肯定能找到imp,就来看方法体里做了啥事儿。

    注意这一步,如果当前类没有实现+(Bool)resloveInstanceMethod:(SEL)sel再次调用一次方法列表递归查找imp lookUpImpOrNil,这一次传递的参数是isa(元类)resloveInstanceMethod,查找是否实现了实例动态方法解析,如果没有实现,就不会再次进入_class_resolveMethod里,防止死递归。

    它是怎么防止死递归的呢?

    1.元类对象里找resloveInstanceMethod的实现;
    2.若元类没找到,则往元类的继承链里找根元类,最后到NSObject;
    3.NSObject肯定是有实现这个resloveInstanceMethod的,只是直接return NO;
    

    接着看方法体做了啥事儿:发送一条消息,即调用元类/根元类里的resolveInstanceMethod的实现。

    image.png

    系统会自发一条消息,来到我们类.m文件里的重写resloveInstanceMethod方法实现,让其sel与imp做一个绑定

    + (BOOL)resolveClassMethod:(SEL)sel {
        if (sel == @selector(walk)) {
            // 我们动态解析我们的 对象方法
            NSLog(@"类方法解析走这里");
            SEL hellowordSEL = @selector(helloWord);
    //        Method hellowordM1= class_getClassMethod(self, hellowordSEL);
            Method hellowordM= class_getInstanceMethod(object_getClass(self), hellowordSEL);
            IMP hellowordImp = method_getImplementation(hellowordM);
            const char *type = method_getTypeEncoding(hellowordM);
            NSLog(@"%s",type);
            return class_addMethod(object_getClass(self), sel, hellowordImp, type);
        }
        return [super resolveClassMethod:sel];
    }
    

    那么问题来了,这里我只是返回Bool并且sel与imp绑定了,并没有返回imp出去,是怎么能调用成功的呢?
    答案是:重新再通过sel找一次imp,它会return imp; 期间还会对imp进行缓存的,来看:

    image.png
    动态类方法解析

    案例:声明一个WJPerson类继承NSObject,给它声明一个实例方法 +(void)walk; 不写walk方法实现
    分析方法调用:[WJPerson say];

    1.缓存查找,没有找到
    2.元类/根元类的方法列表找-(void)say,没有找到
    3.动态方法解析_class_resolveMethod,再去找元类是否实现了+(Bool)resolveClassMethod:(SEL)sel
    4.如果还是没有实现,就去找元类->根元类直到找到NSObject肯定是有实现+(Bool)resolveInstanceMethod:(SEL)sel
    5.给元类发送一条消息(这里苹果做了处理,下面做源码分析),让sel+imp绑定

    接下来看源码:

    image.png

    先是从元类->根元类->NSObject里的方法列表找+(Bool)resolveClassMethod:(SEL)sel的方法实现

    image.png

    上面看到系统自发了一条消息,其中_class_getNonMetaClass是处理元类返回类对象的,什么个操作呢?看下图:

    image.png

    看代码注释的逻辑,当我们调用[WJPerson walk];的时候,这里对元类的处理,返回的是WJPerson这个类对象:

    image.png image.png

    所以在_class_resolveClassMethod这个函数里系统发送一条消息,还是发送给类对象。接着系统自动往方法列表里再通过sel查找imp一遍(lookUpImpOrNil)
    所以找到WJPerson类.m里边去实现+(Bool)resolveClassMethod:(SEL)sel 来处理没有实现的类方法。
    程序继续往下走:

    image.png

    这个判断是,如果我们开发者没有重写去实现这个+(Bool)resolveClassMethod:(SEL)sel,那就会进入这个if体里边:_class_resolveInstanceMethod

    这就跟上面的动态实例方法解析走一样的流程,但是注意这里的cls是元类

    接下来的操作就是,去根元类 -> NSObject方法列表查找+(Bool)resolveInstanceMethod:(SEL)sel是否实现,最后在NSObject找到。
    接着系统给元类发送一条消息SEL_resolveInstanceMethod,并且最终通过lookUpImpOrNil使用sel去查找imp一次。

    不管是调用实例方法还是类方法(都没有写方法实现的情况下),都可以通过在NSObject的分类里重写+(Bool)resolveInstanceMethod:(SEL)sel捕获到,动态添加imp

    消息转发

    resolveInstanceMethod / resolveClassMethod 返回NO就会进入消息转发。

    lookUpImpOrForward查找imp函数里边的动态方法解析下面,就有消息转发:

    image.png

    其中_objc_msgForward_impcache就是消息转发的流程;又到了我们的源码汇编阶段:

    image.png image.png

    __objc_forward_handler又跑回了c

    image.png

    看到上面截图的报错吗,是不是和我们没有找到imp方法是崩溃报错是那么熟悉。

    消息转发过程只有汇编在调用,消息转发是调用CoreFoundation的代码没有开源!看不到函数调用过程了。但是可以想办法让它开源。让它反汇编出来:

    首先我们找到这个路径下的CoreFoundation文件,把它复制到桌面

    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
    

    下载安装一个反编译软件 Hopper Disassembler v4,将CoreFoundation文件拖入反编译软件中得到反编译代码

    接下来看两张图
    第一张是我们Xcode方法调用时没有写方法实现,并且没有做动态方法解析成功的报错

    Xcode方法调用报错

    第二张是反编译软件的代码

    image.png

    点进去__forwarding_prep_0__

    image.png

    再点进去看__forwarding__到底做了什么事

    __forwarding__

    得到了汇编里的__forwarding__很长的一段代码,发现了forwardingTargetForSelector

    如果类里没有实现forwardingTargetForSelector就会 goto loc_12ca57
    之后就会进入慢速转发环节:

    image.png

    就会做僵尸对象判断,然后继续往下走,拿到方法签名sel: methodSignatureForSelector,如果响应了方法名称还会继续往下走,做一些方法签名的判断等等,接着

    image.png image.png

    这就是消息转发流程的汇编的调用顺序。
    所以我们消息转发可以进行如下处理:

    #pragma mark - 动态消息转发流程 ---- 只有汇编进行调用
    // 消息快速转发
    + (id)forwardingTargetForSelector: (SEL)aSelector {
        return [super forwardingTargetForSelector:aSelector];
    }
    
    // 消息慢速转发
    + (NSMethodSignature *)methodSignatureForSelector: (SEL) aSelector {
        if (aSelector == @selector(walk)) {
            return [NSMethodSignature signatureWithObjCTypes:@"v@:@"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    + (void)forwardInvocation: (NSInvocation *)anInvocation {
        NSString *str = @"我是xxx";
        anInvocation.target = [WjStudent class];
        [anInvocation setArgument:&str atIndex:2];
        anInvocation.selector = @selector(run:);
        [anInvocation invoke];
    }
    
    消息转发流程

    以上就是方法调用的实质分析。
    objc_msgSend

    相关文章

      网友评论

        本文标题:Objctive-C方法调用实质(源码分析)

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