iOS 对象的本质和存储

252 阅读8分钟

前言

OC语言的底层是通过c/c++实现的,所以我们可以直接在oc的代码中直接调用c/c++的方法,程序编译的过程如下 OC代码转化过程

对象的本质

对象的本质其实就是结构体,而id,是指向这个结构体的指针, 类的本质也是个结构体(类也是一个对象,我们叫做类对象),而Class是指向这个结构体的指针

OC对象的存储空间

可以通过命令行的形式将OC的源代码生成c++代码

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

xcrun 代码的是xcode
iphoneos 代表运行在iPhone上
-arch 代表指定的架构模式
-rewrite-objc 代表重写
-o 代表输出
struct NSObject_IMPL {
    Class isa;
};

可以看出NSObject的本质就是一个结构体,里面包含了一个Class的变量,我们继续看下Class的定义:

typedef struct objc_class *Class;

Class就是一个指针,指向了objc_class类型

我们需要导入 <objc/runtime.h> 以及获取对象内存大小的头文件 <malloc/malloc.h>

@interface** Person : NSObject
{

}
@end

上面定义了一个Person类,通过class_getInstanceSize方法获取一个对象占多少内存,malloc_size方法获取系统为对象实际分配的内存大小。

NSLog(@"%zd",class_getInstanceSize([person class]));
NSLog(@"%zd",malloc_size(( __bridge const void *)(person)));
**2021-10-29 16:01:38.228592+0800 类的本质[9723:224267] 8
**2021-10-29 16:01:38.228808+0800 类的本质[9723:224267] 16

上面第一个打印的是8,第二个打印的是16。Person类只有从NSObject中继承的isa指针,isa指针占8个字节,所以Person对象占用的内存大小是8,那么为什么系统要分配16个内存空间呢,我们从runtime的源码看下:

  1. 打开runtime源码,搜索rootAllocWithZone点击进入该方法
  2. 点击进入 class_createInstance方法
  3. 点击进入 _class_createInstanceFromZone方法
  4. 点击进入 instanceSize方法
    size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

从方法中可以清楚看到,当分配的size少于16个空间的时候,系统会默认设置为16个空间。所以系统会分配16个空间的大小

在Person中添加一个变量

@interface Person : NSObject

{
    int age;
}

@end
**2021-10-29 16:19:03.307874+0800 类的本质[10115:233461] 16
**2021-10-29 16:19:03.308055+0800 类的本质[10115:233461] 16

这时候可以看到对象占用了16个字节,而系统也分配了16个字节,按道理一个int类型应该只占用4个字节,为啥对象占用的空间不是12个字节。这是因为内存补齐的原则,对象占用的空间必须是最大成员变量的整数倍,在Person中最大的成员是isa指针的8个字节,所以占用的空间应该是8*2 = 16。

继续在Person中添加一个变量

@interface Person : NSObject

{
    int age;
    int num;
}

@end
**2021-10-29 16:19:03.307874+0800 类的本质[10115:233461] 16

**2021-10-29 16:19:03.308055+0800 类的本质[10115:233461] 16

这时输出的还是16和16。因为age只占4个字节,还有剩余的4个字节会被num占据。

继续在Person中添加一个变量

@interface Person : NSObject

{
    int age;
    int num;
    int scope;
}

@end
**2021-10-29 16:19:03.307874+0800 类的本质[10115:233461] 24

**2021-10-29 16:19:03.308055+0800 类的本质[10115:233461] 32

这时候输出的是24 和 32 ,系统分配的空间是按16的整数倍来分配的。当16个字节用完的时候,会在分配16分字节的空间,而24是因为scope占4个字节,因为要是8的整数倍,则需要 8 * 3 = 24个字节才能分配完成员变量,

给成员变量分配内存空间的时候是依次分配的,有可能会造成一些未使用的内存空间

@interface Person : NSObject

{
    int age;
    long num;
}

@end
**2021-10-29 16:19:03.307874+0800 类的本质[10115:233461] 24

**2021-10-29 16:19:03.308055+0800 类的本质[10115:233461] 32

这时候打印的是24和32,系统先分配16个字节,isa指针占用8个字节,而后给age分配,以8的整数倍分配,这时候就分配了16个字节。之后num类型占8个字节,剩余的4个字节不够,系统会再分配16个字节,num之后会占8个。所以打印24、32;age后面的4个字节就没有使用

