OC底层研究4--类的结构分析


本页所使用的objc runtime 756.2,来自GITHUB

1.概念

1.1 类Class 的类型

Class在源码里的真正类型为objc_class结构体

先查看源码——在Xcode按下Shift+Command+O,选择objc-runtime-new.h,搜索objc_class,可以得知,Class底层编译实现的前4行如下:

可知类结构如下:

  • A. //Class ISA

    ​ 继承自父类 objc_classisa ,指向类的指针不需显示(内存占8位),这里它返回一个nonpointer的指针,相关源码如下:

    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
  • B. Class superclass;

    ​ 指向本类的父类(内存占8位)

  • C. cache_t cache;

    ​ 存储指针和vtable,加速方法调用(内存占8位)

  • D. class_data_bits_t bits;

    ​ 存储类的方法、属性、遵循的协议等信息的地方(内存占8位)

  • E. 其他方法/函数 – 不占内存

为何cache_t cache 这个变量占位位16,而不是8?

原因是因为这里的cache_t是结构体,其占内存为根据内部属性结构而定,为防止溢出,给予16位字节,而不是指针所需的8位。

Talk is cheap, show me the code, 伸伸手指,点开cache_t 的内部结构

我们来看看属性:

  • bucket_t, 结构体,占8位字节
  • mask_t,点开后具体实现为如下:
typedef uint32_t mask_t; 

​ 为整型,占4个字节

  • mask_t
typedef uint32_t mask_t;

​ 也是整形,占4个字节

共计8+4+4 = 16个字节

1.2 objc_class继承

在底层实现里,objc_class 继承自objc_object结构体

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

1.3 关于NSobject

万物皆对象 - Class 继承自NSObject

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

2. 类的属性与成员变量

  1. 属性的存储

    上面提到了类对象的结构原理,我们知道了类结构里,类型位class_data_bits_tbits下面我们来探寻一下,类里面的属性上怎么保存的。

    我们在代码里生成一个类,分别有成员变量hobby,以及属性nickName

    代码如下:

    @interface Person : NSObject{
        NSString *hobby;
    }
    
    @property (nonatomic, copy) NSString *nickName;

    在主线代码引入:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            LGPerson *person = [LGPerson alloc];
            Class pClass     = object_getClass(person);
            NSLog(@"%@ -- %p", person, pClass);
        }
        return 0;
    }

    然后打印得到的类pClass,通过x/4gx 获取类结构如下:

    (lldb) x/4gx pClass
    0x1000023d0: 0x001d8001000023a9 0x0000000100b37140
    0x1000023e0: 0x00000001003da280 0x0000000000000000

    观察结果得知0x1000023d0pClass在内存中的首地址,根据内存偏移原则,加上16+8+8共32位,即可得到bits的结构。

    0x1000023d0,偏移32位,得到0x1000023f0。注意,这里要用p打印指针,而非打印值用的po

    (lldb) p 0x1000023f0
    4294976496

    oh NO!得到的是具体的值,这里需要强转一下:

    (lldb) p (class_data_bits_t *)0x1000023f0
    (class_data_bits_t *) $3 = 0x00000001000023f0

    得到$3 的值,并不是我们想要的结构,留意到objc_class里的方法 data() 方法:

    struct objc_class : objc_object {
        // Class ISA; // 8
        Class superclass; // 8
        cache_t cache;    // 16 不是8         // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() { 
            return bits.data();
        }

    data() 返回是class_rw_t,点进去看看,

    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        char *demangledName;

    留意到里面有 methodspropertiesprotocols等,原来这里等值便是类的相关属性方法等等结构。

    现在对$3 执行data() 方法:

    (lldb) p $3->data()
    (class_rw_t *) $5 = 0x0000000000000000

    对得到的$5 进行值打印:

    (lldb) p *$5
    (class_rw_t) $5 = {
      flags = 2148139008
      version = 0
      ro = 0x0000000100002308
      methods = {
        list_array_tt<method_t, method_list_t> = {
           = {
            list = 0x0000000100002240
            arrayAndFlag = 4294976064
          }
        }
      }
      properties = {
        list_array_tt<property_t, property_list_t> = {
           = {
            list = 0x00000001000022f0
            arrayAndFlag = 4294976240
          }
        }
      }
      protocols = {
        list_array_tt<unsigned long, protocol_list_t> = {
           = {
            list = 0x0000000000000000
            arrayAndFlag = 0
          }
        }
      }
      firstSubclass = nil
      nextSiblingClass = NSUUID
      demangledName = 0x0000000000000000
    }

    如上所示,$5 内结构一目了然,方法,属性,协议都显示了。

    继续获取属性值:

    (lldb) p $5.properties
    (property_array_t) $7 = {
      list_array_tt<property_t, property_list_t> = {
         = {
          list = 0x00000001000022f0
          arrayAndFlag = 4294976240
        }
      }
    }

    得到一个二维数组的$7,观察属性列表的类型为property_array_t,继承自list_array_tt,内部拥有

    (lldb) p $7.list
    (property_list_t *) $8 = 0x00000001000022f0
    

    再次对$8 进行* 取值,得到如下结果:

    (lldb) p *$8
    (property_list_t) $9 = {
      entsize_list_tt<property_t, property_list_t, 0> = {
        entsizeAndFlags = 16
        count = 1
        first = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
      }
    }

    从结果可以看出,我们的属性,就保存在properties 里中。

  2. 成员变量的存储

    上面分析了属性的存储,但是我们没有看到成员变量 hobby,我们在properties 属性里,没有看到它,到底在哪里呢?下面我们继续查找。

    既然不在properties 里,那尝试一下在$5 的 ro 里进行取值

    (lldb) p $5.ro
    (const class_ro_t *) $10 = 0x0000000100002308

    得到class_ro_t 类型的结构体,继续对他取值

    (lldb) p *$10
    (const class_ro_t) $11 = {
      flags = 388
      instanceStart = 8
      instanceSize = 24
      reserved = 0
      ivarLayout = 0x0000000100001f8a "\x02"
      name = 0x0000000100001f81 "LGPerson"
      baseMethodList = 0x0000000100002240
      baseProtocols = 0x0000000000000000
      ivars = 0x00000001000022a8
      weakIvarLayout = 0x0000000000000000
      baseProperties = 0x00000001000022f0
    }

    值这里可以看到ivars,就是我们需要的变量列表,打印一下:

    (lldb) p $11.ivars
    (const ivar_list_t *const) $12 = 0x00000001000022a8

    继续对 ivar_list_t 类型的$12 取值:

    (lldb) p *$12
    (const ivar_list_t) $13 = {
      entsize_list_tt<ivar_t, ivar_list_t, 0> = {
        entsizeAndFlags = 32
        count = 2
        first = {
          offset = 0x0000000100002378
          name = 0x0000000100001e64 "hobby"
          type = 0x0000000100001fa7 "@\"NSString\""
          alignment_raw = 3
          size = 8
        }
      }
    }
    (lldb) p $.first
    (const ivar_t) $13 = {
      offset = 0x0000000100002378
      name = 0x0000000100001e64 "hobby"
      type = 0x0000000100001fa7 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }

    可以看到成员变量存在类的 ivars 属性

