iOS 底层原理之类的原理(上)

714 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

前言

上一篇我们探索了对象的底层原理,并知道对象的内存地址分布, 对象的内存地址由isa + 对象的各个属性组成,如下图所示: image.png

那类会不会也有自己的内存结构?答案:是的。Class本质是一个结构体指针typedef struct objc_class *,接下来探索一下结构体objc_class的底层实现。

源码分析类结构

objc_libdylib.A源码库objc-runtime-new类中,找到了objc_class的底层实现,其中发现有4个比较关键的对象,如下图所示:

image.png

  • Class ISA:是继承至objc_objcet的一个属性
  • Class superclass: 指向当前类的父类
  • cache_t cache: 对当前类的一些方法的缓存,后续再深入探索
  • class_data_bits_t bits:存储类的内存数据,包括属性,成员变量,方法,协议等

该文主要对Class ISA走位分析,以及验证class_data_bits_t bits存储类的内存属性。

isa走位图分析

首先来看一张非常经典了isa走位图,是不是以前似懂非懂,今天就彻底搞明白他。其中虚线表示指向,实线表示继承,咋一看还是有点乱,接下来把下图拆成两部分分析,指向和继承。

isa流程图.png

  1. isa 指向分析

image.png

由上图中可以发现:

  • 实例对象isa指向了
  • 类isa指向了该类的元类
  • 该类的元类指向了根元类
  • 根元类指向了根元类

特殊的NSObject isa直接指向根元类

通过两种方式验证:

① lldb验证,因为我使用的是模拟器,所以使用到的掩码是0x00007ffffffffff8ULL,可以源码全局搜索isa_mask查看对应的掩码

image.png 分析:

// 获取对象t的isa 0x011d800100008c8d 便是对象的isa地址
(lldb) x/4gx t 

0x100a27890: 0x011d800100008c8d 0x0000000000000000

0x100a278a0: 0x0000000000000000 0x0000000000000000

// 对象isa & 掩码 =》 类地址。这里验证了 `实例对象isa`指向了`类`

(lldb) p/x 0x011d800100008c8d & 0x00007ffffffffff8ULL

(unsigned long long) $2 = 0x0000000100008c88


// x/4gx 类地址,查看类的内存分部, 0x0000000100008c60 类的isa地址

(lldb) x/4gx 0x0000000100008c88

0x100008c88: 0x0000000100008c60 0x0000000100008d78

0x100008c98: 0x0000000100f04c10 0x0002804400000003
// 类地址 & 掩码 =》 元类,元类是系统自己生成的。 这里验证了 `类isa`指向了`该类的元类`

(lldb) p/x 0x0000000100008c60 & 0x00007ffffffffff8ULL

(unsigned long long) $3 = 0x0000000100008c60

(lldb) po 0x0000000100008c60

DXJTeacher


// x/4gx 元类地址,查看元类的内存分部, 0x00000001008010f0 元类的isa地址
(lldb) x/4gx 0x0000000100008c60

0x100008c60: 0x00000001008010f0 0x0000000100008d50

0x100008c70: 0x0000000100a27db0 0x0004e03500000007
// 元类isa & 掩码 =》 根元类,这里验证了 `元类isa`指向了`根元类`

(lldb) p/x 0x00000001008010f0 & 0x00007ffffffffff8ULL

(unsigned long long) $5 = 0x00000001008010f0

// 0x00000001008010f0 打印的是NSObject,但是这个地址是元类的地址,下方是验证
(lldb) po 0x00000001008010f0

NSObject


// p/x NSObject,可以看到0x0000000100801140 是NSObject的类地址
(lldb) p/x NSObject.class

(Class) $7 = 0x0000000100801140
// x/4gx NSObject类地址,这里可以看到NSObject的根元类地址就是0x00000001008010f0

(lldb) x/4gx 0x0000000100801140

0x100801140: 0x00000001008010f0 0x0000000000000000

0x100801150: 0x0000000100c42160 0x0002801000000003

// 根元类isa & 掩码 无论多少次最终都是指向了根元类,验证了`根元类`指向了`根元类`
(lldb) p/x 0x00000001008010f0 & 0x00007ffffffffff8ULL

(unsigned long long) $8 = 0x00000001008010f0

(lldb) p/x 0x00000001008010f0 & 0x00007ffffffffff8ULL

(unsigned long long) $9 = 0x00000001008010f0

(lldb)

② 代码验证 object_class() 函数

通过代码验证

image.png

  1. isa 继承链

