阅读 880

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

前言

前面探究了类里面的重要的变量,IOS 底层原理之cache分析分析了缓存方法调用流程。追根溯源找到了objc_msgSend,下面探究下objc_msgSend

准备工作

Runtime

Runtime简介

Runtime通常叫它运行时,还有一个大家常说的编译时,它们之间的区别是什么

  • 编译时:顾名思义正在编译的时候,啥叫编译呢?就是编译器把源代码翻译成机器能够识别的代码。编译时会进行词法分析,语法分析主要是检查代码是否符合苹果的规范,这个检查的过程通常叫做静态类型检查
  • 运行时代码跑起来,被装装载到内存中。运行时检查错误和编译时检查错误不一样,不是简单的代码扫描,而是在内存中做操作和判断

Runtime版本

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

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

Runtime调用三种方式

  • Objective-C方式,[penson sayHello]
  • Framework & Serivce方式,isKindOfClass
  • Runtime API方式,class_getInstanceSize

image.png

方法的本质

方法底层的实现

探究方法的底层有两种方式。第一种汇编,第二种C++代码。汇编方式的方法的参数需要读寄存器不方便,所以采用第二种方式生成main.cpp文件。首先自定义LWPerson类,在类中添加实例方法,在main函数中调用

int main(int argc, char * argv[]) {
 
    @autoreleasepool {
        LWPerson * perosn  = [LWPerson alloc];
        [perosn sayHello];
        [perosn showTime:10];
    }
    return 0;
}

复制代码

clangmain.m生成main.cpp文件,查询main函数的实现

image.png

源码分析:所有的方法调用都是通过objc_msgSend发送的,所以方法的本质就是消息发送

既然方法调用都是通过objc_msgSend的,那么我可不可以直接通过objc_msgSend发消息呢

int main(int argc, char * argv[]) {
    @autoreleasepool {
        LWPerson * perosn  = [LWPerson alloc];
        [perosn sayHello];
        //objc_msgSend(void /* id self, SEL op, ... */ )
        objc_msgSend((id)perosn, sel_registerName("sayHello"));
    }
    return 0;
}
复制代码
2021-06-26 18:14:22.659269+0800 objc_msgSend[5461:254082] sayHello
2021-06-26 18:14:22.659722+0800 objc_msgSend[5461:254082] sayHello
复制代码

通过objc_msgSend[perosn sayHello]结果是一样的,同时也验证了方法的本质是消息发送。在用objc_msgSend方式发送消息。验证过程需要注意两点

  • 必须导入相应的头文件#import <objc/message.h>
  • 关闭objc_msgSend检查机制:target --> Build Setting -->搜索objc_msgSend -- Enable strict checking of obc_msgSend calls设置为NO

调用父类方法

调用本类中的方法实际是通过objc_msgSend发送的,那么调用分类的方法消息发送是什么样的呢?自定义LWAllPerson类,LWPerson继承LWAllPerson类。在LWAllPerson类中自定义helloWord,子类对象调用helloWord方法

int main(int argc, char * argv[]) {
    @autoreleasepool {
        LWPerson * perosn  = [LWPerson alloc];
        [perosn helloWord];
    }
    return 0;
}
复制代码

clangmain.m生成mian.cpp文件,查询main函数的实现 image.png

clangLWPerson.m生成LWPerson.cpp文件,查询LWPerson函数的实现

image.png

子类对象可以通过objc_msgSendSuper方式调用父类的方法,方法的本质还是消息发送,只不过通过的不同发送流程,同样现在用objc_msgSendSuper向父类发消息,objc_msgSendSuper的第一个参数是void /* struct objc_super *super类型,在源码中查找objc_super 类型

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
复制代码

objc_super结构体类型里面有两个参数id receiverClass super_class

int main(int argc, char * argv[]) {
    @autoreleasepool {
        LWPerson * perosn  = [LWPerson alloc];
        //(void *)objc_msgSendSuper)((__rw_objc_super){(id)self,
        //(id)class_getSuperclass(objc_getClass("LWPerson"))}, sel_registerName("helloWord"))
        [perosn helloWord];
        struct lw_objc_super{
            id receiver;
            Class super_class;
        };
        struct lw_objc_super lw_super;
        lw_super.receiver = perosn;
        lw_super.super_class = [LWAllPerson class];
        objc_msgSendSuper(&lw_super, sel_registerName("helloWord"));
    }
    return 0;
}
复制代码
2021-06-26 19:21:05.047243+0800 objc_msgSend[6976:329613] 我是父类方法
2021-06-26 19:21:05.047989+0800 objc_msgSend[6976:329613] 我是父类方法
复制代码

[perosn helloWord]和直接通过objc_msgSendSuper给父类发消息的结过是一样的。子类的对象可以调用父类的方法。猜想:方法调用,首先在本类中找,如果没有就到父类中找

objc_msgSend汇编探究

探究objc_msgSend首先找到objc_msgSend所在的底层库。怎么找呢?必须拿出yysd-汇编

image.png

汇编显示objc_msgSendlibobjc.A.dylib系统库,实际上看objc_msgSend前缀是objc猜测应该在 objc源码中。在objc源码中全局搜索objc_msgSend,找到真机的汇编objc-msg-arm64.s

image.png

objc_msgSend汇编入口

下面的汇编会用到p0-p17,大家可能对汇编中x0x1比较熟悉知道是寄存器。p0-p17就是对x0-x17重新定义

image.png

判断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

image.png

GetClassFromIsa_p16核心功能获取class存放在p16寄存器

ExtractISA

// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
   ...
#else
   ...
.macro ExtractISA
	and    $0, $1, #ISA_MASK  // and 表示 & 操作, $0 = $1(isa) & ISA_MASK  = class
.endmacro
// not JOP
#endif
复制代码

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

CacheLookup流程

buckets和下标index

image.png 源码分析:首先是根据不同的架构判断,下面都是以真机为例。上面这段源码主要做了件事

  • 获取_bucketsAndMaybeMask地址也就是cache的地址:p16 = isa(class),p16 + 0x10 = _bucketsAndMaybeMask = p11
  • 获取buckets地址就是缓存内存的首地址:buckets = ((_bucketsAndMaybeMask >> 48 )- 1 )
  • 获取hash下标: p12 =(cmd ^ ( _cmd >> 7))& msak 这一步的作用就是获取hash下标index
  • 流程如下:isa --> _bucketsAndMaybeMask --> buckets -->hash下标

遍历缓存

image.png

  • 根据下标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流程

CacheHit \ModeMode = NORMAL image.png TailCallCachedImp是一个宏定义如下

// 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

遍历缓存流程图

疑问:为什么要判断bucket中的sel = 0,等于0直接查找缓存流程就结束了

image.png

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

mask向前遍历缓存

向前遍历缓存没有查询到就会跳转到mask对应的bucket继续向前查找 image.png

  • 找到最后一个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向前遍历和前面的循环遍历逻辑基本一样

缓存查询流程图

image.png

总结

探究底层发现一个问题,就是每个内容的底层都很复杂,进行了大量计算判断。不像大家平常在上层调用个方法看起来很简单。俗话说的好表面上简单的东西往往越复杂,表面上复杂的往往很简单。我就是表面复杂的。

文章分类
iOS
文章标签