Class_rw_t、Class_ro_t、Class_rw_ext_t
开始分析之前,补充点类结构中class_rw_t、class_ro_t、class_rw_ext_t的用途,class_ro_t属于从磁盘加载进内存时,一经加载就不再发生改变的,它是属于clean memory,它是只读的。
那么相对于dirty memory就是进程运行时会发生更改的内存,类结构一经使用就会变成dirty memory,因为运行时会向它写入新的数据。dirty memory是这个类数据被分成两部分的原因,当一个类第一次被使用时,运行时会为它分配额外的存储容量class_rw_t,用于读取和编写数据,这个数据中存储了只有运行时才会生成的数据信息。另外我们看下class_rw_t和class_ro_t的结构图:
可以看到class_rw_t中也有Methods和Properties,这是因为它们在运行时可以被修改,当category被加载时,它可以向类中添加新的方法,而开发者可以使用运行时API动态地添加它们。因为class_ro_t是只读的,而class_rw_t需要追踪它们,这样也同时会导致占用相当多的内存。如何缩小class_rw_t呢,这就用到了class_rw_ext_t,因为经过论证,大多数类不会经常修改它们的Methods和Properties等,所以可以把它们放在class_rw_ext_t中。
类的内存结构
我们在之前文章OC类原理(上)遗留下一个问题,就是CTTeacher继承于CTPerson,但是我们在Class_rw_t看到FirstSubClass是nil,我们这里进行下调试:
可以看到打印一次CTTeacher之后,firstSubclass就有值了,这是因为类的加载是一种懒加载形式,具体的原因我们后面篇章会详细阐述。上一篇章还有两个遗留点,就是我们在class_rw_t的Methods和Properties中没有找到成员变量subject,那他们存在哪里呢,基于开篇的介绍,我想你大概已经猜到,它存在class_ro_t中,我们同样可以验证下:
我们在class_rw_t这个结构体里找到了ro的get方法,调试下我们可以看到:
上图中,可以清楚看到我们取出了三个成员变量,因为属性默认也会带有一个成员变量。
成员变量&属性
首先我们声明两个实例变量和三个属性,若成员变量是个对象,则可以称为实例变量,如下图:
接着通过clang指令编译成.cpp文件,如下图:
clang -rewrite-objc main.m -o main.cpp 示例
我们可以清楚看到,三个属性已经被转换成带_的成员变量,并且自动生成了各自的set和get方法,并且我们观察的话可以发现interests的set方法里调用的是objc_setProperty方法,而home的set方法里是self+OBJC_IVAR_$_CTTest$_home内存平移赋值,后续我们会分析这里。另外我们在文件中还发现一些奇怪的符号,如下图:
这些奇怪的符号就是编码,我们通过点击ivar_getTypeEncoding()(这个方法直接代码打出)然后快捷键command+shift+0进入apple documentation,可以看到所有相关的编码。
setter方法的底层原理
这里我们解决前面遗留大问题,objc_setProperty和内存平移赋值有什么区别呢。因为这些方法在编译时就已经进行了重定向调用,所以我们在objc底层源码里肯定是看不到过程的,只能去llvm中寻找,接着我们下载llvm-project源码进行查找,我们可直接全局搜索objc_setProperty反向推理它的调用过程:
通过上图找到调用getSetPropertyFn()的地方,接着找到如下函数:
这里我们看出setPropertyFn()获取是根据PropertyImplStrategy来调用的,我们看下它的实现及类型:
``
这里我们可以清楚看到如果包含有
copy关键字时,系统是一定会调用setProperty,如果包含atomic,则必定会调用getProperty方法。我们代码验证下,首先声明两个属性都包含copy关键字的,一个是nonatomic,一个是atomic,看下结果:
经过clang编译后,我们可以看到结果:
清楚看到,interests的set方法调用了objc_setProperty(),get方法也调用了objc_getProperty(),而testStr只有set方法调用了objc_setProperty(),get方法通过内存平移获取值。但为啥这里会需要调用objc_setProperty()呢,这在objc底层源码能清楚的看到:
上图中我们可以看到,不论是objc_setProperty(),还是其他的objc_setProperty_atomin()等,都是调用的reallySetProperty()方法,我们进去看下:
我们发现它其实涉及到内存的拷贝问题,所以在有copy关键时,才会调用objc_SetProperty(),那么就清楚了strong修饰的属性就是内存平移直接赋值的。
类方法的存储
我们之前都已经通过class_rw_t这个结构体里的methods()方法拿到所有的实例方法,那类方法在哪里呢,我们先看看我们之前的lldb调试结果:
我么可以看到总共有6个方法,其中4个方法是属性的set和get方法,剩下的是实例方法sayNB和init初始化函数,都是实例方法,但其实我们通过MachOView是可以看到那个类方法的,如下图:
那么我们就想了,实例方法是存在类里面的,那类方法会不会在元类里呢?我们可以验证下:
我们首先拿到CTPerson这个类的isa,然后通过掩码ISA_MASK(0x00007ffffffffff8ULL)找到元类,那么接下来获取class_rw_t以及methods跟之前类的操作是一样的:
我们可以看到上图中已经找到了这个类方法say666,证明我们的猜测没错,类方法是存储在元类(MetaClass)中。
类方法存储的API方法解析
我们前面都是通过lldb调试来获取相关的对象方法和类方法,这里我们通过runtime的一些API调用来获取下:
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);
}
}
上图中是通过传入的类来获取所有的方法,然后打印出所有的方法名字,下面我们通过传入的类获取到它的实例方法,如下图:
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类他们各自获取的方法:
上面调试结果,我们可以看到所有的对象方法包括run、name、setName,.cxx_destruct是自动生成的c++析构函数,我们不用关心。我们可以发现类方法eat不在其中,接着我们看ctInstanceMethod_classWithMetaClass方法调用打印结果可以看出,类方法eat在元类metaClass的方法列表中,而不在类CTPerson的方法中。
我们接下来通过传入的类获取下他们各自的类方法:
我们看到对象方法run通过class_getClassMethod()获取空的,这毫无疑问,+(void)eat通过CTPerson调用class_getClassMethod()获取到它自身的类方法也没什么问题,问题是元类metaClass怎么也获取到eat这个类方法呢,对它来说正常eat应该相当于对象方法的,我们打开class_getClassMethod()底层源码看下:
我们看到它最终还是调用的class_getInstancMethod(),我们再看下cls->getMeta()这个方法的调用,getMeta()方法里做了判断,如果是metaClass,直接返回它自己,所以问题迎刃而解,如下图:
我们接着获取下他们各自方法的实现:
CTPerson获取对象方法run的实现有值,metaClass获取类方法+(void)eat的实现有值都是正常的,但metaClass获取对象方法和CTPerson获取类方法实现为何也有值呢,他们本应该是空的,我们看下class_getMethodImplementation的方法实现:
我们看到当找不到imp时,它返回_objc_msgForward,所以其实那两个地址并不是run和eat的方法地址,至于_objc_msgForward走向哪里,我们后续篇章会讲到。