今天我们将进行类的结构体的探索,其中有些内容我们在iOS底层探索 ---Runtime(一)--- 基础知识有做过探索。不过没关系,今天我们再来回顾一下。
本章我们将探索一下内容:
1、类的结构(类,元类,根类之间的关系)
2、objc_object & objc_class
3、类中信息的探索
1、类的结构(类,元类,根类之间的关系)
1.1 类(Class)
我们都知道,我们创建的类基本上都继承自NSObject
,而NSObject
里面有一个默认参数isa
;也就是说,继承自NSObject
的类里面都会有一个isa
。如下:
假设我们现在有一个Person
类,那么Person
对象的首地址就是isa
地址,同样的isa
也代表当前的类:
1.2 元类(Meta Class)
这个时候,我们如果继续查看类地址
,会打印什么呢?下面我们就来尝试一下:
可以看到,继续打印
类地址
,输出的是类的内存分布
。我们还可以通过
x Person.class
来查看类的内存分布
:
- 此时有一点大家注意了,我们在上面通过
isa
打印出来了Person
。对应的类地址是0x00000001000080e8
。 - 同时我们还打印出了
类的内存分布
信息。如果我们继续打印类的内存分布
信息的首地址,会发生什么呢?打印结果如下:
大家可以看到,此时我们同样打印出了Person
。这就奇怪了!!!
第一点:首先我们通过
isa
打印出Person
,这一点相信大家没有什么问题。(有疑问的同学可以参考这一篇文章iOS底层探索 --- OC对象原理(下))第二点:为什么我们通过
0x1000080e8
也能都打印出Person
呢?明明是两个不同的地址呀!!!这里就要引出元类(Meta Class)
这个知识点了
1、
Meta Class(元类)
就是类对象
所属的类
。一个对象
所属的类叫做类对象
,而一个类对象
所属的类就叫做元类
。2、
Meta Class(元类)
的定义和创建,都是由编译器自动完成。3、所有的类方法,都存储在
Meta Class(元类)
中。
1.3 根类
在上面我们已经找到了Meta Class (元类)
,那我们继续往下去寻找又会有什么发现呢?
我们会发现,居然指向了NSObject
。
此时我们我们如果执行p/x NSObject.class
这句指令,打印出来的NSObject
地址会是什么呢?
此时有没有发现问题,NSObject
的地址不一样呀。这是怎么回事呢?
这是因为p/x NSObject.class
中打印的NSObject
并不是根类,而是根元类。怎么证明呢?我们只需要打印一下NSObject.class
的内存信息,就会看到起isa
和上面打印的NSObject
的地址是一样的。
换句话说,就是元类
的isa
指向了根元类
。
(注:严谨一点是要& ISA_MASK
,这里由于isa
中只有shifcls
信息,可以直接看出来)。执行结果如下:
- 此时我们已经跟踪到了根类元类,如果我们在根类元类的基础上,继续追踪,又会出现什么情况呢?那我们就继续试一下:
大家有没有发现,根类元类的isa
指向了自己。
此时就回到我们在iOS底层探索 ---Runtime(一)--- 基础知识中探索过的那幅图:
[图片上传失败...(image-caaf2f-1624499873237)]
1.4 类对象在内存中的个数
类对象在内存中,有且仅有一份!!!。
这也是一道经常被问到的面试题,下面我们来证明一下:
我们创建多个类对象,然后打印,看一下地址是否相同
通过打印可以发现,虽然创建了多个类对象,但是地址都是一样的。因此我们可以说:类对象在内存中,有且仅有一份。
1.5 类的继承关系的坑点
假设有一个类
Man
,继承自类Person
;同时Man
有一个对象m
,Person
有一个对象p
。
那么问:m
和p
有什么关系?
答案:没有关系。(注意:一定不要说是继承关系)我们都知道
子类
继承自父类
,父类
继承自根类
。
那么问:根类
继承自什么?
答案:根类
的继承关系来自于nil
。(注意:根类
不是继承自自己)
2 objc_object & objc_class
大家还记不记得,我们在iOS底层探索 --- OC对象原理(下)中,在我们转换mian.cpp
文件中有这样一段代码:
当时我们只是对objc_class
和objc_object
简单的讲解了一下。那么它们两个具体是什么样子的,又有什么样的关系呢?今天我们来详细的探索一下。
2.1 源码寻找
首先我们要在源码中去寻找objc_object
和objc_class
到底长什么样子。
objc_class
我们在源码中搜索objc_class
的时候,会出现两个不同的定义,其中有一个是已经废弃的。-
废弃的
objc_class
:
新的
objc_class
(可以看到新的objc_class
继承自objc_object
):
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
......
......
......
}
-
objc_object
同样的,在源码中搜索objc_object
的时候,也搜索到两个 - 位于
objc.h
文件里面的:
- 位于
objc-privat.h
文件里面的:
通过
main.cpp
文件,我们可以确定,使用的是objc.h
里面的objc_object
(看注释,这个是给类的实例对象用的)。
2.2 objc_class
和 objc_object
之间的关系
结构体
objc_class
继承自结构体objc_object
。objc_object
有一个isa
属性,所以objc_class
也有一个isa
。(在新的objc_class
中有一段注释// Class ISA;
,也从侧面表现了这一点。)NSObject
也拥有isa
。因为NSObject
在底层的表现形式就是objc_object
。或者说,所有的NSObject
对象,都拥有isa
。
大家在捋这层关系的时候,要注意
objc_object
在源码中有两个定义,使用的范围也不一样。不要搞混了,不然会陷入死循环。注意:
objc_class
继承的是objc-privat.h
里面的objc_object
3 类中信息的探索
在日常开发过程中,我们定义一个类的时候,不仅仅是只有isa
,还会有成员变量,方法等等一些信息。下面我们我们就来看一下这些信息在哪里。
3.1 内存平移,获取bits
信息
在探索类里面信息之前,我们要先达成一个共识,内存的查找是内存平移来获得的。也就是说objc_class
里面的变量,是通过内存平移来获得的。举个例子如下:
在objc_class
中,isa
为8字节
,superclass
也为8字节
;我们不知道的是cache
的内存大小。那么关键的就是获取cache
的内存大小。
下面我们就来探索一下cache
的大小,首先进入cache_t
:
发现代码有很长,这个时候要怎么确定
cache
的大小呢?
注意:大家不要看到
cache_t
是一个结构体,就觉得cache
是8字节
。结构体指针是
8字节
。结构体的大小要根据其内部的属性来计算。
计算的时候只计算属性,方法不算,静态的属性也不计算
通过上面我们就可以确定,cache
内存大小的计算,只需要计算其中四个属性的大小(因为有if
判断,大家可以仔细阅读以下这段源码。):
_buckets
的类型为struct bucket_t *
,是一个结构体指针;所以为8字节
。-
_mask
的类型为mask_t
,mask_t
是unsigned int
的别名;所以为4字节
。
-
_maskAndBuckets
的类型为uintptr_t
,mask_t
是unsigned long
的别名;所以为8字节
。
uint16_t
是unsigned short
的别名;所以为2字节
整体算下来之后,
cache
的内存大小为8 + 4 + 2 + 2 = 16
那么
bits
的内存地址,就是首地址
平移32个字节
(8 + 8 + 16
)
接下来我们就可以通过控制台打印一下bits
信息了:
到这里,我们已经拿到了
bits
里面的信息了;但是我们并没有看到我们想要的成员变量
,方法
等等的信息。不要灰心,下面我们继续探索。
3.2 继续探索类信息
3.2.1 method(方法)
上面我们知道bits
的类型是class_data_bits_t
,那么我们就跟进去找一下data
:
继续跟进class_rw_t
(这个里有个小技巧,我们可以先找method
相关的内容,其他看不懂的内容可以忽略),我们发现了这个:
这就是我们要找的内容呀,方法是存放在方法列表里面的,而这里正好是一个array
。
-
method_array_t
这个时候,我们继续在控制台操作:
我们给
Person
添加几个方法,再来查看一下:
)
重复上面的步骤,此时可以看到methods
里面有信息了:
接下来就是去找我们的Method
的了,这个控制台流程如下:
这里面有一个
big()
方法,是因为在objc4-818.2
的版本中,method_list_t
里面的元素是method_t
,而method_t
被重写了。新的
method_t
长这个样子,我们要拿到方法名,就要通过big
:
而method_t
里面正好有对应的big()
方法:
3.2.2 property(属性)
这个跟探索method
前期是一样的操作,只不过后面获取property
的时候略有不同;因为property_t
直接就有name
相关的属性:
整个控制台的操作如下:
3.2.3 会遇到的一些问题
- 我们上面命名定义了属性
jaxNum
之外,还定义了一个成员变量NSString *jaxName
,但是只能打印出一个jaxNum
(打印第二个的时候,提示数组越界),因此我们可以说property_list
里面没有成员变量:
既然属性列表里面没有成员变量的信息,那我们就需要继续寻找了。
同样的bits->data()
的返回类型是class_rw_t
(注:上面已知);那我们就在class_rw_t
里面找。(正常来讲,优先查找methods()
附近的方法)
查找过之后,我们会发现,成员变量ivar
在这里:
下面就看我们能不将成员变量打印出来了,打印流程如下:
通过上图我们发现,最后怎么没办法执行get
方法了呢?
没关系,$7
没法用get
,那我们就使用$6
:
最后成功打印成员变量jaxName
。
- 在方法列表里面,找不到类方法
我们都知道,类方法存储在元类
中,因此在当前类
的method_lit_t
中是找不到对应的类方法的。
这个时候,我们既要去元类中寻找;同样的根据我们查找当前类
的method_lidt_t
的经验,我们在元类中也这样查找。查找流程如下: