iOS 类的原理分析 (二)

好好学习,不急不躁,每天进步一点点;

上一文章中,我们分析了类的部分原理,了解到,isa的走位图,以及类的继承与类的内部结构;

isa的走位,可以简单的概括为:isa —> 类 —-> 类 isa —-> 元类 —-> 根元类;

类的继承关系链: 类 —> 父类 —-> 根元类(NSObject)—-> nil;

我们通过获取类的 class_data_bits_t,查看到实例方法与属性的存储位置,是存储在class_data_bits_t里的 data的中,我们通过data里的properties() 和 methods()方法,可以查看,但是却没有看到类方法,和成员变量;接下来,我们分析一下,类方法,和成员变量存放在哪里?

类的存储结构

在介绍类的存储结构之前,我们先来了解一下,苹果对类是如何定义与存储的;

苹果针对 runtime 底层的讲解视频

我们通过上述视频,可以看到苹果针对class的存储结构;

类存储在磁盘中,他的存储结构如下图:

截屏2021-06-26 下午1.58.28.png

当类第一次从磁盘加载到内存时,类一开始的结构就是如此,但一经使用,他们就会改变;
在了解变化之前,我们需要先了解 clean memory 和 dirty memory;

Clean Memory 与 Dirty Memory

clean memory

是指:加载后,不会发生更变的内存,class_ro_t就属于 clean memory,因为他是只读的;clean memory 可以进行移除,节省更多内存空间,如果需要,系统会从磁盘重新加载

dirty memory

是指:进程运行时,会发生改变的内存。类一经使用,就会变成 dirty memory,因为运行时,会向它写入新的数据

dirty memory 比 clean memory 贵,只要进程在运行,它就必须一直存在,dirty memory 是将类数据分成两部分的重要因素,可以保持清洁的数据,越多越好,通过分离出那些不会更变的数据,把大部分存储在clean memory

虽然上面的数据结构,足够程序使用,但是运行时,需要追踪每个类的更多信息,所以当一个类首次被使用,运行时会为它分配额外的存储容量,就是 class_rw_t,用于读写数据

截屏2021-06-26 下午2.15.15.png

但苹果根据调研发现,只有10%的类真正改变了它方法,所以苹果又将 rw结构,进行拆分;

截屏2021-06-26 下午2.39.42.png

所以,我们想查看更多的数据,可以从ro中查看;

ivars.png

get.png

类方法的读取

通过对类的内存布局,属性以及成员变量的了解,我们了解到了属性与成员变量的存储,与对象方法的存储,他们的存储位置是不一样的,但他们都存在里面;可以避免多份浪费内存;

而在上一篇文章中,我们通过获取methods,可以读取到对象方法,也就是 - (void) sayHi 这个方法;
而且我们始终没有看到 类方法,那类方法又是存储在哪里?还是说,类方法,并不存在?

在之前的文章中,我们可以通过MachOView,了解到元类,现在我们再次使用MachOView,查看是否存在类方法;

将项目工程,拖入MachOView中,查看function starts,可以看到,+sayNB这个类方法,那么可以确定,类方法是存在的;

截屏2021-06-29 下午11.43.12.png

可我们要如何获取这个类方法呢?

我们都知道,所谓的类方法,对象方法,都是OC上层的一种称呼,而在底层C++,是没有这种区分的,都是统一的称为函数,
如果 oc 类里,有两个名称一样的方法,那在底层C++中,让程序如何去区分呢?

如下两个方法:

- (void)sayNB;
+ (void)sayNB;

如果这两个方法,都存储在类里里面,C++底层,要如何区分这两个函数?编译器要如何寻找?
复制代码

所以苹果为了能够区分这种情况,元类的作用,就来了;既然类方法,不能存储了,那么就存储到元类中吧

既然我们知道,类方法存储在元类里,那么我们需要通过当前类,去获取它的元类,再通过元类地址,获取元类内存信息;

截屏2021-06-30 上午9.48.49.png

获取.png

成员变量/属性/实例变量

实例变量.png

成员变量:写在类声明的大括号中的变量,称为成员变量;

