iOS底层原理07:类 & 类结构分析中我们对类结构
有了大概的认识,本文主要探索objc_class
的bits属性
,探索成员变量
、属性
、方法
(对象方法、类方法)、协议
等是如何存储的
【WWDC2020】类数据结构的优化
WWDC2020中关于数据结构的变化(Class data structures changes)视频地址
Object-C运行时会使用这些数据结构
来跟踪类,👇下面我们先来了解 Clean Memory
和Dirty Memory
的区别,方便我们更好得理解类数据结构的优化
Clean Memory 和 Dirty Memory的区别
Clean Memory
-
clean memory
是指加载后不会发生更改的内存 -
class_ro_t
就属于clean memory
,因为它是只读的 -
clean memory
可以进行移除,从而节省更多的内存空间,因为如果你需要clean memory
,系统可以从磁盘中重新加载
Dirty Memory
-
dirty memory
是指在进程运行时会发生更改的内存 - 类结构一经使用就会变成
dirty memory
,因为运行时
会向它写入新的数据。(例如:创建一个新的方法缓存,并从类中指向它) -
dirty memory
比clean memory
要昂贵得多,只要进程在运行,它就必须一直存在 -
macOS
可以选择换出dirty memory
,但因为iOS
不使用swap
,所以dirty memory
在iOS中代价很大
因此,苹果为了性能优化,类数据被分成两部分
,可以保持清洁的数据越多越好,通过分离出那些永远不会更改的数据
(即class_ro_t
),可以把大部分的类数据存储为clean memory
类结构的优化
虽然class_ro_t
这些数据足够我们使用类,但因为OC
的动态特性,运行时
需要跟踪每个类的更多信息,所以当一个类首次被使用
,runtime
会为它分配额外的存储空间
。
- 这个运行时分配的存储容量是
class_rw_t
,用于读取-编写数据
,在这个数据结构中,我们存储了只有在运行时才会生成的新消息
优化之前,类结构如下👇
所有的类
都会链接成一个树状结构
,通过使用First Subclass
和Next Sibling Class
指针实现的,这允许运行时遍历当前使用的所有类。
【问题】 为什么class_rw_t
和class_ro_t
中都存在方法、属性
呢?
-
class_rw_t
可以在运行时进行更改 - 当
category
被加载时,它可以向类中添加新的方法
- 我们还可以使用
运行时 API
动态添加方法,如method_setImplementation
-
class_ro_t
是只读的,所以我们需要在class_rw_t
中来跟踪这些东西
在任何给定的设备中,都有许多类在使用,苹果开发人员在iPhone
上的整个系统中测量了,class_rw_t
结构占用了相当多的内存,【切记】我们在读取-编写
部分需要这些东西,因为他们可以在运行时更改。
【问题】如果缩小class_rw_t
的结构呢?
- 苹果开发人员发现,大约只有10%的类真正地更改了他们的方法
- 而且只有
Swift
类会使用这个demangled name
字段(只有访问它们Objective-C名称时才需要
)
所以我们可以拆掉
那些平时不用的部分,以达到内存优化,如下图所示:
- 这样
class_rw_t
的大小会减少一半 - 对于那些确实
需要额外信息
的类
,才会有分配这些扩展记录,即多了一层class_rw_ext_t
数据,而剩下90%的类,从来不需要这些扩展数据,也就没有class_rw_ext_t
这层数据结构
我们可以通过heap
来检查正在运行的进程所使用的堆内存
//查看微信进程,在活动监视器中查看,微信进程=591
heap 591 | egrep "class_rw|COUNT|class_ro"
总结
class_rw_t
优化,其实就是对class_rw_t
不常用的部分进行了剥离。如果需要用到这部分就从扩展记录中分配一个,滑到类中供其使用。现在大家对类应该有个更清楚的认识。
lldb调试分析
准备工作
定义两个类
- 继承自
NSObject
的类HTPerson
@interface HTPerson : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
+ (void)sayBye;
@end
@implementation HTPerson
- (void)sayHello {
NSLog(@"%@",__func__);
}
+ (void)sayBye {
NSLog(@"%@",__func__);
}
@end
- 继承自
HTPerson
的类HTTeacher
@interface HTTeacher : HTPerson
@end
@implementation HTTeacher
@end
-
main.m
中代码如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson *p1 = [HTPerson alloc];
HTPerson *p2 = [[HTPerson alloc] init];
HTTeacher *t = [[HTTeacher alloc] init];
NSLog(@"end--%@--%@", @"123", p1);
}
return 0;
}
lldb调试class_rw_t结构
断点调试步骤如下
获取类的首地址:
p/x HTPerson.class
获取
bits
的地址:类首地址 + 0x20
,p/x (0x00000001000082f8 + 0x20)
通过
bits->data()
获取class_rw_t
结构体地址打印
class_rw_t
结构体数据【1】执行完
HTPerson *p1 = [HTPerson alloc];
,查看class_rw_t
数据,发现witness
的值为0
,firstSubclass
的值为nil
- 【2】执行完
HTPerson *p2 = [[HTPerson alloc] init];
,查看class_rw_t
数据,发现witness
的值变为1
- 【3】执行完
HTTeacher *t = [[HTTeacher alloc] init];
,即使用子类,发现firstSubclass
的值变成了HTTeacher
属性探究
-
class_rw_t
结构体提供了properties()函数
来获取类的属性,得到property_array_t
数据
- 查看
property_array_t
数据结构,发现它继承自list_array_tt
,并且有property_t
和property_list_t
两层数据
- 继续查看
list_array_tt
数据结构,发现内部有个联合体数据,其中list
即属性列表property_list_t
的指针地址
- 查看
property_list_t
数据结构,继承自entsize_list_tt
,提供了泛型模版数据
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
- 继续查看
entsize_list_tt
数据结构,终于找到了get()方法
(获取对应index位置的属性)
👇通过lldb断点来查看HTPerson
的属性
通过class_rw_t -> properties()
获取的属性列表,只存储了两个属性:name
和age
【问题】我们声明的变量-hobby
保存在哪里呢?
成员变量探究
类属性列表
中没有存储变量
,观察发现class_rw_t
还有一个获取class_ro_t *
的方法const class_ro_t *ro() const {}
,成员变量会不会在class_ro_t
中,源码查看class_ro_t
结构体定义
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
// This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
_objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
// ...省略
class_ro_t
是结构体类型,有一个const ivar_list_t * ivars;
变量。从名字我们可以猜到里面应该存储变量。👇通过lldb验证如下图:
总结
-
变量
的底层实现是ivar_t
,存储在class_ro_t
中的变量列表ivars
里 - 系统会给
属性
自动生成一个带_属性名
变量,存储在class_ro_t
的变量列表中
方法探究
实例方法探究
lldb调试如下👇
- 类的
对象方法列表
通过bits->data()->methods()
获取 - 类的
对象方法列表
在底层结构是method_list_t
-
p $7.get(index)
在方法列表中获取不到具体的值,因为method_t
中进行了处理,需要通过big()获取方法
- 类的方法列表中没有
类方法
从上图打印结构可以看出,类会为属性
提供默认的set、get
方法,
但是我们没有发现HTPerson
的类方法+ (void)sayBye;
类方法探究
对象的方法
是存储在类
中,那么类方法
可能存储在元类
中。按照这个思路探究下
从打印结果可以得知:类方法
存储在元类
的方法列表
中
协议探索
- 新增一个协议
HTPersonProtocol
,让HTPerson
遵守协议
@protocol HTPersonProtocol <NSObject>
- (void)protocolMethod1;
- (void)protocolMethod2;
@end
@interface HTPerson : NSObject<HTPersonProtocol>
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
@property(nonatomic, assign) NSInteger age;
- (void)sayHello;
+ (void)sayBye;
@end