WWDC2020对runtime的优化
- 视频的观看地址:https://developer.apple.com/videos/play/wwdc2020/10163/ (最好用Safari浏览器打开)
- LLVM源码地址:https://github.com/apple/llvm-project
看完视频后总结:本次改动不需要改动任何代码,也不用学习新的API,这次主要是runtime关于内存的优化
。在这种环境下我们不用改APP也会运行得比之前更快更高效。
类的数据结构
在《类的探究分析》一文中就详细地解读了类的结构。在APP编译成二进制文件中,类的数据结构发生了变化,其中包含了最常被访问的信息,指向元类、父类和方法缓存的指针。以下是类的数据结构图:
Clean Memory 和 Dirty Memory的区别
Clean Memory
Clean Memory
指的是在程序运行中不会发生改变的内存。
class_ro_t就是属于
Clean Memory的,
class_ro_t`内存图解:
-
clean memory
加载后不会发生改变的内存 -
class_ro_t
就属于clean memory
,因为它是只读的,不会对齐内存进行修改 -
clean memory
是可以进行移除的,从而节省更多的内存空间,因为如果你有需要clean memory
,系统可以从磁盘中重新加载
Dirty Memory
Dirty Memory
指的是在程序运行中会发生改变的内存,也就是我们俗称的脏数据
。类的结构一经使用就会变成Dirty Memory
,因为运行时会向它写入新的数据。例如往类中添加方法
又或者加载类的子类
或父类
,这里指的是class_rw_t。class_rw_t
内存图解如下:
-
Dirty Memory
是这个类被分成两部分的原因,可以保持类加载后不会发生更改的数据越多越好,通过分离永远不会更改的数据,可以把大量的类数据存储为Clean Memory
。 -
class_rw_t(读写)
:类在加载的时候,属性(properties)
、协议(prococols)
和方法(methods)
会被运行时动态的添加,也可以动态的修改(Method Swizzling)
。所以类需要保存在class_rw_t
中。 -
First Subclass、Next Subling Class
:包含了运行时才会生成的信息First Subclass、Next Subling Class
,所有的类都会变成一个树状结构
,就是通过First Subclass
和Next Subling Class
指针实现的,它允许运行时遍历当前使用的所有类 -
Demangled Name
:这个字段使用的频率是比较少的,swift中才会使用。
总结:
dirty memory
要比clean memory
更有价值而且要多,只要进行运行它就必须一直存在,通过分离出那些不会被改变的数据,可以把大部分的类数据存储为clean memory
,这样才能不断的提高程序的性能。
class_rw_t 的优化
dirty memory
在类第一次加载的时候就一直存在,runtime
会为它分配额外的内存
。运行时分配的存储容量时class_rw_t
用于读取-编写
数据,但是dirty memory
中仍然存在着比较多的clean memory
,为了提高空间的利用率,拆分出更多的clean memory
,减少dirty memory
容量是比奴可少的。
第一步:拆分出class_ro_t
,即运行时不被修改的内存。如下图:
注意:由上图发现一个疑点,为什么方法,属性在
class_ro_t
中时,class_rw_t
还要有方法,属性呢?
- 属性和方法在运行时中有可能会
发生更改
,这需要放在class_rw_t中。 - 在类加载的时候,可以往类中添加属性和方法。
-
class_ro_t
只是可读的,需要放在class_rw_t
中跟踪类的相关信息。
第二步:拆分class_rw_t
,提取其中的clean memory
在读取-编写属性和方法的时候,只有10%的类都需要修改或者添加的,那么90%类可以说是不被修改的,那么就可以对class_rw_t
进行拆分,拆分如下:
这样的话class_rw_t
的大小就会减少一半,对于真的用到了被拆分出去的数据的时候,可以使用扩展(extension)
来完成这些,添加到类中供其使用(大约90%的类不需要这个扩展)如下图:
总结
- 当有类使用了
category
的时候,那么此时的类就有了class_rw_t
的结构,如果未使用分类,那么类就是一个单纯的class_ro_t
的结构。 - 类结构的优化其实最要是分离出
class_ro_t
和class_rw_t
优化,其实就是对class_rw_t
不常用的部分进行了剥离。如果需要用到这部分就从扩展
记录中分配一个,滑到类中供其使用。
成员变量/实例变量和属性的区别
《类的探究分析》一文中提到了成员变量存放在class_ ro_t
中,那么我们用过查找objc4源码得出下图:
代码层面探究:
@interface XXPerson : NSObject
{
int hobby; //成员变量
NSObject *objc; //实例变量
}
@property (nonatomic,strong) NSString *name; //属性
@property (nonatomic,strong) NSString *nickName;
@property (nonatomic,assign) int age;
@end
通过xrun
编译成main.cpp
文件,查看底层代码:
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (模拟器)
-
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp (手机)
结论: - 属性在编译过程中自动加上
setter
和gette
r方法。 - 属性在底层编译阶段会变成
_
方式的成员变量。
补充
官方类型编码
注意:
- 编码文档存放在Apple Documents地址
-
Objective-C
不支持long double
类型,@encode(long double)
返回d
,和double
类型的编码值一样。
案例分析:
setName(v24@0:8@16)
分析结果: -
v
:void
,代表无返回值。 -
24
:setName
函数的占用字节数。 -
@
:参数,id
或者self
。 -
0
:从0
号位置开始。 -
:
:SEL
。 -
8
:从8
号位置开始。 -
@
:参数,setName
。 -
16
:从16
号位置开始。
objc_setProperty与copy的关系
objc_setProperty
方法相当于一个中间层方法,主要是避免了每个类都调用底层的objc_setProperty
方法。当用copy
关键字修饰属性时,该属性在编译时候setter方法就会从定向到objc_setProperty
方法,不像其他属性·setter·方法使用首地址+内存偏移
的方式找到方法实现。
示例代码:
@interface XJPerson : NSObject
@property (nonatomic,copy) NSString *name; //注意每个属性的关键字
@property (nonatomic,strong) NSString *nickName;
@property (atomic,copy) NSString *address;
@property (atomic) NSString *school;
@end
编译之后查看main.cpp
,得到下图:
结论:
- 使用
copy
关键字修饰的属性底层setter
方法重定向到objc_setProperty
方法 - 没使用
copy
关键字修饰的属性底层setter
方法通过首地址+内存偏移
来寻找并实现。
LLVM验证对象属性为copy时,setter方法的访问
验证流程图:
LLVM源码流程:
objc_setProperty
-> getSetPropertyFn
-> GetPropertySetFunction
-> PropertyImplStrategy
-> IsCopy(判断copy关键字)
结论:无论属性是否是原子性还是非原子性的,用到copy关键字修饰的属性setter方法底层都用
objc_setProperty
实现,strong
关键字无法通过最后得判断,需要通过首地址+内存偏移
的方式实现。
总结:
底层代码的分析需要很好的耐心,过程也是非常的枯燥。但是当你弄明白了原理之后,就会发现知识是环环相扣的。非常高兴自己又多了一分收获,加油!