引言
上一篇讲到了内存偏移的知识和操作,接下来内存偏移将在本文用到具体的示例。我们对对象的探究已经了解了对象的底层结构,isa的走向和对象的继承链。本文将还原探究类内部结构的过程。

类的探索

一、寻找objcclass
001-OC对象原理探究 - alloc这篇文章中,我们用到了objc源码环境调试。本文我们也在此基础上,探究类的结构。我们在对象的底层结构探索的时候,发现了类Class的底层为typedef struct objc_class *Class;也就是说,Class是一个结构体指针的别名。
具体寻找步骤:
1、objc源码调试环境工程,搜索Class {,在结果中我们得到runtime.h中的class结构:
006--iOS底层 - 类的结构(属性、成员变量、方法的探索) - 图3
2、这似乎就是我们要找的Class底层,但是看到`#if !_OBJC2
以及OBJC2_UNAVAILABLE,才知道,整个结构体struct objc_class并不适用于objc2中(本文调试的环境的是objc4_818_2)<br />3、那么我们怎么找到正确的Class呢?请看框起来的注释/ UseClassinstead of`struct objc_class */
源码如下:

  1. /// An opaque type that represents an Objective-C class.
  2. typedef struct objc_class *Class;
  3. /// Represents an instance of a class.
  4. struct objc_object {
  5. Class _Nonnull isa OBJC_ISA_AVAILABILITY;
  6. };
  7. /// A pointer to an instance of a class.
  8. typedef struct objc_object *id;

在此源码中,我们还得到一个信息:OC中id类型的底层竟然是typedef struct objc_object *id;,这就是为什么我们在定义id类型的变量时,不加*号的原因。
探索源码真的能学到很多!!!
4、搜索框输入struct objc_class,其中objc_runtime-new.h中的结果就是我们想要的,结果如下:006--iOS底层 - 类的结构(属性、成员变量、方法的探索) - 图4

二、bits
上图的objc_class内部可知bitsClass superclasscache_t cache之后。我们调试得到bits,需要上篇文章提到的内存偏移来得到。因此,我们需要知道偏移了多少字节,接下来我们开始探索Class superclasscache_t cache的内存大小。
1、superclassClass指针类型,因此superclass8字节
2、cache_t为结构体类型,其内部结构如下:(由于我们需要知道结构体内存的大小,只需要知道其成员变量的大小即可,cache_t内部的static和方法函数均不影响结构体内存大小,因此以下源码为简化后的cache_t):

  1. struct cache_t {
  2. private:
  3. explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
  4. union {
  5. struct {
  6. explicit_atomic<mask_t> _maybeMask;
  7. #if __LP64__
  8. uint16_t _flags;
  9. #endif
  10. uint16_t _occupied;
  11. };
  12. explicit_atomic<preopt_cache_t *> _originalPreoptCache;
  13. };
  14. }

3、分析
a)、_bucketsAndMaybeMaskexplicit_atomic的泛型变量,因此实际大小为泛型的大小,即uintptr_t的大小,uintptr_t的源码为:typedef unsigned long uintptr_t;因此占8字节
b)、union为共用体,内存大小为最大的成员的大小。
1)struct中,_maybeMaskmask_t,源码为typedef uint32_t mask_t;4字节uint16_t大小为2字节。结构体最大占用内存4 + 2 + 2 = 8字节
2)_originalPreoptCachepreopt_cache_t *,结构体指针类型,我们知道指针类型的大小为8字节
3)其实换个角度来看,union中,我们只需要看_originalPreoptCache的大小即可知道union占用大小为8字节
4、cache_t所占内存大小为:16字节
到此为止,我们只需要或许到类的首地址后,将其平移isa:8 + superclass:8 + cache_t:16 = 32字节才能得到bits。简化后的objc_class源码如下:

  1. struct objc_class : objc_object {
  2. Class ISA; //8字节
  3. Class superclass;// 8字节
  4. cache_t cache; // 16字节
  5. class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
  6. }

下面我们开始获取bits
1、QLPerson设计如下:

  1. @interface QLPerson : NSObject{
  2. NSString *fullName;
  3. }
  4. @property (nonatomic,copy) NSString *nickName;
  5. @property (nonatomic,assign) NSInteger age;
  6. - (void)test1;
  7. + (void)test1;
  8. @end

