类结构探索之 内存结构

类和对象的关系

要说起类和对象的关系,我可能只知道对象是类创建(alloc init,new)出来的,而且一个类可以创建很多个对象。但这只是很浅层的关系,如果要深挖出背后的秘密,还是得从地址和内存入手。那么接下来让我们一步步的揭开类和对象之间神秘的关系。

从之前的学习中我已经了解到一些lldb指令,而读取地址和内存也需要用到,那么就再复习一下。

指令 作用
p/x 以16进制读取对象的地址或者值
x/4gx 以16进制形式读取4个8位的内存空间里面存储的值

下面开始对象和类内存关系的探索之旅:

  • 首先我在源码工程781里面创建了一个SYPerson类的对象person,具体如下图:

从图上可以看出po person,p/x person打印的都是指向person对象内存的指针。而x/4gx person打印的则是person对象的内存信息。从以前的学习中可以得知0x001d8001000020e9这段代表的就是person对象isa,这里面就存储了person的类信息。
我们要打印出isa里面的类信息,需要做一步操作。

p/x 0x001d8001000020e9 & 0x00007ffffffffff8ULL

具体原因可以看前面的文章 对象的内存结构分析。
那么接下来我们打印一下isa里面的类信息,具体操作如下图所示:

打印person的isa

从图片上可以看到,用po指令打印0x00000001000020e8得到的是SYPerson,这说明0x001d8001000020e9就是person对象isa,那如果接下来我们继续读取0x00000001000020e8的内存信息又会得到什么呢?请看下图:

我用x/4gx 0x00000001000020e8读取person对象的isa类信息的指针指向的内存,然后再次读取isa 0x00000001000020c0指针所指向的内存信息,然后通过po指令打印发现还是SYPerson。这时候疑问就来了,前面我们打印过person对象的指针是0x00000001007771e0,那么说明0x00000001000020c0不是person对象,那会是什么呢?我们大胆的猜测一下,这会不会是SYPerson类的指针呢?下面来验证一下:
直接打印类的指针

从图片可以看出,不论是通过类的class方法还是用底层方法获取对象的类都打印的是0x00000001000020e8,从上文一看就可以知道,person对象的isa的类信息指针就是SYPerson的类指针。用文字描述起来可能有点绕,那么接下来用一幅图来总结一下上述过程。
对象和类的指针及内存关系图

在经过不停的读取isa里面类信息指针所指向的内存,发现最终都是指向的同一个地址:0x00000001003340f0,po这个地址打印的是NSObject,那么这个地址是否就是NSObject类的指针呢?

从上图的打印信息来看并不是同一个,那这又是怎么回事呢?那么类或者说类信息在内存中到底存在着几份呢?下面通过一组对象地址的打印来验证一下。

从上图可以看出,通过不同的方式创建了3个不同的对象,但是最后打印的指针是一模一样的。这就说明类信息在内存中只存在一份。

既然类信息在内存中只存在一份,那么为什么又能打印出两个不同的地址呢?而且由元类的isa所指向的,那么就称之为根元类吧。

类和对象的isa关系经典图

在上面一部分内容中,如果我们一直不停的打印isa的内存,最后发现永远都是同一个地址了。那就说明根元类的isa指向的还是根元类自己。
以图为证:



再通过打印类、元类、根元类的指针来说明一下:



从图片上可以看出,根元类,根根元类的指针地址都是一模一样的。结合上述文章中的内容,可以用一个图来总结。
类和对象的isa关系图

再来个经典的图吧:


isa流程图

探索objc_class与objc_object

要探索类的结构,可以先看看被编译过的类包含了哪些东西,然后再从内存里面取出来。首先打开之前用Clang命令编译出来的main.cpp文件,我们直接搜索上次的LGPerson_IMPL函数,发现上面有一行:

typedef struct objc_object LGPerson;

那么这个struct objc_object 是什么呢?这个可以去源码里搜索一下,看看到底是个什么。
看到main.cpp的长度有11万行,漫无目的的看也没办法,那么思考一下,类都有哪些东西?类有声明和实现,有属性、有成员变量,有类方法,有成员方法,还有协议,有分类...
既然有这么多东西,那去文件里面找找跟LGPerson相关的property,protocol,method。看看是否存在。
在main.cpp搜索LGPerson,然后果然看到了熟悉的_INSTANCE_METHODS_LGPerson,_PROP_LIST_LGPerson,这就说明了类在被编译后把属性、方法等都存起来了,那么要知道内存分布怎么办呢?那就只能找源码了。

