通过这篇文章可以获得什么
- WWDC2020关于runtime优化之数据结构的变化
- Clean Memory与Dirty Memory是什么?
- Dirty Memory拆分优化原理
- 如何缩小class_rw_t的结构大小?
- class_rw_t与class_ro_t的区别
- 通过.cpp文件分析成员变量和属性的区别
- typeEncoding如何理解?
- objc_setProperty的作用是什么,与copy的关系是什么?
- LLVM验证源码objc_setProperty的访问流程
WWDC2020关于runtime优化
引用作者Ben的原话:此次优化不需要改动任何代码,并且不需要学习新的API,运气好的话,什么都不需要做,你的app也会变得很快,是runtime关于内存的优化
数据结构的变化(Class Data Structures Changes)
在磁盘上,app二进制文件中的类:包含了最常被访问的信息,指向元类、父类和方法缓存的指针
类内存图解:
Clean Memory
是指加载后不会发生更改的内存,class_ro_t
是属于Clean Memory
的。
class_ro_t(只读)
:一个指向更多数据的指针,存储额外信息的结构class_ro_t
,ro
代表只读,存放了方法、协议、实例变量的信息:
class_ro_t内存图解:
Dirty Memory
是指在进程运行时会发生更改的内存,类的结构一经使用就会变成Dirty Memory
,因为运行时会向它写入数据,这里指的是class_rw_t
。
Dirty Memory
是这个类被分成两部分的原因,可以保持类加载后不会发生更改的数据越多越好,通过分离永远不会更改的数据,可以把大量的类数据存储位Clean Memory
class_rw_t(读写)
: Methods、Properties、Protocols,当category
被加载时,它可以向类中添加新方法,可以根据Method Swizzling
方式修改,因为class_ro_t
是只读,所以要把这些放在Class_rw_t
中。
First Subclass
、Next Subling Class
:包含了运行时才会生成的信息First Subclass、Next Subling Class,所有的类都会变成一个树状结构
,就是通过First Subclass和Next Subling Class指针实现的,它允许运行时遍历当前使用的所有类Methods
、Properties
、protocols
:包含这3个是因为它们可以在运行时进行修改,当category
被加载时,它可以向类中添加新的方法,也可以通过runtime API
添加它们Demangled Name
:这个是只有Swift
才会使用的字段,因为整个数据结构OC与Swift是共享的
,但是Swift
类本身并不需要这个字段,是为了有人要访问Swfit的OC名称的时候使用的,利用率比较低。·
class_rw_t内存图解:
特点:
Dirty Memonry
要比Clean Memory
要多,只要进程在运行,它就一定存在Clean Memory
可以进行移除,从而获得更多的空间,因为如果需要Clean Memory可以从磁盘中重新加载
Dirty Memory拆分优化原理
Dirty Memonry
即类第一次加载就会存在,运行时就会为它分配额外的内存,运行时分配的存储容量时class_rw_t
用于读取-编写数据,但是Dirty Memory
里面存在很多Clean Memory
,为了更好的空间利用率,拆分就说必要的了!
第一步:拆分出class_ro_t
,运行加载时不会被修改的内存
第二步:这时的class_rw_t
还是太大,因为里面包含了Methods
、Properties
、Protoclos
,这3个因素只有使用了category向class中添加方法或使用了Method swizzle才会触发的特性,90%
的类不会被使用,所以把它们拆分出是必要的,Demangled Name
这个Swift使用的字段也拆出去也是必要的,毕竟使用率低,那么Dirty memory内存结构就变成了class_rw_t
和class_rw_ext_t
两部分。
如何缩小class_rw_t的结构大小:
拆掉那些平时不用的部分,可以将class_rw_t
减小一半,对于真的用到了被拆分出去的数据的时候,可以使用extension
来完成这些,添加到类中供其使用(大约90%
的类不需要这个扩展)
class_rw_ext_t内存图解:
###通过终端实际验证微信和Safari的class_rw占用的内存
终端命令
//微信
heap WeChat | egrep 'class_ro|COUNT'
//Safari
heap Safari | egrep 'class_ro|COUNT'
复制代码
微信调试结果图:
Safari调试结果图:
数据分析:
- 微信(开发者应用):class_rw_t占用字节:201408,class_rw_ext_t占用字节:22944,class_rw_ext_t占用了9%
- Safari(Apple应用): class_rw_t占用字节:199040,class_rw_ext_t占用字节:29952,class_rw_ext_t占用了13%
结论:通过微信
与Safari
的比较来看,Safari的category的使用量,也就是class_rw_ext_t
更多一些,微信的相对与safari要少一些,但是数据接近,整体在class_rw_ext_t
的拆分就变得特别有意义,最大程度的保证了内存使用效率,这还是比较大型的应用,普通应用的category
使用量就更少了。
class_rw_t与class_ro_t的区别
当有类使用了category
的时候,那么此时的类就有了class_rw_t
的结构,如果未使用分类,那么类就是一个单纯的class_ro_t
的结构。
类的内存结构中,有category Class
的时候有class_rw_t
,没有的时候class_ro_t
(clean memonry)
成员变量和属性的区别
成员变量存储位置:
class_ro_t
图解:
案例代码:
@interface FFPerson: NSObject {
int age;
NSString *hobby;
}
@property (nonatomic, copy) NSString * name;
@end
@implementation FFPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
FFPerson *person = [FFPerson alloc];
NSLog(@"%@",person);
}
return 0;
}
复制代码
通过clang
编译成.cpp文件查看
clang -rewrite-objc main.m -o main.cpp
复制代码
关键点图解:
在cpp
文件中查找FFPerson
这个关键字,看到了类被编译成结构体了,@property
方式声明的属性没有了,被以“_”
的方式添加在了类的成员变量中,并且还默认生成了getter
和setter
方法。
结论:
- 成员变量是生命在类的
{}
中的 - 属性是用
@property
方式声明的 - 属性在底层编译阶段会变成
_
方式的成员变量 - 属性会自动生成
getter
和setter
方法
补充
实例变量:以对象类型声明的(特殊的成员变量),例如NSString * hobby,hobby就是实例变量。
TypeEncoding
返回实例变量的类型字符串。Apple Documents地址
官网数据:
起源与在.cpp文件中观察到一个特殊的字符串
按照上表可以很清楚的知道.cpp
文件中的那些字符的含义了
objc_setProperty与copy的关系
objc_setProperty
是一个中间层代码。为了每一个属性在使用setter
方法的时候不必为其在底层创建单独的实现方法,当检测到属性对象存在copy
属性的时候,会将此对象的setter
方法重定向到objc_setProperty()
,然后只要在底层实现setProperty
方法就可以将属性对象的setter
方法实现了。
案例代码:
@interface FFPerson: NSObject {
int age;
NSString *hobby;
}
@property (nonatomic, copy) NSString * name;
@property (nonatomic, strong) NSString * height;
@property (atomic, copy) NSString * weight;
@property (atomic) NSString *address;
@end
@implementation FFPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
FFPerson *person = [FFPerson alloc];
NSLog(@"%@",person);
}
return 0;
}
复制代码
编译成.cpp
文件,属性的setter
方法如下:
对象属性为strong时,getter与sett方法的内存访问
现在已知的是对象的getter/setter
方法是通过对象的首地址
+内存偏移
找到对应对象的内存地址,然后进行值的获取或存储。
但是此案例中出现了访问name
、weight
的setter方法的时候并不是从对象的首地址开始+地址偏移
来查找对象的真实地址的,出现了如上图所示的现象,通过objc_setProperty+内存偏移地址
方式查找,这是为什么呢?
LLVM验证对象属性为copy时,setter方法的访问
llvm源码验证流程:objc_setProperty
-> getSetPropertyFn
-> GetPropertySetFunction
-> PropertyImplStrategy
-> IsCopy(判断)
图解流程
通过此案例得出结论:
- 只要为对象设置了
copy
属性,无论是原子性
还是非原子性
,都没有影响,setter
方法都会被重定向到objc_setProperty
- 如果声明的对象不设置如何初原子性之外的属性,那么默认属性是
strong
,不会触发objc_setProperty