2、对QLPerson类进行lldb调试
3、p *$2->data()objc_class中的方法

  1. class_rw_t *data() const {
  2. return bits.data();
  3. }

006--iOS底层 - 类的结构(属性、成员变量、方法的探索) - 图5
但是似乎未能得到我们想要的东西。换种思路继续
4、我们继续objc_class向下翻找,治世之尊没有找到能看到类的属性和方法的关键词。后来看到bits后面的注释,我们要的东西是否在class_rw_t里。点进去终于看到了

  1. const method_array_t methods() const {
  2. auto v = get_ro_or_rwe();
  3. if (v.is<class_rw_ext_t *>()) {
  4. return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
  5. } else {
  6. return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
  7. }
  8. }
  9. const property_array_t properties() const {
  10. auto v = get_ro_or_rwe();
  11. if (v.is<class_rw_ext_t *>()) {
  12. return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
  13. } else {
  14. return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
  15. }
  16. }
  17. const protocol_array_t protocols() const {
  18. auto v = get_ro_or_rwe();
  19. if (v.is<class_rw_ext_t *>()) {
  20. return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
  21. } else {
  22. return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
  23. }
  24. }

methods()properties()protocols()这正是我们日思夜想的东西嘛。lldb调试如下:006--iOS底层 - 类的结构(属性、成员变量、方法的探索) - 图6由图可知:我们的@property声明的属性,均存在property_list_t中,用上图的lldb调试可以找到属性的值。但是问题来了,我们的方法成员变量均未得到,它们放在哪儿呢?
补充:properties()返回类型为property_array_t,继承自list_array_tt,源码如下:

  1. struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
  2. };

说明:关于二维数组容器list_array_tt的知识请移步这篇文章
5、properties()探索结束,未能达到我们的目的,我们接着探索methods()006--iOS底层 - 类的结构(属性、成员变量、方法的探索) - 图7
说明:我们用探索properties的方式来探索methods最终得到了该类的实例方法getter setter方法,达到部分目的,因为,我们的+(void)test2还未出现。
6、methods()探索结束我们仍未找到类方法成员变量ivar的存储位置,我们接着往下探索,在class_rw_t内找到一个ro()方法,源码如下:

  1. const class_ro_t *ro() const {
  2. auto v = get_ro_or_rwe();
  3. if (slowpath(v.is<class_rw_ext_t *>())) {
  4. return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
  5. }
  6. return v.get<const class_ro_t *>(&ro_or_rw_ext);
  7. }

class_ro_t内部部分源码为:

  1. struct class_ro_t {
  2. void *baseMethodList;
  3. const ivar_list_t * ivars;

这个ivars操作如下:
006--iOS底层 - 类的结构(属性、成员变量、方法的探索) - 图8
到此为止,我们拿到了成员变量的存储位置已经搞清楚。

7、类方法探索,换种思路,实例方法也叫做对象方法类方法似乎与对象无关,那么它是否在元类里呢?按照这个思路,我们对QLPerson的元类进行上面的查找操作:006--iOS底层 - 类的结构(属性、成员变量、方法的探索) - 图9
由图可得到类方法存储在元类中

总结
1、探索类的结构是一个漫长而复杂的过程,有些地方卡在那里,如果不转换思路,将进入死胡同。对类的探索,应该多借鉴前辈的肩膀,偶尔用上帝视角去解决遇到的难题。
2、存储位置:
类的首地址+0x20得到bits
bits->data()得到class_rw_t
a)获取类的属性(@property标记):bits中的class_rw_t中的properties()
b)获取成员变量(类大括号内的声明):bits中的class_rw_t中的ro()
c)获取实例方法(也叫对象方法-()):bits中的class_rw_t中的methods(),每一项需要加.big()来打印
d)获取类方法(+()):元类中的bits->class_rw_t->methods(),每一项需要加.big()来打印

3、以method_array_t为例,结构图如下:006--iOS底层 - 类的结构(属性、成员变量、方法的探索) - 图10