这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
一. 类的原理分析
类的原理分析主要是分析 isa
以及 继承关系
.
从 isa 开始探索 – isa走位链
首先我们先获得isa的掩码 0x00007ffffffffff8ULL
,然后获得对象的地址,拿到对象的isa。
至此,我们得到了isa的掩码:0x00007ffffffffff8ULL
以及对象的isa:0x011d800100008365
,然后我们将这两个进行与运算,就可以得到我们的类的地址 0x0000000100008360
,并且我们po 一下这个地址,证明确实是LGPerson类。
那么我们猜想一下,我们 是否可以 x/4gx 这个类的地址?我们来实验一下。
由图片可以知道,类也有相应的内存结构,那么 0x0000000100008338
是不是类的 isa
呢 ?
我们把 0x0000000100008338
与掩码进行一下与运算试一试。
从图片可知,两者运算后,得到的依然是LGPerson类,那么我们试着打印出两者运算后的地址
得到了两者运算后的地址为0x0000000100008338
,我们从前面的运算可以知道,LGPerson类的地址为0x0000000100008360
,那么为什么这个0x0000000100008338也是LGPerson类呢?是不是因为类和对象一样无限开辟,内存不止有一个类呢?我们来验证一下。
这里我们创建了多个LGPerson的class,我们打印他们的地址,看看他们的地址是否相同。
这就意味着,只有 0x100008360
才是我们的LGPerson类,而0x0000000100008338
不是,他是一个新的东西,这个东西就是 元类
。
元类
对象的isa
是指向类,类其实也是一个对象,可以称为类对象
,其isa的位域指向苹果定义的元类
,元类是类对象的类。- 元类在代码里是不存在的,元类是
系统
给的,其定义
和创建
都是由编译器
完成,在这个过程中,类的归属来自于元类。 - 元类的存在是
必需
的,因为他存储了一个类的所有类方法
。每个类的元类都是独一无二
的,因为每个类都有一系列独特的类方法。 - 元类本身是
没有名称
的,由于与类相关联,所以使用了同类名一样
的名称
用MachOView(烂苹果)进行探索元类
烂苹果是分析Macho
的必备工具,我们打开烂苹果,并打开Section64 (_DATA,_objc,classrefs)
下的 ObjC2 References
。
这里只有LGPerson
,LGTeacher
以及NSObject
.我们再打开Section64 (_DATA_CONST,_objc,classlist)
.
依然只有 LGTeacher:0000001000008310
以及 LGPerson:0000001000008360
.
接下来我们去到 Symbol table
里面的 symbols
查找 class
.
这里面多了一个东西,就是_OBJC_METACLASS_$_LGTeacher
, 以及_OBJC_METACLASS_$_LGPerson
,说明了我们多了一个东西就是元类
,并且元类
是系统进行生成和编译的.
探索到此,我们知道了对象的isa
指向类
,类的isa
指向了元类
,那么元类的isa指向哪里呢?一起来探索一番.
我们用 0x00007fff80715fe0
与掩码进行与运算,并且po输出得到的地址,看看他到底是个啥。
那么我们就可以看到,元类的isa 指向的是NSObject
,也就是根元类
.那么根元类的isa又指向哪里呢?
由图片可知,根元类的isa指向了自己。
接下来我们继续探索NSObject.class
.
由上图看到,我们的NSObject和上面的地址不一样,我们打印一下我们得到的地址.
由上图看到,NSObject的isa是0x00007fff80715fe0
,我们再将这个地址与掩码进行与运算.
然后将再打印一下0x00007fff80715fe0
.
我们发现,我们得到的与之前的根元类相同,并且isa也指向了自己。从对象的isa到根元类,
我们总共走了3步。
对象isa ➡️ 类isa ➡️ 元类isa➡️ 根元类 。
而从NSObject对象到根元类,则只需要2步。
根对象isa ➡️ 根类isa ➡️ 根元类
这就得出来一个非常经典的isa走位图:
继承链
类之间的继承关系
我们用一个继承
自LGPerson
类的LGTeacher
类来探索类之间的继承关系。
由上面的图片可以得知,LGTeacher
继承自 LGPerson
, LGPerson
继承自NSObject
,而NSObjec
t继承自Nil
。
由此可以得出类之间的继承关系:
类(subClass
) 继承自父类(superClass)
.父类(superClass)
继承自根类(RootClass)
,此时的根类是指NSObject
.根类
继承自nil
,所以根类即NSObject
可以理解为万物起源
,即无中生有.
元类之间的继承关系
探索完类之间的继承关系,我们继续探索 元类之间的继承关系
。
我们先来看LGTeacher 的元类,从输出可以看到,继承自LGPerson,那么他是继承自LGPerson类还是元类呢,我们来验证一下.
从上面的图可以看出, LGTeacher的元类
是继承自 LGPerson的元类
的。
那么LGPerson元类是继承自哪里的呢,我们来看一下:
由上图中可以看到, LGPerson的元类
是继承自 NSObject 元类
的。
继续探索一下NSObject元类是继承自哪里的:
我们先获得了 根元类(NSObject)
,然后打印出根元类的superclass,由lldb可以看出, 根元类
继承自 根类(NSObject)
。
我们最后来看一下根类NSObject的继承关系:
由输出可以看到, 根类
是继承自 null
的。
那么由上面的探索过程,我们可以得到:
元类也存在继承,元类之间的继承关系如下:
子类的元类(metal SubClass)
继承自父类的元类(metal SuperClass)
父类的元类(metal SuperClass)
继承自根元类(Root metal Class)
根元类(Root metal Class)
继承于根类(Root class)
,此时的根类是指NSObject根类
继承于null
继承链关系图:
结合isa的走位图以及继承链,我们可以证明一张来自苹果官方文档的图。
二. 类的结构分析
首先看到我们的LGPerson类的内容。
输出一下LGPerson类
由上图可以看出,LGPerson类是有内存的,那么内存里面储存的是啥呢。我们知道类的本质是objc_class.
我们在objc源码中搜索 objc_class
的定义:
我们可以看到这里有一个关于objc_class结构体的定义,但是我们注意到,下面有 OBJC2_UNAVAILABLE
, 这就说明了这个结构体再objc2 中是无效的,所有这个并不是我们要找的。
继续在objc源码中寻找:
终于我们找到了objc_class结构体的定义,并且发现 objc_class
是继承自 objc_object
的,那么我们在源码中寻找objc_object,看看objc_object里面有什么成员。
有图中可以看到, objc_object
里面有 isa
,由于objc_class 继承自objc_object,所以objc_class也拥有了 isa属性
。
我们还看到 objc_class
中有三个成员: Class superclass
, cache_t cache
以及 class_data_bits_t bits
。 其中 superclass
我们知道是当前的 父类
,那么 cache 以及bits 是什么东西呢。
内存偏移
普通指针
由图片可以知道,
-
a、b都指向10,但是a、b的
地址不一样
,这是一种拷贝
,属于值拷贝
,也称为深拷贝
-
a,b的地址之间相差 4 个字节,这取决于a、b的
类型
地址指向如下图
对象指针
由图片可以知道,
- p1、p2 是
指针
,p1,p2 是 指向 [CJLPerson alloc]创建的空间地址,即内存地址.p1、p2 地址不同,指向的空间也不同。 - &p1、&p2是 指向 p1、p2对象指针的地址,这个指针就是
二级指针
.
数组指针
由上图可得:
&c
和&c[0]
都是取首地址
,即数组名等于首地址- &c 与 &c[1] 相差
4个字节
,地址之间相差的字节数,主要取决于存储的数据类型
- 可以通过
首地址+偏移量
取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于偏移量 * 数据类型字节数
类的地址平移
平移地址计算
由上面的探索我们知道,类有4个成员:
isa
属性:继承自objc_object的isa,占8
字节superclass
属性:Class类型,Class是由objc_object定义的,是一个指针,占8
字节cache
属性:cache_t
类型,不知道占多少字节bits
属性:class_data_bits_t
,不知道占多少字节
由上图可知道,0x00000001000083a8
指向LGPerson的元类
,可以证明 0x00000001000083a8 确实是isa
。
由上图可知道,0x000000010036a140 是指向NSObject
,可以证明 0x000000010036a140
确实是superclass
。
我们来看一下cache_t
类型占多少字节,我们找到cache_t的结构体:
因为方法在方法区
不占结构体内存以及static在全局区
也不在结构体内存中,所以底下的方法以及static属性可以忽略掉。所以我们只需要看这些变量就可以。
首先是 explicit_atomic<uintptr_t> _bucketsAndMaybeMask
这个属性,这个属性占多少内存呢。
我们看到explicit_atomic
是一个范型
,那么真正决定占用多少内存的就是uintptr_t
这个家伙,而这个家伙实际上是unsigned long
类型,unsigned long在64位中占8
个字节。也就是说, explicit_atomic<uintptr_t> _bucketsAndMaybeMask
这个属性是占8
个字节的。
接下来是一个联合体,我们知道联合体变量是互斥的,所有的成员共占一段内存,占用的内存等于最大的成员占用的内存。
在联合体中,我们可以看到是 explicit_atomic<preopt_cache_t *> _originalPreoptCache 这个指针所占用的内存最大为8,所以这个联合体占用的内存是8.
所以我们只要平移LGPerson类的首地址平移 8 + 8 + 16 = 32 位,就可以得到bits的地址。
类的属性获取
这里我们知道这个是class_data_bits_t 的地址,所以我们可以将地址转为class_data_bits_t 指针类型,
然后我们用结构体中提供的方法data()获取数据,因为是指针,所以我们用->,如果是对象则用点语法就可以。
我们看到,这里的firstSubclass 是nil,但是我们的LGTeacher是继承自LGPerson的,那么这里为什么是nil 呢?因为我们的类加载采用的是懒加载的模式,我们访问一下这个类,LGPerson就会有firstSubclass啦。实验一下:
获取到了数据,我们就将数据打印出来。看到这里并没有我们所需要的数据,我们在进入到class_rw_t 的结构体中,并找到以下方法。
我们先获取属性数组:
然后获取List
在获取list 的ptr
还原里面的数据
获取里面的数据
这里就得到了我们所要的属性 name 和 hobby。
类的方法获取
我们再来获取类的方法列表。先获取方法数组。
接着直接获取ptr
然后还原里面的数据
接着挨个获取里面的数据
这样我们就获得所有的方法了。
这里我们注意到获取方法和获取属性的方法不一样,这是为啥呢?
这是因为property_t的结构和method_t的结构不一样。method_t的成员变量在big 中,所以我们需要额外输入big()调用big方法来获取成员变量
类的协议获取
先声明一个协议,并添加一个方法,然后为LGPerson添加这个协议。
实现一下
继续之前的流程。
这里就获取到了protocol list。
这里看到protocols()方法返回的是protocol_array_t,我们去看一下protocol_array_t是什么样子的。
看到我们最终获得一个protocol_ref_t,点进去看一下protocol_ref_t是什么。
protocol_ref_t是一个无符号长整形。
打印一下$5的值,发现是protocol_list_t类型。
看一下protocol_list_t是什么样子的。
没有打印protocol_ref_t的地方。那么该如何从protocol_ref_t里面获取信息呢?在源码里搜索一下protocol_ref_t。
看到这里有remapProtocol,而之前protocol_ref_t那里写着but unremapped,并且这里返回了protocol_t类型,那么这是不是所需要的方法呢?我们跟着这个办法,强制转换protocol_ref_t为 protocol_t *类型。
在protocol_list_t中 protocol_ref_t是list[0]:
我们获取protocol_list_t 中的 protocol_ref_t。
然后将protocol_ref_t强转为protocol_t *类型
打印一下,就得到了我们要的数据。
打印一下方法。
也是我们之前创建的方法。
类方法的位置
ivar的位置
先添加几个成员变量。
然后逐步获取: