iOS底层原理-探寻OC对象本质

释放双眼,带上耳机,听听看~!

本篇学习总结:

  • NSObject对象/自定义类的对象/继承关系的类的类的对象内存分配情况以及类信息情况
  • OC对象类别有哪些呢?
  • OC对象的类信息存储位置在哪里呢?
  • OC对象中常说的isa指针是怎么回事呢?
  • OC对象中常说的superclass指针怎么回事呢?

好了,带着问题,我们一一开始阅读吧 ?

一.NSObject 对象内存分配情况

1.面试题:在64bit环境下,一个NSObject对象占用多少内存?

探寻OC对象的本质,我们平时编写的Objective-C代码,转化成底层都是CC++代码。

OC代码转化过程.png

OC代码如下:

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}

我们要想看到OC代码转化为C++文件,需要通过命令行进行操作,现在将OC的main.m 文件转化为C++文件,
第一个方法是安装插件转换:

 

oc代码转c++插件.png

第二个方法是直接 cd main.m文件所在文件夹

 

cd main.m文件所在文件夹.png

然后再执行下面的命令行工具(用于mac命令行项目)

clang -rewrite-objc main.m -o main.cpp // 这种方式没有指定架构例如arm64架构 其中cpp代表(c plus plus)
生成 main.cpp

我们还可以指定架构模式的命令行,使用Xcode工具xrun(用于iOS应用)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
生成 main-arm64.cpp

下面提示代码c++转化成功:

 

转化成功.png
转化成功后的文件.png

我们将生成的文件添加到项目中,不需要编译

 

C++转换文件.png

我们打开main-arm64.cpp文件,搜索NSObject,可以找到NSObjcet_IMPL (IMPL代表 implementation 实现),代码如下:

struct NSObject_IMPL {
Class isa;
};

发现里面只有一个Class类型的isa成员变量,顺势点进入Class查看一下它的结构,代码如下:

typedef struct objc_class *Class;
查看 objc_class,结构如下:
struct objc_class {
Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class                              OBJC2_UNAVAILABLE;
const char * _Nonnull name                               OBJC2_UNAVAILABLE;
long version                                             OBJC2_UNAVAILABLE;
long info                                                OBJC2_UNAVAILABLE;
long instance_size                                       OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

原来typedef struct objc_class Class就是一种结构体的指针。

这时候我们回到第一个面试题,一个NSObject对象在内存中占用多少内存,其实就是isa结构体类型的指针在内存中占用的空间,如果64bit占用8个字节,如果32bit占用4个字节。咱们这里探讨的是64bit,也就是说一个NSObjec对象所占用的内存是8个字节。到这里我们已经可以基本解答第一个问题。但是我们发现NSObject对象中还有很多方法,那这些方法不占用内存空间吗?其实类的方法等也占用内存空间,但是这些方法所占用的存储空间并不在NSObject对象中,如果是自定义的类,又是如何计算内存呢,我们继续探讨OC对象本质问题。

二.自定义类的实例对象内存分配情况

2.面试题:在64bit环境下,自定类的实例对象占用多少内存呢?

首先创建一个Student

@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
NSLog(@"%zd", class_getInstanceSize([Student class]));
NSLog(@"%zd", malloc_size((__bridge const void *)stu));
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
}
return 0;
}

我们按照上面的OC代码转C++文件的方式进行转换。我们从 main-arm64.cpp 文件中搜索 Student,搜索结果代码如下:

struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};

我们发现Student类转化为C++的结构体后第一项是struct NSObject_IMPL ,前面探讨NSObject对象的时候写过

struct NSObject_IMPL {
Class isa;
};

我们将这部分代码进行替换,替换结果如下:

struct Student_IMPL {
Class *isa;
int _no;
int _age;
};

遵循上面计算NSObject对象内存的方式,结构体内的各个成员变量占用内存总和就是结构体占用总的内存大小,咱们给Student对象计算一下内存大小吧:
isa指针8个字节空间+int类型_no4个字节空间+int类型_age4个字节空间共16个字节空间

