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

前言

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

objc-cache.mm头部注释

准备资料

  • objc4-818.2 源码
  • Objective-C Runtime
  • arm汇编指令

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_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 运行时&方法的本质)