类和对象的关系
要说起类和对象的关系,我可能只知道对象是类创建(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里面的类信息,具体操作如下图所示:
从图片上可以看到,用
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指向的还是根元类自己。
以图为证:
再通过打印类、元类、根元类的指针来说明一下:
从图片上可以看出,根元类,根根元类的指针地址都是一模一样的。结合上述文章中的内容,可以用一个图来总结。
再来个经典的图吧:
探索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_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个字节,那么这样一打印肯定就会有问题,那么有没有其他方法呢?
- ISA:继承于
指针偏移
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
}
出来能直接看出firstSubclass
是LGTeacher
再也看不到其他熟悉的信息了。那么类的属性、方法都哪去了呢?
- 查看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:暂时没看到内容
问题:类方法怎么存到元类里面的。。。什么时候存的。