iOS底层原理04:类的原理之结构分析

902 阅读7分钟

前言

通过前面学习得知,万物皆对象,类有isa指针,那么类的isa又指向哪里呢?

一、元类探索

案列分析如下

截屏2021-10-05 下午6.29.24.png

根据调试过程,为什么图中的po 0x0000000100008360 与 po 0x0000000100008338 中的类信息打印出来都指向LGPerson
补充:x/4gx 看内存信息 p/x 看指针地址

猜想:类和对象,可以无限开辟,内存不止一个类?执行以下部分代码,继续分析 ^_^

Class class1 = [LGPerson class];
Class class2 = [LGPerson alloc].class;
Class class3 = object_getClass([LGPerson alloc]);
Class class4 = [LGPerson alloc].class;
NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);

打印结果如下:

0x100008360-
0x100008360-
0x100008360-
0x100008360

元类(METACLASS)诞生

结果可以看出0x0000000100008338不是类信息,那么它是什么呢?

  • 0x0000000100008360p对象的isa指针0x011d800100008365 & 0x00007ffffffffff8得到结果是创建p的类LGPerson
  • 0x0000000100008338isa中获取的类信息所指的类的isa的指针地址,既LGPerson类的类的isa指针地址,简称为 元类(METACLASS)
  • 元类(METACLASS),是系统编译器自动创建的
  • 执行流程:对象 -> isa -> 类的isa -> 元类

二、元类 & isa走位 分析

1. isa走位

对象元类都有isa,具体isa走位流程是如何的呢?创建LGPerson对象结合lldb进行探索如下:

截屏2021-10-05 下午11.10.46.png

  • 结果分析

    • 对象isa指向
    • isa指向元类
    • 元类isa指向根元类(NSObject)
    • 根元类(NSObject)isa指向它自己根元类(NSObject)

    内存中只存在存在一份根元类NSObject根元类元类是指向它自己

    isa 探索元类的走位图

截屏2021-10-05 下午11.46.10.png

2. 元类 继承链 分析

引入以下代码分析 继承链关系,其中对象关系:LGTeacher <- LGPerson <- NSObject

#pragma mark **- NSObject 元类链**
void lgTestNSObject(void){

    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);

    // LGPerson元类
    Class pMetaClass = object_getClass(LGPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);
    NSLog(@"%@ - %p",psuperClass,psuperClass);

    // LGTeacher -> LGPerson -> NSObject
    // 元类也有一条继承链
    Class tMetaClass = object_getClass(LGTeacher.class);
    Class tsuperClass = class_getSuperclass(tMetaClass);
    NSLog(@"%@ - %p",tsuperClass,tsuperClass);

    // NSObject 根类特殊情况
    Class nsuperClass = class_getSuperclass(NSObject.class);
    NSLog(@"%@ - %p",nsuperClass,nsuperClass);

    // 根元类 -> NSObject
    Class rnsuperClass = class_getSuperclass(metaClass);
    NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);

}

运行结果

截屏2021-10-06 上午12.20.39.png

  • isa走位分析

    • LGPerson元类的走位链:person(对象) ——> LGPerson(类) ——> LGPerson(元类) ——> NSObject(根元类) ——> NSObject(自己类)
  • superclass走位链分析

    • LGPerson类的继承关系链:LGPerson(子类) --> NSObject(根类) --> null
    • LGTeacher类的继承关系链:LGTeacher(子类) --> LGPerson(父类) --> NSObject(根类) --> null

superclass走位图分析:

截屏2021-10-06 上午12.52.09.png

根据以上各种验证分析,对象、类、元类、根元类的关系图如下:

isa流程图.png

总结

isa走位说明
  • 实例对象(Instance of Subclass)isa指向类(Class)
  • 类(Class)isa指向元类(Meta Class)
  • 元类(Meta Class)isa指向根元类(Root Meta Class)
  • 根元类(Root Meta Class)isa指向它自己
superclass 继承链说明
  • 类的继承关系

    • 类(Subclass)继承自父类(Superclass)
    • 父类(Superclass)继承自根类(Rootclass)
    • 根类(Rootclass)继承自nil根类(NSObject)可以理解万物起源无中生有
  • 元类的继承关系

    • 类的元类(Subclass meta)继承自父类的元类(Superclass meta)

    • 父类的元类(Superclass meta)继承自根类的元类(Root class meta)

    • 根类的元类(Root class meta)继承自根类(Root class),既NSObject

