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

2021-07-02  本文已影响0人  冼同学

前言

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

objc-cache.mm头部注释

准备资料

runtime

runtime定义

编译时

运行时

runtime的版本

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

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

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调用方法

注意:

调用类方法

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

注意:

调用父类方法

穿件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的数据结构

通过查找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_msgSend汇编探究

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


saySomething汇编实现

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


objc_msgSend汇编实现

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小对象类型。

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
源码分析:

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

遍历缓存

遍历缓存
源码分析:

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 的时候就直接跳出了缓存的查找呢?

遍历缓存流程图
分析得出:

mask向前遍历缓存

向前遍历缓存
分析:

缓存查询流程图

缓存查询流程

objc_msgSend流程图

objc_msgSend流程
上一篇下一篇

猜你喜欢

热点阅读