005-类的内存结构优化

861 阅读7分钟

通过这篇文章可以获得什么

WWDC2020关于runtime优化

视频地址

引用作者Ben的原话:此次优化不需要改动任何代码,并且不需要学习新的API,运气好的话,什么都不需要做,你的app也会变得很快,是runtime关于内存的优化

数据结构的变化(Class Data Structures Changes)

在磁盘上,app二进制文件中的类:包含了最常被访问的信息,指向元类、父类和方法缓存的指针

类内存图解:

Class.jpeg

Clean Memory

是指加载后不会发生更改的内存,class_ro_t是属于Clean Memory的。

class_ro_t(只读):一个指向更多数据的指针,存储额外信息的结构class_ro_t,ro代表只读,存放了方法、协议、实例变量的信息:

class_ro_t内存图解:

class_ro_t.jpeg

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中。

  1. First SubclassNext Subling Class:包含了运行时才会生成的信息First Subclass、Next Subling Class,所有的类都会变成一个树状结构,就是通过First Subclass和Next Subling Class指针实现的,它允许运行时遍历当前使用的所有类
  2. MethodsPropertiesprotocols:包含这3个是因为它们可以在运行时进行修改,当category被加载时,它可以向类中添加新的方法,也可以通过runtime API添加它们
  3. Demangled Name:这个是只有Swift才会使用的字段,因为整个数据结构OC与Swift是共享的,但是Swift类本身并不需要这个字段,是为了有人要访问Swfit的OC名称的时候使用的,利用率比较低。·

class_rw_t内存图解:

class_rw_t.jpeg

特点:

  • Dirty Memonry要比Clean Memory要多,只要进程在运行,它就一定存在
  • Clean Memory可以进行移除,从而获得更多的空间,因为如果需要Clean Memory可以从磁盘中重新加载

Dirty Memory拆分优化原理

Dirty Memonry即类第一次加载就会存在,运行时就会为它分配额外的内存,运行时分配的存储容量时class_rw_t用于读取-编写数据,但是Dirty Memory里面存在很多Clean Memory,为了更好的空间利用率,拆分就说必要的了!

第一步:拆分出class_ro_t,运行加载时不会被修改的内存

类内存优化01.jpeg

第二步:这时的class_rw_t还是太大,因为里面包含了MethodsPropertiesProtoclos,这3个因素只有使用了category向class中添加方法或使用了Method swizzle才会触发的特性,90%的类不会被使用,所以把它们拆分出是必要的,Demangled Name这个Swift使用的字段也拆出去也是必要的,毕竟使用率低,那么Dirty memory内存结构就变成了class_rw_tclass_rw_ext_t两部分。

类内存优化02.jpeg

如何缩小class_rw_t的结构大小:

拆掉那些平时不用的部分,可以将class_rw_t减小一半,对于真的用到了被拆分出去的数据的时候,可以使用extension来完成这些,添加到类中供其使用(大约90%的类不需要这个扩展)

class_rw_ext_t内存图解:

class_rw_ext_t.jpeg

###通过终端实际验证微信和Safari的class_rw占用的内存 终端命令

//微信
heap WeChat | egrep 'class_ro|COUNT'
//Safari
heap Safari | egrep 'class_ro|COUNT'

微信调试结果图:

微信class_rw信息.png Safari调试结果图:

Safari的class_rw信息.png

数据分析:

  • 微信(开发者应用):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

图解:

ivars的位置.png

案例代码:

@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文件中表现形式.png

cpp文件中查找FFPerson这个关键字,看到了类被编译成结构体了,@property方式声明的属性没有了,被以“_”的方式添加在了类的成员变量中,并且还默认生成了gettersetter方法。

结论:

  1. 成员变量是生命在类的{}中的
  2. 属性是用@property方式声明的
  3. 属性在底层编译阶段会变成_方式的成员变量
  4. 属性会自动生成gettersetter方法

补充

实例变量:以对象类型声明的(特殊的成员变量),例如NSString * hobby,hobby就是实例变量。

TypeEncoding

返回实例变量的类型字符串。Apple Documents地址

官网数据:

TypeEncoding官网数据.png

起源与在.cpp文件中观察到一个特殊的字符串

TypeEncoding-01.png

按照上表可以很清楚的知道.cpp文件中的那些字符的含义了

setter方法TypeEcoding.jpeg

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方法如下:

objc_setProperty.png

对象属性为strong时,getter与sett方法的内存访问

现在已知的是对象的getter/setter方法是通过对象的首地址+内存偏移找到对应对象的内存地址,然后进行值的获取或存储。

getter和setter.jpg

但是此案例中出现了访问nameweight的setter方法的时候并不是从对象的首地址开始+地址偏移来查找对象的真实地址的,出现了如上图所示的现象,通过objc_setProperty+内存偏移地址方式查找,这是为什么呢?

LLVM验证对象属性为copy时,setter方法的访问

llvm源码验证流程:objc_setProperty -> getSetPropertyFn -> GetPropertySetFunction -> PropertyImplStrategy -> IsCopy(判断)

图解流程

llvm-setProperty流程.jpg

通过此案例得出结论:

  1. 只要为对象设置了copy属性,无论是原子性还是非原子性,都没有影响,setter方法都会被重定向到objc_setProperty
  2. 如果声明的对象不设置如何初原子性之外的属性,那么默认属性是strong,不会触发objc_setProperty