前言
上一章类的底层结构探究上篇中我们知道了
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
被加载时,它可以给类添加方法; - 通过
runtime
api动态添加属性和方法;
- 当
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 |
b num |
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类型参数self
16
:上面参数从16
位置开始@
: 参数0
:参数从0
位置开始:
:SEL
8
:SEL
从8
位置开始