iOS中OC对象的本质详解(附面试题) - 底层原理总结

开胃面试题

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代码转化过程

OC代码如下

#import 

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  {
#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 
#import 
#import 

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个字节,_no4个字节,_age4个字节。

Student对象的存储空间

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:
2019-11-25 14:06:42.508522+0800 demo[17733:127185] obj2:
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.其他一些信息...


class对象中存储的信息

拿成员变量来说,实例对象存储的是成员变量具体的值(如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)的类对象(这个比较特殊)

isa指针指向

2、superclass指针

superclass指针存在于类对象和元类对象中。那么这些superclass指针指向哪里,有什么用?

superclass指针存在于类对象和元类对象中,实例对象中没有superclass指针。类对象的superclass指针指向其父类的类对象,元类对象的superclass指针指向其父类的元类对象。当对象调用其父类的对象方法时,就需要使用superclass指针

创建一个继承自NSObject的Person类,再创建一个继承自Person的Student类。当Student对象要调用Person的对象方法时,就会通过Student对象的isa指针找到Student的类对象,发现Student类对象中没有这个方法,就会通过Student的superclass指针去Person的类对象中找这个方法,找到后进行调用。

superclass指针指向
superclass调用类方法的过程与调用对象方法的过程类似,只不过调用对象方法是去类对象里面去找,调用类方法是去元类对象里面去找。

对isa指针和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(附面试题) - 底层原理总结

文章坚持优化更新中...

你可能感兴趣的:(iOS中OC对象的本质详解(附面试题) - 底层原理总结)