上面的方法是根据类型推算出来的内存大小,我们还可以根据代码计算出来

 NSLog(@"NSObject = %zd",class_getInstanceSize([NSObject class]));
//类对象实际需要内存大小
NSLog(@"Student = %zd", class_getInstanceSize([Student class]));
//系统分配
NSLog(@"Student = %zd", malloc_size((__bridge const void *)stu));
OC对象本身占用内存大小.png

窥探内存结构
我们还需要进一步直观的看到内存数据,那怎么做呢?
方式一:通过打断点
Debug Workflow -> viewMemory address中输入一个NSObject对象的地址,stu对象的内存地址查看方式也是同样的操作。

查看内存地址方式.png

 

查看结果.png

从上图中,我们可以发现读取数据从高位数据开始读,查看前16位字节,每四个字节读出的数据为
16进制 0x0000004(4字节) 0x0000005(4字节) isa的地址为 00D1081000001119(8字节)

方式二:通过lldb指令xcode自带的调试器
先看几个常用的命令行

memory read 0x10074c450
// 简写  x 0x10074c450
// 增加读取条件
// memory read/数量格式字节数  内存地址
// 简写 x/数量格式字节数  内存地址
// 格式 x是16进制,f是浮点,d是10进制
// 字节大小   b:byte 1字节,h:half word 2字节,w:word 4字节,g:giant word 8字节
示例:x/4xw    //   /后面表示如何读取数据 w表示4个字节4个字节读取,x表示以16进制的方式读取数据,4则表示读取4次

同时也可以通过lldb修改内存中的值

memory write 0x100400c68 6
将_no的值改为了6
libo 查看内存结果图.png
三.继承关系的类的类的对象内存分配情况

3.面试题:在64bit环境下,继承关系的子父类占用内存情况如何呢?

// Person
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic, assign) int height;
@end
@implementation Person
@end
//Student
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"stu - %zd", class_getInstanceSize([Student class]));
NSLog(@"person - %zd", class_getInstanceSize([Person class]));
}
return 0;
}
//打印结果如下:
Interview01-OC对象的本质[2872:67593] stu - 24
Interview01-OC对象的本质[2872:67593] person - 16

其实这道题主要考察的面试题是什么呢?继承类的内存大小如何计算呢?
我们依次将上面的Student子类跟Person父类转化成C++结构体写出来

struct NSObject_IMPL {
Class isa;//8
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8
int _age; // 4
}; // 16 内存对齐:结构体的大小必须是最大成员大小的倍数
struct Student_IMPL {
struct Person_IMPL Person_IVARS; // 16
int _no; // 4
}; // 16

这时候你会疑问了Person_IMPL 不是占用12个字节吗,怎么显示16呢?那是因为系统给对象分配内存时会遵循内存对齐:结构体的大小必须是最大成员大小的倍原则,也就说Person_IMPL结构体中的成员变量(isa_age)实际需要12字节空间,但是系统根据原则确分配了16字节,所以结果是16字节。
而** Student_IMPL怎么又成了16字节呢,上面说了系统给Person_IMPL分配了16字节,实际占用12字节,还留有4字节空余,恰好放_no**4字节的变量,这样出来的结果就是系统分配16字节恰好够Student_IMPL对象使用。

敲黑板了!!!

所以,综上,我们总结一下系统给对象分配存储空间的原则:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数。
为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
我们可以总结内存对齐为两个原则:
原则 1. 前面的地址必须是后面的地址正数倍,不是就补齐。
原则 2. 整个Struct的地址必须是最大字节的整数倍。

如果有兴趣可以进一步研究底层实现,这里我就做个学习总结.

四.OC对象的类别以及存储信息

4.面试题:OC对象都有哪些呢?
5.面试题:OC的类信息存储在哪里呢?

