好好学习,不急不躁,每天进步一点点;
在上一文章中,我们分析了类的部分原理,了解到,isa的走位图,以及类的继承与类的内部结构;
isa的走位,可以简单的概括为:isa —> 类 —-> 类 isa —-> 元类 —-> 根元类;
类的继承关系链: 类 —> 父类 —-> 根元类(NSObject)—-> nil;
我们通过获取类的 class_data_bits_t,查看到实例方法与属性的存储位置,是存储在class_data_bits_t里的 data的中,我们通过data里的properties() 和 methods()方法,可以查看,但是却没有看到类方法,和成员变量;接下来,我们分析一下,类方法,和成员变量存放在哪里?
类的存储结构
在介绍类的存储结构之前,我们先来了解一下,苹果对类是如何定义与存储的;
我们通过上述视频,可以看到苹果针对class的存储结构;
类存储在磁盘中,他的存储结构如下图:
当类第一次从磁盘加载到内存时,类一开始的结构就是如此,但一经使用,他们就会改变;
在了解变化之前,我们需要先了解 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
,用于读写数据
但苹果根据调研发现,只有10%
的类真正改变了它方法,所以苹果又将 rw
结构,进行拆分;
所以,我们想查看更多的数据,可以从ro中查看;
类方法的读取
通过对类的内存布局,属性以及成员变量的了解,我们了解到了属性与成员变量的存储,与对象方法的存储,他们的存储位置是不一样的,但他们都存在类
里面;可以避免多份浪费内存;
而在上一篇文章中,我们通过获取methods,可以读取到对象方法,也就是 - (void) sayHi
这个方法;
而且我们始终没有看到 类方法,那类方法又是存储在哪里?还是说,类方法,并不存在?
在之前的文章中,我们可以通过MachOView,了解到元类,现在我们再次使用MachOView,查看是否存在类方法;
将项目工程,拖入MachOView中,查看function starts,可以看到,+sayNB这个类方法,那么可以确定,类方法是存在的;
可我们要如何获取这个类方法呢?
我们都知道,所谓的类方法,对象方法,都是OC上层的一种称呼,而在底层C++,是没有这种区分的,都是统一的称为函数,
如果 oc 类里,有两个名称一样的方法,那在底层C++中,让程序如何去区分呢?
如下两个方法:
- (void)sayNB;
+ (void)sayNB;
如果这两个方法,都存储在类里里面,C++底层,要如何区分这两个函数?编译器要如何寻找?
复制代码
所以苹果为了能够区分这种情况,元类的作用,就来了;既然类方法,不能存储了,那么就存储到元类中吧
既然我们知道,类方法存储在元类里,那么我们需要通过当前类,去获取它的元类,再通过元类地址,获取元类内存信息;
成员变量/属性/实例变量
成员变量
:写在类声明的大括号中的变量,称为成员变量;
属性
:写在大括号之外的,默认使用@property声明的变量,称为属性;
实例变量
:实例变量是一种特殊的成员变量,它是以对象为类型的一种成员变量;
我们通过 .cpp 文件,分析一下这些变量与属性的底层结构;
.cpp 文件的生成方式:
可以看到,在C++文件中,属性都会被优化掉;变成带下划线的成员变量;那么带下划线的成员变量与不带下划线的有什么区别呢?
在 C++ 文件中,属性与成员变量的区别就是,属性会自动生成get/set
方法;
在C++文件中,我们可以看到,类似"@16@0:8"
或是"v24@0:8@16"
的编码结构,我们给大家讲解一下如何理解这些编码结构:
查找编码的方式
command + shift + 0,进入 apple developer documentation,输入关键字ivar_getTypeEncoding搜索 ,
编码内容解析
set_property
在C++文件中,明显看到 name 与 nickName,get方法存在区别,nickName多了一个objc_setProperty()方法;我们都知道,在OC中,我们给一个属性赋值或者是取值,执行的是 set/get
方法,但是 C++文件,nickName 的 set
方法执行后,接下来执行的是objc_setProperty()
,为什么要执行这一步呢?有什么作用吗?
在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
这个函数方法,在底层是如何获取的
我们不知道什么时候会触发 objc_setProperty方法,也不知道出发条件是什么? 那么我们往回寻找一下,看看是什么条件,可以出发这里的函数;
可以看到,switch的内部进行的是一些列的内存平移,数据赋值等操作,但是switch的判断条件,是如何产生的,这个判断条件才是真正触发objc_setProperty的主要因素,所以我们需要查询 switch的判断条件是如何产生的;
结论
从底层代码可以,如果成员变量是
copy
修饰词,则会生成setProperty方法;
类的加载
那么有没有人想过,类是怎么来的?在底层,它又是如何实现的?今天我们就来探索一下,类的来源;
在上一篇文章中,不知道有没有人注意过,data()里的firstSubclass 为空呢?即使QLYPerson存在子类的, firstSubclass还是为nil;
我们试试打印,QLYStudent,看是否存在;
我们打印 QLYStudent,可以明显看到,QLYStudent是存在的。并且再次打印 $2的时候,firstSubclass却有值了。这是什么原理呢?是如何加载的呢?
结语
其实,类的加载是一种懒加载,后续文章,咱们继续补充