底层研究 - 类的底层探索(中)
前言
在底层研究 - 类的底层探索(上)中我们已经探索得知了类对象本质为objc_class结构体,同时类中的信息都存储在 class_rw_t 和 class_ro_t 中,那么接下来的重点就是探索下 class_rw_t 和class_ro_t以及使用runtime来获取类的基本信息。
1、clean memory & dirty memory
在WWDC 2020有一段介绍提到了关于class_rw_t 和 class_ro_t 之间的关系,推荐观看,十分牛掰!!o( ̄▽ ̄)d
接下来我们结合视频和源码进行探索下,首先我们要先知道2个概念:
-
clean memory
clean memory 是指加载后不会发生改变的内存。它可以进行移除来节省更多的内存空间,需要时再从磁盘加载。
class_ro_t就是属于 clean memory。
-
dirty memory
dirty memory是指在运行时会发生改变的内存。当类开始使用时,系统会在运行时为它分配一块额外的内存空间,也就是dirty memory,只要进程在运行,它就会一直存在,因此使用代价很高。
2、 ro & rw & rwe
-
class_ro_t
ro,也就是readonly,class_ro_t 是在编译的时候生成的。当类在编译的时候,类的属性,实例⽅法,协议这些内容就存在class_ro_t这个结构体⾥⾯了,这是⼀块clean memory,不允许被修改,但可以在不使用的时候被删除,需要的时候再从磁盘加载。
-
class_rw_t
rw,即readwrite,class_rw_t是在运⾏的时候⽣成的。当一个类第一次被使用时,rumtime会为其分配额外的内存,即class_rw_t ,它会先将class_ro_t的内容 剪切 到class_rw_t中存储,整个内存中其实只有一份。
通过 First Subclass 和 Next Sibling Class 指针实现了把类连接成一个树状结构,这就决定了runtime能够遍历当前使用的所有类
可以发现Methods、Properties、Protocols不仅存在于ro,也存在了rw中,这是因为在运行时它们是可以发生改变的,比如通过category添加方法或者通过runtime的api动态添加它们,系统需要去跟踪这些内容。
但按这种做法,会导致占用相当多的内存,那怎么取缩小这些结构呢?
在对bits的源码探索中,我们发现了 rw 提供三个方法method()、properties()、protocols()来分别返回方法,属性和协议,也就是说rw并没有直接存储这三者,而是存在于一个ro_or_rw_ext,因为rw属于dirty memory,使用开销大,因为把一些类的信息分离出来,能减小开销。
-
class_rw_ext_t
class_rw_ext_t可以减少内存的消耗。苹果在wwdc2020⾥⾯说过,只有⼤约10%左右的类需要动态修改。所以只有10%左右的类⾥⾯需要⽣成class_rw_ext_t这个结构体。这样的话,可以节约很⼤⼀部分内存。对于动态修改的类可以通过class_rw_t结构体中提供的ext()方法获取class_rw_ext_t。
内存结构之class_rw_ext_t我们通过源码也可以发现,在methods方法中,也是先判断的是否存在rwe,有则从rwe获取方法,无则直接从ro中获取。
那么 class_rw_ext_t 的⽣成的条件是什么呢?
- ⽤过runtime的Api进⾏动态修改的时候。
- 有分类的时候,且分类和本类都为⾮懒加载类的时候。实现了+load⽅法即为⾮懒加载类。
因此我们也能得到一个rw,ro,rwe的具体关系图
rw,ro,rwe的具体关系图
3、runtime的基本使用
- 获取类的成员变量
-(void)class_copyIvarList:(Class)pClass {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(pClass, &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
const char *cName = ivar_getName(ivar);
const char *cType = ivar_getTypeEncoding(ivar);
NSLog(@"name = %s type = %s",cName,cType);
}
free(ivars);
}
- 获取类的属性
-(void)class_copyPropertyList:(Class)pClass {
unsigned int outCount = 0;
objc_property_t *perperties = class_copyPropertyList(pClass, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = perperties[i];
const char *cName = property_getName(property);
const char *cType = property_getAttributes(property);
NSLog(@"name = %s type = %s",cName,cType);
}
free(perperties);
}
- 获取类的方法
-(void)class_copyMethodList:(Class)pClass {
unsigned int outCount = 0;
Method *methods = class_copyMethodList(pClass, &outCount);
for (int i = 0; i < outCount; i++) {
Method method = methods[i];
NSString *name = NSStringFromSelector(method_getName(method));
const char *cType = method_getTypeEncoding(method);
NSLog(@"name = %@ type = %s",name,cType);
}
free(methods);
}
- 交换方法的实现,仅限当前类有效
-(void)class_replaceMethod{
//参数:1.类Class 2.⽅法名SEL 3.⽅法的实现IMP 4.⽅法参数描述
//返回:BOOL
BOOL result = class_replaceMethod([self class], @selector(method1), [self
methodForSelector:@selector(method2)], NULL);
NSLog(@"class_replaceMethod:%d",result);
}
-(void)method1{
NSLog(@"method1");
}
-(void)method2{
NSLog(@"method2");
}
- 分类交换方法
+(void)load{
NSString *className = NSStringFromClass(self.class);
NSLog(@"classname %@", className);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = object_getClass((id)self);
SEL originalSelector = @selector(systemMethod_PrintLog);
SEL swizzledSelector = @selector(ll_imageName);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
稍微讲解下:
-
dispatch_once
dyld虽然能够保证调用Class的load时是线程安全的,但还是推荐使用dispatch_once做保护,防止极端情况下load被显示强制调用时,重复交换(第一次交换成功,下次又换回来了...),造成逻辑混乱。 -
class_addMethod
给指定Class添加一个SEL的实现(或者说是SEL和指定IMP的绑定),如果该SEL在父类中有实现,则会添加一个覆盖父类的方法,添加成功返回YES,SEL已经存在或添加失败返回NO。
因为iOS Runtime消息传递机制的影响,只执行method_exchangeImplementations操作时可能会影响到父类的方法,这也是先使用class_addMethod的原因。 -
class_replaceMethod
- 如果该Class不存在指定SEL,则class_replaceMethod的作用就和class_addMethod一样;
- 如果该Class存在指定的SEL,则class_replaceMethod的作用就和method_setImplementation一样。
4、总结
- clean memory是指加载后不会发生改变的内存
- dirty memory是指在运行时会发生改变的内存
- ro、rw、rwe具体流程
(1) 当编译的时候,类会自动生成ro
(2) 当类被使用时,会生成rw,并把ro 剪切 到rw中
(3) 当类被修改时,rw内会新增rwe,把允许修改的内容转移到rwe,读取时优先读取rwe的内容
(4) 内存不足时,ro 可以在不使用的时候被移除 - 常用的runtime方法:
(1) 成员变量:class_copyIvarList、ivar_getName、ivar_getTypeEncoding
(2) 属性:class_copyPropertyList、property_getName、property_getAttributes
(3) 方法:class_copyMethodList、method_getTypeEncoding、class_replaceMethod、class_addMethod、method_exchangeImplementations