三、补充知识 - 指针和内存平移

1. 普通指针

// 普通指针
int a = 10; //
int b = 10; //
LGNSLog(@"%d -- %p",a,&a);
LGNSLog(@"%d -- %p",b,&b);

打印结果如下图

截屏2021-10-06 下午3.11.31.png

ab都指向10,但是ab的地址不一样 ,这是copy,也叫值拷贝
ab的地址之间相差4个字节,取决于ab的类型int

指针指向关系图如下:

截屏2021-10-06 下午3.27.16.png

2. 对象指针

// 对象 -
LGPerson *p1 = [LGPerson alloc];
LGPerson *p2 = [LGPerson alloc];
LGNSLog(@"%@ -- %p",p1,&p1);
LGNSLog(@"%@ -- %p",p2,&p2);

打印结果如下图:

截屏2021-10-06 下午3.39.18.png

当前的p1p2地址不同,空间也不相同
p1是指向[LGPerson alloc]创建的空间地址(内存地址)p2同理
&p1&p2是指向p1p2对象的指针地址,既二级指针

对象指针关系图

截屏2021-10-06 下午4.00.26.png

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);

for (int i = 0; i<4; i++) {
    int value =  *(d+i);
    NSLog(@"%d",value);
}

打印结果

截屏2021-10-06 下午4.07.51.png

&c&c[0]都是数组的首地址
&c[0]&c[1]相差4字节,地址之间相差字节数根据当前的数据类型 int
首地址(&c) + 偏移量(1)可取出数组中其他元素,偏移量数组下标首地址+偏移量获取元素的内存地址 = 偏移量 * 数据类型字节数 + 首地址

截屏2021-10-06 下午4.27.19.png

四、类结构分析

在上一篇文章iOS底层原理 03:OC对象初始化之本质与iSA中,使用clang编译过main.m文件,从中可以得出:

  • NSObject 同样指向 struct objc_object的结构体
  • isa的类型为Class,被定义为指向 objc_class的结构体指针

1. objc_class 分析

  • objc源码中搜索objc_class定义,得到有两个地方的定义:

  • runtime.h 文件中,objc_class定义,由于执行流程是非__OBJC2__,已弃用。

截屏2021-10-06 下午2.24.18.png

  • objc_runtime-new.h,objc_class定义,现阶段阶段正在使用,如下图所示。

截屏2021-10-06 下午2.32.17.png

从源码的定义中可以看出,objc_class结构体类型是继承自objc_object的。

  • 属性分析:

    • isa:继承自objc_objectisa,所以objc_class也拥有了isa(结构体指针) 8字节
    • superclass:Class 类型, Classobjc_object定义,8字节
    • cachecache_t类型,而cache_t是结构体,并且结构体大小需要内部的属性来确定。
    • bitsclass_data_bits_t类型,求解地址:首地址 + (isa + superclass cache),首地址经过3个属性内存大小总和的平移。

2. 求解cache类型的内存大小

  • 进入cache_t的定义。排除其中的方法函数(方法不在结构体区域内存中),得出以下几个属性

截屏2021-10-06 下午6.53.43.png

  • _bucketsAndMaybeMask: 看uintptr_t类型,而 uintptr_tunsigned long定义的,占 8 字节

  • union:联合体结构,其中有 2 个数据,根据联合体的情况,只去其一

    • struct:是结构体定义,根据图中所示,占 8 字节
    • _originalPreoptCache: 带 * 号类型,占 8 字节
  • 总结:cache的内存大小 = 8 + 8 = 16字节,原因:union 是互斥关系,只取一瓢。

3. 获取 bits 内容

截屏2021-10-06 下午7.15.30.png

  • 上一步已计算出cache的内存大小,想要获取bits内容,需要将首地址 平移32字节即可

以下是通过lldb命令调试获取bits的过程

获取首地址

