OC类原理——(下)

386 阅读6分钟

Class_rw_t、Class_ro_t、Class_rw_ext_t

  开始分析之前,补充点类结构中class_rw_tclass_ro_tclass_rw_ext_t的用途,class_ro_t属于从磁盘加载进内存时,一经加载就不再发生改变的,它是属于clean memory,它是只读的。
  那么相对于dirty memory就是进程运行时会发生更改的内存,类结构一经使用就会变成dirty memory,因为运行时会向它写入新的数据。dirty memory是这个类数据被分成两部分的原因,当一个类第一次被使用时,运行时会为它分配额外的存储容量class_rw_t,用于读取和编写数据,这个数据中存储了只有运行时才会生成的数据信息。另外我们看下class_rw_tclass_ro_t的结构图:

截屏2022-04-19 下午3.54.41.png

可以看到class_rw_t中也有MethodsProperties,这是因为它们在运行时可以被修改,当category被加载时,它可以向类中添加新的方法,而开发者可以使用运行时API动态地添加它们。因为class_ro_t是只读的,而class_rw_t需要追踪它们,这样也同时会导致占用相当多的内存。如何缩小class_rw_t呢,这就用到了class_rw_ext_t,因为经过论证,大多数类不会经常修改它们的MethodsProperties等,所以可以把它们放在class_rw_ext_t中。

类的内存结构

我们在之前文章OC类原理(上)遗留下一个问题,就是CTTeacher继承于CTPerson,但是我们在Class_rw_t看到FirstSubClass是nil,我们这里进行下调试:

截屏2022-04-19 下午4.18.30.png

截屏2022-04-19 下午4.20.55.png

可以看到打印一次CTTeacher之后,firstSubclass就有值了,这是因为类的加载是一种懒加载形式,具体的原因我们后面篇章会详细阐述。上一篇章还有两个遗留点,就是我们在class_rw_tMethodsProperties中没有找到成员变量subject,那他们存在哪里呢,基于开篇的介绍,我想你大概已经猜到,它存在class_ro_t中,我们同样可以验证下:

截屏2022-04-19 下午4.36.26.png

我们在class_rw_t这个结构体里找到了roget方法,调试下我们可以看到:

截屏2022-04-19 下午4.44.40.png 截屏2022-04-19 下午4.46.40.png

上图中,可以清楚看到我们取出了三个成员变量,因为属性默认也会带有一个成员变量。

成员变量&属性

首先我们声明两个实例变量和三个属性,若成员变量是个对象,则可以称为实例变量,如下图:

截屏2022-04-19 下午5.16.43.png

接着通过clang指令编译成.cpp文件,如下图:

clang -rewrite-objc main.m -o main.cpp 示例

截屏2022-04-19 下午5.19.32.png 截屏2022-04-19 下午5.20.21.png

我们可以清楚看到,三个属性已经被转换成带_的成员变量,并且自动生成了各自的setget方法,并且我们观察的话可以发现interestsset方法里调用的是objc_setProperty方法,而homeset方法里是self+OBJC_IVAR_$_CTTest$_home内存平移赋值,后续我们会分析这里。另外我们在文件中还发现一些奇怪的符号,如下图:

截屏2022-04-19 下午5.28.00.png

这些奇怪的符号就是编码,我们通过点击ivar_getTypeEncoding()(这个方法直接代码打出)然后快捷键command+shift+0进入apple documentation,可以看到所有相关的编码。

setter方法的底层原理

这里我们解决前面遗留大问题,objc_setProperty和内存平移赋值有什么区别呢。因为这些方法在编译时就已经进行了重定向调用,所以我们在objc底层源码里肯定是看不到过程的,只能去llvm中寻找,接着我们下载llvm-project源码进行查找,我们可直接全局搜索objc_setProperty反向推理它的调用过程:

截屏2022-04-19 下午10.24.25.png

通过上图找到调用getSetPropertyFn()的地方,接着找到如下函数:

截屏2022-04-19 下午10.27.14.png 截屏2022-04-19 下午10.26.42.png

这里我们看出setPropertyFn()获取是根据PropertyImplStrategy来调用的,我们看下它的实现及类型:

截屏2022-04-19 下午10.27.58.png

截屏2022-04-19 下午10.29.49.png `` 这里我们可以清楚看到如果包含有copy关键字时,系统是一定会调用setProperty,如果包含atomic,则必定会调用getProperty方法。我们代码验证下,首先声明两个属性都包含copy关键字的,一个是nonatomic,一个是atomic,看下结果:

截屏2022-04-19 下午10.43.32.png

经过clang编译后,我们可以看到结果:

截屏2022-04-19 下午10.45.56.png