**2021-10-29 16:32:05.083440+0800 类的本质[10481:241315] person - 0x100708790

**2021-10-29 16:32:05.083474+0800 类的本质[10481:241315] age - 0x100708798

**2021-10-29 16:32:05.083507+0800 类的本质[10481:241315] num - 0x1007087a0

从内存地址也可以看出 age跟num直接相差8个字节

合理设置变量的顺序有助于减少未使用的内存空间,如下:

@interface Person : NSObject

{
    int age;
    long num;
    int scope;
}

@end
**2021-10-29 16:19:03.307874+0800 类的本质[10115:233461] 32

**2021-10-29 16:19:03.308055+0800 类的本质[10115:233461] 32

改变 num和scope的顺序

@interface Person : NSObject

{
    int age;
    int scope;
    long num;
}

@end
**2021-10-29 16:19:03.307874+0800 类的本质[10115:233461] 24

**2021-10-29 16:19:03.308055+0800 类的本质[10115:233461] 32

下面的方式可以减少系统分配内存,如果后面在分配一个long类型的数据,系统也只用分配32字节,而前面一种,当在添加一个long类型的数据,会在分配32字节。

sizeof、class_getInstanceSize、malloc_size方法的区别

sizeof,class_getInstanceSizemalloc_size这几个方法特别容易混淆,我们总结一下:

  • sizeof是运算符,编译的时候就替换为常数.返回的是一个类型所占内存的大小.
  • class_getInstanceSize传入一个类对象,返回一个对象的实例至少需要多少内存,它等价于sizeof.需要导入#import <objc/runtime.h>
  • malloc_size返回系统实际分配的内存大小,需要导入#import <malloc/malloc.h>

实例对象、类对象、元类对象、根元类对象

实例对象(instance)

通过alloc或者new生成的对象为实例对象,每个实例对象的地址都不一样 实例对象只存储了isa指针和成员变量

Person  *person = [[Person alloc]init];
Person  *person1 = [[Person alloc]init];
**2021-10-29 18:36:47.806872+0800 类的本质[13130:293968] person = <Person: 0x104041310>
**2021-10-29 18:36:47.807116+0800 类的本质[13130:293968] person1 = <Person: 0x104041370>

类对象(Class)

在实例对象里面我们了解到实例对象只存储了isa指针和成员变量,那么类的方法存储在哪里呢。这时候就要讲到类对象了,因为实例对象有很多个,所以不可能每个实例对象都存一份方法。所有的实例对象共用一份方法就可以了。 一个类有且只有一个类对象,会把实例方法保存在类对象中,而实例对象的isa指针指向类对象的地址。当实例对象调用方法时,会通过isa指针找到对应的方法,然后调用。 类对象系统会自动帮我们创建

1. 获取类对象

Person *person = [[Person alloc]init];
Class objcClass1 = [person class];
Class objcClass2 = [Person class];
Class objcClass3 = object_getClass(person);

上面的 objClass1,objClass12,objClass13他们的内存地址都是一样的

1. 类对象的本质

通过上面的图可以看出,类对象存放了

  1. isa指针(指向元类对象)
  2. super指针(指向父类类对象)
  3. 类的属性信息(@property)、类的对象方法信息(instance method)
  4. 类的协议信息(@protcol)、类的成员变量信息(ivar)

元类对象(meta-class)

1. 获取元类对象(通过类对象来获取元类对象)

Class objcClass1 = [Person class];
Class objectMetaClass = object_getClass(objcClass1);
  • 每个类只有一个元类对象,由系统创建
  • 元类对象和类对象的结构是一样的,都是objc_class类型的结构体,元类对象中存放类方法
  • 元类对象中存放了
  1. isa指针(指向根元类对象,也就是NSObjcet的元类对象)
  2. super指针(指向父类的元类对象)
  3. 类的类方法信息
  • 当调用一个类的类方法的时候,或直接找到类对象,通过类对象的isa指针找到元类对象,然后再元类对象的方法列表中找。元类对象中没有方法,就去父类的元类对象中找,依次到NSObject。

  • 类对象的继承关系其实就是元类对象的继承关系,都需要都是super指针依次找对应方法

整理理解图:

image.png