(lldb) p/x LGPerson.class
(Class) $0 = 0x0000000100008370
(lldb) x/4gx LGPerson.class
0x100008370: 0x0000000100008398 0x0000000100372140
0x100008380: 0x000000010100bf90 0x0002802000000003

  • p/x LGPerson.class直接获取
  • x/4gx LGPerson.class 通过内存信息获取

截屏2021-10-06 下午7.40.49.png

4. 属性列表(properties)分析

根据上一步分析,获取数据内容已进入class_rw_t结构,分析当前class_rw_t结构,发现当前结构体中相应的方法获取 属性列表(properties)方法列表(methods)

截屏2021-10-06 下午8.01.58.png

  • 准备工作,添加 LGPerson对象 如下:
@interface LGPerson : NSObject
{
    NSString *subject;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;
#import "LGPerson.h"
@implementation LGPerson
- (instancetype)init{
    if (self = [super init]) {
        self.name = @"Cat";
    }
    return self;
}
@end

通过class_rw_t提供的方法,继续分析bits中的属性列表,以下是lldb分析流程

截屏2021-10-06 下午8.27.19.png

  • p $5.properties()中的properties方法是由class_rw_t提供的。
  • 通过 count = 2可知,当前属性列表含有2个字段。 p $9.get(0)p $9.get(1)分别获取到了 namehobby字段。
  • 属性列表(properties)中无成员变量(subject)

5.方法列表(methods)分析

  • 准备工作,在上一步的LGPerson对象中添加2个方法(实例方法 & 类方法)
@interface LGPerson : NSObject
{
    NSString *subject;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;
- (void)sayNB;
+ (void)say666;
#import "LGPerson.h"
@implementation LGPerson
- (instancetype)init{
    if (self = [super init]) {
        self.name = @"Cat";
    }
    return self;
}
- (void)sayNB{ 
}
+ (void)say666{
}
@end

通过class_rw_t方法,查找bits中的方法列表lldb分析流程如下:

截屏2021-10-06 下午9.08.57.png

  • p $5.methods()获取方法列表的list结构,分析源码可知,methods方法是由class_rw_t提供的,const method_array_t methods() const

  • 通过lldb调试可知,count = 6,说明当前对象中含有 6 个方法,但是,输出第一个元素打印出来为空???

    • 进一步源码分析,通过method_array_t 方法层层推进,得出以下逻辑链:method_array_t -> method_list_t -> method_t,最终在 method_t 结构体中找到 big 结构体,

      截屏2021-10-06 下午9.22.06.png

  • p $p.get(0).big() 打印添加 big()方法继续打印,结果如下:

截屏2021-10-06 下午9.24.30.png

  • 可以看出,通过p $9.get(i).big() 内存偏移的方式,6个方法成功输出。

  • 方法类型如下:实例方法(sayNB)name habby系统生成的get/set方法、init方法

  • 6 个方法中无 + (void)say666;method_array_t中只有 实例方法,没有类方法,

6. 成员变量(ivars)分析

有上面的属性列表分析,得出propertys 中只有属性,没有成员变量, 那么成员变量存储在哪里呢?

分析 class_rw_t的定义发现,当前里面无存取成员变量的方法,但是在 ro() const方法中,进入ro()方法的 class_ro_t定义,在其中发现有一个 ivars属性,可猜测成员变量存在ivars属性中呢?

以下是从ro()方法中,通过lldb调试寻找突破口:

截屏2021-10-06 下午10.05.55.png

  • 通过lldb调试发现,获取 ivars属性,其中count = 3,除了包括成员变量,还有属性定义的成员变量也在ivars中。

  • 对象{ NSString *subject }定义的成员变量,会存储在bits属性中,查找流程: bits -> data() -> ro() -> ivars

7. 类方法分析

方法列表(methods)只有实例方法,无类方法,那么类方法存储在哪里呢?

前文,提及了元类,类对象的isa指向元类。对象的方法存储在类的bits中,那么类的方法是否可能存储在元类的bits中呢?可通过lldb调试验证这个猜想,如下图所示:

截屏2021-10-06 下午10.41.26.png

通过图中元类方法列表的打印结果可知,上面的猜想验证通过。

总结:

  • 类方法存储在元类的bits属性中,
  • 获取类方法流程: --> 元类 --> 元类bits --> data() --> methods() --> list --> ptr --> get(i).big()