开胃面试题
1.一个NSObject对象占用多少内存?
2.一个继承自NSObject的Person对象,有一个NSString *name,一个int age,这个Person对象占用多少内存?
3.对象的isa指针指向哪里?
4.OC的类信息存放在哪里?
看这篇文章之前可以先回答一下这几个面试题,然后带着问题耐心看完这篇文章,再来回答一下看看
一、OC对象在内存中的结构
1、转换代码,查看底层
我们平时编写的OC代码,底层都是通过C\C++的结构体来实现的,我们在编译OC代码的时候,编译器会先把OC代码转成C\C++,再转成汇编语言,然后最终转成机器码。我们在探索OC对象的本质时,可以通过终端将我们写的OC代码转成C\C++代码来探索它的底层实现。
OC代码如下
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
NSLog(@"Hello, World!");
}
return 0;
}
我们在Mac终端中使用命令行将main.m的OC代码转化为C\C+代码。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
我们可以看到生成了一个main-arm64.cpp文件,将这个文件拖入Xcode进行查看(记得不要勾选编译选项,我们只需查看即可,编译会报错)。在main-arm64.cpp文件中,有非常多的代码,搜索NSObject_IMPL(IMPL代表implementation实现),我们来看一下NSObject_IMPL内部
struct NSObject_IMPL {
Class isa;
};
// 查看Class本质
typedef struct objc_class *Class;
我们发现Class其实就是一个指针,对象底层实现其实就是这个样子。
我们点击NSObject进入它的里面,发现NSObject的内部实现是这样的
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end
转化为底层后其实就是一个C语言的结构体
struct NSObject_IMPL {
Class isa;
};
那么这个结构体占多大的内存空间呢,可以看到这个结构体只有一个成员,即isa指针。在64位架构中,指针需要8个字节内存空间,那么一个NSObject对象占用的内存空间就是8个字节。
但是,NSObject对象中还有很多的方法,这些方法不占用内存空间吗?答案是这些方法也占用内存空间,但是这些方法有专门的地方放它们,所以这些方法占用的内存空间不在NSObject对象中。
2、实际需要与实际分配
从上面的分析中,可以知道一个NSObject对象需要8个字节内存空间,我们现在通过两个函数来打印一下NSObject对象的内存大小。
#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
NSLog(@"%zd",class_getInstanceSize([NSObject class]));
NSLog(@"%zd",malloc_size((__bridge const void *)(object)));
}
return 0;
}
打印结果
2019-05-03 15:02:00.154767+0800 Test[16522:802010] 8
2019-05-03 15:02:00.155803+0800 Test[16522:802010] 16
Program ended with exit code: 0
可以看到,一个结果是8,一个结果是16。为什么会是这个结果呢?因为第一个函数,打印的是NSObject对象的实际大小,第二个函数打印的是系统为NSObject对象实际开辟的,内存空间的大小。
在OC中,系统给对象分配内存空间,都是按照16个字节的倍数进行分配的,不会因为一个对象只需要1个字节空间,就给分配1个字节空间,或者只需要4个字节,就只分配4个字节空间。
3、自定义类的底层实现
我们定义一个继承自NSObject的Student类,按照前面的步骤同样生成C\C++代码,并查找Student_IMPL
OC代码
@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 {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
可以看到,Student_IMPL
结构体里的第1个成员是NSObject_IMPL
的实现。而前面我们已经知道了NSObject_IMPL
内部其实就是Class isa
,最终相当于这样
struct Student_IMPL {
Class *isa;
int _no;
int _age;
};
所以Student_IMPL
结构体占用多少内存空间,对象就占用多少内存空间。Student_IMPL
结构体占用的内存空间为,isa
指针8个字节 + int类型的_no
占用的4个字节 + _age
的4个字节共16个字节空间。
Student *stu = [[Student alloc] init];
stu -> _no = 4;
stu -> _age = 5;
那么上述代码实际上在内存中的体现为,创建Student对象首先会分配16个字节空间,存储3个东西,isa
指针8个字节,_no
4个字节,_age
4个字节。
Student
对象的3个成员变量分别有自己的地址,而stu
指向结构体第1个成员变量,即isa指针
的地址。因此stu
的地址为0x100400110
,stu对象在内存中占用16个字节的空间。并且经过赋值,_no
里面存储着4,_age
里面存储着5。
验证Student在内存中的布局
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 强制转化
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"_no = %d, _age = %d", stuImpl->_no, stuImpl->_age); // 打印出 _no = 4, _age = 5
}
return 0;
}
上述代码将OC对象类型强转成Student_IMPL
类型的结构体,也就是把指向OC对象的指针,指向这个结构体。如果Student
对象在内存中的布局与结构体Student_IMPL
在内存中的布局相同,那么就可以转化成功,从而验证之前的分析。说明stu
这个对象指向的内存确实是一个结构体。
我们再通过打印看一下stu对象的内存大小
NSLog(@"%zd",class_getInstanceSize([Student class]));
NSLog(@"%zd",malloc_size((__bridge const void *)(stu)));
stu
的内存大小打印结果为
2019-05-03 16:11:46.384189+0800 Test[22695:848804] 16
2019-05-03 16:11:46.384754+0800 Test[22695:848804] 16
Program ended with exit code: 0
可以看到,stu
对象的实际内存大小是16字节,系统实际分配的内存大小也是16字节。这也验证了我们前面说的按照16字节的倍数分配规则。
二、OC对象的分类
OC对象主要分为3种:instance对象(实例对象),class对象(类对象),meta-class对象(元类对象)。
1、实例对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象
代码
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
NSLog(@"obj1:%@",obj1);
NSLog(@"obj2:%@",obj2);
代码执行结果
2019-11-25 14:06:42.507663+0800 demo[17733:127185] obj1:<NSObject: 0x10054f200>
2019-11-25 14:06:42.508522+0800 demo[17733:127185] obj2:<NSObject: 0x10054c4a0>
Program ended with exit code: 0
obj1和obj2都是NSObject类的实例对象,但他们是两个不同的对象,占据着两块不同的内存。
实例对象主要用来调用对象方法,存储成员变量的具体值(包括一个特殊的成员变量isa,和其他成员变量)。我们知道,NSObject对象还有很多方法可以调用,那这些方法在哪里呢?它们占用内存空间吗?类的的方法当然也占用内存空间,但这些方法占用的内存空间并不在NSObject类的实例对象中,而是在下面介绍的2个对象中。
2、类对象:我们通过类的+class方法或者runtime方法可以得到类对象,每次调用+class方法或者runtime方法得到的都是同一个类对象,每一个类在内存中有且只有一个类对象
代码
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
// class方法
Class objClass1 = [obj1 class];
Class objClass2 = [obj2 class];
Class objClass3 = [NSObject class];
// runtime方法
Class objClass4 = object_getClass(obj1);
Class objClass5 = object_getClass(obj2);
NSLog(@"%p %p %p %p %p", objClass1, objClass2, objClass3, objClass4, objClass5);
代码执行结果
2019-11-25 14:12:22.101947+0800 demo[18095:130169] 0x7fff901be118 0x7fff901be118 0x7fff901be118 0x7fff901be118 0x7fff901be118
Program ended with exit code: 0
可以看到,打印的结果都是一样的,这说明不管怎么获取的一个类的类对象,都是同一个。为什么实例对象可以在内存中有很多个,而类对象只有一个呢?这要从类对象在内存中存储的信息说起,先来看一下有哪些信息:
1.isa指针
2.superclass指针
3.类的属性信息(@property),类的成员变量信息(ivar)
4.类的对象方法信息(instance method),类的协议信息(protocol)
5.其他一些信息...
拿成员变量来说,实例对象存储的是成员变量具体的值(如Person对象的具体age值),类对象存储是成员变量的类型和名字(int类型,名字是age)。每一个Person对象的具体age值可能是不一样的,但是每一个Person对象的这个成员变量的类型都是int,名字都age。所以,实例对象可以有多个,而类对象一个就可以了。
3、元类对象跟类对象一样,在内存中有且只有一个元类对象,我们一般使用runtime方法获取元类对象
其实,元类对象和类对象是一种类型,都是Class类型。他们在内存中的结构是一样的,存储的信息也可以是一样的。但是由于实际用途不一样,所以元类对象实际上存储的信息和类对象存储的信息并不一致。主要包括:
1.isa指针
2.superclass指针
3.类的类方法信息(class method)
三、isa指针与superclass指针
1、isa指针
到这里我们知道了对象有实例对象、类对象、元类对象,他们都有isa指针。那么这些isa指针指向哪里,有什么用?
1.1 实例对象的isa指针指向类对象
我们已经知道,类的对象方法存在类对象中。当我们使用实例对象调用方法的时候,实际上是实例对象通过它的isa指针,找到类对象,最后在类对象里面找到方法进行调用。
1.2 类对象的isa指针指向元类对象
类的类方法存在元类对象中,当我们调用类方法的时候,实际上是通过类对象的isa指针,找到元类对象,最后在元类对象中找到方法进行调用。
1.3 元类对象的isa指针指向基类(即NSObject)的元类对象
1.4. 基类(NSObject)的元类的isa指针指向基类(即NSObject)的类对象(这个比较特殊)
2、superclass指针
superclass指针存在于类对象和元类对象中。那么这些superclass指针指向哪里,有什么用?
superclass指针存在于类对象和元类对象中,实例对象中没有superclass指针。类对象的superclass指针指向其父类的类对象,元类对象的superclass指针指向其父类的元类对象。当对象调用其父类的对象方法时,就需要使用superclass指针
创建一个继承自NSObject的Person类,再创建一个继承自Person的Student类。当Student对象要调用Person的对象方法时,就会通过Student对象的isa指针找到Student的类对象,发现Student类对象中没有这个方法,就会通过Student的superclass指针去Person的类对象中找这个方法,找到后进行调用。
superclass调用类方法的过程与调用对象方法的过程类似,只不过调用对象方法是去类对象里面去找,调用类方法是去元类对象里面去找。
对isa指针和superclass指针的指向进行总结,可以的得到这张总结图
isa、superclass总结:
1.instance的isa指向class
2.class的isa指向meta-class
3.meta-class的isa指向基类(NSObject)的class,如果没有父类,superclass指针为nil
4.class的superclass指向父类的class,如果没有父类,superclass指针为nil
5.meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
6.instance调用对象方法的轨迹:instance的isa找class,方法不存在,就通过superclass找父类
7.class调用类方法的轨迹,class的isa找meta-class,方法不存在,就通过superclass找父类
掌握OC对象的相关知识和OC类的相关知识,对于掌握runtime有莫大的帮助
iOS中OC类的本质 - 底层原理总结
iOS中的Runtime(附面试题) - 底层原理总结