美文网首页iOS
[iOS] 类 & 类结构分析

[iOS] 类 & 类结构分析

作者: 沉江小鱼 | 来源:发表于2020-12-29 22:48 被阅读0次

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 调试,调试的过程如下图所示:
Artboard@1x.png

补充下 lldb 关于 x 命令的使用:

  1. x 就是 memory read 内存读取并打印的作用
  2. x/4gx 就是打印 4 段内存:
  • 4: 就是打印 4 段
  • g :格式化输出,这样比较方便分析(iOS 内存为小端模式,不格式化输出比较难以分析)
  • x:每一段以 16 进制打印

在上面的调试过程中,我们发现图中 p/x 0x000000010e1df6c0 & 0x00007ffffffffff8ULLp/x 0x000000010e1df698 & 0x00007ffffffffff8ULL中的类信息打印出来都是 Person?

  • 0x000000010e1df6c0person 对象的 isa 指针地址,其经过 & 运算之后得到的是 isa 中存储的类信息,也就是Person 类
  • 0x000000010e1df698Person 类的 isa 指针地址,指向的也就是 Person 类的类,简称为 Person 类的元类
  • 所以,两个打印都是 Person 的原因是因为元类导致的。
1.2 元类的理解

我们知道对象的 isa 指向对象所属的类,对象的方法存储在所属的类中,那么类方法存储在哪?其实类其实也是一个对象,称为类对象,其 isa 指向的是它所属的元类,类方法也存储在其所属的元类中。

  • 元类是系统给的,其定义和创建都是由编译器完成的,在这个过程中,类的归属来自于元类
  • 元类是类对象的类,每个类都有一个独一无二的元类用来存储类方法的相关信息
  • 元类本身是没有名称的,由于与类相关联,所以使用了和类名一样的名称

我们看下 isa 走位图:


image.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,也就是根元类。

得到了根元类之后,再验证下NSObjectisa 中的类信息是否为根元类:

(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_classobjc_object,下面我们也会在这两个结构体的基础上,进行探索。

NSObject 的底层编译是 NSObject_IMPL结构体:

struct NSObject_IMPL {
    Class isa;
};

typedef struct objc_class *Class;
  • Classisa 指针的类型,也就是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_classobjc_object的关系:

  • 结构体类型 objc_class 继承自 objc_object类型,其中 objc_object也是一个结构体,且有一个 isa 属性,所以objc_class也拥有了isa属性
  • main.cpp底层编译文件中,NSObject中的 isa 在底层是由 Class 定义的,其中 class的底层编码来自 objc_class类型,所以NSObject也拥有了 isa 属性
  • NSObject 是一个类,用它初始化一个实例对象 objcobjc 满足 objc_object的特性(即有 isa 属性),主要是因为isa是由 NSObjectobjc_class继承过来的,而 objc_class继承自 objc_objectobjc_objectisa 属性,所以对象都有一个 isaisa 表示指向,来自于当前的objc_object
    -objc_object是当前的根对象,所有的对象都有这样一个特性,即拥有isa 属性
  • 所有的对象都是以 objc_object为模板继承过来的,底层是一个 objc_object的结构体类型

objc_class、objc_object、isa、object、NSObject等的整体关系,如下图所示:

image.png

3. 类结构分析

主要是分析对象的isa 指针的类信息中存储了哪些内容。

3.1 内存偏移

在分析类结构之前,需要先了解内存偏移,因为类信息中访问时,需要使用内存偏移。

3.1.1 普通指针
image.jpeg

上面看到:

  • a、b 都指向10,但是 a、b 的地址不一样
  • a、b 的地址之间相差 4 个字节,这取决于 a、b 的类型

其地址指向如图所示:


image.png
3.1.2 对象指针
Person *p1 = [Person alloc]; // p1 是指针
Person *p2 = [Person alloc];
NSLog(@"%d -- %p", p1, &p1);
NSLog(@"%d -- %p", p2, &p2);

image.png

上面看到:
-p1、 p2是对象指针,p1p2 指向了 [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);

打印结果如下:


image.png

上面可以看出:

  • &c&c[0] 都是取首地址,即数组名等于首地址
    -&c&c[1]相差 4 个字节,取决于存储的数据类型,int 类型占 4 个字节
  • 可以通过首地址 + 偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于 偏移量 * 数据类型占用的字节数

其指针指向如下所示:


image.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_objectisa,占 8 个字节
  • superclass :Class类型,Classobject_object定义的,是一个指针,占 8 个字节
  • cache: 简单从类型 class_data_bits_t目前无法得知,而 class_data_bits_t是一个结构体类型,结构体的内存大小需要根据内部成员来确定
  • bits: 只有首地址通过上面 3 个成员占用内存大小总和的平移,才能获取到bits
3.2.2 cache_t占用内存大小

进入 cachecache_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)即可得到:

截屏2020-12-29 下午9.55.26.png

获取类的首地址有两种方式

  • 通过p/x Person.class直接获取 16 位的首地址
  • 通过x/4gx Person.class,打印内存信息获取
image.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定义的源码发现,结构体中有提供相应的方法去获取属性列表方法列表协议列表等,如下所示:

截屏2020-12-27 下午9.00.05.png

在获取 bits 并打印bits.data() 信息的基础上,通过class_rw_t提供的方法,继续探索属性列表,以下是lldb探索的过程图示:

截屏2020-12-29 下午10.03.01.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_classbits属性中存储数据的类class_rw_t的定义发现,除了methods、properties、protocols方法,还有一个ro方法,其返回类型是class_ro_t *,通过查看其定义,发现其中有一个ivars属性,我们可以做如下猜测:是否成员变量就存储在这个ivar_list_t类型的ivars属性中呢?
下面是lldb的调试过程

截屏2020-12-29 下午10.24.32.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调试来获取方法列表,步骤如图所示

截屏2020-12-29 下午10.34.54.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命令的调试流程:

截屏2020-12-29 下午10.46.43.png

通过图中元类方法列表的打印结果,我们可以知道,我们的猜测是正确的,所以可以得出以下结论:

  • 实例方法存储在类的bits属性中,通过bits --> methods() --> list获取实例方法列表,例如Person类的实例方法work 就存储在 Person类的bits中,类中的方法列表除了包括实例方法,还包括属性的set方法get方法

  • 类方法存储在元类的bits中,通过元类bits --> methods() --> list获取类方法列表,例如Person中的类方法run 就存储在Person类的元类(名称也是Person)的bits中。

相关文章

  • [iOS] 类 & 类结构分析

    1. 类的分析 1.1 元类的引入 我们可能之前已经知道类其实也是一个对象,类的 isa 指针指向的是它的元类,...

  • iOS-底层分析之类的结构分析

    类的结构分析 本文主要分析iOS中的类以及类的结构,下面我们通过一个例子来探索类的结构 我们定义一个WPerson...

  • iOS类结构:cache_t分析

    一、cache_t 内部结构分析 1.1 在iOS类的结构分析中,我们已经分析过类(Class)的本质是一个结构体...

  • iOS 类的结构分析(下)

    在上一篇 iOS 类的结构分析(上) 分析了类的结构、isa 的走位以及类的内存分布(属性列表&实例方法列表),这...

  • iOS 类结构分析

    前言 通过本篇文章可以了解1.isa的走位2.类结构的分析3.什么是元类4.supclass的走位5.objc_c...

  • iOS类结构分析

    本文主要来探索一下iOS中类的结构,作为一个iOS开发者,我们有必要去了解关于类的底层知识。下面开始我们的探索。 ...

  • iOS - 类结构分析

    我们都知道,一个类可以创建多个不同实例对象,类自己也是对象(类对象),那么类在内存中会存在几份呢?看下面结果 得出...

  • iOS底层之cache_t探究

    我们在iOS底层之类的结构分析分析了类的内部结构,而类的C/C++底层实际是objc_class结构体,其中包含了...

  • iOS-底层原理 08:类 & 类结构分析

    iOS 底层原理 文章汇总 本文的主要目的是分析 类 & 类的结构,整篇都是围绕一个类展开的一些探索 类 的分析 ...

  • iOS底层原理08:类结构分析——bits属性

    iOS底层原理07:类 & 类结构分析[https://www.jianshu.com/p/05f725a5ccb...

网友评论

    本文标题:[iOS] 类 & 类结构分析

    本文链接:https://www.haomeiwen.com/subject/rhaonktx.html