清楚看到,interestsset方法调用了objc_setProperty()get方法也调用了objc_getProperty(),而testStr只有set方法调用了objc_setProperty(),get方法通过内存平移获取值。但为啥这里会需要调用objc_setProperty()呢,这在objc底层源码能清楚的看到: 截屏2022-04-21 下午3.03.56.png 上图中我们可以看到,不论是objc_setProperty(),还是其他的objc_setProperty_atomin()等,都是调用的reallySetProperty()方法,我们进去看下:

截屏2022-04-21 下午3.07.45.png

我们发现它其实涉及到内存的拷贝问题,所以在有copy关键时,才会调用objc_SetProperty(),那么就清楚了strong修饰的属性就是内存平移直接赋值的。

类方法的存储

我们之前都已经通过class_rw_t这个结构体里的methods()方法拿到所有的实例方法,那类方法在哪里呢,我们先看看我们之前的lldb调试结果:

截屏2022-04-21 下午4.23.40.png

截屏2022-04-21 下午4.35.03.png 截屏2022-04-21 下午4.35.46.png

我么可以看到总共有6个方法,其中4个方法是属性的setget方法,剩下的是实例方法sayNB和init初始化函数,都是实例方法,但其实我们通过MachOView是可以看到那个类方法的,如下图:

截屏2022-04-21 下午4.43.39.png

那么我们就想了,实例方法是存在类里面的,那类方法会不会在元类里呢?我们可以验证下:

截屏2022-04-21 下午4.55.38.png

我们首先拿到CTPerson这个类的isa,然后通过掩码ISA_MASK(0x00007ffffffffff8ULL)找到元类,那么接下来获取class_rw_t以及methods跟之前类的操作是一样的:

截屏2022-04-21 下午4.59.18.png

我们可以看到上图中已经找到了这个类方法say666,证明我们的猜测没错,类方法是存储在元类(MetaClass)中。

类方法存储的API方法解析

我们前面都是通过lldb调试来获取相关的对象方法和类方法,这里我们通过runtime的一些API调用来获取下:

截屏2022-04-21 下午5.38.22.png

void ct_getCopyMethodList(Class cls){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(cls, &count);
    for (unsigned int i = 0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *methodStr = NSStringFromSelector(method_getName(method));

        NSLog(@"method name ===== %@",methodStr);
    }
}

上图中是通过传入的类来获取所有的方法,然后打印出所有的方法名字,下面我们通过传入的类获取到它的实例方法,如下图:

截屏2022-04-21 下午5.52.17.png

    const char *className = class_getName(cls);
    Class metaClass = objc_getMetaClass(className);

    Method methodOne = class_getInstanceMethod(cls, @selector(run));
    Method methodTwo = class_getInstanceMethod(metaClass, @selector(run));

    Method methodThree = class_getInstanceMethod(cls, @selector(eat));
    Method methodFour = class_getInstanceMethod(metaClass, @selector(eat));
    NSLog(@"%s ----- one=%p--two=%p---three=%p---four=%p",__func__,methodOne,methodTwo,methodThree,methodFour);
}

这里我们要明白一点,我们在研究底层这里,要清楚其实类和元类等都是对象,下面我们看根据传入的CTPerson类他们各自获取的方法:

截屏2022-04-21 下午5.52.43.png

截屏2022-04-21 下午5.57.20.png

上面调试结果,我们可以看到所有的对象方法包括run、name、setName.cxx_destruct是自动生成的c++析构函数,我们不用关心。我们可以发现类方法eat不在其中,接着我们看ctInstanceMethod_classWithMetaClass方法调用打印结果可以看出,类方法eat在元类metaClass的方法列表中,而不在类CTPerson的方法中。
    我们接下来通过传入的类获取下他们各自的类方法:

截屏2022-04-21 下午8.16.38.png

我们看到对象方法run通过class_getClassMethod()获取空的,这毫无疑问,+(void)eat通过CTPerson调用class_getClassMethod()获取到它自身的类方法也没什么问题,问题是元类metaClass怎么也获取到eat这个类方法呢,对它来说正常eat应该相当于对象方法的,我们打开class_getClassMethod()底层源码看下:

截屏2022-04-21 下午8.25.11.png

我们看到它最终还是调用的class_getInstancMethod(),我们再看下cls->getMeta()这个方法的调用,getMeta()方法里做了判断,如果是metaClass,直接返回它自己,所以问题迎刃而解,如下图:

截屏2022-04-21 下午8.35.33.png

我们接着获取下他们各自方法的实现:

截屏2022-04-21 下午8.42.03.png

CTPerson获取对象方法run的实现有值,metaClass获取类方法+(void)eat的实现有值都是正常的,但metaClass获取对象方法和CTPerson获取类方法实现为何也有值呢,他们本应该是空的,我们看下class_getMethodImplementation的方法实现:

截屏2022-04-21 下午8.49.10.png

我们看到当找不到imp时,它返回_objc_msgForward,所以其实那两个地址并不是runeat的方法地址,至于_objc_msgForward走向哪里,我们后续篇章会讲到。