先来一段代码

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
/* Person */
@interface Person : NSObject <NSCopying>
{
@public
int _age;
}
@property (nonatomic, assign) int height;
- (void)personMethod;
+ (void)personClassMethod;
@end
@implementation Person
- (void)personMethod {}
+ (void)personClassMethod {}
@end
/* Student */
@interface Student : Person <NSCoding>
{
@public
int _no;
}
@property (nonatomic, assign) int score;
- (void)studentMethod;
+ (void)studentClassMethod;
@end
@implementation Student
- (void)studentMethod {}
+ (void)studentClassMethod {}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
Student *stu = [[Student alloc] init];
[Student load];
Person *p1 = [[Person alloc] init];
p1->_age = 10;
[p1 personMethod];
[Person personClassMethod];
Person *p2 = [[Person alloc] init];
p2->_age = 20;
}
return 0;
}

OC对象类分为几类呢?

  • instance对象(实例对象)
  • class对象(类对象)
  • meta-class对象(元类对象)

instance对象
通过类alloc 出来的对象,每次调用alloc 都会产生新的instance对象。

NSObjcet *object1 = [[NSObjcet alloc] init];
NSObjcet *object2 = [[NSObjcet alloc] init];
//内存打印地址如下:
object1 = 0x100723a60 object2 = 0x100723720

object1和object2都是NSObject 的instance对象(实例对象),但是是两个不同的对象,从打印结果就能看出来,它们分别占据不同的内存地址。
instance对象在内存中存储的信息包括:
1.isa指针
2.成员变量具体的数据

instance对象存储信息.png

class对象
我们通过class方法或者runtime方法得到一个class对象,class对象也就是类对象

Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = [NSObject class];
// runtime
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);
NSLog(@"%p %p %p %p %p", objectClass1, objectClass2, objectClass3, objectClass4, objectClass5);
//内存打印地址如下:
objectClass1 = 0x7fff97528118 objectClass2 = 0x7fff97528118 objectClass3 = 0x7fff97528118 objectClass4 = 0x7fff97528118 objectClass5 = 0x7fff97528118
// 而调用类对象的class方法时得到还是类对象,无论调用多少次都是类对象
Class cls = [[NSObject class] class];
Class objectClass6 = [NSObject class];
NSLog(@"objectClass = %p cls = %p", objectClass6, cls); // 后面两个地址相同,说明多次调用class得到的还是类对象
//打印结果如下:
objectClass = 0x7fff97528118 cls = 0x7fff97528118

每个类在内存中有且只有一个class对象(类对象),通过打印内存地址就可以看出来。
class对象在内存中存储的信息包括:
1.isa指针
2.superclass指针
3.类的属性信息(@property),类的成员变量信息(ivar)
4.类的方法信息(method),类的协议信息(protocol)

class对象存储信息.png

写到这里有人就有疑问了,刚才不是说在instance对象中存储成员变量信息吗,怎么class对象中也存储成员变量和属性变量呢,这里要特意说明一点:
成员变量的值时存储在实例对象中的,因为只有当我们创建实例对象的时候才为成员变赋值。但是成员变量叫什么名字,是什么类型,只需要有一份就可以了。所以存储在class对象中。
meta-class对象
只能是通过class对象获取到meta-class对象,通过下面的方法获取到。

//runtime中传入类对象此时得到的就是元类对象
Class objectMetaClass = object_getClass([NSObject class]);
NSLog(@"objectMetaClass = %p",objectMetaClass);
//内存打印地址如下:
objectMetaClass = 0x7fff975280f0
//检查是否为元类对象
BOOL ismetaclass = class_isMetaClass(objectMetaClass);// 判断该对象是否为元类对象
NSLog(@"objectMetaClass 是否是元类对象 - %ld",ismetaclass);
//打印结果如下:
objectMetaClass 是否是元类对象 - 1

每一个类的meta-class对象在内存中有且只有一个,class对象跟meta-class对象结构一样,都是*struct objc_class Class,但是用途不一样。
meta-class对象在内存中存储的信息包括:
1.isa指针
2.superclass指针
3.类的类方法信息(class-method)

meta-class存储信息.png

