前言
上一章类的底层结构探究上篇中我们知道了
isa的指向图,并且引入了元类,其中类的isa会指向元类;- 并且还知道了
类的继承链; - 继而我们还探究了
类的底层结构,并且针对与类里面的bits变量做了主要探究(这边大家在探究的时候注意一下要在objc源码中探究),知道了bits里面存储了属性,方法,协议等; - 进一步还得到成员变量没有存在
class_rw_t的propertys中,而是在class_ro_t的ivar_t中; - 对象方法存在类
class_rw_t的methods中,类方法存在元类class_rw_t的methods中.
看类的底层结构我们会经常性的看到class_rw_t和class_ro_t,那这两兄弟是什么呢?我们知道系统也会为属性生成_属性名的成员变量存在class_ro_t的ivar_t中,那属性,成员变量,实例变量之间又有个什么关系呢?那我们就一起来看看类的底层结构下篇。
Class in Memory
在探究之前我们先来看看Apple在WWDC 2020讲解runtime时内存优化的视频片段;
通过视频我们可以了解到本次主要是对内部数据结构做了优化,使得App运行更快,这其中就有两个概念Clean Memory 和 Dirty Memory,那这两者究竟是什么呢?继续看视频可以得出:
Clean Memory
- 加载后不会发生更改的内存;
class_ro_t就是属于Clean Memory,因为class_ro_t是只读的;Clean Memory可以进行移除从而达到节省更多的内存空间,因为如果你需要Clean Memory,系统可以从磁盘中重新加载
Dirty Memory
- 在进程运行时会发生更改的内存;
- 类结构一经使用就会变成
Dirty Memory,因为运行时会向它写入新的数据;例如创建一个新的方法缓存并从类中指向它; Dirty Memory要比Clean Memory昂贵的多,只要进程运行,它就必须一直存在。 在macOS中可以通过swap选择换出Dirty Memory,而在iOS中不使用swap,所以Dirty Memory在iOS中代价很大,所以Dirty Memory是这个类数据被分成两部分的原因,保持清洁的数据越多越好,通过分离那些永远不会更改的数据,所以可以把大部分类数据存储在Clean Memory中。
我们知道类在磁盘中
在运行时需要知道更多类的信息,所以当类第一次加载到内存中时,runtime会为它分配额外的存储容量,额外分配的存储容量是class_rw_t读取-编写数据的;在这个数据结构中我们会存储只有在运行时生成的新信息,例如所有类都会链接成一个树状结构,这就是通过First SubClass和Next Sibling Class指针实现的,这样就允许runtime遍历当前所有用到的类,使得方法缓存无效非常有用。
根据上面的图解,我们肯定会有疑问啊,既然为了节省内存空间而优化,那为什么方法,属性在class_ro_t中时,class_rw_t中也存在呢?视频中也给我讲解了:
- 它们在
runtime的时候可以被修改;- 当
Category被加载时,它可以给类添加方法; - 通过
runtimeapi动态添加属性和方法;
- 当
class_ro_t是只读的,需要在class_rw_t追踪这些数据。
我们知道项目中会有很多的类,那么这么做的话将会占用相当大的内存空间,那如何缩小这些结构呢?
class_rw_t优化
runtime可以动态添加属性和方法,但是Apple在实际监测中发现大约只有10%的类动态修改了,这其中的一个字段demangledName只有在swift中有需要访问其objective-c名称时才需要;
所以就可以拆分掉那些平时不用的部分:
这样的话,class_rw_t的大小减少了一半;对那些需要修改内存的,需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用:
class_rw_t的优化总结
class_rw_t的优化实质上就是将其内部不常用的数据拆分出来放在Class_rw_ext_t扩展中;如果需要使用这部分数据就从Class_rw_ext_t扩展中分配一个滑到类中供其使用。
类中的变量,属性,方法
我们知道一个类中一般都会有变量,属性和方法。下面就是我们声明的一个继承自NSObject的类LhkhPerson:
nickName和objc均为成员变量,但是我们这边需要注意一点,其实objc准确的讲我们应该叫它为实例变量(定义为除基础数据类型以外的成员变量,所以实例变量是一种特殊的成员变量),因为NSObject不是基础类型;总结来说就是基础数据类型的都是成员变量,除此之外就是实例变量;name1,name2以及age就是属性;saySomething为对象方法,sayHello为类方法
我们上一节补充中知道了成员变量存储在class_ro_t的ivar_t中,
我们发现它的成员变量显示有
5个,但是我们只声明2个啊,还有3个是什么?
我们可以通过取ivars里面的数据可以得出
这三个与我们定义的属性有点像啊,怎么会有个‘
_’呢?接下来我们通过clang编译成.cpp文件来看一下下层代码实现:
编译时系统将属性转换成为了_属性的成员变量和对应的getter和setter方法。
所以我们得出:
- 属性 =
_属性的成员变量 + getter方法 + setter方法
细心的我们肯定会发现同样都是NSString的name1和name2的setter方法是不一样的额
在上图中我们可以知道,
name2是通过首地址加上偏移量赋值的,而name1是通过一个objc_setProperty方法,相同的类型为什么会出现不同的set方式呢?就是因为我们给的修饰词不同出现的吗?
objc_setProperty补充
我们需要先了解一下objc_setProperty这个是什么意思?
LLVM源码中的objc_setProperty
由于这是在编译时就出现了,那我们就得从LLVM下手了,使用vscode打开LLVM源码:
通过搜索objc_setProperty我们发现了这个方法,在运行时创建objc_setProperty方法,既然找到了创建这个方法,那么我们就逆着找呗,什么情况下才会调用getSetPropertyFn()这个方法呢,继续搜索:
在
GetPropertySetFunction()方法中会调用,这个是一个中间方法,那继续找GetPropertySetFunction()调用:
我们发现这边是一个Switch,而会调用GetPropertySetFunction()是在Switch的PropertyImplStrategy策略下,而这个会对应两个GetSetProperty``和SetPropertyAndExpressionGet,那我们现在就只需要知道什么时候赋值这个策略不就ok了吗,继续找:
是否是copy修饰词;
还有
retain(MRC模式下,现在基本都是在ARC模式下,所有retain基本就可以不需要理解),atomic等修饰词。
那我们来个实例验证一下:
总结
所以我们基本可以得到的是在copy修饰的情况下,不管是原子性还是非原子性,系统生成的setter方法都会重定向到objc_setProperty方法。
objc源码中的objc_setProperty
看到这个方法前面带着objc我们想着objc源码中有没有方法实现呢,经过搜索我们发现
从源码中我们发现有5个方法,我们同样发现
atomic,copy,nonatomic;而且5个方法中都会调用reallySetProperty方法,而在这个方法中实质原理就是新值retain,旧值release。
编码
我们在生成的cpp文件中会看到
这个中
"v16@0:8"、"@16@0:8"这些个编码是啥意思呢。。。请看官方编码解释:
官方编码
我们可以通过打开xcode--> command+shift+0--> 搜索ivar_getTypeEncoding--> 点击Type Encodings
| Code | Meaning |
|---|---|
c | A char |
i | An int |
s | A short |
l | A long``l is treated as a 32-bit quantity on 64-bit programs. |
q | A long long |
C | An unsigned char |
I | An unsigned int |
S | An unsigned short |
L | An unsigned long |
Q | An unsigned long long |
f | A float |
d | A double |
B | A C++ bool or a C99 _Bool |
v | A void |
* | A character string (char *) |
@ | An object (whether statically typed or typed id) |
# | A class object (Class) |
: | A method selector (SEL) |
| [array type] | An array |
| {name=type... } | A structure |
| (name=type... ) | A union |
bnum | A bit field of num bits |
^type | A pointer to type |
? | An unknown type (among other things, this code is used for function pointers) |
也可以通过代码打印
Objective-C 不支持long double类型,@encode(long double)返回d,和double类型的编码值一样。
我们这边以name1a这个属性的编码为例 "@16@0:8"解释一下:
@: 对应id类型参数self16:上面参数从16位置开始@: 参数0:参数从0位置开始::SEL8:SEL从8位置开始