上面看到了struct objc_object,那么可以先去源码看看是什么东西,在objc781源码里面搜索struct objc_object可以看到有两个实现,一个里面是创建了一个Class的isa,一个里面创建了各种类和对象所包含的东西。

实在演不下去了!!!

大家看到这里一定感觉很牵强,不开上帝视角的话哪有这么好找。那么我有一个新的方法,可以直接找到我们所研究的类的结构。
直接打开源码,在main方法里面输入Class *class。然后通过Class点进去,可以发现都是typedef struct objc_class *Class;,这说明Class(类)都是struct objc_class创建而来的。那我们继续查看struct objc_class,可以发现一共去到了两个实现方法,但是其中一个打上了OBJC2_UNAVAILABLE标签,说明是不可用的了,那就不看了;看看新的方法里面的实现:

objc_class实现

可以看到objc_class是继承于objc_object的,而objc_class本身是没有isa的,isa继承自objc_object,对象又是由类创建的。

可以发现,对象、类、元类都有isa,那么可以说所有的对象都是按照objc_object的模板继承而来的,所有的对象都继承于objc_object.

该回合曾出现过的面试题:谈谈对象与objc_object 的关系。

类的内存结构

  • objc_class 结构探索。
    上一小节已经知道了objc_class是继承于objc_object的,而且还有4个属性。

    • ISA:继承于objc_object,是每个类或者对象都需要属性。
    • Class superclass:父类
    • cache_t cache:缓存指针和函数表
    • class_data_bits_t bits:class_rw_t加上自定义的属性、方法,初始化的标记..(翻译的什么乱七八糟的,反正属性、方法那些东西就存在这里了。)
      好了,既然知道了类的内存分布了,那么我们就来查看一下类的内存,然后看看是否能够直接打印出来。
      打印类的内存信息

      从图中我们可以看到,除了0x00000001000020c0是isa之外,我们只能打印出superclass 0x0000000100334140的值,其他两个属性直接就是一串数字,这该怎么解呢?而且有可能cache根本就不止占8个字节,那么这样一打印肯定就会有问题,那么有没有其他方法呢?
  • 指针偏移
    C语言有个操作叫指针偏移,意思就是可以通过指针的变化来直接读取内存里面的值。具体过程可以看类结构探索之-内存偏移。

  • 研究 cache_t 的大小
    既然已经知道可以通过指针偏移来取类对象里面的值。但是应该偏移多少呢?来看看objc_class的内存构成。

    • isa:8个字节
    • superClass:8个字节
    • cache:需要查看cache_t的大小

进入cache_t的函数定义可以看到:


计算cache的大小只需要计算红框中的属性,其他的static属性,方法都不需要计算大小。而且前3个框起来的属性是在if条件语句里面的,所以只需要计算其中一组即可。

  • 计算第一组
    explicit_atomic _buckets;
    explicit_atomic _mask;

第一个属性_buckets看起来是explicit_atomic类型,但是实际上应该读取里面的值,因为是个结构体,所以经过计算结构体里面的属性的字节数的和。

//struct bucket_t 结构体定义
#if __arm64__
    explicit_atomic _imp;
    explicit_atomic _sel;
#else
    explicit_atomic _sel;
    explicit_atomic _imp;
#endif

通过查看发现uintptr_t实际就是unsigned long无符号长整型,占8个字节,而SEL是方法,不用计算。所以第一个属性就是8个字节。
第二个属性mask_t实际就是uint32_t,而uint32_t就是unsigned int无符号整型,占4个字节。所以第二个属性就是4个字节。

  • 其实第二组、第三组都是一样的计算方法且是一样的大小,这里就不一一计算了。

  • 接下来就是两个uint16_t,实际上就是unsigned short无符号短整型,占2个字节,所以一共是4个字节。

那么cache所占的总内存就是16字节了。从而要想拿到bits的值,需要偏移的总大小就是8 + 8 + 16 = 32字节。