既然class对象跟meta-class对象结构一样,那么class对象中是不是也有类方法信息呢?meta-class对象中是不是也有class对象中存储的属性信息,成员变量信息,方法信息,协议信息呢?答案是有的,只不过对应的值可能是空的,所以忽略不计。

五.OC对象的isa指针指向问题

前面已经说到了一个NSObject对象转化为C++文件后,

struct NSObject_IMPL {
Class isa;
};

所以说任何一个继承NSObject的对象都包含一个isa指针,那么各个对象的isa指针又分别指向哪里呢?

我们先看两个常用的调用方法

MJStudent *student = [[MJStudent alloc]init];
//方法1:调用实例方法
[student studentInstanceMethod];

方法1:student实例对象调用了实例方法,我们在前面讲到过,实例方法信息存储在class对象中,这时候instace对象中存储的isa指针起到作用了,instace对象中的isa指针指向class对象,我们通过isa指针找到class对象,进而找到实例方法列表,调用对应方法。

 

对象方法调用轨迹.png
//方法2:调用类方法
[MJStudent studentClassMethod];

方法2:MJStudent类对象调用了类方法,我们在前面讲到过,类方法信息存储在meta-class对象中,这时候class对象中存储的isa指针起到作用了,class对象中的isa指针指向meta-class对象,我们通过isa指针找到meta-class对象,进而找到类方法列表,调用对应方法。仿照上图“对象方法调用轨迹.png”

总结:instance对象——<isa指针>——class对象——<isa指针>——meta-class对象——<isa指针>——基类NSObject元类对象

详细看下面isa指针图例:

对象isa指针指向图例.png

总结两点比较坑的地方:

a.基类的元类对象的superclass指针指向基类的类对象
b.类的元类对象的isa指针指向基类的元类对象

六.OC对象的superclass指针指向位置

我们还是以Student类和Person类进行说明,不清楚类信息的小伙伴请往前浏览一下。Student类是子类,Person类是父类。

//方法1:调用父类的实例方法
[student personInstanceMethod];
//方法2:调用父类的类方法
[MJStudent personClassMethod];

方法1:当给student实例对象发送personInstanceMethod消息时,student实例对象会通过isa指针找到对应MJStudent类对象,因为类对象中存储中对象方法信息,先从MJStudent类对象的实例方法信息中查找对应的方法,如果找到进行相应,没找到则继续向父类查找,那么子类怎么才能找到父类呢,这时候需要用到superclass指针了,通过superclass指针找到MJPerson的类对象,继续从类对象那个的实例方法中查找,如果找到进行相应,没找到则继续通过superclass查找基类NSObject类对象方法列表,如果还没找到,返回nil,就是咱们常见的报错信息,找不到此方法。

方法2:跟方法1类似,简单说一下吧
当给MJStudent类对象发送personClassMethod消息时,MJStudent类对象会通过isa指针找到对应MJStudent元类对象,因为元类对象中存储中类方法信息,先从MJStudent元类对象的类信息中查找对应的方法,如果找到进行相应,没找到则继续向父类查找,那么子类怎么才能找到父类呢,这时候需要用到superclass指针了,通过superclass指针找到MJPerson的元类对象,继续从元类对象那个的类方法中查找,如果找到进行相应,没找到则继续通过superclass查找基类NSObject元类对象方法列表,如果还没找到,这个时候跟方法1的查找不太一样了,如果NSObject的元类对象的类方法中找到,就从NSObject的类方法的实例方法中去查找,还没有找到,则返回nil,就是咱们常见的报错信息,找不到此方法。

 

子类调用父类方法查找顺序.png

总结:
1.子类类对象——<superclass指针>——父类类对象——<superclass指针>——基类类对象
2.子类元类对象——<superclass指针>——父类元类对象——<superclass指针>——基类元类对象——<superclass指针>基类的类对象

看完以上的解析,再来看这经典的图也不是那么的晦涩了

 

isa-superclass.png

