iOS

[iOS] 类 & 类结构分析

2020-12-29  本文已影响0人  沉江小鱼

1. 类的分析

1.1 元类的引入

我们可能之前已经知道类其实也是一个对象,类的 isa 指针指向的是它的元类,下面我们也通过一个代码去验证一下:

@interface Person : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *name;

- (void)work;
+ (void)run;

@end

@implementation Person

- (void)work{
    
}

+ (void)run{
    
}

@end
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *person = [[Person alloc] init];
    
    NSLog(@"hhh : %@",person);
}


@end
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?

1.2 元类的理解

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

我们看下 isa 走位图:


image.png

isa 的走向:

类之间的继承关系:

元类之间的继承关系:

从上面的总结出,我们看到有一条:元类的 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;

objc 源码中搜索objc_class的定义,源码中对其的定义有两个版本:

在源码中继续搜索objc_object,居然也有两个版本:

经过 clang 编译之后我们得到的 main.cpp中的 objc_object的定义:

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

objc_classobjc_object的关系:

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

image.png

3. 类结构分析

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

3.1 内存偏移

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

3.1.1 普通指针
image.jpeg

上面看到:

其地址指向如图所示:


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]创建的地址空间,即内存地址;

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

上面可以看出:

其指针指向如下所示:


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

    ... 方法部分
}
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

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

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

那么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,所以可以得出以下一些结论:

3.2.5 方法列表 methods_list

Person类中有一个实例方法:-(void)work,和一个类方法:+ (void)run,我们也是通过lldb调试来获取方法列表,步骤如图所示

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

上面得出methods list 中只有实例方法,没有类方法,那么问题来了,类方法存储在哪里?为什么会有这种情况?下面我们来仔细分析下。

3.2.6 类方法的存储

在文章前半部分,我们曾提及了元类,类对象的isa指向就是元类,元类是用来存储类的相关信息的,所以我们猜测:是否类方法存储在元类的bits中呢?可以通过lldb命令来验证我们的猜测。下图是lldb命令的调试流程:

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

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

上一篇 下一篇

猜你喜欢

热点阅读