[iOS] 类 & 类结构分析
1. 类的分析
1.1 元类的引入
我们可能之前已经知道类其实也是一个对象,类的 isa
指针指向的是它的元类,下面我们也通过一个代码去验证一下:
- 定义一个
Person
类,继承NSObject
,接下来将会围绕这个类进行分析:
@interface Person : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
- (void)work;
+ (void)run;
@end
@implementation Person
- (void)work{
}
+ (void)run{
}
@end
- 使用这个自定义的对象:person
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"hhh : %@",person);
}
@end
- 我们通过lldb 验证元类,直接下断点,运行程序
- 开启 lldb 调试,调试的过程如下图所示:
![](https://img.haomeiwen.com/i10432329/d0ea091129ea3c55.png)
补充下 lldb 关于 x 命令的使用:
- x 就是
memory read 内存读取
并打印的作用- x/4gx 就是打印 4 段内存:
- 4: 就是打印 4 段
- g :格式化输出,这样比较方便分析(
iOS 内存为小端模式
,不格式化输出比较难以分析)- x:每一段以 16 进制打印
在上面的调试过程中,我们发现图中 p/x 0x000000010e1df6c0 & 0x00007ffffffffff8ULL
和 p/x 0x000000010e1df698 & 0x00007ffffffffff8ULL
中的类信息打印出来都是 Person?
-
0x000000010e1df6c0
是person 对象的 isa 指针地址
,其经过 & 运算之后得到的是 isa 中存储的类信息,也就是Person 类 -
0x000000010e1df698
是Person 类的 isa 指针地址
,指向的也就是 Person 类的类,简称为Person 类的元类
。 - 所以,两个打印都是 Person 的原因是因为元类导致的。
1.2 元类的理解
我们知道对象的 isa 指向对象所属的类
,对象的方法存储在所属的类中,那么类方法存储在哪?其实类其实也是一个对象,称为类对象,其 isa 指向的是它所属的元类
,类方法也存储在其所属的元类中。
- 元类是
系统给的
,其定义和创建都是由编译器完成的,在这个过程中,类的归属来自于元类 - 元类是类对象的类,每个类都有一个独一无二的元类用来存储类方法的相关信息
- 元类本身是没有名称的,由于与类相关联,所以使用了和类名一样的名称
我们看下 isa 走位图:
![](https://img.haomeiwen.com/i10432329/27784db2f4633354.png)
isa
的走向:
- 实例对象的
isa
指向其所属的类 - 类对象的
isa
指向元类 - 元类的
isa
指向根元类 - 根元类的
isa
指向它自己,形成闭环,这里的根元类就是NSObject
类之间的继承关系:
- 类继承自父类
- 父类继承自根类,根类就是
NSObject
- 根类继承自
nil
元类之间的继承关系:
- 子类的元类继承自父类的元类
- 父类的元类继承自根元类
- 根元类继承于根类,此时的根类指
NSObject
从上面的总结出,我们看到有一条:元类的 isa 指向根元类
,也就是 NSObject
,我们可以继续通过上面的代码来验证一下,此时获取到 Person
的元类地址为0x000000010e1df698
:
(lldb) x/4gx 0x000000010e1df698
0x10e1df698: 0x00007fff89c1ecd8 0x00007fff89c1ecd8
0x10e1df6a8: 0x0000600000cd5100 0x0003c03500000007
(lldb) p/x 0x00007fff89c1ecd8 & 0x00007ffffffffff8ULL
(unsigned long long) $8 = 0x00007fff89c1ecd8
(lldb) po 0x00007fff89c1ecd8
NSObject
我们继续查看元类信息,拿到元类的 isa
指针中的类信息,输出为 NSObject
,也就是根元类。
得到了根元类之后,再验证下NSObject
的isa
中的类信息是否为根元类:
(lldb) x/4gx NSObject.class
0x7fff89c1ed00: 0x00007fff89c1ecd8 0x0000000000000000
0x7fff89c1ed10: 0x0000600001ec5000 0x000980100000000f
(lldb) p/x 0x00007fff89c1ecd8 & 0x00007ffffffffff8ULL
(unsigned long long) $11 = 0x00007fff89c1ecd8
我们发现,NSObject
类的isa
中的类信息果然也是NSObject
(根元类),而且这个根元类和我们平时开发中用的 NSObject
是同一个。。
2. objc_class & objc_object
关于isa
和元类
我们已经理解,那么我们想一下,为什么对象
和类
都有 一个isa
呢?这里就不得不提到两个结构体类型:objc_class
和 objc_object
,下面我们也会在这两个结构体的基础上,进行探索。
NSObject
的底层编译是 NSObject_IMPL
结构体:
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
-
Class
是isa
指针的类型,也就是objc_class
结构体指针 - 而
objc_class
是一个结构体,在iOS
中,所有的Class
都是以objc_class
为模板创建的
在 objc
源码中搜索objc_class
的定义,源码中对其的定义有两个版本:
-
旧版本
位于runtime.h
中,在OBJC2
中已经被废弃不用了
截屏2020-12-27 下午7.50.55.png
-
新版本
位于objc-runtime-new.h
中:
截屏2020-12-27 下午7.52.32.png
在上面的定义中,我们可以看到objc_class
结构体类型是继承自objc_object
的。
在源码中继续搜索objc_object
,居然也有两个版本:
- 一个位于
objc.h
,没有被废除,目前也是使用的这个版本的objc_object
截屏2020-12-27 下午8.05.33.png
- 一个位于
objc-private.h
中
截屏2020-12-27 下午8.07.58.png
经过 clang
编译之后我们得到的 main.cpp
中的 objc_object
的定义:
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
objc_class
和 objc_object
的关系:
- 结构体类型
objc_class
继承自objc_object
类型,其中objc_object
也是一个结构体,且有一个isa
属性,所以objc_class
也拥有了isa
属性 -
main.cpp
底层编译文件中,NSObject
中的isa
在底层是由Class
定义的,其中class
的底层编码来自objc_class
类型,所以NSObject
也拥有了isa
属性 -
NSObject
是一个类,用它初始化一个实例对象objc
,objc
满足objc_object
的特性(即有isa
属性),主要是因为isa
是由NSObject
从objc_class
继承过来的,而objc_class
继承自objc_object
,objc_object
有isa
属性,所以对象都有一个isa
,isa
表示指向,来自于当前的objc_object
-objc_object
是当前的根对象,所有的对象都有这样一个特性,即拥有isa
属性 - 所有的对象都是以
objc_object
为模板继承过来的,底层是一个objc_object
的结构体类型
objc_class、objc_object、isa、object、NSObject
等的整体关系,如下图所示:
![](https://img.haomeiwen.com/i10432329/534d47131b54090e.png)
3. 类结构分析
主要是分析对象的isa
指针的类信息
中存储了哪些内容。
3.1 内存偏移
在分析类结构之前,需要先了解内存偏移
,因为类信息中访问时,需要使用内存偏移。
3.1.1 普通指针
![](https://img.haomeiwen.com/i10432329/7529673c1ca21a29.jpeg)
上面看到:
- a、b 都指向10,但是 a、b 的地址不一样
- a、b 的地址之间相差 4 个字节,这取决于 a、b 的类型
其地址指向如图所示:
![](https://img.haomeiwen.com/i10432329/5e6ad723fa1f98b8.png)
3.1.2 对象指针
Person *p1 = [Person alloc]; // p1 是指针
Person *p2 = [Person alloc];
NSLog(@"%d -- %p", p1, &p1);
NSLog(@"%d -- %p", p2, &p2);
![](https://img.haomeiwen.com/i10432329/af6607c938fb66d8.png)
上面看到:
-p1、 p2
是对象指针,p1
和 p2
指向了 [Person alloc]
创建的地址空间,即内存地址;
-
&p1 &p2
分别是取p1 对象指针
和p2 对象指针
的地址
3.1.3 数组指针
//数组指针
int c[4] = {1, 2, 3, 4};
int *d = c;
NSLog(@"%p -- %p - %p", &c, &c[0], &c[1]);
NSLog(@"%p -- %p - %p", d, d+1, d+2);
打印结果如下:
![](https://img.haomeiwen.com/i10432329/a18d1edb5b674da9.png)
上面可以看出:
-
&c
和&c[0]
都是取首地址,即数组名等于首地址
-&c
和&c[1]
相差 4 个字节,取决于存储的数据类型,int
类型占4
个字节 - 可以通过
首地址 + 偏移量
取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于偏移量 * 数据类型占用的字节数
其指针指向如下所示:
![](https://img.haomeiwen.com/i10432329/fb9ae25c2b5586ff.png)
3.2 类信息中的内容
3.2.1 objc_class
结构体分析
在探索类信息
中都有什么时,我们就可以通过类得到一个首地址
,然后通过地址平移
去获取里面所有的值。
根据前文提到的 objc_class
的定义,有以下几个变量:
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
... 方法部分
}
-
isa
: 继承自objc_object
的isa
,占 8 个字节 -
superclass
:Class
类型,Class
由object_object
定义的,是一个指针,占8 个字节
-
cache
: 简单从类型class_data_bits_t
目前无法得知,而class_data_bits_t
是一个结构体类型,结构体的内存大小需要根据内部成员
来确定 -
bits
: 只有首地址通过上面3
个成员占用内存大小总和的平移,才能获取到bits
3.2.2 cache_t
占用内存大小
进入 cache
类 cache_t
的定义(只贴出了结构体中非 static 修饰
的属性,主要是因为static 类型的属性不存在结构体的内存中
)
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
#if __LP64__
uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
#endif
uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
得出cache_t
结构体占用内存是 12 + 2 + 2 = 16
字节
3.2.3 获取 bits
数据
想要获取 bits
中的内容,只需要通过类的首地址
平移 32 个字节
(上面👆计算的isa + superclass + cache 占用字节数 = 32
)即可得到:
![](https://img.haomeiwen.com/i10432329/581099dcc1adbab7.png)
获取类的首地址有两种方式
- 通过
p/x Person.class
直接获取 16 位的首地址 - 通过
x/4gx Person.class
,打印内存信息获取
![](https://img.haomeiwen.com/i10432329/cd74a29da63a3273.png)
从$2
指针打印结果中可以看到bits.data()
中存储的信息,我们objc_class
结构体源码中可以看出 data()
方法里边获取的其实也是bits.data()
,其类型是 class_rw_t*
,是一个结构体指针类型:
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
// data()方法
class_rw_t *data() const {
return bits.data();
}
.....
}
struct class_data_bits_t {
...
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
...
}
那么属性列表
、方法列表
在哪呢?
3.2.4 属性列表 property_list
通过查看 class_rw_t
定义的源码发现,结构体中有提供相应的方法去获取属性列表
、方法列表
和协议列表
等,如下所示:
![](https://img.haomeiwen.com/i10432329/56affe6637b9d719.png)
在获取 bits
并打印bits.data()
信息的基础上,通过class_rw_t
提供的方法,继续探索属性列表,以下是lldb
探索的过程图示:
![](https://img.haomeiwen.com/i10432329/862bec4632381d61.png)
-
p $3.properties()
命令中的properties
方法是由class_rw_t
提供的,方法中返回的实际类型为property_array_t
- 由于
list
的类型是property_list_t
,是一个指针,所以通过p *$5
获取内存中的信息,同时也证明bits
中存储了property_list
,即属性列表 -
p $6.get(1)
,想要获取第二个属性, 发现会报错,提示数组越界了,说明property_list
中只有 一个属性name
,没有hobby
这个成员变量。
那么property_list
中只有属性,没有成员变量,属性和成员变量的区别就是有没有 set、get
方法,有则是属性,没有则是成员变量,那么成员变量存储在哪里?
通过查看objc_class
中bits
属性中存储数据的类class_rw_t
的定义发现,除了methods、properties、protocols
方法,还有一个ro
方法,其返回类型是class_ro_t *
,通过查看其定义,发现其中有一个ivars
属性,我们可以做如下猜测:是否成员变量就存储在这个ivar_list_t
类型的ivars
属性中呢?
下面是lldb
的调试过程
![](https://img.haomeiwen.com/i10432329/62ea44338b0a986e.png)
class_ro_t
结构体中的属性如下所示,想要获取ivars
,需要ro
的首地址平移48
字节:
struct class_ro_t {
uint32_t flags; //4
uint32_t instanceStart;//4
uint32_t instanceSize;//4
#ifdef __LP64__
uint32_t reserved; //4
#endif
const uint8_t * ivarLayout; //8
const char * name; //1 ? 8
method_list_t * baseMethodList; // 8
protocol_list_t * baseProtocols; // 8
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
//方法省略
}
通过图中可以看出,获取的ivars
属性,其中的count 为2
,通过打印发现 成员列表中除了有hobby
,还有name
,所以可以得出以下一些结论:
- 通过
{}定义
的成员变量,会存储在类的 bits
中,通过bits -> data() -> ro() -> ivars
获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量 - 通过
@property
定义的属性,也会存储在bits
属性中,通过bits -> data() -> properties() -> list
获取属性列表,其中只包含属性。
3.2.5 方法列表 methods_list
Person
类中有一个实例方法:-(void)work
,和一个类方法:+ (void)run
,我们也是通过lldb调试来获取方法列表,步骤如图所示
![](https://img.haomeiwen.com/i10432329/abb450e8cff8ec22.png)
- 通过
p $3.methods()
获得具体的方法列表的list
结构,其中methods
也是class_rw_t
提供的方法 - 通过打印的
count = 4
可知,存储了4
个方法,可以通过p $6.get(i)
内存偏移的方式获取单个方法,i
的范围是0-3
- 如果在打印
p $6.get(4)
,获取第五个方法,也会报错,提示数组越界
上面得出methods list
中只有实例方法,没有类方法,那么问题来了,类方法存储在哪里?为什么会有这种情况?下面我们来仔细分析下。
3.2.6 类方法的存储
在文章前半部分,我们曾提及了元类,类对象的isa
指向就是元类,元类是用来存储类的相关信息的,所以我们猜测:是否类方法存储在元类的bits
中呢?可以通过lldb
命令来验证我们的猜测。下图是lldb
命令的调试流程:
![](https://img.haomeiwen.com/i10432329/aa3ad0166dcb8554.png)
通过图中元类方法列表
的打印结果,我们可以知道,我们的猜测是正确的,所以可以得出以下结论:
-
类
的实例方法
存储在类的bits属性
中,通过bits --> methods() --> list
获取实例方法列表
,例如Person
类的实例方法work
就存储在Person类的bits
中,类中的方法列表
除了包括实例方法
,还包括属性的set方法
和get方法
。 -
类
的类方法
存储在元类的bits
中,通过元类bits --> methods() --> list
获取类方法列表
,例如Person
中的类方法run
就存储在Person
类的元类
(名称也是Person)的bits
中。