3. 类的方法

3.1 类的实例方法

我们继续对类进行代码编辑,对Person类添加实例方法和类方法

- (void)sayHello;        // 实例方法
+ (void)sayHappy;        // 类方法

还是对类结构$5 进行解析,这次我们打印它的 methods 属性

(lldb) p $5.methods
(method_array_t) $14 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002240
      arrayAndFlag = 4294976064
    }
  }
}

得到的是method_array_t 类型的$14, 是个数组类型,继续打印:

(lldb) p $14.list
(method_list_t *) $15 = 0x0000000100002240
(lldb) p *$15
(method_list_t) $16 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100001f8c "v16@0:8"
      imp = 0x0000000100001b90 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
    }
  }
}

可见,实例方法sayHello 方法,存储在类的methods属性里

3.2 类的类方法存储

类方法在上述的过程中没有看到,那么它会在哪里呢?

为了找到它,我们重新回到最初的pClass

(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000

得到isa值为0x001d800100002389,这时,唤出它的掩码 ISA_MASK 来获取它的元类

(lldb) p/x 0x001d800100002389 & 0x0000000ffffffff8
(long) $1 = 0x0000000100002388

继续对元类$1的结构进行解析:

(lldb) x/4gx 0x0000000100002388
0x100002388: 0x001d800100b370f1 0x0000000100b370f0
0x100002398: 0x0000000100f946c0 0x0000000100000003

可知0x100002388是这个元类的地址,根据上文提到的内存便宜,我们继续加上2个16进制位,得到0x1000023a8,p一下:

(lldb) p (class_data_bits_t *)$2
(class_data_bits_t *) $3 = 0x00000001000023a8

继续执行上文实例方法类似的方案,执行data() — 取值 – 获取methods 的思路,具体代码如下:

(lldb) p $3->data()
(class_rw_t *) $4 = 0x0000000100f94620
(lldb) p *$4
(class_rw_t) $5 = {
  flags = 2685075456
  version = 7
  ro = 0x00000001000021f8
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x00000001000021d8
        arrayAndFlag = 4294975960
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff942e6990
  demangledName = 0x0000000000000000
}
(lldb) p $5.methods
(method_array_t) $10 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000021d8
      arrayAndFlag = 4294975960
    }
  }
}
(lldb) p $10.list
(method_list_t *) $11 = 0x00000001000021d8
(lldb) p *$11
(method_list_t) $12 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "sayHappy"
      types = 0x0000000100001f8c "v16@0:8"
      imp = 0x0000000100001bc0 (LGTest`+[LGPerson sayHappy] at LGPerson.m:17)
    }
  }
}
(lldb) 

可见,类方法sayHappy 方法,存储在类的元类 methods属性里

4. 总结

  • 类的本质上一个类型为objc_class的结构体,包含有isa、父类、属性、成员变量及方法列表等
  • 类属性存在结构体的properties里
  • 类的成员变量存在结构体的 ivars里
  • 类的实例方法,存在结构体 的methods 属性里
  • 类的类方法,存在父类的class_ro_t 里的methods里

如下图示:


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
方法的本质1--cache_t方法缓存分析 方法的本质1--cache_t方法缓存分析
本页所使用的objc runtime 756.2,来自GITHUB 1.概念1.1 objc_class 结构前面探索了类的结构,知道了类的结构本质上是objc_class的结构体,而在 C 源码例, objc_class 结构体的结构
2020-01-21 李佳
下一篇 
2020新年感想 2020新年感想
2020年是一个闰年,第一天从星期三开始。 今天看了小羊肖恩大电影,故事讲得真好看,小朋友全程开心,全情投入,大朋友也觉得耳目一新,深受感动。 这是我们带他看的第一部电影,新的一年,从第一开始。 用一句涂鸦来鼓舞自己 宁愿最后徒劳无
2020-01-02 李佳
  目录