类结构探索之 内存结构

类和对象的关系

要说起类和对象的关系,我可能只知道对象是类创建(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<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;

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

//struct bucket_t 结构体定义
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _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<unsigned long> = 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<property_t, property_list_t> = {
     = {
      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<property_t, property_list_t, 0> = {
    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:暂时没看到内容

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343