属性:写在大括号之外的,默认使用@property声明的变量,称为属性;

实例变量:实例变量是一种特殊的成员变量,它是以对象为类型的一种成员变量;

我们通过 .cpp 文件,分析一下这些变量与属性的底层结构;

.cpp 文件的生成方式:

cpp.png

cpp1.png

可以看到,在C++文件中,属性都会被优化掉;变成带下划线的成员变量;那么带下划线的成员变量与不带下划线的有什么区别呢?

set.png

在 C++ 文件中,属性与成员变量的区别就是,属性会自动生成get/set方法;

在C++文件中,我们可以看到,类似"@16@0:8"或是"v24@0:8@16" 的编码结构,我们给大家讲解一下如何理解这些编码结构:

查找编码的方式

command + shift + 0,进入 apple developer documentation,输入关键字ivar_getTypeEncoding搜索 ,

截屏2021-06-27 下午7.23.15.png

type.png

截屏2021-06-27 下午7.28.35.png

编码内容解析

截屏2021-06-27 下午7.37.22.png

set_property

在C++文件中,明显看到 name 与 nickName,get方法存在区别,nickName多了一个objc_setProperty()方法;我们都知道,在OC中,我们给一个属性赋值或者是取值,执行的是 set/get 方法,但是 C++文件,nickName 的 set 方法执行后,接下来执行的是objc_setProperty() ,为什么要执行这一步呢?有什么作用吗?

多了.png

在OC中,每一个对象的成员变量,都可以使用 set 方法为其进行赋值,都会执行setXXX方法,每一个成员变量都是不一样的,那么就会导致出现很多setXXX方法;但是在苹果底层,它实际的操作,就是对底层内存进行赋值而已,如果苹果上层传递到底层的赋值方法,五花八门,那么苹果底层就需要适配这些五花八门的方法,这对底层源码结构来说,是非常不合理的;

所以就出现了一个中间转化层,通过 objc_setProperty 将上层的方法进行统一的封装,下层就接收和解析setProperty就可以;

上层与中间层之间,需要做一些动态处理,将不同的方法名称,动态指向objc_setProperty;但是我们一直没有见过 objc_setProperty 这个方法,那么它是怎么处理的?又是在哪里处理的呢?同时,不同的setXXX方法,怎么才能合理的指向objc_setProperty这个方法呢?

可以通过 ivar 作为指向入口, 在内存编译的时候,将ivar加载进来,在类的加载中,获取ivar这些数据,通过sel定向找到imp,因为这些imp都是未实现的,所以可以将这些imp重定向指向objc_setProperty,那么底层又是如何重定向的呢? 接下来,我们通过LLVM,查看一下setProperty的流程;

LLVM setProperty 流程

首先,需要知道setProperty这个函数方法,在底层是如何获取的

截屏2021-06-28 下午11.21.48.png

截屏2021-06-28 下午11.33.33.png
我们不知道什么时候会触发 objc_setProperty方法,也不知道出发条件是什么? 那么我们往回寻找一下,看看是什么条件,可以出发这里的函数;

截屏2021-06-28 下午11.42.27.png

可以看到,switch的内部进行的是一些列的内存平移,数据赋值等操作,但是switch的判断条件,是如何产生的,这个判断条件才是真正触发objc_setProperty的主要因素,所以我们需要查询 switch的判断条件是如何产生的;

截屏2021-06-28 下午11.55.55.png

结论

从底层代码可以,如果成员变量是copy修饰词,则会生成setProperty方法;

类的加载

那么有没有人想过,类是怎么来的?在底层,它又是如何实现的?今天我们就来探索一下,类的来源;

在上一篇文章中,不知道有没有人注意过,data()里的firstSubclass 为空呢?即使QLYPerson存在子类的, firstSubclass还是为nil;

有.png

我们试试打印,QLYStudent,看是否存在;

chuxianl.png

我们打印 QLYStudent,可以明显看到,QLYStudent是存在的。并且再次打印 $2的时候,firstSubclass却有值了。这是什么原理呢?是如何加载的呢?

结语

其实,类的加载是一种懒加载,后续文章,咱们继续补充

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享