通过指针偏移读取类内存中的值
  • 偏移32字节怎么计算?
    拿到LGPerson类的首地址为:0x00000001000022d8,那么偏移32字节后地址为:0x00000001000022f8,这是为什么呢?
    首先因为地址0x00000001000022d8是用16进制表示的,然后我们需要偏移32位,也就是需要加32位,但是32是10进制的数值,所以我们需要先转换成16进制数之后,然后再相加。32转换为16进制为0x20,那么0x00000001000022d8 + 0x20 等于多少呢?
  0x00000001000022d8
+ 0x0000000000000020
= 0x00000001000022f8
//怎么算的呢?8 + 0 = 8、d + 2 = f。
//因为16进制是用 0 - f 表示的。如果是e + 2,那么就需要往前一位加1了,因为已经满了16位了。
  • 开始打印class_data_bits_t的内存,直接打印发现输出:(long) $24 = 4294976248,看到前面的long,猜测可能是数据类型不对,那么带上类型强转试试。
    p (class_data_bits_t *)0x00000001000022f8,结果得到(class_data_bits_t *) $1 = 0x00000001000022f8,这下就对了,然后下一步。从源码里面看到:
    class_rw_t *data() const {
        return bits.data();
    }

data()方法会直接返回bits.data()数据,那么尝试一下调用data()方法,看看能取到什么?

(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000100791680

果然取到了class_rw_t类型的数据,那么接下来用p *$2取值,发现打印了下面一堆东西:

(class_rw_t) $3 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic = 4294975792
  }
  firstSubclass = LGTeacher
  nextSiblingClass = NSUUID
}

出来能直接看出firstSubclassLGTeacher再也看不到其他熟悉的信息了。那么类的属性、方法都哪去了呢?

  • 查看class_rw_t的结构和方法
    进入class_rw_t的结构体定义里面看看,有没有开放的接口可以供调用获取信息的。
    首先看到class_rw_t结构体定义的属性跟前面p *$2取到的值的属性一模一样,这说明这个方向一定是对的。
    然后就看到了private:,私有的,这说明这一部分函数外部应该是调用不了的,就直接pass掉。
    接下来看到public,公共的,这里的函数应该是可以直接调用的,那么先找一些熟悉的方法来调用一下试试。
const method_array_t methods() const {}
const property_array_t properties() const {}
const protocol_array_t protocols() const {}

看到有熟悉属性的函数只有这些了,那么接下来就试试用$3调用properties()方法,看看输出些什么。

(const property_array_t) $4 = {
  list_array_tt = {
     = {
      list = 0x0000000100002228
      arrayAndFlag = 4294976040
    }
  }
}

接下来就直接用$4调用list方法,得到一个property_list_t类型的$5

(property_list_t *const) $5 = 0x0000000100002228

然后取$5的值,p *$5打印之后:

(lldb) p * $5
(property_list_t) $26 = {
  entsize_list_tt = {
    entsizeAndFlags = 7935620
    count = 1
    first = (name = "\x10\310   ", attributes = 0x0000000000000000)
  }
}

或者也可以用p $5[0]来打印对应的值.
然后methods()方法可以用同样的步骤来实现。最后发现,属性列表里面只有属性,没有成员变量,方法列表也只有成员方法,没有类方法。那么类的成员变量和类方法到底存在哪里呢?

类方法类方法,有可能存在父类(即元类)的内存里面,那么获取元类的地址,然后偏移,按照前面的流程走一遍,发现类方法果然在元类的内存里面。具体操作步骤如下图:


元类的内存读取过程

总结:类的属性和成员方法存储在类的内存中,而类的类方法存储在元类的内存中。

  • 类的内存

    • method list:属性的get\set方法、公共成员的方法,成员方法
    • properties:属性
    • protocols:暂时没看到内容
  • 元类的内存

    • methods:类方法、成员方法、属性的set\get方法、成员变量的方法、协议、一些系统的方法
    • properties:暂时没看到内容
    • protocols:暂时没看到内容

问题:类方法怎么存到元类里面的。。。什么时候存的。

你可能感兴趣的:(类结构探索之 内存结构)