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 <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个字节,_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:<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.其他一些信息...


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

文章坚持优化更新中...
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容