对isa、superclass总结
instance的isa指向class
class的isa指向meta-class
meta-class的isa指向基类的meta-class,基类的isa指向自己
class的superclass指向父类的class,如果没有父类,superclass指针为nil
meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
instance调用对象方法的轨迹,isa找到class,方法不存在,就通过superclass找父类
class调用类方法的轨迹,isa找meta-class,方法不存在,就通过superclass找父类

七.代码求证isa指针指向是否正确

我们先写一段代码:

NSObject *object = [[NSObject alloc] init];//instance对象
Class objectClass = [NSObject class];//类对象
Class objectMetaClass = object_getClass([NSObject class]);//元类对象
NSLog(@"object - %p objectClass - %p objectMetaClass - %p", object, objectClass, objectMetaClass);
//打印结果如下:
object - 0x10051e0b0    //instance对象内存地址
objectClass - 0x7fff9abb6118 //类对象内存地址
objectMetaClass - 0x7fff9abb60f0  //元类对象内存地址

我们通过命令行打印如下:

instance对象isa指针内存地址.png

程序打印:类对象内存地址 – 0x7fff9abb6118
命令行打印:实例对象->isa指针获取到的类对象内存地址 – 0x001dffff9abb6119
怎么不一样了呢?
原因是:这是因为从64bit开始,isa需要进行一次位运算,才能计算出真实地址。而位运算的值我们可以通过下载objc源代码找到。
arm64:表示真机应用
x86_64:表示mac应用
小码哥视频中创建的demo是mac应用,所以用 0x00007ffffffffff8计算

ISA_MASK.png

我们按照这个原则再来操作一遍:

实例对象位运算结果.png

果然跟程序打印出来的结果一样,这足以证明上面总结的isa指针指向的正确性。

但我们再次尝试验证类方法的isa指针指向的元类对象的内存地址跟程序自然打印的是否一样的时候,发现了如下问题

 

类对象isa指针内存地址.png

 

小码哥教给的做法是:为了拿到isa指针的地址,我们自己创建一个同样的结构体并通过强制转化拿到isa指针。

struct mj_objc_class {
Class isa;
Class superclass;
};
struct mj_objc_class *nsobjectclass = (__bridge struct mj_objc_class *)([NSObject class]);

将OC中的类对象转化成C语言的结构体指针的时候需要进行桥接,直接点fix就行

 

oc对象转c语言结构体变量.png
类对象isa指针内存地址.png

 

程序打印:元类对象内存地址 – 0x7fff9abb60f0
命令行打印:类对象->isa指针获取到的元类类对象内存地址 – 0x00007fff9abb60f0

 

类对象位运算结果.png

总结一下本文面试题:

  • 1.面试题:一个NSObject对象占用多少内存?

答:一个NSObject对象在内存中占用多少内存,其实就是isa结构体类型的指针在内存中占用的空间(64bit占8个字节,32bit占4个字节)

  • 2.面试题:自定义类的实例对象占用多少内存?

答:根据情况而定,具体分析方法看上述说明

  • 3.面试题:继承关系的子父类的实例对象占用多少内存?

答:根据情况而定,具体分析方法看上述说明

  • 4.面试题:OC对象的类别有哪些呢?

instance对象(实例对象)
class对象(类对象)
meta-class对象(元类对象)

  • 5.面试题:OC对象的isa指针指向哪里呢?

答:instance对象的isa指针指向class对象,class对象的isa指针指向meta-class对象,meta-class对象的isa指针指向基类的meta-class对象,基类自己的isa指针也指向自己。

  • 6.面试题:OC的类信息存放在哪里?

instance对象(实例方法):存放isa指针,成员变量的具体数据
class对象(类对象):存放isa指针,superclass指针,类的成员变量(ivar),类的属性信息(property),类的协议信息(protocol),类的方法列表(instance method list)
meta-class对象(元类对象:存放isa指针,superclass指针,类的方法列表(class method list)

 

人已赞赏
iOS文章

iOS底层原理总结 - 探寻KVO本质

2020-5-10 22:18:04

iOS文章

iOS底层原理总结探寻Class的本质

2020-5-10 23:33:02

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索