image.png 由上图中可以发现:

  • 元类isa继承至该类的父元类
  • 该类的父元类继承至根元类
  • 根元类继承至NSObject
  • NSObject继承至nil

通过两种方式验证:

① lldb验证

image.png ② 代码验证class_getSuperclass()函数

// isa 继承链

/**

 对象是没有继承关系的;

 类的继承链 类->父类->父类的父类->...->NSObject->nil 不是这次所关心的,我们要探索的是isa的继承链

 类的isa指向的是元类,所以直接从元类开始探索

 */

void getISAInherit(**void**) {

    // 获取类地址

    Class tClass = DXJTeacher.class;

    // 获取tClass的isa指向的类 =》元类

    Class tMateClass = object_getClass(tClass);

    // 获取元类tMateClass继承的父类 =》 父元类

    Class super_tMateClass = class_getSuperclass(tMateClass);

    // 如果这里有多层继承关系,依旧会找相应的父元类的父元类

    // 获取父元类super_tMateClass继承的父类 =》 根元类

    Class root_tMateClass = class_getSuperclass(super_tMateClass);

    // 获取根元类root_tMateClass继承的父类 =》 根类

    Class root = class_getSuperclass(root_tMateClass);

    // 获取根类的父类 =》 nil

    Class obj = class_getSuperclass(root);

    NSLog(@"\n==== DXJTeacher isa 继承链 ====\ntClass:%p %@\ntMateClass: %p %@\nsuper_tMateClass:%p %@\nroot_tMateClass:%p %@\nroot:%p %@\nobj:%p %@\n",tClass, tClass, tMateClass, tMateClass,super_tMateClass, super_tMateClass,root_tMateClass, root_tMateClass, root, root, obj, obj);

}

image.png

获取类的内存数据(平移)

我们知道类是加载到内存五大区中的中, 栈的特点就是先进后出,并且内存空间是连续的。文章开始我们已经知道了类有比较重要的四个对象Class isa, Class superClass, cache_t cache, class_data_bits_t bits,并且也知道了isa是类的首地址,那现在我们利用栈的内存空间连续的特性,探索一下class_data_bits_t bits。相信工作很多年,会经常听到类的属性放在rw中,实例方法也放在rw中,成员变量放在ro中,类方法放在元类中,之所以探索class_data_bits_t bits,是因为我们想知道的rw ro rwe都在bits中,那现在就来验证一下是否是这样的?

获取class_data_bits_t bits

    // Class ISA; 8字节

    Class superclass; // 8字节

    cache_t cache;             // 16字节

    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

根据空间的连续性,利用内存偏移获取bits地址,先获取到isa地址,这个我们已经轻车熟路了,下面计算下到bits的偏移量:

  • Class ISA , Class是一个结构体指针占8个字节
  • Class superclass; Class是一个结构体指针占8个字节
  • cache_t cache; cache_t占用16字节

分析cache_t为什么是16字节?如下图,cache_t结构体内容很多,但是绝大多数是方法,是不占结构体空间的,所以只剩下篮筐和红框两部分,篮筐使用sizeof(uintptr_t)打印8字节,红框中是一个联合体(互斥),联合体内部的结构体是共同体占用8字节(或者使用sizeof(preopt_cache_t)打印也是8字节),cache_t是结构体是共存的,所以内存大小事篮框➕红框的的大小即8字节+8字节 = 16字节 image.png

最终到bits的偏移量为: ISA + superClass + cache_t = 8 + 8 + 16 = 32,也就是拿到isa地址再偏移32字节就是bits的内存地址了。用16进制表示即为0x20

属性 类的属性放在rw的属性列表中

  1. 获取类地址

image.png 2. 偏移0x20

image.png 3. 将获取到的地址强转为class_data_bits_t类型

image.png 4. 获取class_rw_t *

image.png 5. 获取属性列表

image.png

image.png 因为list_array_tt内部是一个迭代器实现的,所以使用get()方法即可获取对应的属性,DXJTeacher只有一个属性,取get(1)就越界了

image.png

成员变量 类的属性放在ro的成员列表中

  1. 获取ro地址

image.png

2.获取成员变量ivars地址 image.png 3. 解析数据

image.png

实例方法 类的属性放在rw的属性列表中

  1. 获取方法列表,并解析

image.png

image.png

这里有写的实例方法,以及系统自动生成的hobbygetter setter方法,但是没有类方法。

类方法 类方法放在元类的方法列表中

  1. 获取元类地址

image.png

  1. 偏移0x20

image.png

  1. 将地址转为class_data_bits_t类型

image.png

  1. 获取元类方法列表

image.png