前言
OC语言的底层是通过c/c++实现的,所以我们可以直接在oc的代码中直接调用c/c++的方法,程序编译的过程如下
对象的本质
对象的本质其实就是结构体,而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的源码看下:
- 打开runtime源码,搜索
rootAllocWithZone点击进入该方法 - 点击进入
class_createInstance方法 - 点击进入
_class_createInstanceFromZone方法 - 点击进入
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_getInstanceSize和malloc_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. 类对象的本质
通过上面的图可以看出,类对象存放了
- isa指针(指向元类对象)
- super指针(指向父类类对象)
- 类的属性信息(@property)、类的对象方法信息(instance method)
- 类的协议信息(@protcol)、类的成员变量信息(ivar)
元类对象(meta-class)
1. 获取元类对象(通过类对象来获取元类对象)
Class objcClass1 = [Person class];
Class objectMetaClass = object_getClass(objcClass1);
- 每个类只有一个元类对象,由系统创建
- 元类对象和类对象的结构是一样的,都是objc_class类型的结构体,元类对象中存放类方法
- 元类对象中存放了
- isa指针(指向根元类对象,也就是NSObjcet的元类对象)
- super指针(指向父类的元类对象)
- 类的类方法信息
-
当调用一个类的类方法的时候,或直接找到类对象,通过类对象的isa指针找到元类对象,然后再元类对象的方法列表中找。元类对象中没有方法,就去父类的元类对象中找,依次到NSObject。
-
类对象的继承关系其实就是元类对象的继承关系,都需要都是super指针依次找对应方法
整理理解图: