前言
我们编写的OC代码,其实底层实现都是C/C++代码。所以,对象和类也都是基于C/C++的数据结构实现的。 所以你能猜到OC的对象和类是通过什么数据结构实现的吗?
1、instance-实例对象
1.1、定义
实例对象是通过类alloc
出来的对象,每次调用alloc
都会产生新的实例对象。eg:
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
以上object1、object2都是实例对象,占用两块儿不同的内存。
1.2、底层实现
首先,终端定位到需要转化文件的文件夹下,用以下命令行将OC代码转成C++/C代码。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件(eg:main.m) -o 输出的CPP文件(eg:main.cpp)
以NSObject
对象为例,我们用以上方法看看它的底层实现,eg:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
转成C++代码后,我们找到了这个对象的实现
struct NSObject_IMPL {
Class isa;
};
//Class是指针类型,指向objc_class类型的结构体
typedef struct objc_class *Class;
obj
对象内部实际上只有一个isa指针,指向objc_class
类型的结构体。 那isa指针到底指向谁,它又有什么用呢? 在下文中我们会讲到。
1.3、更复杂的继承结构
我们举一反三,设计一个父类Father
,继承于NSObject
,再设计一个子类son
继承于父类,看看他们的底层实现。eg:
@interface Father : NSObject {
int _age;
}
@end
@interface Son : Father {
double _height;
}
@end
把代码转成C++,看看内部实现。直接查找类名_IMPL
,
struct Father_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
};
struct Son_IMPL {
struct Father_IMPL Father_IVARS;
double _height;
};
上述代码相当于
struct Father_IMPL {
Class isa;
int _age;
};
struct Son_IMPL {
Class isa;
int _age;
double _height;
};
所以实例对象的本质是结构体,(在c++文件中查找类名_IMPL
就能找到这个结构体),里面有一个isa指针和其他成员变量。所以实例对象在内存中存储的内容是 isa指针 + 其他成员变量。
1.4、内存大小
从1.2和1.3我们知道,一个实例对象,在内存中存储的是 isa指针 + 其他成员变量。那系统到底给这个对象分配了多少内存,它实际只需要多少?这两个概念也比较容易混淆,我们来理一理。
1.4.1、实际需要
即创建一个实例对象,实际需要多少内存。
可以通过导入头文件#import <objc/runtime.h>
,用class_getInstanceSize
方法获得。eg:
NSLog(@"%zd", class_getInstanceSize([NSObject class])); //8
NSLog(@"%zd", class_getInstanceSize([Father class])); //16
NSLog(@"%zd", class_getInstanceSize([Son class])); //24
他们需要的内存分别是8
,16
,24
。我们可以一一推算一下他们是怎么来的。
NSObject:内存中只有一个isa指针,指针在64位系统中占8个字节,所以NSObject类型的对象实际占用8个字节。
Father:isa指针 (8字节)+ int类型变量(4字节)= 12字节
Son:isa指针(8字节)+ int类型变量(4字节)+ double类型变量(8字节) = 20字节。
为什么我们算出来的字节会有偏差?我们可以看看class_getInstanceSize
方法的源码。
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
uint32_t alignedInstanceSize() {
//返回内存对齐后的大小
return word_align(unalignedInstanceSize());
}
我们查看class_getInstanceSize
源码,align是校准对齐的意思,发现实际的占用大小应该再经过一次内存对齐操作word_align
,内存对齐:最后的占用大小是最大元素的倍数。
所以,实际需要内存 = 内存对齐(isa指针 + Ivars)。
但是,一个对象实际需要多少内存,系统就会给它实际分配多少内存吗?带着这样的疑问,我们接着往下验证。
1.4.2、实际分配
即创建一个实例对象,实际上分配了多少内存。
可以通过导入头文件#import <malloc/malloc.h>
,用malloc_size((__bridge const void *)obj);
方法获得。eg:
NSObject *obj = [[NSObject alloc] init];
Father *father = [[Father alloc] init];
Son *son= [[Son alloc] init];
NSLog(@"%zd", malloc_size((__bridge const void *)obj)); //16
NSLog(@"%zd", malloc_size((__bridge const void *)father)); //16
NSLog(@"%zd", malloc_size((__bridge const void *)son)); //32
以上三个对象实际分配的内存分别为16
,16
,32
。
我们可以看出,给对象实际分配的内存和对象实际需要的内存是不一样的。
实际上,苹果提前有一块儿一块儿的内存块儿,这些内存块儿都是16的倍数,最大是256。
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */
所以给对象分配内存也是一块儿一块儿分的。当实际需要的内存不是16的倍数,比如24,苹果会分配一块儿最适合的32给它。
思考题🤔:孙类Grandson继承son,Grandson中有两个成员变量int _no; NSString *_name;
,那创建一个孙类的实例对象,实际需要多少内存? 系统实际分配了多少内存?
2、class-类对象
从第一部分我们了解到,实例对象只存储isa指针和成员变量,那类里面的方法啊,属性啊等等一些类信息存放在哪里?为什么这些信息没有存放在实例对象里?
因为实例对象可能有很多个,不可能每创建一个实例对象都存一份方法、属性.....的。 这些只需要存一份就可以了。一个类有且只有一个类对象。把属性啊方法啊等信息存在类对象中也再合适不过了。
2.1 创建类对象
NSObject *obj = [[NSObject alloc] init];
Class objClass1 = [NSObject class];
Class objClass2 = [obj class];
Class objClass3 = object_getClass(obj);
我们可以打印一下,objClass1
,objClass12
,objClass13
他们的内存地址都是一样的。也验证了一个类有且一有一个类对象。
2.2 类对象的本质
点进class发现,类对象其实是 objc_class
类型的结构体。 我们打开源码,看看这个结构体到底是什么。
如上我们可以看出,类对象中存放了
1、isa指针
2、super指针
3、类的属性信息(@property)、类的对象方法信息(instance method)
4、类的协议信息(protocol)、类的成员变量信息(ivar)
........
类对象里面也有一个isa指针,还有一个super指针,那他们分别指向哪里,又有什么作用呢? 我们稍后就会讲到。 当然这里还有一个疑问,既然类对象里面存放的是对象方法信息,那类方法信息存放在哪里呢?
3、meta-class-元类对象
构建
Class objectMetaClass = object_getClass([NSObject class]);
如上,objectMetaClass 就是 NSObject的元类对象,并且 每个类只有一个元类对象
元类对象和类对象的结构是一样的,都是objc_class
类型的结构体,元类对象存放类方法信息
1、isa指针
2、super指针
3、类的类方法信息(class method)
.........
meta-class对象和class对象的内存结构是一样的,所以meta-class中也有类的属性信息,类的对象方法信息等成员变量,但是其中的值可能是空的。
4、isa指针和super指针
实例对象,类对象,元类对象中都有isa
指针,类对象和元类对象中有super
指针。他们分别指向哪里?
4.1、 实例对象的isa指针指向
eg1:子类Son
中有一个实例方法- (void)sonTest
,创建一个实例对象son
,然后用这个对象调用方法[son sonTest];
。
类对象中存储着实例方法信息。当实例对象调用实例方法时
实例方法存储在类对象中。实例对象调用实例方法时,实例对象对象通过isa指针找到类对象,进而找到类对象中相应的实例方法进行调用。
实例对象的isa指针指向它的类对象。
4.2、类对象的isa指针指向
元类对象中存储着类方法信息。当类对象调用类方法时,类对象通过isa指针找到元类对象,进而找到元类对象中相应的类方法进行调用,类对象的isa指针指向它的元类对象。
4.3、元类对象的isa指针指向
元类对象的isa指针指向基类的元类对象(eg:Son的元类对象和Father的元类对象都指向NSObject的元类对象)
4.4、类对象的super指针指向
eg4:父类Father
中有一个实例方法- (void)fatherTest
,创建一个子类实例对象son
,然后用这个对象调用方法[son fatherTest];
。
从4.1我们知道,son
对象的isa指针会找到它的类对象,但是类对象中没有fatherTest
这个对象方法,所以类对象会通过它的super
指针找到父类的类对象,而fatherTest
这个方法是存放在Father的类对象中的,进而调用。类对象的super指针是指向父类的类对象的。
特例:当这个类没有父类时(基类),则指向nil
4.5、元类对象的super指针指向
元类对象的super指针指向父类的元类对象
特例:基类的元类对象super指针指向基类的类对象。
以下是对这些情况的总结图
结束语:这篇文章是对 小码哥底层原理视频的总结,以及我的理解~ 希望能对各位有所帮助,喜欢就点个赞吧(*  ̄3)(ε ̄ *)