美文网首页
IOS底层原理之Runimte 运行时&方法的本质

IOS底层原理之Runimte 运行时&方法的本质

作者: 冼同学 | 来源:发表于2021-07-02 17:32 被阅读0次

    前言

    《cache底层分析》一文中详细得剖析了cache的底层原理以及其相关的流程。那么我们有没有留意到cahche调用insert方法之前做了哪些操作呢?哪些操作又是以什么形式传递的呢?
    那么查看objc-cache.mm文件的头部注释中写着insert()的插入时机是通过最上层的objc_msgSend触发的,如下图:

    objc-cache.mm头部注释

    准备资料

    runtime

    runtime定义

    编译时

    • 编译时 顾名思义就是正在编译的时候. 编译器把源代码翻译成机器能识别的代码(当然只是⼀般意义上这么说,实际上可能只是翻译成某个中间状态的语⾔)。编译时通过语法分析、词法分析等编译时类型检查(静态类型检查)来发现代码中的errorswarning等编译时的错误信息。
    • 静态检查不会把代码放内存中运⾏起来,⽽只是当作⽂本来扫描检查,⼀些⼈说编译时还分配内存啥的肯定是错误的说法。

    运行时

    • 运⾏时就是代码通过dyld被装载到内存中执行的过程。运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样。不是简单的扫描代码,⽽是在内存中做操作和判断

    runtime的版本

    runtime有两个版本,一个Legacy版本(早期版本),一个Modern版本(现行版本)。

    • 早期版本对应的编程接口:Objective-C 1.0
    • 现行版本对应的编程接口:Objective-C 2.0,源码中经常看到的OBJC2
    • 早期版本用于Objective-C 1.032位的Mac OS X的平台
    • 现行版本用于Objective-C 2.0iPhone程序和Mac OS X v10.5及以后的系统中的64位程序

    注意:runtime就是c/c++/汇编写的一套API

    runtime三种实现方式

    • Objective-C方式,[penson sayHappy]
      -Framework & Serivce方式,isKindOfClass
    • Runtime API方式,class_getInstanceSize
      runtime的实现方式

    方法的本质

    探究底层又两个方式,第一种就是看汇编代码,其次就是C/C++编译之后的代码。如果分析汇编代码的话会设计到寄存器数据的一系列读取操作,过程比较繁琐,那么我们就采用第二种方式来看看方法底层的实现是怎么样子的。首先编译生成main.cpp文件,然后自定义XXPerson类,在XXPerson类中添加实例方法,在main函数中调用如下:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
     XXPerson *person = [[XXPerson alloc]init];
            [person saySomething];
            [person sayHappy:@"happy!"];
     }
        return 0;
    }
    

    xrun导出main.cpp文件,查看到main函数的底层实现如下:

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            XXPerson *person = ((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("alloc")), sel_registerName("init"));
            ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
            ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("sayHappy:"), (NSString *)&__NSConstantStringImpl__var_folders_mq_n7r4vx491nz9b2b3wpmz1mg00000gn_T_main_12c37c_mi_1);
        }
        return 0;
    }
    

    分析:

    • 通过底层的代码,方法的实现是通过objc_msgSend来发送的。
    • 方法的本质其实就是消息的发送

    通过底层objc_msgSend来实现法法,情况如下:

    objc_msgSend调用方法
    • objc_msgSend能够调用类的方法,跟对象调用的结果一样。

    注意:

    • 运行项目之前必须导入<objc/message.h>头文件。
    • 关闭objc_msgSend检查机制:target --> Build Setting -->搜索objc_msgSend -- Enable strict checking of obc_msgSend calls设置为NO

    调用类方法

    创建XXPerson类方法sayNB,通过调用,已经底层的main.cpp可以得出一下代码:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
     XXPerson *person = [[XXPerson alloc]init];
            [person saySomething];
            [XXPerson sayNB];
     }
        return 0;
    }
    //底层代码
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            XXPerson *person = ((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("alloc")), sel_registerName("init"));
            ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
            ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("sayNB"));
        }
        return 0;
    }
    
    • 类方法的调用也是通过objc_msgSend来进行消息发送。

    注意:

    • 通过之前类结构的学习,底层是不分类方法跟实例方法的,只是查找方法的地方不一样(实例方法保存在本类钟,类方法保存在元类中)
    • 类方法其实就是元类的实例方法

    调用父类方法

    穿件XXTeacher类继承XXPerson类,并用XXTeacher实例调用父类的saySomething方法如下:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
     XXTeacher *teacher = [[XXTeacher alloc]init];
            [teacher saySomething];
     }
        return 0;
    }
    

    xrun导出main.cpp文件,查看底层代码实现

    main底层实现
    在用XXTeacher.m文件重写saySomething方法,然后用xrunXXTeacher.m生成XXTeacher.cpp文件,查询XXTeacher函数的实现:
    重写saySomething
    objc_msgSendSuper调用
    • 子类调用父类的方法可以通过objc_msgSendSuper来进行消息的发送,其本质也就是消息的发送
    • objc_msgSendSuper是通过向父类发送消息,与objc_msgSend流程有点不一样。

    objc_msgSendSuper的数据结构

    通过查找objc4的源码发现:

    //objc_msgSendSuper的定义
    objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
        OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
    

    查看参数objc_super的结构如下(提取__OBJC2__的部分):

    struct objc_super {
       //消息的接收者
        __unsafe_unretained _Nonnull id receiver;     
     //方法最先查找的class是super_class ,如果super_class查找不到会查找super_class的父类
        __unsafe_unretained _Nonnull Class super_class;  
    
    };
    

    objc_msgSendSuper代码案例

    通过objc_msgSendSuper的方式,调用XXPersonsaySomething方法:

    objc_msgSendSuper案例分析
    • objc_msgSendSuper能够向父类发送消息,调用父类的方法。
    • 方法调用,首先在本类中找,如果没有就到父类中找。(receiver只是指定调用的是谁,但是方法是在super_class找)

    objc_msgSend汇编探究

    首相我们在saySomething方法调用时候下个汇编断点,如下图:


    saySomething汇编实现

    然后我们进入objc_msgSend的汇编实现(打objc_msgSend的符号断点),如下图:


    objc_msgSend汇编实现
    • 汇编显示objc_msgSendlibobjc.A.dylib系统库。
    • objc_msgSend也可以在objc4的源码中找到。

    objc_msgSend在objc4源码中的实现

    到这里源码的实现,有些同学就会想到objc_msgSend可能是c或者是c++来实现的。可是实践告诉我们,objc_msgSend的底层实现在源码中是汇编语言。
    源码查找流程:在objc源码中全局搜索objc_msgSend,找到真机的汇编objc-msg-arm64.s

    查找图
    源码中寄存器的对应发生了一丢丢改变(如p0 = x0),为了方便理解方法体代码,如下图:
    寄存器的转换
    objc_msgSend入口汇编代码
    objc_msgSend汇编实现
    判断receiver是否等于nil, 再判断是否支持Taggedpointer小对象类型。
    • 支持Taggedpointer小对象类型,小对象为空 ,返回nil,不为nil处理isa获取class跳转CacheLookup流程 。
    • 不支持Taggedpointer小对象类型且receiver = nil,跳转LReturnZero流程返回nil
    • 不支持Taggedpointer小对象类型且receiver != nil,通过GetClassFromIsa_p16把获取到class存放在p16的寄存器中,然后走CacheLookup流程。

    GetClassFromIsa_p16获取Class汇编流程

    GetClassFromIsa_p16汇编实现
    GetClassFromIsa_p16核心功能获取class存放在p16寄存器。(那么就是着重看ExtractISA方法的实现)

    ExtractISA方法的汇编实现

    // A12 以上 iPhone X 以上的
    #if __has_feature(ptrauth_calls)
       ...
    #else
       ...
    .macro ExtractISA             //ExtractISA 主要功能 isa & ISA_MASK = class 存放到p16寄存器
        and    $0, $1, #ISA_MASK  // and 表示 & 操作, $0 = $1(isa) & ISA_MASK  = class
    .endmacro
    // not JOP
    #endif
    

    ExtractISA主要功能isa & ISA_MASK = class 存放到p16寄存器。

    重点:CacheLookup汇编实现流程

    《cache底层分析》一文中已经根据objc4底层源码分析过整个insert的流程了,那么通过CacheLookup汇编的形式来看看这个流程跟之前的是否能够衔接上,拭目以待!!

    buckets和下标index

    查找buckets与index
    源码分析:
    • 获取_bucketsAndMaybeMask地址也就是cache的地址:p16 = isa(class)p16 + 0x10 = _bucketsAndMaybeMask = p11
    • 获取buckets容器的首地址:buckets = _bucketsAndMaybeMask & 0xffffffffffff(maskShift不同架构也会不同)
    • 获取hash下标:p12 =(cmd ^ ( _cmd >> 7))& msak这一步的作用就是获取hash下标index

    流程:isa --> _bucketsAndMaybeMask -->buckets -->hash -->index

    遍历缓存

    遍历缓存
    源码分析:
    • 根据下标index 找到index对应的bucketp13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask) << (1+PTRSHIFT))
    • 先获取对应的bucket然后取出impsel存放到p17p9,然后*bucket--向前移动。
    • 1流程:p9= sel和 传入的参数_cmd进行比较。如果相等走2流程,如果不相等走3流程。
    • 2流程:缓存命中直接跳转CacheHit流程。
    • 3流程:判断sel = 0条件是否成立。如果成立说明buckets里面没有传入的参数_cmd的缓存,没必要往下走直接跳转__objc_msgSend_uncached流程。如果sel != 0说明这个bucket被别的方法占用了。你去找下一个位置看看是不是你需要的。然后在判断下个位置的bucket和第一个bucket地址大小,如果大于第一个bucket的地址跳转1流程循环查找,如果小于等于则接继续后面的流程。
    • 如果循环到第1bucket里都没有找到符合的_cmd。那么会接着往下走,因为下标index后面的可能还有bucket还没有查询。

    CacheHit

    // A12 以上 iPhone X 以上的
    #if __has_feature(ptrauth_calls)
       ...
    #else   //这是我们需要研究的
    .macro TailCallCachedImp
        // $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class(也就是isa)
        eor $0, $0, $3   // $0 = imp ^ class 这一步是对imp就行解码,获取运行时的imp地址
        br  $0           //调用 imp,意思是找到方法了并调用了
    .endmacro
    ...
    #endif
    

    缓存查询到以后直接对bucketimp进行解码操作。即imp = imp ^ class,然后调用解码后的imp

    遍历缓存流程图

    带着疑问:为什么sel = 0 的时候就直接跳出了缓存的查找呢?

    遍历缓存流程图
    分析得出:
    • 如果既没有hash冲突又没有目标方法的缓存,那么hash下标对应的bucket就是空的直接跳出缓存查找。
    • 不会出现中间是有空的bucket,两边有目标bucket这种情况。

    mask向前遍历缓存

    向前遍历缓存
    分析:
    • 找到最后一个bucket的位置:p13 = buckets + (mask << 1+3)找到最后一个bucket的位置。
    • 先获取对应的bucket然后取出impsel存放到p17p9,然后*bucket--向前移动。
    • p9= sel和 传入的参数_cmd进行比较。如果相等走2流程。
    • 如果不相等在判断(sel != 0 && bucket > 第一次确定的hash下标bucket)接着循环缓存查找,如果整个流程循环完仍然没有查询到或者遇到空的bucket。说明该缓存中没有缓存)sel = _cmd的方法,缓存查询结束跳转__objc_msgSend_uncached流程。
    • mask向前遍历和前面的循环遍历逻辑基本一样。

    缓存查询流程图

    缓存查询流程

    objc_msgSend流程图

    objc_msgSend流程

    相关文章

      网友评论

          本文标题:IOS底层原理之Runimte 运行时&方法的本质

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