类的底层原理(下)

192 阅读4分钟

上篇文章我们找到了属性和对象方法的存储位置,那成员变量和类方法是存储在什么地方的呢?

class_rw_t这个结构体中发现没有ivar相关的方法,不过其有个class_ro_t *ro()

struct class_rw_t {
    ......省略部分......
    const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
        }
        return v.get<const class_ro_t *>(&ro_or_rw_ext);
    }
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }
     ......省略部分......
};

我们点击进入class_ro_t结构体中看一下其内部结构:

struct class_ro_t {
    ......省略部分......
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    ......省略部分......
};

可以看到里面有一个ivar_list_t * ivars,那我们同样使用获取属性的流程来获取到成员变量: lei1.png 这说明成员变量的存放位置是类对象里面的class_rw_t这个结构体中的class_ro_t结构体中,那为什么这么存储呢?class_rw_tclass_ro_t有什么区别呢?

WWDC 2020里面有一段是苹果介绍他对整个runtime的机制进行调整,从而优化了类在内存中的占用。具体地址如下:Advancements in the Objective-C runtime。下面我们针对其中关于类的结构的地方做一些总结。

clean memory & dirty memory

clean memory

  • clean memory是指加载后不会发生改变的内存
  • class_ro_t属于clean memory因为它是只读的
  • clean memory可以进行移除,节省更多的内存空间,当需要使用时再从磁盘加载

dirty memory

  • dirty memory是指在进程运行时会发生改变的内存
  • 当类开始使用的时候,系统在运行时会为它分配一片额外的内存,这片内存就是dirty memory
  • dirty memory使用起来代价很高,只要进程在运行,它就必须一直存在

类的结构优化

在2020年之前,苹果设计的类的结构是下面这样的 le2.png 我们看到每一个类,在使用后都会创建出一片内存用来存储他在运行时可能修改的数据,也就是dirty memory。我们知道dirty memory是非常昂贵的,苹果显然也意识到了这一点。在苹果的统计数据中,只有大约10%的类真正的修改了他们的方法。因此苹果将class_rw_t这个结构中的一些数据进行拆分,将class_rw_t中不是每个类必须存在的方法拆分到了class_rw_ext_t这个结构中, lei3.png 这样,整个class_rw_t的结构大小大约就减少了一半,对于不需要那些额外信息的类就如图示下面这部分内存不需要申请可以直接节省下来。对于那些确实需要额外信息的类,则如下图的流程 lei4.png 我们写的大部分的类都是不需要这些扩展数据的,只有通过非懒加载的category,runtime对类进行动态操作等的类才会有这方面的开销,这样就起到了节省内存的作用。我们看rw这个结构体中的ro()methods()等函数中可看到了他先判断了是否有创建的class_rw_ext_t,有就从class_rw_ext_t中获取,没有就从class_ro_t中获取数据。

我们现在找到了成员变量,还剩下一个类方法,那么类方法会存储在哪里呢?我们知道,实例方法都是类的实例调用的,而实例方法却在类中可以找到。我们同样知道,类也是一个对象,他是元类的实例对象,那么类方法是不是就存储在元类里面呢?我们继续我们的探索 通过方法的获取流程,我们在元类中找到了类方法的存在。 lei5.png 为什么会有元类的存在?实际上在C/C++的底层函数中,并不存在实例方法类方法的区别,统一称为函数。而在OC中,我们将他们区分了开来,但是如果他们都储存在类中,如果有同名的方法,在调用时底层并不知道要调用哪个方法,在objc_msgSend时就需要传递更多的参数来区分调用哪个方法。元类就是为了解决这个问题,而诞生的一个结构,其复用了消息机制,使类存储实例方法,元类存储类方法。 之前我们都是使用lldb来查找属性和方法,其实我们也可以使用runtime进行查找,在源码中搜索runtime的方法class_copyIvarListclass_copyPropertyListclass_copyMethodList,类方法使用class_getClassMethod查找:

Method * class_copyMethodList(Class cls, unsigned int *outCount) {
    ......省略部分......
    const auto methods = cls->data()->methods();
......省略部分......
}

class_copyIvarList(Class cls, unsigned int *outCount){
    ......省略部分......
    if ((ivars = cls->data()->ro()->ivars)  &&  ivars->count) {
    ......省略部分......
}

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) {
    ......省略部分......
    auto rw = cls->data();
    property_t **result = nil;
    auto const properties = rw->properties();
    ......省略部分......
}

Method class_getClassMethod(Class cls, SEL sel) {
    if (!cls  ||  !sel) return nil;
    return class_getInstanceMethod(cls->getMeta(), sel);
}

查找到的获取和我们lldb中是相同的,类方法也是查找的元类的对象方法。

总结

我们根据lldb和runtime找到了类内属性、方法、成员变量等的存储方式,了解了rw、ro、rwe的关系和区别,rw中存储了ro,只有通过非懒加载的category、runtime对类进行动态操作等的类时会创建rwe将ro中可修改的部分剪切到rwe中进行动态操作;元类存在的意义就是复用消息机制,保证单一职责。