1.WWDC20runtime
对于类的数据结构的优化
引用作者Ben的原话:此次优化不需要改动任何代码,并且不需要学习新的API,运气好的话,什么都不需要做,你的app也会变得很快。是runtime关于内存的优化。
1.1 类的运行时数据结构的变化
在磁盘上,APP二进制文件中类对象本身的数据结构中包含了最常被访问的信息:指向元类、父类和方法缓存的指针,同时还有一个指向更多数据的指针,存储额外信息的地方叫做class_ro_t
,Ro
代表只读,里面包括类名称、方法、协议和实例变量的信息等,Swift类和OC来共享这一基础结构。
当类第一次从磁盘加载到内存中时,它们也是这样的,但是一经使用,它们就会发生变化,在了解变化之前,需要先了解Clean Memory
和Dirty Memory
的区别。
1.1.1 Clean Memory
Clean Memory
是指加载后不会发生更改的内存。class_ro_t
就属于Clean Memory
,因为它是只读的。
1.1.2 Dirty Memory
Dirty Memory
是指在进程运行时会发生更改的内存。当一个类首次被使用时,运行时会为它分配额外的存储容量class_rw_t
,用于读写数据,class_rw_t
就属于Dirty Memory
。
Dirty Memory
是这个类被分成两部分的原因,可以保持类加载后不会发生更改的数据越多越好,通过分离永远不会更改的数据,可以把大量的类数据存储为Clean Memory
。
class_rw_t
数据分析:
1. `First Subclass`、`Next Sibling Class`:运行时才会生成的新信息,通过使用`First Subclass`和`Next Sibling Class`指针将所有的类都链接成一个树状结构,这允许运行时遍历当前的所有类。
2. `Methods`、`Properties`、`Protocols`:当`category`被加载时,它可以向类中添加新的方法,而且程序员可以通过运行时API动态的添加他们。
3. `Demangled Name`:这个是只有Swift才会使用的字段,因为整个数据结构OC与Swift是共享的,但是Swift类本身并不需要这个字段,是为了有人要访问Swfit的OC名称的时候使用的,利用率比较低。
复制代码
特点:
- 内存中
Dirty Memory
比Clean Memory
更多,只要进程在运行,它就一直存在。 Clean Memory
可以从内存中移除,需要时再从磁盘中加载
1.1.3 Dirty Memory
拆分优化方案
Dirty Memory
会占用相当多的内存,但是有些部分只在运行时读写数据才需要,大约只有10%的类真正地更改了他们的方法、属性、协议,而且只有Swift
类会使用Demangled Name
字段,所以,我们可以拆分那些平时不用的部分为class_rw_ext_t
,这可以将class_rw_t
的大小减少一半,对于那些确实需要额外信息的类,可以分配这些扩展记录中的一个,并将它滑到类中供其使用(大约90%的类不需要这个扩展)。这样可以提升空间利用率。
1.1.4 通过终端实际验证微信和keynote
的class_rw_t
占用的内存
// 微信
heap WeChat | egrep 'class_rw|COUNT'
// Keynote
heap Keynote | egrep 'class_rw|COUNT'
复制代码
微信验证结果:
Keynote验证结果:
数据分析:
- 微信:
class_rw_t
占用内存192064字节,class_rw_ext_t
占用内存21120字节,class_rw_ext_t
仅为11%。 - Keynote:
class_rw_t
占用内存256000字节,class_rw_ext_t
占用内存38880字节,class_rw_ext_t
仅为15%。
结论:
从上面的验证结果来看,class_rw_ext_t
的需求量确实不高,这样的拆分特别有意义,最大程度的保证了内存使用效率。
2.成员变量和属性的区别
成员变量存储于class_ro_t
中。
图解:
示例代码:
@interface XJPerson : NSObject
{
NSString *_nickName;
NSString *_hobby;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation XJPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XJPerson *person = [XJPerson alloc];
NSLog(@"%@", person);
NSLog(@"Hello, World!");
}
return 0;
}
复制代码
通过clang
编译成.cpp
文件查看
clang -rewrite-objc main.m -o main.cpp
复制代码
图解:
从.cpp
文件中可以看出类被编译成了结构体,@property
声明的属性被注释了,同时以_
+ 属性名的方式生成成员变量,添加进了成员变量中,并且还默认生成了getter
和setter
方法。
结论:
- 属性在底层编译阶段会变成
_
+ 属性名的成员变量。 - 属性默认会自动生成
getter
和setter
方法。
3.Type Encodings
.cpp
文件方法列表中,每个方法都会有Type Encodings
,按照上表可以很清楚的理解各个方法Type Encodings
的意思。
4. 属性的setter
方法深入解析
细心的你应该已经发现了上面.cpp
源码里name
的setter
方法和age
的setter
方法的不同之处,name
的setter
里面调用了void objc_setProperty (id, SEL, long, id, bool, bool)
函数,而age
的setter
方法却是直接赋值给首地址加上成员变量age
的地址偏移量。下面就探究下为什么会有这种分别。
代码示例:
@interface XJPerson : NSObject
{
NSInteger _age;
NSString *_hobby;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *address;
@property (atomic, copy) NSString *nickName;
@property (atomic, strong) NSString *tel;
@property (nonatomic) NSObject *obj;
@property (atomic) CGFloat height;
@end
@implementation XJPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
XJPerson *person = [XJPerson alloc];
NSLog(@"%@", person);
NSLog(@"Hello, World!");
}
return 0;
}
复制代码
编译成.cpp
文件,查看属性的setter
方法:
4.1 非copy
修饰的属性的getter
和setter
方法的原理
在OC底层原理初探之对象的本质(三)alloc探索下这边文章里曾经分析过对象的getter/setter
方法是通过对象首地址
+ 内存平移
的方式找到对象成员变量的内存地址,然后进行读值和取值。
getter
方法图解:
setter
方法图解:
4.2 copy
修饰的属性的setter
方法的深入解析
copy
修饰的属性的getter
方法与非copy
修饰的属性没有什么区别,但是setter
方法却调用了objc_setProperty(id, SEL, long, id, bool, bool)
函数,在objc4-818.2
源码里搜索此函数,发现函数对copy修饰的属性进行了相关的copy
处理。
图解:
4.2.1 LLVM分析copy
修饰的属性的setter
方法
objc4-818.2
源码里只有objc_setProperty
函数的实现,却没有相关调用信息,通过llvm
源码逆向查找确定了objc_setProperty
函数的调用条件。
llvm
源码逆向验证流程:objc_setProperty
->getSetPropertyFn
->GetPropertySetFunction
->PropertyImplStrategy
->IsCopy(判断)
。
图解(逆向查找):
结论:
copy
修饰的属性,无论是是nonatomic
还是atomic
,编译器都会将setter
方法重定向到objc_setProperty
函数。
来都来了,不点个赞,不点个关注???