iOS--oc对象,类,和元类本质

发布于:2024-06-17 ⋅ 阅读:(18) ⋅ 点赞:(0)

前言

最近在学习runtime的过程中,发现其中消息发送-动态方法解析-消息转发中涉及到了大量的类与对象的底层知识,看别人博客的时候,别人往往也会先讲一遍oc对象的本质 ;
具体参考了iOS底层原理总结 - 探寻OC对象的本质

实例对象的具体结构

在探讨oc对象的本质前,首先我们要明白oc语言他的底层实现都是c/c++代码 ;

如图:
在这里插入图片描述

oc中的对象,在c/c++中往往以结构体的形式存在 ;
NSobject对象如下:

struct NSObject_IMPL {
	Class isa;
};
// 查看Class本质
typedef struct objc_class *Class;
我们发现Class其实就是一个指针,对象底层实现其实就是这个样子。

也就是说Nsobject结构体中只存有一个isa指针,至于这个指针指向哪里,其实我们也可以大致猜到了,这个isa指针指向了类(结构体对象);
这里注意一个点,这里的isa指针在arm64架构前是一个单纯的指针,但在arm64架构优化指针后底层是一个联合图或者说共用体(union);这里的isa使用的共用体为了节省空间,不断的进行值覆盖的操作,结合位域可以更大限度的节约内存空间,还不用覆盖旧值 ;
其中的具体细节就不说了,只需要知道优化后的共用体不仅存放这类对象或元类对象地址,还存放了很多额外属性 ;

  • 还有,指针在64位架构中占8个字节;
  • 在Objective-C中,对象的地址确实可以视为指向其内部isa指针的地址,因为isa是对象内存布局的第一个元素。所以,当你说“objc存储的就是isa的地址”,这一点是对的。

自定义类对象的结构

这里用别人的一个例子:

@interface Student : NSObject{
    
    @public
    int _no;
    int _age;
}
@end
@implementation Student

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Student *stu = [[Student alloc] init];
        stu -> _no = 4;
        stu -> _age = 5;
        
        NSLog(@"%@",stu);
    }
    return 0;
}
@end

这里中的对象的结构体可以转换为如下的形式:

struct Student_IMPL {
	Class *isa;
	int _no;
	int _age;
};

此结构体占用多少存储空间,对象就占用多少存储空间。因此结构体占用的存储空间为,isa指针8个字节空间+int类型_no4个字节空间+int类型_age4个字节空间共16个字节空间;
具体内存大小的分析记得要内存对齐 ;

那么一个NSObject对象占用多少内存? NSObjcet实际上是只有一个名为isa的指针的结构体,因此占用一个指针变量所占用的内存空间大小,如果64bit占用8个字节,如果32bit占用4个字节。

继承关系

/* Person */
@interface Person : NSObject
{
    int _age;
}
@end

@implementation Person
@end

/* Student */
@interface Student : Person
{
    int _no;
}
@end

@implementation Student
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"%zd  %zd",
              class_getInstanceSize([Person class]),
              class_getInstanceSize([Student class])
              );
    }
    return 0;
}

类对象实质上是以结构体的形式存储在内存中;下面是类对象结构体的图示:
在这里插入图片描述

注意一下,上面的类对象结构体是不完整的,应该是一种简化版本 ;

  • 我们发现只要是继承自NSObject的对象,那么底层结构体内一定有一个isa指针。

那么他们所占的内存空间是多少呢?单纯的将指针和成员变量所占的内存相加即可吗?上述代码实际打印的内容是16 16,也就是说,person对象和student对象所占用的内存空间都为16个字节。
其实实际上person对象确实只使用了12个字节。但是因为内存对齐的原因。使person对象也占用16个字节。

类信息的存放

从实例对象的结构出发,我们知道了它的结构体中只有isa指针和成员变量 ;
instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象;

instance对象在内存中存储的信息包括

  • isa指针
  • 其他成员变量

在这里插入图片描述

既然instance对象结构体中不包括类的方法信息 ;那我们该如何访问类的方法信息 ?

class对象 我们通过class方法或runtime方法得到一个class对象。class对象也就是类对象

每一个类在内存中有且只有一个class对象。

class对象在内存中存储的信息主要包括

  • isa指针
  • superclass指针
  • 类的属性信息(@property),类的成员变量信息(ivar)
  • 类的对象方法信息(instance method),类的协议信息(protocol)

在这里插入图片描述

所以instance对象的isa指针指向class对象来访问方法信息 ;
成员变量的值时存储在实例对象中的,因为只有当我们创建实例对象的时候才为成员变赋值。但是成员变量叫什么名字,是什么类型,只需要有一份就可以了。所以存储在class对象中。

同样的class对象的isa指针指向的是它的元类对象 ;

元类对象 meta-class

在内存中存储的信息主要包括

  • isa指针
  • superclass指针
  • 类的类方法的信息(class method)
    在这里插入图片描述

meta-class对象和class对象的内存结构是一样的,所以meta-class中也有类的属性信息,类的对象方法信息等成员变量,但是其中的值可能是空的。
class的isa指向meta-class 当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用

1.当对象调用实例方法的时候,我们上面讲到,实例方法信息是存储在class类对象中的,那么要想找到实例方法,就必须找到class类对象,那么此时isa的作用就来了。

2.当类对象调用类方法的时候,同上,类方法是存储在meta-class元类对象中的。那么要找到类方法,就需要找到meta-class元类对象,而class类对象的isa指针就指向元类对象

3.当对象调用其父类对象方法的时候,又是怎么找到父类对象方法的呢?,此时就需要使用到class类对象superclass指针。
当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用,同样如果Person发现自己没有响应的对象方法,又会通过Person的superclass指针找到NSObject的class对象,去寻找响应的方法

这里我猜想可能与runtime的消息流程有关,先从isa指针寻找,找不到就寻找到父类中,然后从父类的isa指针寻找 ;下面寻找父类的类方法也是一样的 ;

4.当类对象调用父类的类方法时,就需要先通过isa指针找到meta-class,然后通过superclass去寻找响应的方法

在这里插入图片描述

对isa、superclass总结

  • instance的isa指向class
  • class的isa指向meta-class
  • meta-class的isa指向基类的meta-class,基类的isa指向自己
  • class的superclass指向父类的class,如果没有父类,superclass指针为nil
  • meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
  • instance调用对象方法的轨迹,isa找到class,方法不存在,就通过superclass找父类
  • class调用类方法的轨迹,isa找meta-class,方法不存在,就通过superclass找父类